From fae7e4afa48c5682383c17777b077c1aea7bfd2f Mon Sep 17 00:00:00 2001 From: Insality Date: Fri, 4 Nov 2022 20:09:13 +0200 Subject: [PATCH] Initial commit with rich text --- druid/custom/rich_text/rich_text.gui | 199 +++++ druid/custom/rich_text/rich_text.lua | 85 +++ druid/custom/rich_text/rich_text/color.lua | 70 ++ druid/custom/rich_text/rich_text/parse.lua | 203 +++++ druid/custom/rich_text/rich_text/richtext.lua | 700 ++++++++++++++++++ druid/custom/rich_text/rich_text/tags.lua | 127 ++++ druid/extended/lang_text.lua | 1 - example/example.collection | 129 ++++ example/example.gui_script | 1 + .../custom/rich_text/rich_text.collection | 39 + .../examples/custom/rich_text/rich_text.gui | 308 ++++++++ 11 files changed, 1861 insertions(+), 1 deletion(-) create mode 100644 druid/custom/rich_text/rich_text.gui create mode 100644 druid/custom/rich_text/rich_text.lua create mode 100644 druid/custom/rich_text/rich_text/color.lua create mode 100755 druid/custom/rich_text/rich_text/parse.lua create mode 100755 druid/custom/rich_text/rich_text/richtext.lua create mode 100644 druid/custom/rich_text/rich_text/tags.lua create mode 100644 example/examples/custom/rich_text/rich_text.collection create mode 100644 example/examples/custom/rich_text/rich_text.gui 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("(.-)()(.*)") + + -- 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