diff --git a/druid/custom/rich_text/rich_text.gui b/druid/custom/rich_text/rich_text.gui
new file mode 100644
index 0000000..dc4c340
--- /dev/null
+++ b/druid/custom/rich_text/rich_text.gui
@@ -0,0 +1,199 @@
+script: ""
+fonts {
+ name: "game"
+ font: "/example/assets/fonts/game.font"
+}
+textures {
+ name: "kenney"
+ texture: "/example/assets/images/kenney.atlas"
+}
+background_color {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+}
+nodes {
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 300.0
+ y: 200.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: ""
+ id: "root"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ layer: ""
+ inherit_alpha: true
+ slice9 {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+ }
+ clipping_mode: CLIPPING_MODE_NONE
+ clipping_visible: true
+ clipping_inverted: false
+ alpha: 1.0
+ template_node_child: false
+ size_mode: SIZE_MODE_MANUAL
+ custom_type: 0
+ enabled: true
+ visible: false
+}
+nodes {
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 200.0
+ y: 100.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_TEXT
+ blend_mode: BLEND_MODE_ALPHA
+ text: "Text"
+ font: "game"
+ id: "text_prefab"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ outline {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ shadow {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ adjust_mode: ADJUST_MODE_FIT
+ line_break: false
+ parent: "root"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ outline_alpha: 1.0
+ shadow_alpha: 0.0
+ template_node_child: false
+ text_leading: 1.0
+ text_tracking: 0.0
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 77.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 36.0
+ y: 36.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: "kenney/slider_move"
+ id: "icon_prefab"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "root"
+ layer: ""
+ inherit_alpha: true
+ slice9 {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+ }
+ clipping_mode: CLIPPING_MODE_NONE
+ clipping_visible: true
+ clipping_inverted: false
+ alpha: 1.0
+ template_node_child: false
+ size_mode: SIZE_MODE_AUTO
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+material: "/builtins/materials/gui.material"
+adjust_reference: ADJUST_REFERENCE_PARENT
+max_nodes: 512
diff --git a/druid/custom/rich_text/rich_text.lua b/druid/custom/rich_text/rich_text.lua
new file mode 100644
index 0000000..5ba247b
--- /dev/null
+++ b/druid/custom/rich_text/rich_text.lua
@@ -0,0 +1,85 @@
+local component = require("druid.component")
+local rich_text = require("druid.custom.rich_text.rich_text.richtext")
+
+local RichText = component.create("rich_text")
+
+local SCHEME = {
+ ROOT = "root",
+ TEXT_PREFAB = "text_prefab",
+ ICON_PREFAB = "icon_prefab"
+}
+
+
+local ALIGN_MAP = {
+ [gui.PIVOT_CENTER] = { rich_text.ALIGN_CENTER, rich_text.VALIGN_MIDDLE },
+ [gui.PIVOT_N] = { rich_text.ALIGN_CENTER, rich_text.VALIGN_TOP },
+ [gui.PIVOT_S] = { rich_text.ALIGN_CENTER, rich_text.VALIGN_BOTTOM },
+ [gui.PIVOT_NE] = { rich_text.ALIGN_RIGHT, rich_text.VALIGN_TOP },
+ [gui.PIVOT_E] = { rich_text.ALIGN_RIGHT, rich_text.VALIGN_MIDDLE },
+ [gui.PIVOT_SE] = { rich_text.ALIGN_RIGHT, rich_text.VALIGN_BOTTOM },
+ [gui.PIVOT_SW] = { rich_text.ALIGN_LEFT, rich_text.VALIGN_BOTTOM },
+ [gui.PIVOT_W] = { rich_text.ALIGN_LEFT, rich_text.VALIGN_MIDDLE },
+ [gui.PIVOT_NW] = { rich_text.ALIGN_LEFT, rich_text.VALIGN_TOP },
+}
+
+
+function RichText:init(template, nodes)
+ self:set_template(template)
+ self:set_nodes(nodes)
+ self.root = self:get_node(SCHEME.ROOT)
+ self.druid = self:get_druid()
+
+ self.text_prefab = self:get_node(SCHEME.TEXT_PREFAB)
+ self.icon_prefab = self:get_node(SCHEME.ICON_PREFAB)
+
+ gui.set_enabled(self.text_prefab, false)
+ gui.set_enabled(self.icon_prefab, false)
+
+ self._text_font = gui.get_font(self.text_prefab)
+ self._settings = self:_get_settings()
+end
+
+
+function RichText:set_text(text)
+ self:_clean_words()
+ pprint(self._settings)
+ local words, metrics = rich_text.create(text, self._text_font, self._settings)
+
+ self._words = words
+ self._metrics = metrics
+end
+
+
+function RichText:on_remove()
+ self:_clean_words()
+end
+
+
+function RichText:_get_settings()
+ local root_size = gui.get_size(self.root)
+ local anchor = gui.get_pivot(self.root)
+ pprint(ALIGN_MAP[anchor])
+ return {
+ width = root_size.x,
+ parent = self.root,
+ color = gui.get_color(self.text_prefab),
+ shadow = gui.get_shadow(self.text_prefab),
+ outline = gui.get_outline(self.text_prefab),
+ align = ALIGN_MAP[anchor][1],
+ valign = ALIGN_MAP[anchor][2],
+ }
+end
+
+
+function RichText:_clean_words()
+ if not self._words then
+ return
+ end
+
+ rich_text.remove(self._words)
+ self._words = nil
+ self._metrics = nil
+end
+
+
+return RichText
diff --git a/druid/custom/rich_text/rich_text/color.lua b/druid/custom/rich_text/rich_text/color.lua
new file mode 100644
index 0000000..f4e4736
--- /dev/null
+++ b/druid/custom/rich_text/rich_text/color.lua
@@ -0,0 +1,70 @@
+-- Source: https://github.com/britzl/defold-richtext version 5.19.0
+-- Author: Britzl
+-- Modified by: Insality
+
+local M = {}
+
+function M.parse_hex(hex)
+ local r,g,b,a = hex:match("#?(%x%x)(%x%x)(%x%x)(%x?%x?)")
+ if a == "" then a = "ff" end
+ if r and g and b and a then
+ return vmath.vector4(
+ tonumber(r, 16) / 255,
+ tonumber(g, 16) / 255,
+ tonumber(b, 16) / 255,
+ tonumber(a, 16) / 255)
+ end
+ return nil
+end
+
+
+function M.parse_decimal(dec)
+ local r,g,b,a = dec:match("(%d*%.?%d*),(%d*%.?%d*),(%d*%.?%d*),(%d*%.?%d*)")
+ if r and g and b and a then
+ return vmath.vector4(tonumber(r), tonumber(g), tonumber(b), tonumber(a))
+ end
+ return nil
+end
+
+
+function M.add(name, color)
+ if type(color) == "string" then
+ color = M.parse_hex(color) or M.parse_decimal(color)
+ end
+ assert(type(color) == "userdata" and color.x and color.y and color.z and color.w, "Unable to add color")
+ M.COLORS[name] = color
+end
+
+
+M.COLORS = {
+ aqua = M.parse_hex("#00ffffff"),
+ black = M.parse_hex("#000000ff"),
+ blue = M.parse_hex("#0000ffff"),
+ brown = M.parse_hex("#a52a2aff"),
+ cyan = M.parse_hex("#00ffffff"),
+ darkblue = M.parse_hex("#0000a0ff"),
+ fuchsia = M.parse_hex("#ff00ffff"),
+ green = M.parse_hex("#008000ff"),
+ grey = M.parse_hex("#808080ff"),
+ lightblue = M.parse_hex("#add8e6ff"),
+ lime = M.parse_hex("#00ff00ff"),
+ magenta = M.parse_hex("#ff00ffff"),
+ maroon = M.parse_hex("#800000ff"),
+ navy = M.parse_hex("#000080ff"),
+ olive = M.parse_hex("#808000ff"),
+ orange = M.parse_hex("#ffa500ff"),
+ purple = M.parse_hex("#800080ff"),
+ red = M.parse_hex("#ff0000ff"),
+ silver = M.parse_hex("#c0c0c0ff"),
+ teal = M.parse_hex("#008080ff"),
+ white = M.parse_hex("#ffffffff"),
+ yellow = M.parse_hex("#ffff00ff"),
+}
+
+
+function M.parse(c)
+ return M.COLORS[c] or M.parse_hex(c) or M.parse_decimal(c)
+end
+
+
+return M
diff --git a/druid/custom/rich_text/rich_text/parse.lua b/druid/custom/rich_text/rich_text/parse.lua
new file mode 100755
index 0000000..2c74f95
--- /dev/null
+++ b/druid/custom/rich_text/rich_text/parse.lua
@@ -0,0 +1,203 @@
+-- Source: https://github.com/britzl/defold-richtext version 5.19.0
+-- Author: Britzl
+-- Modified by: Insality
+
+local utf8 = require("druid.system.utf8")
+local tags = require("druid.custom.rich_text.rich_text.tags")
+
+local M = {}
+
+local function parse_tag(tag, params)
+ local settings = { tags = { [tag] = params }, tag = tag }
+ if not tags.apply(tag, params, settings) then
+ settings[tag] = params
+ end
+
+ return settings
+end
+
+
+-- add a single word to the list of words
+local function add_word(text, settings, words)
+ -- handle HTML entities
+ text = text:gsub("<", "<"):gsub(">", ">"):gsub(" ", " ")
+
+ local data = { text = text }
+ for k,v in pairs(settings) do
+ data[k] = v
+ end
+ words[#words + 1] = data
+end
+
+
+-- split a line into words
+local function split_line(line, settings, words)
+ assert(line)
+ assert(settings)
+ assert(words)
+ local ws_start, trimmed_text, ws_end = line:match("^(%s*)(.-)(%s*)$")
+ if trimmed_text == "" then
+ add_word(ws_start .. ws_end, settings, words)
+ else
+ local wi = #words
+ for word in trimmed_text:gmatch("%S+") do
+ add_word(word .. " ", settings, words)
+ end
+ local first = words[wi + 1]
+ first.text = ws_start .. first.text
+ local last = words[#words]
+ last.text = utf8.sub(last.text, 1, utf8.len(last.text) - 1) .. ws_end
+ end
+end
+
+
+-- split text
+-- split by lines first
+local function split_text(text, settings, words)
+ assert(text)
+ assert(settings)
+ assert(words)
+ -- special treatment of empty text with a linebreak
+ if text == "" and settings.linebreak then
+ add_word(text, settings, words)
+ return
+ end
+
+ -- we don't want to deal with \r\n, remove all \r
+ text = text:gsub("\r", "")
+
+ -- the Lua pattern expects the text to have a linebreak at the end
+ local added_linebreak = false
+ if text:sub(-1)~="\n" then
+ added_linebreak = true
+ text = text .. "\n"
+ end
+
+ -- split into lines
+ for line in text:gmatch("(.-)\n") do
+ split_line(line, settings, words)
+ -- flag last word of a line as having a linebreak
+ local last = words[#words]
+ last.linebreak = true
+ end
+
+ -- remove the last linebreak if we manually added it above
+ if added_linebreak then
+ local last = words[#words]
+ last.linebreak = false
+ end
+end
+
+
+-- Merge one tag into another
+local function merge_tags(dst, src)
+ for k,v in pairs(src) do
+ if k ~= "tags" then
+ dst[k] = v
+ end
+ end
+ for tag,params in pairs(src.tags or {}) do
+ dst.tags[tag] = (params == "") and true or params
+ end
+end
+
+
+--- Parse the text into individual words
+-- @param text The text to parse
+-- @param default_settings Default settings for each word
+-- @return List of all words
+function M.parse(text, default_settings)
+ assert(text)
+ assert(default_settings)
+
+ text = text:gsub("&zwsp;", "\226\128\139")
+ local all_words = {}
+ local open_tags = {}
+ while true do
+ -- merge list of word settings from defaults and all open tags
+ local word_settings = { tags = {}}
+ merge_tags(word_settings, default_settings)
+ for _,open_tag in ipairs(open_tags) do
+ merge_tags(word_settings, open_tag)
+ end
+
+ -- find next tag, with the text before and after the tag
+ local before_tag, tag, after_tag = text:match("(.-)(?%S->)(.*)")
+
+ -- no more tags, split and add rest of the text
+ if not before_tag or not tag or not after_tag then
+ if text ~= "" then
+ split_text(text, word_settings, all_words)
+ end
+ break
+ end
+
+ -- split and add text before the encountered tag
+ if before_tag ~= "" then
+ split_text(before_tag, word_settings, all_words)
+ end
+
+ -- parse the tag, split into name and optional parameters
+ local endtag, name, params, empty = tag:match("<(/?)(%a+)=?(%S-)(/?)>")
+
+ local is_endtag = endtag == "/"
+ local is_empty = empty == "/"
+ if is_empty then
+ -- empty tag, ie tag without content
+ -- example
and
+ local empty_tag_settings = parse_tag(name, params)
+ merge_tags(empty_tag_settings, word_settings)
+ add_word("", empty_tag_settings, all_words)
+ elseif not is_endtag then
+ if name == "repeat" then
+ local text_to_repeat = after_tag:match("(.-)")
+ local repetitions = tonumber(params)
+ if repetitions > 1 then
+ after_tag = text_to_repeat:rep(repetitions - 1) .. after_tag
+ end
+ else
+ -- open tag - parse and add it
+ local tag_settings = parse_tag(name, params)
+ open_tags[#open_tags + 1] = tag_settings
+ end
+ else
+ if name ~= "repeat" then
+ -- end tag - remove it from the list of open tags
+ local found = false
+ for i=#open_tags,1,-1 do
+ if open_tags[i].tag == name then
+ table.remove(open_tags, i)
+ found = true
+ break
+ end
+ end
+ if not found then print(("Found end tag '%s' without matching start tag"):format(name)) end
+ end
+ end
+
+ if name == "p" then
+ local last_word = all_words[#all_words]
+ if last_word then
+ if not is_endtag then
+ last_word.linebreak = true
+ end
+ if is_endtag or is_empty then
+ last_word.paragraph_end = true
+ end
+ end
+ end
+
+ -- parse text after the tag on the next iteration
+ text = after_tag
+ end
+ return all_words
+end
+
+
+--- Get the length of a text, excluding any tags (except image and spine tags)
+function M.length(text)
+ return utf8.len(text:gsub("", " "):gsub("", " "):gsub("<.->", ""))
+end
+
+
+return M
diff --git a/druid/custom/rich_text/rich_text/richtext.lua b/druid/custom/rich_text/rich_text/richtext.lua
new file mode 100755
index 0000000..03d21fc
--- /dev/null
+++ b/druid/custom/rich_text/rich_text/richtext.lua
@@ -0,0 +1,700 @@
+-- Source: https://github.com/britzl/defold-richtext version 5.19.0
+-- Author: Britzl
+-- Modified by: Insality
+
+local parser = require("druid.custom.rich_text.rich_text.parse")
+local utf8 = require("druid.system.utf8")
+
+local M = {}
+
+M.ALIGN_CENTER = hash("ALIGN_CENTER")
+M.ALIGN_LEFT = hash("ALIGN_LEFT")
+M.ALIGN_RIGHT = hash("ALIGN_RIGHT")
+M.ALIGN_JUSTIFY = hash("ALIGN_JUSTIFY")
+
+M.VALIGN_TOP = hash("VALIGN_TOP")
+M.VALIGN_MIDDLE = hash("VALIGN_MIDDLE")
+M.VALIGN_BOTTOM = hash("VALIGN_BOTTOM")
+
+
+local V4_ZERO = vmath.vector4(0)
+local V4_ONE = vmath.vector4(1)
+local V3_ZERO = vmath.vector3(0)
+local V3_ONE = vmath.vector3(1)
+
+local id_counter = 0
+
+local function new_id(prefix)
+ id_counter = id_counter + 1
+ return hash((prefix or "") .. tostring(id_counter))
+end
+
+local function round(v)
+ if type(v) == "number" then
+ return math.floor(v + 0.5)
+ else
+ return vmath.vector3(math.floor(v.x + 0.5), math.floor(v.y + 0.5), math.floor(v.z + 0.5))
+ end
+end
+
+
+local function deepcopy(orig)
+ local orig_type = type(orig)
+ local copy
+ if orig_type == 'table' then
+ copy = {}
+ for orig_key, orig_value in next, orig, nil do
+ copy[deepcopy(orig_key)] = deepcopy(orig_value)
+ end
+ else -- number, string, boolean, etc
+ copy = orig
+ end
+ return copy
+end
+
+
+local function get_font(word, fonts)
+ local font_settings = fonts[word.font]
+ local font = nil
+ if font_settings then
+ if word.bold and word.italic then
+ font = font_settings.bold_italic
+ end
+ if not font and word.bold then
+ font = font_settings.bold
+ end
+ if not font and word.italic then
+ font = font_settings.italic
+ end
+ if not font then
+ font = font_settings.regular
+ end
+ end
+ if not font then
+ font = word.font
+ end
+ return font
+end
+
+
+local function get_layer(word, layers)
+ local node = word.node
+ if word.image then
+ return layers.images[gui.get_texture(node)]
+ elseif word.spine then
+ return layers.spinescenes[gui.get_spine_scene(node)]
+ end
+ return layers.fonts[gui.get_font(node)]
+end
+
+
+-- compare two words and check that they have the same size, color, font and tags
+local function compare_words(one, two)
+ if one == nil
+ or two == nil
+ or one.size ~= two.size
+ or one.color ~= two.color
+ or one.shadow ~= two.shadow
+ or one.outline ~= two.outline
+ or one.font ~= two.font then
+ return false
+ end
+ local one_tags, two_tags = one.tags, two.tags
+ if one_tags == two_tags then
+ return true
+ end
+ if one_tags == nil or two_tags == nil then
+ return false
+ end
+ for k, v in pairs(one_tags) do
+ if two_tags[k] ~= v then
+ return false
+ end
+ end
+ for k, v in pairs(two_tags) do
+ if one_tags[k] ~= v then
+ return false
+ end
+ end
+ return true
+end
+
+
+-- position all words according to the line alignment and line width
+-- the list of words will be empty after this function is called
+local function position_words(words, line_width, line_height, position, settings)
+ if settings.align == M.ALIGN_RIGHT then
+ position.x = position.x - line_width
+ elseif settings.align == M.ALIGN_CENTER then
+ position.x = position.x - line_width / 2
+ end
+
+ local spacing = 0
+ if settings.align == M.ALIGN_JUSTIFY then
+ local words_width = 0
+ local word_count = 0
+ for i=1,#words do
+ local word = words[i]
+ if word.metrics.total_width > 0 then
+ words_width = words_width + word.metrics.total_width
+ word_count = word_count + 1
+ end
+ end
+ if word_count > 1 then
+ spacing = (settings.width - words_width) / (word_count - 1)
+ end
+ end
+ for i=1,#words do
+ local word = words[i]
+ -- align spine animations to bottom of line since
+ -- spine animations ignore pivot (always PIVOT_S)
+ if word.spine then
+ position.y = position.y - line_height
+ gui.set_position(word.node, position)
+ position.y = position.y + line_height
+ elseif word.image and settings.image_pixel_grid_snap then
+ gui.set_position(word.node, round(position))
+ else
+ gui.set_position(word.node, position)
+ end
+ position.x = position.x + word.metrics.total_width + spacing
+ words[i] = nil
+ end
+end
+
+
+--- Get the length of a text ignoring any tags except image tags
+-- which are treated as having a length of 1
+-- @param text String with text or a list of words (from richtext.create)
+-- @return Length of text
+function M.length(text)
+ assert(text)
+ if type(text) == "string" then
+ return parser.length(text)
+ else
+ local count = 0
+ for i=1,#text do
+ local word = text[i]
+ local is_text_node = not word.image and not word.spine
+ count = count + (is_text_node and utf8.len(word.text) or 1)
+ end
+ return count
+ end
+end
+
+
+local size_vector = vmath.vector3()
+local function create_box_node(word)
+ local node = gui.new_box_node(V3_ZERO, V3_ZERO)
+ local word_image = word.image
+ local image_width = word_image.width
+ local image_height = word_image.height
+ gui.set_id(node, new_id("box"))
+ if image_width then
+ gui.set_size_mode(node, gui.SIZE_MODE_MANUAL)
+ size_vector.x = image_width
+ size_vector.y = image_height
+ size_vector.z = 0
+ gui.set_size(node, size_vector)
+ else
+ gui.set_size_mode(node, gui.SIZE_MODE_AUTO)
+ end
+ gui.set_texture(node, word.image.texture)
+ local word_size = word.size
+ size_vector.x = word_size
+ size_vector.y = word_size
+ size_vector.z = word_size
+ gui.set_scale(node, size_vector)
+
+ gui.play_flipbook(node, hash(word.image.anim))
+
+ -- get metrics of node based on image size
+ local size = gui.get_size(node)
+ local metrics = {}
+ metrics.total_width = size.x * word.size
+ metrics.width = size.x * word.size
+ metrics.height = size.y * word.size
+ return node, metrics
+end
+
+
+local function create_spine_node(word)
+ local node = gui.new_spine_node(V3_ZERO, word.spine.scene)
+ gui.set_id(node, new_id("spine"))
+ gui.set_size_mode(node, gui.SIZE_MODE_AUTO)
+ gui.set_scale(node, vmath.vector3(word.size))
+ gui.play_spine_anim(node, word.spine.anim, gui.PLAYBACK_LOOP_FORWARD)
+
+ local size = gui.get_size(node)
+ local metrics = {}
+ metrics.total_width = size.x
+ metrics.width = size.x
+ metrics.height = size.y
+ return node, metrics
+end
+
+
+local function get_text_metrics(word, font, text)
+ text = text or word.text
+ font = font or word.font
+
+ local metrics
+ if utf8.len(text) == 0 then
+ metrics = gui.get_text_metrics(font, "|")
+ metrics.width = 0
+ metrics.total_width = 0
+ metrics.height = metrics.height * word.size
+ else
+ metrics = gui.get_text_metrics(font, text)
+ metrics.width = metrics.width * word.size
+ metrics.height = metrics.height * word.size
+ metrics.total_width = metrics.width
+ end
+ return metrics
+end
+
+
+local function create_text_node(word, font, metrics)
+ local node = gui.new_text_node(V3_ZERO, word.text)
+ gui.set_id(node, new_id("textnode"))
+ gui.set_font(node, font)
+ gui.set_color(node, word.color)
+ if word.shadow then gui.set_shadow(node, word.shadow) end
+ if word.outline then gui.set_outline(node, word.outline) end
+ gui.set_scale(node, V3_ONE * word.size)
+
+ metrics = metrics or get_text_metrics(word, font)
+ gui.set_size_mode(node, gui.SIZE_MODE_MANUAL)
+ gui.set_size(node, vmath.vector3(metrics.width, metrics.height, 0))
+ return node, metrics
+end
+
+
+local function combine_node(previous_word, word, metrics)
+ local text = previous_word.text .. word.text
+ previous_word.text = text
+ previous_word.metrics = metrics
+ gui.set_size(previous_word.node, vmath.vector3(metrics.width, metrics.height, 0))
+ gui.set_text(previous_word.node, text)
+end
+
+
+local function create_node(word, parent, font, node, metrics)
+ if word.image then
+ if not node then
+ node, metrics = create_box_node(word)
+ end
+ elseif word.spine then
+ if not node then
+ node, metrics = create_spine_node(word)
+ end
+ else
+ node, metrics = create_text_node(word, font, metrics)
+ end
+ gui.set_parent(node, parent)
+ gui.set_inherit_alpha(node, true)
+ return node, metrics
+end
+
+
+local function measure_node(word, font, previous_word)
+ local node, metrics, combined_metrics
+ if word.image then
+ node, metrics = create_box_node(word)
+ elseif word.spine then
+ node, metrics = create_spine_node(word)
+ else
+ metrics = get_text_metrics(word, font)
+ if previous_word then
+ combined_metrics = get_text_metrics(word, font, previous_word.text .. word.text)
+ end
+ end
+ return metrics, combined_metrics, node
+end
+
+local function split_word(word, font, max_width)
+ local one = deepcopy(word)
+ local two = deepcopy(word)
+ local text = word.text
+ local metrics = get_text_metrics(one, font)
+ local char_count = utf8.len(text)
+ local split_index = math.floor(char_count * (max_width / metrics.total_width))
+ local rest = ""
+ while split_index > 1 do
+ one.text = utf8.sub(text, 1, split_index)
+ one.linebreak = true
+ metrics = get_text_metrics(one, font)
+ if metrics.width <= max_width then
+ rest = utf8.sub(text, split_index + 1)
+ break
+ end
+ split_index = split_index - 1
+ end
+ two.text = rest
+ return one, two
+end
+
+
+--- Create rich text gui nodes from text
+-- @param text The text to create rich text nodes from
+-- @param font The default font
+-- @param settings Optional settings table (refer to documentation for details)
+-- @return words
+-- @return metrics
+function M.create(text, font, settings)
+ assert(text, "You must provide a text")
+ assert(font, "You must provide a font")
+ settings = settings or {}
+ settings.align = settings.align or M.ALIGN_LEFT
+ settings.valign = settings.valign or M.VALIGN_TOP
+ settings.size = settings.size or 1
+ settings.fonts = settings.fonts or {}
+ settings.fonts[font] = settings.fonts[font] or { regular = hash(font) }
+ settings.layers = settings.layers or {}
+ settings.layers.fonts = settings.layers.fonts or {}
+ settings.layers.images = settings.layers.images or {}
+ settings.layers.spinescenes = settings.layers.spinescenes or {}
+ settings.color = settings.color or V4_ONE
+ settings.shadow = settings.shadow or V4_ZERO
+ settings.outline = settings.outline or V4_ZERO
+ settings.position = settings.position or V3_ZERO
+ settings.line_spacing = settings.line_spacing or 1
+ settings.paragraph_spacing = settings.paragraph_spacing or 0.5
+ settings.image_pixel_grid_snap = settings.image_pixel_grid_snap or false
+ settings.combine_words = settings.combine_words or false
+ if settings.align == M.ALIGN_JUSTIFY and not settings.width then
+ error("Width must be specified if text should be justified")
+ end
+
+ local line_increment_before = 0
+ local line_increment_after = 1
+ local pivot = gui.PIVOT_NW
+ if settings.valign == M.VALIGN_MIDDLE then
+ line_increment_before = 0.5
+ line_increment_after = 0.5
+ pivot = gui.PIVOT_W
+ elseif settings.valign == M.VALIGN_BOTTOM then
+ line_increment_before = 1
+ line_increment_after = 0
+ pivot = gui.PIVOT_SW
+ end
+
+ -- default settings for a word
+ -- will be assigned to each word unless tags override the values
+ local word_settings = {
+ color = settings.color,
+ shadow = settings.shadow,
+ outline = settings.outline,
+ font = font,
+ size = settings.size
+ }
+ local words = parser.parse(text, word_settings)
+ local text_metrics = {
+ width = 0,
+ height = 0,
+ char_count = 0,
+ img_count = 0,
+ spine_count = 0,
+ }
+ local line_words = {}
+ local line_width = 0
+ local line_height = 0
+ local paragraph_spacing = 0
+ local position = vmath.vector3(settings.position)
+ local word_count = #words
+ local i = 1
+ repeat
+ local word = words[i]
+ if word.image then
+ text_metrics.img_count = text_metrics.img_count + 1
+ elseif word.spine then
+ text_metrics.spine_count = text_metrics.spine_count + 1
+ else
+ text_metrics.char_count = text_metrics.char_count + parser.length(word.text)
+ end
+
+ -- get font to use based on word tags
+ local font_for_word = get_font(word, settings.fonts)
+
+ -- get the previous word, so we can combine
+ local previous_word
+ if settings.combine_words then
+ previous_word = line_words[#line_words]
+ if not compare_words(previous_word, word) then
+ previous_word = nil
+ end
+ end
+
+ -- get metrics first, without creating the node (if possible)
+ local word_metrics, combined_metrics, node = measure_node(word, font_for_word, previous_word)
+ local should_create_node = true
+
+ -- check if the line overflows due to this word
+ local overflow = false
+ if settings.width then
+ if combined_metrics then
+ overflow = (line_width - previous_word.metrics.total_width + combined_metrics.width) > settings.width
+ else
+ overflow = (line_width + word_metrics.width) > settings.width
+ end
+
+ -- if we overflow and the word is longer than a full line we
+ -- split the word and add the first part to the current line
+ if overflow and word.text and word_metrics.width > settings.width then
+ local remaining_width = settings.width - line_width
+ local one, two = split_word(word, font_for_word, remaining_width)
+ word_metrics, combined_metrics, node = measure_node(one, font_for_word, previous_word)
+ words[i] = one
+ word = one
+ table.insert(words, i + 1, two)
+ word_count = word_count + 1
+ overflow = false
+ end
+ end
+
+ if overflow and not word.nobr then
+ -- overflow, position the words that fit on the line
+ text_metrics.height = text_metrics.height + (line_height * line_increment_before * settings.line_spacing)
+ position.x = settings.position.x
+ position.y = settings.position.y - text_metrics.height
+ position_words(line_words, line_width, line_height, position, settings)
+
+ -- add the word that didn't fit to the next line instead
+ line_words[#line_words + 1] = word
+
+ -- update text metrics
+ text_metrics.width = math.max(text_metrics.width, line_width)
+ text_metrics.height = text_metrics.height + (line_height * line_increment_after * settings.line_spacing) + paragraph_spacing
+ line_width = word_metrics.total_width
+ line_height = word_metrics.height
+ paragraph_spacing = 0
+ else
+ -- the word fits on the line, add it and update text metrics
+ if combined_metrics then
+ line_width = line_width - previous_word.metrics.total_width + combined_metrics.total_width
+ line_height = math.max(line_height, combined_metrics.height)
+ combine_node(previous_word, word, combined_metrics)
+ should_create_node = false
+ else
+ line_width = line_width + word_metrics.total_width
+ line_height = math.max(line_height, word_metrics.height)
+ line_words[#line_words + 1] = word
+ end
+ text_metrics.width = math.max(text_metrics.width, line_width)
+ end
+
+ if should_create_node then
+ word.node, word.metrics = create_node(word, settings.parent, font_for_word, node, word_metrics)
+ gui.set_pivot(word.node, pivot)
+
+ -- assign layer
+ local layer = get_layer(word, settings.layers)
+ if layer then
+ gui.set_layer(word.node, layer)
+ end
+ else
+ -- queue this word for deletion
+ word.delete = true
+ end
+
+ if word.paragraph_end then
+ local paragraph = word.paragraph
+ if paragraph then
+ paragraph_spacing = math.max(
+ paragraph_spacing,
+ line_height * (paragraph == true and settings.paragraph_spacing or paragraph)
+ )
+ end
+ end
+
+ -- handle line break
+ if word.linebreak then
+ -- position all words on the line up until the linebreak
+ text_metrics.height = text_metrics.height + (line_height * line_increment_before * settings.line_spacing)
+ position.x = settings.position.x
+ position.y = settings.position.y - text_metrics.height
+ position_words(line_words, line_width, line_height, position, settings)
+
+ -- update text metrics
+ text_metrics.height = text_metrics.height + (line_height * line_increment_after * settings.line_spacing) + paragraph_spacing
+ line_height = word_metrics.height
+ line_width = 0
+ paragraph_spacing = 0
+ end
+
+ i = i + 1
+ until i > word_count
+
+ -- position remaining words
+ if #line_words > 0 then
+ text_metrics.height = text_metrics.height + (line_height * line_increment_before * settings.line_spacing)
+ position.x = settings.position.x
+ position.y = settings.position.y - text_metrics.height
+ position_words(line_words, line_width, line_height, position, settings)
+ text_metrics.height = text_metrics.height + (line_height * line_increment_after * settings.line_spacing)
+ end
+
+ -- compact words table
+ local j = 1
+ for i = 1, word_count do
+ local word = words[i]
+ if not word.delete then
+ words[j] = word
+ j = j + 1
+ end
+ end
+ for i = j, word_count do
+ words[i] = nil
+ end
+
+ return words, text_metrics
+end
+
+
+--- Detected click/touch events on words with an anchor tag
+-- These words act as "hyperlinks" and will generate a message when clicked
+-- @param words Words to search for anchor tags
+-- @param action The action table from on_input
+-- @return true if a word was clicked, otherwise false
+function M.on_click(words, action)
+ for i=1,#words do
+ local word = words[i]
+ if word.anchor and gui.pick_node(word.node, action.x, action.y) then
+ if word.tags and word.tags.a then
+ local message = {
+ node_id = gui.get_id(word.node),
+ text = word.text,
+ x = action.x, y = action.y,
+ screen_x = action.screen_x, screen_y = action.screen_y
+ }
+ msg.post("#", word.tags.a, message)
+ return true
+ end
+ end
+ end
+ return false
+end
+
+
+--- Get all words with a specific tag
+-- @param words The words to search (as received from richtext.create)
+-- @param tag The tag to search for. Nil to search for words without a tag
+-- @return Words matching the tag
+function M.tagged(words, tag)
+ local tagged = {}
+ for i=1,#words do
+ local word = words[i]
+ if not tag and not word.tags then
+ tagged[#tagged + 1] = word
+ elseif word.tags and word.tags[tag] then
+ tagged[#tagged + 1] = word
+ end
+ end
+ return tagged
+end
+
+
+--- Truncate a set of words such that only a specific number of characters
+-- and images are visible
+-- @param words List of words to truncate
+-- @param length Maximum number of characters to show
+-- @param options Optional table with truncate options. Available options are: words
+-- @return Last visible word
+function M.truncate(words, length, options)
+ assert(words)
+ assert(length)
+ local last_visible_word = nil
+ if options and options.words then
+ for i=1, #words do
+ local word = words[i]
+ local visible = i <= length
+ if visible then
+ last_visible_word = word
+ end
+ gui.set_enabled(word.node, visible)
+ end
+ else
+ local count = 0
+ for i=1, #words do
+ local word = words[i]
+ local is_text_node = not word.image and not word.spine
+ local word_length = is_text_node and utf8.len(word.text) or 1
+ local visible = count < length
+ if visible then
+ last_visible_word = word
+ end
+ gui.set_enabled(word.node, visible)
+ if count < length and is_text_node then
+ local text = word.text
+ -- partial word?
+ if count + word_length > length then
+ -- remove overflowing characters from word
+ local overflow = (count + word_length) - length
+ text = utf8.sub(word.text, 1, word_length - overflow)
+ end
+ gui.set_text(word.node, text)
+ word.metrics = get_text_metrics(word, word.font, text)
+ end
+ count = count + word_length
+ end
+ end
+ return last_visible_word
+end
+
+
+--- Split a word into it's characters
+-- @param word The word to split
+-- @return The individual characters
+function M.characters(word)
+ assert(word)
+
+ local parent = gui.get_parent(word.node)
+ local font = gui.get_font(word.node)
+ local layer = gui.get_layer(word.node)
+ local pivot = gui.get_pivot(word.node)
+
+ local word_length = utf8.len(word.text)
+
+ -- exit early if word is a single character or empty
+ if word_length <= 1 then
+ local char = deepcopy(word)
+ char.node, char.metrics = create_node(char, parent, font)
+ gui.set_pivot(char.node, pivot)
+ gui.set_position(char.node, gui.get_position(word.node))
+ gui.set_layer(char.node, layer)
+ return { char }
+ end
+
+ -- split word into characters
+ local chars = {}
+ local position = gui.get_position(word.node)
+ local position_x = position.x
+
+ for i = 1, word_length do
+ local char = deepcopy(word)
+ chars[#chars + 1] = char
+ char.text = utf8.sub(word.text, i, i)
+ char.node, char.metrics = create_node(char, parent, font)
+ gui.set_layer(char.node, layer)
+ gui.set_pivot(char.node, pivot)
+
+ local sub_metrics = get_text_metrics(word, font, utf8.sub(word.text, 1, i))
+ position.x = position_x + sub_metrics.width - char.metrics.width
+ gui.set_position(char.node, position)
+ end
+
+ return chars
+end
+
+---Removes the gui nodes created by rich text
+function M.remove(words)
+ assert(words)
+
+ local num = #words
+ for i=1,num do
+ gui.delete_node(words[i].node)
+ end
+end
+
+
+return M
diff --git a/druid/custom/rich_text/rich_text/tags.lua b/druid/custom/rich_text/rich_text/tags.lua
new file mode 100644
index 0000000..ded8372
--- /dev/null
+++ b/druid/custom/rich_text/rich_text/tags.lua
@@ -0,0 +1,127 @@
+-- Source: https://github.com/britzl/defold-richtext version 5.19.0
+-- Author: Britzl
+-- Modified by: Insality
+
+local color = require("druid.custom.rich_text.rich_text.color")
+
+local M = {}
+
+local tags = {}
+
+
+function M.apply(tag, params, settings)
+ local fn = tags[tag]
+ if not fn then
+ return false
+ end
+
+ fn(params, settings)
+ return true
+end
+
+
+function M.register(tag, fn)
+ assert(tag, "You must provide a tag")
+ assert(fn, "You must provide a tag function")
+ tags[tag] = fn
+end
+
+
+M.register("color", function(params, settings)
+ settings.color = color.parse(params)
+end)
+
+
+M.register("shadow", function(params, settings)
+ settings.shadow = color.parse(params)
+end)
+
+
+M.register("outline", function(params, settings)
+ settings.outline = color.parse(params)
+end)
+
+
+M.register("font", function(params, settings)
+ settings.font = params
+end)
+
+
+M.register("size", function(params, settings)
+ settings.size = tonumber(params)
+end)
+
+
+M.register("b", function(params, settings)
+ settings.bold = true
+end)
+
+
+M.register("i", function(params, settings)
+ settings.italic = true
+end)
+
+
+M.register("a", function(params, settings)
+ settings.anchor = true
+end)
+
+
+M.register("br", function(params, settings)
+ settings.linebreak = true
+end)
+
+
+M.register("nobr", function(params, settings)
+ settings.nobr = true
+end)
+
+
+-- Split string at first occurrence of token
+-- If the token doesn't exist the whole string is returned
+-- @param s The string to split
+-- @param token The token to split string on
+-- @return before The string before the token or the whole string if token doesn't exist
+-- @return after The string after the token or nul
+local function split(s, token)
+ if not s then return nil, nil end
+ local before, after = s:match("(.-)" .. token .. "(.*)")
+ before = before or s
+ return before, after
+end
+
+
+M.register("img", function(params, settings)
+ local texture_and_anim, params = split(params, ",")
+ local width, height
+ width, params = split(params, ",")
+ height = split(params, ",")
+ local texture, anim = split(texture_and_anim, ":")
+
+ width = width and tonumber(width)
+ height = height and tonumber(height) or width
+
+ settings.image = {
+ texture = texture,
+ anim = anim,
+ width = width,
+ height = height
+ }
+end)
+
+
+M.register("spine", function(params, settings)
+ local scene, anim = params:match("(.-):(.*)")
+ settings.spine = {
+ scene = scene,
+ anim = anim
+ }
+end)
+
+
+M.register("p", function(params, settings)
+ settings.paragraph = tonumber(params) or true
+end)
+
+
+return M
diff --git a/druid/extended/lang_text.lua b/druid/extended/lang_text.lua
index f78b70b..f30b48a 100755
--- a/druid/extended/lang_text.lua
+++ b/druid/extended/lang_text.lua
@@ -14,7 +14,6 @@
---
-local const = require("druid.const")
local Event = require("druid.event")
local settings = require("druid.system.settings")
local component = require("druid.component")
diff --git a/example/example.collection b/example/example.collection
index 097a84f..4109d63 100644
--- a/example/example.collection
+++ b/example/example.collection
@@ -16,6 +16,8 @@ embedded_instances {
" z: 0.0\n"
" w: 1.0\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
""
position {
@@ -51,6 +53,8 @@ embedded_instances {
" z: 0.0\n"
" w: 1.0\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
""
position {
@@ -139,6 +143,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -202,6 +208,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -265,6 +273,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -328,6 +338,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -391,6 +403,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -454,6 +468,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -517,6 +533,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -580,6 +598,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -643,6 +663,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -706,6 +728,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -769,6 +793,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -832,6 +858,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -895,6 +923,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -958,6 +988,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1021,6 +1053,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1084,6 +1118,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1147,6 +1183,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1210,6 +1248,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1273,6 +1313,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1336,6 +1378,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1399,6 +1443,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1462,6 +1508,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1525,6 +1573,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1588,6 +1638,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1651,6 +1703,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1714,6 +1768,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1777,6 +1833,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1840,6 +1898,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1903,6 +1963,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -1966,6 +2028,8 @@ embedded_instances {
" value: \"true\"\n"
" type: PROPERTY_TYPE_BOOLEAN\n"
" }\n"
+ " property_decls {\n"
+ " }\n"
"}\n"
"embedded_components {\n"
" id: \"collectionfactory\"\n"
@@ -2003,3 +2067,68 @@ embedded_instances {
z: 1.0
}
}
+embedded_instances {
+ id: "custom_rich_text"
+ data: "components {\n"
+ " id: \"screen_factory\"\n"
+ " component: \"/monarch/screen_factory.script\"\n"
+ " position {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " }\n"
+ " rotation {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " w: 1.0\n"
+ " }\n"
+ " properties {\n"
+ " id: \"screen_id\"\n"
+ " value: \"custom_rich_text\"\n"
+ " type: PROPERTY_TYPE_HASH\n"
+ " }\n"
+ " properties {\n"
+ " id: \"popup\"\n"
+ " value: \"true\"\n"
+ " type: PROPERTY_TYPE_BOOLEAN\n"
+ " }\n"
+ " property_decls {\n"
+ " }\n"
+ "}\n"
+ "embedded_components {\n"
+ " id: \"collectionfactory\"\n"
+ " type: \"collectionfactory\"\n"
+ " data: \"prototype: \\\"/example/examples/custom/rich_text/rich_text.collection\\\"\\n"
+ "load_dynamically: false\\n"
+ "\"\n"
+ " position {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " }\n"
+ " rotation {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " w: 1.0\n"
+ " }\n"
+ "}\n"
+ ""
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale3 {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ }
+}
diff --git a/example/example.gui_script b/example/example.gui_script
index e8f0709..ea26bb7 100644
--- a/example/example.gui_script
+++ b/example/example.gui_script
@@ -168,6 +168,7 @@ local function init_lobby(self)
self.lobby_grid:add(get_title(self, "Custom components"))
self.lobby_grid:add(get_button(self, "Rich Input", "custom_rich_input", "/custom/rich_input/rich_input.gui_script"))
self.lobby_grid:add(get_button(self, "Pin Knob", "custom_pin_knob", "/custom/pin_knob/pin_knob.gui_script"))
+ self.lobby_grid:add(get_button(self, "Rich Text", "custom_rich_text", "/custom/rich_text/rich_text.gui_script"))
self.lobby_grid:add(get_title(self, "System"))
self.lobby_grid:add(get_button_disabled(self, "Styles"))
diff --git a/example/examples/custom/rich_text/rich_text.collection b/example/examples/custom/rich_text/rich_text.collection
new file mode 100644
index 0000000..1a3756e
--- /dev/null
+++ b/example/examples/custom/rich_text/rich_text.collection
@@ -0,0 +1,39 @@
+name: "rich_text"
+scale_along_z: 0
+embedded_instances {
+ id: "go"
+ data: "components {\n"
+ " id: \"rich_text\"\n"
+ " component: \"/example/examples/custom/rich_text/rich_text.gui\"\n"
+ " position {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " }\n"
+ " rotation {\n"
+ " x: 0.0\n"
+ " y: 0.0\n"
+ " z: 0.0\n"
+ " w: 1.0\n"
+ " }\n"
+ " property_decls {\n"
+ " }\n"
+ "}\n"
+ ""
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale3 {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ }
+}
diff --git a/example/examples/custom/rich_text/rich_text.gui b/example/examples/custom/rich_text/rich_text.gui
new file mode 100644
index 0000000..3c2643b
--- /dev/null
+++ b/example/examples/custom/rich_text/rich_text.gui
@@ -0,0 +1,308 @@
+script: "/example/examples/custom/rich_text/rich_text.gui_script"
+fonts {
+ name: "game"
+ font: "/example/assets/fonts/game.font"
+}
+textures {
+ name: "kenney"
+ texture: "/example/assets/images/kenney.atlas"
+}
+background_color {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+}
+nodes {
+ position {
+ x: 300.0
+ y: 415.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 600.0
+ y: 830.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: "kenney/empty"
+ id: "root"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ layer: ""
+ inherit_alpha: true
+ slice9 {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+ }
+ clipping_mode: CLIPPING_MODE_NONE
+ clipping_visible: true
+ clipping_inverted: false
+ alpha: 1.0
+ template_node_child: false
+ size_mode: SIZE_MODE_MANUAL
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 200.0
+ y: 100.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_TEMPLATE
+ id: "rich_text"
+ parent: "root"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ template: "/druid/custom/rich_text/rich_text.gui"
+ template_node_child: false
+ custom_type: 0
+ enabled: true
+}
+nodes {
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 300.0
+ y: 200.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 0.4
+ y: 0.6
+ z: 0.6
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: ""
+ id: "rich_text/root"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_E
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "rich_text"
+ layer: ""
+ inherit_alpha: true
+ slice9 {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+ }
+ clipping_mode: CLIPPING_MODE_NONE
+ clipping_visible: true
+ clipping_inverted: false
+ alpha: 1.0
+ overridden_fields: 5
+ overridden_fields: 14
+ overridden_fields: 46
+ template_node_child: true
+ size_mode: SIZE_MODE_MANUAL
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 200.0
+ y: 100.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_TEXT
+ blend_mode: BLEND_MODE_ALPHA
+ text: "Text"
+ font: "game"
+ id: "rich_text/text_prefab"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ outline {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ shadow {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ adjust_mode: ADJUST_MODE_FIT
+ line_break: false
+ parent: "rich_text/root"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ outline_alpha: 1.0
+ shadow_alpha: 0.0
+ template_node_child: true
+ text_leading: 1.0
+ text_tracking: 0.0
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 77.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ rotation {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ scale {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 36.0
+ y: 36.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 1.0
+ z: 1.0
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: "kenney/slider_move"
+ id: "rich_text/icon_prefab"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "rich_text/root"
+ layer: ""
+ inherit_alpha: true
+ slice9 {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 0.0
+ }
+ clipping_mode: CLIPPING_MODE_NONE
+ clipping_visible: true
+ clipping_inverted: false
+ alpha: 1.0
+ template_node_child: true
+ size_mode: SIZE_MODE_AUTO
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+layers {
+ name: "image"
+}
+layers {
+ name: "text"
+}
+material: "/builtins/materials/gui.material"
+adjust_reference: ADJUST_REFERENCE_PARENT
+max_nodes: 512