diff --git a/druid/custom/rich_text/rich_text.gui b/druid/custom/rich_text/rich_text.gui
index dc4c340..68c170b 100644
--- a/druid/custom/rich_text/rich_text.gui
+++ b/druid/custom/rich_text/rich_text.gui
@@ -4,7 +4,7 @@ fonts {
font: "/example/assets/fonts/game.font"
}
textures {
- name: "kenney"
+ name: "items"
texture: "/example/assets/images/kenney.atlas"
}
background_color {
@@ -33,8 +33,8 @@ nodes {
w: 1.0
}
size {
- x: 300.0
- y: 200.0
+ x: 400.0
+ y: 100.0
z: 0.0
w: 1.0
}
@@ -90,7 +90,7 @@ nodes {
w: 1.0
}
size {
- x: 200.0
+ x: 400.0
y: 100.0
z: 0.0
w: 1.0
@@ -103,7 +103,7 @@ nodes {
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
- text: "Text"
+ text: "Rich text"
font: "game"
id: "text_prefab"
xanchor: XANCHOR_NONE
@@ -116,9 +116,9 @@ nodes {
w: 1.0
}
shadow {
- x: 1.0
- y: 1.0
- z: 1.0
+ x: 0.0
+ y: 0.0
+ z: 0.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
@@ -127,7 +127,7 @@ nodes {
layer: ""
inherit_alpha: true
alpha: 1.0
- outline_alpha: 1.0
+ outline_alpha: 0.0
shadow_alpha: 0.0
template_node_child: false
text_leading: 1.0
@@ -138,7 +138,7 @@ nodes {
}
nodes {
position {
- x: 77.0
+ x: 110.0
y: 0.0
z: 0.0
w: 1.0
@@ -156,8 +156,8 @@ nodes {
w: 1.0
}
size {
- x: 36.0
- y: 36.0
+ x: 21.0
+ y: 20.0
z: 0.0
w: 1.0
}
@@ -169,7 +169,7 @@ nodes {
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
- texture: "kenney/slider_move"
+ texture: "items/checkmark"
id: "icon_prefab"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
diff --git a/druid/custom/rich_text/rich_text.lua b/druid/custom/rich_text/rich_text.lua
index 5756cd4..91a01d6 100644
--- a/druid/custom/rich_text/rich_text.lua
+++ b/druid/custom/rich_text/rich_text.lua
@@ -1,6 +1,7 @@
local component = require("druid.component")
local rich_text = require("druid.custom.rich_text.rich_text.richtext")
+---@class druid.rich_text
local RichText = component.create("rich_text")
local SCHEME = {
@@ -10,19 +11,6 @@ local SCHEME = {
}
-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)
@@ -36,106 +24,66 @@ function RichText:init(template, nodes)
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
+---@return rich_text.word[], rich_text.lines_metrics
function RichText:set_text(text)
- self:_clean_words()
- local is_already_adjusted = self._settings.adjust_scale ~= 1
+ self:clean()
- -- Make text singleline if prefab without line break
- local is_multiline = gui.get_line_break(self.text_prefab)
- if not is_multiline then
- text = string.format("%s", text)
- end
-
- local words, metrics = rich_text.create(text, self._text_font, self._settings)
+ local words, settings, line_metrics = rich_text.create(text, self._settings)
+ line_metrics = rich_text.adjust_to_area(words, settings, line_metrics)
self._words = words
- self._metrics = metrics
+ self._line_metrics = line_metrics
- for _, word in ipairs(words) do
- print(word.text)
- end
-
- if not is_multiline then
- local scale_koef = self.root_size.x / self._metrics.width
- self._settings.adjust_scale = math.min(scale_koef, 1)
- else
- local scale_koef = math.sqrt(self.root_size.y / self._metrics.height)
- if self._metrics.width * scale_koef > self.root_size.x then
- scale_koef = math.sqrt(self.root_size.x / self._metrics.width)
- end
- self._settings.adjust_scale = math.min(scale_koef, 1)
- end
-
- if not is_already_adjusted and self._settings.adjust_scale < 1 then
- print("Again set text with adjusted scale", self._settings.adjust_scale)
- self:set_text(text)
- return
- end
-
- -- Align vertically, different behaviour from rich text
- self:_align_vertically()
-
- pprint(self._metrics)
+ return words, line_metrics
end
function RichText:on_remove()
- self:_clean_words()
+ self:clean()
+end
+
+
+function RichText:tagged(tag)
+ if not self._words then
+ return
+ end
+
+ return rich_text.tagged(self._words, tag)
+end
+
+
+---@return druid.rich_text_word[]
+function RichText:get_words()
+ return self._words
end
function RichText:_get_settings()
- local anchor = gui.get_pivot(self.root)
- local align = ALIGN_MAP[anchor][1]
- local valign = ALIGN_MAP[anchor][2]
-
return {
width = self.root_size.x,
parent = self.root,
- color = gui.get_color(self.text_prefab),
+ text_prefab = self.text_prefab,
+ node_prefab = self.icon_prefab,
shadow = gui.get_shadow(self.text_prefab),
outline = gui.get_outline(self.text_prefab),
- text_scale = gui.get_scale(self.text_prefab),
- default_texture = gui.get_texture(self.icon_prefab),
- default_anim = gui.get_flipbook(self.icon_prefab),
- combine_words = true,
- adjust_scale = 1,
- align = align,
- valign = valign,
+ size = gui.get_scale(self.text_prefab).x,
+ image_scale = gui.get_scale(self.icon_prefab),
+ default_animation = gui.get_flipbook(self.icon_prefab),
}
end
-function RichText:_clean_words()
+function RichText:clean()
if not self._words then
return
end
rich_text.remove(self._words)
self._words = nil
- self._metrics = nil
-end
-
-
-function RichText:_align_vertically()
- local text_height = self._metrics.height
- local offset = 0
- if self._settings.valign == rich_text.VALIGN_MIDDLE then
- offset = text_height * 0.5
- end
- if self._settings.valign == rich_text.VALIGN_BOTTOM then
- offset = text_height
- end
-
- for _, word in ipairs(self._words) do
- word.position.y = word.position.y + offset
- gui.set_position(word.node, word.position)
- end
end
diff --git a/druid/custom/rich_text/rich_text/color.lua b/druid/custom/rich_text/rich_text/color.lua
index f4e4736..4f1178a 100644
--- a/druid/custom/rich_text/rich_text/color.lua
+++ b/druid/custom/rich_text/rich_text/color.lua
@@ -36,30 +36,7 @@ function M.add(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"),
-}
+M.COLORS = {}
function M.parse(c)
diff --git a/druid/custom/rich_text/rich_text/parse.lua b/druid/custom/rich_text/rich_text/parse.lua
index a68d581..278f79f 100755
--- a/druid/custom/rich_text/rich_text/parse.lua
+++ b/druid/custom/rich_text/rich_text/parse.lua
@@ -22,10 +22,11 @@ local function add_word(text, settings, words)
-- handle HTML entities
text = text:gsub("<", "<"):gsub(">", ">"):gsub(" ", " ")
- local data = { text = text }
+ local data = { text = text, source_text = text }
for k,v in pairs(settings) do
data[k] = v
end
+
words[#words + 1] = data
end
@@ -35,6 +36,7 @@ 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)
@@ -45,8 +47,10 @@ local function split_line(line, settings, words)
end
local first = words[wi + 1]
first.text = ws_start .. first.text
+ first.source_text = first.text
local last = words[#words]
last.text = utf8.sub(last.text, 1, utf8.len(last.text) - 1) .. ws_end
+ last.source_text = last.text
end
end
@@ -91,12 +95,12 @@ end
-- Merge one tag into another
local function merge_tags(dst, src)
- for k,v in pairs(src) do
+ 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
+ for tag, params in pairs(src.tags or {}) do
dst.tags[tag] = (params == "") and true or params
end
end
@@ -113,11 +117,12 @@ function M.parse(text, 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
+ for _, open_tag in ipairs(open_tags) do
merge_tags(word_settings, open_tag)
end
@@ -149,54 +154,33 @@ function M.parse(text, default_settings)
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
+ -- open tag - parse and add it
+ local tag_settings = parse_tag(name, params)
+ open_tags[#open_tags + 1] = tag_settings
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 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
-- 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("<.->", ""))
+ return utf8.len(text:gsub("", " "):gsub("<.->", ""))
end
diff --git a/druid/custom/rich_text/rich_text/richtext.lua b/druid/custom/rich_text/rich_text/richtext.lua
index 89058bf..cdd9a1b 100755
--- a/druid/custom/rich_text/rich_text/richtext.lua
+++ b/druid/custom/rich_text/rich_text/richtext.lua
@@ -2,40 +2,64 @@
-- Author: Britzl
-- Modified by: Insality
+local helper = require("druid.helper")
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.ADJUST_STEPS = 10
+M.ADJUST_SCALE_DELTA = 0.02
-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
+---@class rich_text.metrics
+---@field width number
+---@field height number
+---@field offset_x number|nil
+---@field offset_y number|nil
+---@field node_size vector3|nil @For images only
-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
+---@class rich_text.lines_metrics
+---@field text_width number
+---@field text_height number
+---@field lines table
+
+---@class rich_text.word
+---@field node Node
+---@field relative_scale number
+---@field color vector4
+---@field position vector3
+---@field offset vector3
+---@field scale vector3
+---@field size vector3
+---@field metrics rich_text.metrics
+---@field pivot Pivot
+---@field text string
+---@field shadow vector4
+---@field outline vector4
+---@field font string
+---@field image rich_text.word.image
+---@field default_animation string
+---@field anchor number
+---@field br boolean
+---@field nobr boolean
+
+---@class rich_text.settings
+---@field parent Node
+---@field size number
+---@field fonts table
+---@field color vector4
+---@field shadow vector4
+---@field outline vector4
+---@field position vector3
+---@field line_spacing number
+---@field image_pixel_grid_snap boolean
+---@field combine_words boolean
+---@field default_animation string
+---@field node_prefab Node
+---@field text_prefab Node
local function deepcopy(orig)
@@ -53,38 +77,9 @@ local function deepcopy(orig)
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)]
+-- Trim spaces on string start
+local function ltrim(text)
+ return text:match('^%s*(.*)')
end
@@ -120,50 +115,6 @@ local function compare_words(one, two)
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
- word.position = vmath.vector3(position)
- 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)
@@ -174,9 +125,9 @@ function M.length(text)
return parser.length(text)
else
local count = 0
- for i=1,#text do
+ for i = 1, #text do
local word = text[i]
- local is_text_node = not word.image and not word.spine
+ local is_text_node = not word.image
count = count + (is_text_node and utf8.len(word.text) or 1)
end
return count
@@ -184,388 +135,459 @@ function M.length(text)
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
- gui.set_id(node, new_id("box"))
- gui.set_size_mode(node, gui.SIZE_MODE_AUTO)
- gui.set_texture(node, word.image.texture or word.default_texture)
- gui.play_flipbook(node, hash(word.image.anim or word.default_anim))
-
- if image_width then
- local image_size = gui.get_size(node)
- gui.set_size_mode(node, gui.SIZE_MODE_MANUAL)
- size_vector.x = image_width
- -- Use height or autoscale to keep aspect ratio
- size_vector.y = word_image.height or ((image_size.y / image_size.x) * image_width)
- size_vector.z = 0
- gui.set_size(node, size_vector)
- end
-
- local word_size = word.size * word.adjust_scale
- size_vector.x = word_size
- size_vector.y = word_size
- size_vector.z = word_size
- word.scale = vmath.vector3(size_vector)
- gui.set_scale(node, word.scale)
-
- -- get metrics of node based on image size
- local size = gui.get_size(node)
- local metrics = {}
- metrics.total_width = size.x * word.size * word.adjust_scale
- metrics.width = size.x * word.size * word.adjust_scale
- metrics.height = size.y * word.size * word.adjust_scale
- 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)
- word.scale = vmath.vector3(word.size)
- gui.set_scale(node, word.scale)
- 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
+---@param word rich_text.word
+---@param previous_word rich_text.word|nil
+---@param settings rich_text.settings
+---@return rich_text.metrics
+local function get_text_metrics(word, previous_word, settings)
+ local text = word.text
+ local font_resource = gui.get_font_resource(word.font)
+ ---@type rich_text.metrics
local metrics
+ local word_scale_x = word.relative_scale * settings.text_scale.x * settings.adjust_scale
+ local word_scale_y = word.relative_scale * settings.text_scale.y * settings.adjust_scale
+
if utf8.len(text) == 0 then
- metrics = gui.get_text_metrics(font, "|")
+ metrics = resource.get_text_metrics(font_resource, "|")
metrics.width = 0
- metrics.total_width = 0
- metrics.height = metrics.height * word.size * word.text_scale.y * word.adjust_scale
+ metrics.height = metrics.height * word_scale_y
else
- metrics = gui.get_text_metrics(font, text)
- metrics.width = metrics.width * word.size * word.text_scale.x * word.adjust_scale
- metrics.height = metrics.height * word.size * word.text_scale.y * word.adjust_scale
- metrics.total_width = metrics.width
+ metrics = resource.get_text_metrics(font_resource, text)
+ metrics.width = metrics.width * word_scale_x
+ metrics.height = metrics.height * word_scale_y
+
+ if previous_word and not previous_word.image then
+ local previous_word_metrics = resource.get_text_metrics(font_resource, previous_word.text)
+ local union_metrics = resource.get_text_metrics(font_resource, previous_word.text .. text)
+
+ local without_previous_width = metrics.width
+ metrics.width = (union_metrics.width - previous_word_metrics.width) * word_scale_x
+ -- Since the several characters can be ajusted to fit the space between the previous word and this word
+ -- For example: chars: [.,?!]
+ metrics.offset_x = metrics.width - without_previous_width
+ end
end
+
+ metrics.offset_x = metrics.offset_x or 0
+ metrics.offset_y = metrics.offset_y or 0
+
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
- word.scale = word.text_scale * word.size * word.adjust_scale
- gui.set_scale(node, word.scale)
+---@param word rich_text.word
+---@param settings rich_text.settings
+---@return rich_text.metrics
+local function get_image_metrics(word, settings)
+ local node_prefab = settings.node_prefab
+ gui.play_flipbook(node_prefab, word.image.anim)
+ local node_size = gui.get_size(node_prefab)
+ local aspect = node_size.x / node_size.y
+ node_size.x = word.image.width or node_size.x
+ node_size.y = word.image.height or (node_size.x / aspect)
- 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
+ return {
+ width = node_size.x * word.relative_scale * settings.node_scale.x * settings.adjust_scale,
+ height = node_size.y * word.relative_scale * settings.node_scale.y * settings.adjust_scale,
+ node_size = node_size,
+ }
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
+---@param word rich_text.word
+---@param settings rich_text.settings
+---@param previous_word rich_text.word|nil
+---@return rich_text.metrics
+local function measure_node(word, settings, previous_word)
+ local metrics = word.image and get_image_metrics(word, settings) or get_text_metrics(word, previous_word, settings)
+ return metrics
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)
+function M.create(text, settings)
assert(text, "You must provide a text")
- assert(font, "You must provide a font")
+
+ ---@class rich_text.settings
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.adjust_scale = 1
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
- settings.text_scale = settings.text_scale or V3_ONE
- settings.default_texture = settings.default_texture or nil
- settings.default_anim = settings.default_anim or nil
- 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
+ settings.default_animation = settings.default_animation or nil
+ settings.node_prefab = settings.node_prefab
+ settings.text_prefab = settings.text_prefab
+ settings.text_leading = gui.get_leading(settings.text_prefab)
+ settings.text_scale = gui.get_scale(settings.text_prefab)
+ settings.node_scale = gui.get_scale(settings.node_prefab)
+ settings.is_multiline = gui.get_line_break(settings.text_prefab)
+ settings.parent = settings.parent
-- default settings for a word
-- will be assigned to each word unless tags override the values
- local word_settings = {
- color = settings.color,
+ local font = gui.get_font(settings.text_prefab)
+ local word_params = {
+ node = nil, -- Autofill on node creation
+ relative_scale = 1,
+ color = nil,
+ position = nil, -- Autofill later
+ scale = nil, -- Autofill later
+ size = nil, -- Autofill later
+ pivot = nil, -- Autofill later
+ offset = nil, -- Autofill later
+ metrics = {},
+ -- text params
+ source_text = nil,
+ text = nil, -- Autofill later in parse.lua
+ text_color = gui.get_color(settings.text_prefab),
shadow = settings.shadow,
outline = settings.outline,
font = font,
- size = settings.size,
- -- Autofill properties
- text_scale = settings.text_scale,
- adjust_scale = settings.adjust_scale, -- scale for content adjust to fit into root size
- position = nil,
- scale = nil,
- default_texture = nil, -- for image only
- default_anim = nil, -- for image only
+ -- Image params
+ ---@type rich_text.word.image
+ image = nil,
+ image_color = gui.get_color(settings.node_prefab),
+ default_animation = nil,
+ -- Tags
+ anchor = nil,
+ br = nil,
+ nobr = nil,
}
- 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 parsed_words = parser.parse(text, word_params)
+ local lines = M._split_on_lines(parsed_words, settings)
+ local lines_metrics = M._position_lines(lines, settings)
+ M._update_nodes(lines, settings)
+
+ local words = {}
+ for index = 1, #lines do
+ for jindex = 1, #lines[index] do
+ table.insert(words, lines[index][jindex])
+ end
+ end
+
+ return words, settings, lines_metrics
+end
+
+
+---@param words rich_text.word
+---@param metrics rich_text.metrics
+---@param settings rich_text.settings
+function M._fill_properties(word, metrics, settings)
+ word.metrics = metrics
+ word.position = vmath.vector3(0)
+
+ if word.image then
+ word.scale = gui.get_scale(settings.node_prefab) * word.relative_scale * settings.adjust_scale
+ word.pivot = gui.get_pivot(settings.node_prefab)
+ word.size = metrics.node_size
+ word.offset = vmath.vector3(0, 0, 0)
+ if word.image.width then
+ word.size.y = word.image.height or (word.size.y * word.image.width / word.size.x)
+ word.size.x = word.image.width
+ end
+ else
+ word.scale = gui.get_scale(settings.text_prefab) * word.relative_scale * settings.adjust_scale
+ word.pivot = gui.get_pivot(settings.text_prefab)
+ word.size = vmath.vector3(metrics.width, metrics.height, 0)
+ word.offset = vmath.vector3(metrics.offset_x, metrics.offset_y, 0)
+ end
+end
+
+
+---@param words rich_text.word[]
+---@param settings rich_text.settings
+---@return rich_text.word[][]
+function M._split_on_lines(words, settings)
local i = 1
+ local lines = {}
+ local current_line = {}
+ local word_count = #words
+ local current_line_width = 0
+ local current_line_height = 0
+
repeat
local word = words[i]
if word.image then
- word.default_texture = settings.default_texture
- word.default_anim = settings.default_anim
- 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)
+ word.default_animation = settings.default_animation
end
- -- get font to use based on word tags
- local font_for_word = get_font(word, settings.fonts)
+ -- Reset texts to start measure again
+ word.text = word.source_text
-- get the previous word, so we can combine
- local previous_word
+ local previous_word = current_line[#current_line]
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
+ local word_metrics = measure_node(word, settings)
+
+ local next_words_width = word_metrics.width
+ -- Collect width of nobr words from current to next words with nobr
+ if word.nobr then
+ for index = i + 1, word_count do
+ if words[index].nobr then
+ local next_word_measure = measure_node(words[index], settings, words[index-1])
+ next_words_width = next_words_width + next_word_measure.width
+ else
+ break
+ end
+ end
+ end
+ local overflow = (current_line_width + next_words_width) > settings.width
+ local is_new_line = (overflow or word.br) and settings.is_multiline
+
+ -- We recalculate metrics with previous_word if it follow for word on current line
+ if not is_new_line and previous_word then
+ word_metrics = measure_node(word, settings, previous_word)
+ end
+
+ -- Trim first word of the line
+ if is_new_line or not previous_word then
+ word.text = ltrim(word.text)
+ word_metrics = measure_node(word, settings, nil)
+ end
+ M._fill_properties(word, word_metrics, settings)
-- 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
+ if not is_new_line then
-- 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
+ current_line_width = current_line_width + word.metrics.width
+ current_line_height = math.max(current_line_height, word.metrics.height)
+ current_line[#current_line + 1] = word
else
- -- queue this word for deletion
- word.delete = true
- end
+ -- overflow, position the words that fit on the line
+ lines[#lines + 1] = current_line
- 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
+ word.text = ltrim(word.text)
+ current_line = { word }
+ current_line_height = word.metrics.height
+ current_line_width = word.metrics.width
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)
+ if #current_line > 0 then
+ lines[#lines + 1] = current_line
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
+ return lines
+end
+
+
+---@param lines rich_text.word[][]
+---@param settings rich_text.settings
+---@return rich_text.lines_metrics
+function M._position_lines(lines, settings)
+ local lines_metrics = M._get_lines_metrics(lines, settings)
+ -- current x-y is left top point of text spawn
+
+ local parent_size = gui.get_size(settings.parent)
+ local pivot = helper.get_pivot_offset(gui.get_pivot(settings.parent))
+ local offset_y = (parent_size.y - lines_metrics.text_height) * (pivot.y - 0.5) - (parent_size.y * (pivot.y - 0.5))
+
+ local current_y = offset_y
+ for line_index = 1, #lines do
+ local line = lines[line_index]
+ local line_metrics = lines_metrics.lines[line_index]
+ local current_x = (parent_size.x - line_metrics.width) * (pivot.x + 0.5) - (parent_size.x * (pivot.x + 0.5))
+ local max_height = 0
+ for word_index = 1, #line do
+ local word = line[word_index]
+ local pivot_offset = helper.get_pivot_offset(word.pivot)
+ local word_width = word.metrics.width
+ word.position.x = current_x + word_width * (pivot_offset.x + 0.5) + word.offset.x
+ word.position.y = current_y + word.metrics.height * (pivot_offset.y - 0.5) + word.offset.y
+
+ -- Align item on text line depends on anchor
+ word.position.y = word.position.y - (word.metrics.height - line_metrics.height) * (pivot_offset.y - 0.5)
+
+ current_x = current_x + word_width
+
+ -- TODO: check if we need to calculate images
+ if not word.image then
+ max_height = math.max(max_height, word.metrics.height)
+ end
+
+ if settings.image_pixel_grid_snap and word.image then
+ word.position.x = helper.round(word.position.x)
+ word.position.y = helper.round(word.position.y)
+ end
+ end
+
+ current_y = current_y - line_metrics.height
+ end
+
+ return lines_metrics
+end
+
+
+---@param lines rich_text.word[][]
+---@param settings rich_text.settings
+---@return rich_text.lines_metrics
+function M._get_lines_metrics(lines, settings)
+ local metrics = {}
+ local text_width = 0
+ local text_height = 0
+ for line_index = 1, #lines do
+ local line = lines[line_index]
+ local width = 0
+ local height = 0
+ for word_index = 1, #line do
+ local word = line[word_index]
+ local word_width = word.metrics.width
+ width = width + word_width
+ -- TODO: Here too
+ if not word.image then
+ height = math.max(height, word.metrics.height)
+ end
+ end
+
+ if line_index > 1 then
+ height = height * settings.text_leading
+ end
+
+ text_width = math.max(text_width, width)
+ text_height = text_height + height
+
+ metrics[#metrics + 1] = {
+ width = width,
+ height = height,
+ }
+ end
+
+ ---@type rich_text.lines_metrics
+ local lines_metrics = {
+ text_width = text_width,
+ text_height = text_height,
+ lines = metrics,
+ }
+
+ return lines_metrics
+end
+
+
+---@param lines rich_text.word[][]
+---@param settings rich_text.settings
+function M._update_nodes(lines, settings)
+ for line_index = 1, #lines do
+ local line = lines[line_index]
+ for word_index = 1, #line do
+ local word = line[word_index]
+ local node
+ if word.image then
+ node = word.node or gui.clone(settings.node_prefab)
+ gui.set_size_mode(node, gui.SIZE_MODE_MANUAL)
+ gui.play_flipbook(node, hash(word.image.anim or word.default_animation))
+ gui.set_color(node, word.color or word.image_color)
+ else
+ node = word.node or gui.clone(settings.text_prefab)
+ gui.set_outline(node, word.outline)
+ gui.set_shadow(node, word.shadow)
+ gui.set_text(node, word.text)
+ gui.set_color(node, word.color or word.text_color)
+ end
+ word.node = node
+ gui.set_enabled(node, true)
+ gui.set_parent(node, settings.parent)
+ gui.set_size(node, word.size)
+ gui.set_scale(node, word.scale)
+ gui.set_position(node, word.position)
end
end
- for i = j, word_count do
- words[i] = nil
+end
+
+
+---@param words rich_text.word[]
+---@param settings rich_text.settings
+---@param scale number
+---@return rich_text.lines_metrics
+function M.set_text_scale(words, settings, scale)
+ settings.adjust_scale = scale
+
+ local lines = M._split_on_lines(words, settings)
+ local line_metrics = M._position_lines(lines, settings)
+ M._update_nodes(lines, settings)
+
+ return line_metrics
+end
+
+
+---@param words rich_text.word[]
+---@param settings rich_text.settings
+---@param lines_metrics rich_text.lines_metrics
+function M.adjust_to_area(words, settings, lines_metrics)
+ local last_line_metrics = lines_metrics
+
+ local area_size = gui.get_size(settings.parent)
+ if not settings.is_multiline then
+ if lines_metrics.text_width > area_size.x then
+ last_line_metrics = M.set_text_scale(words, settings, area_size.x / lines_metrics.text_width)
+ end
+ else
+ -- Multiline adjusting is very tricky stuff...
+ -- It's do a lot of calculations, beware!
+ if lines_metrics.text_width > area_size.x or lines_metrics.text_height > area_size.y then
+ local scale_koef = math.sqrt(area_size.y / lines_metrics.text_height)
+ if lines_metrics.text_width * scale_koef > area_size.x then
+ scale_koef = math.sqrt(area_size.x / lines_metrics.text_width)
+ end
+ local adjust_scale = math.min(scale_koef, 1)
+
+ local lines = M.apply_scale_without_update(words, settings, adjust_scale)
+ local is_fit = M.is_fit_info_area(lines, settings)
+ local step = is_fit and M.ADJUST_SCALE_DELTA or -M.ADJUST_SCALE_DELTA
+
+ for i = 1, M.ADJUST_STEPS do
+ -- Grow down to check if we fit
+ if step < 0 and is_fit then
+ last_line_metrics = M.set_text_scale(words, settings, adjust_scale)
+ break
+ end
+ -- Grow up to check if we still fit
+ if step > 0 and not is_fit then
+ last_line_metrics = M.set_text_scale(words, settings, adjust_scale - step)
+ break
+ end
+
+ adjust_scale = adjust_scale + step
+ local lines = M.apply_scale_without_update(words, settings, adjust_scale)
+ is_fit = M.is_fit_info_area(lines, settings)
+
+ if i == M.ADJUST_STEPS then
+ last_line_metrics = M.set_text_scale(words, settings, adjust_scale)
+ end
+ end
+ end
end
- return words, text_metrics
+ return last_line_metrics
+end
+
+
+---@return boolean @If we fit into area size
+function M.apply_scale_without_update(words, settings, scale)
+ settings.adjust_scale = scale
+ return M._split_on_lines(words, settings)
+end
+
+
+---@param lines rich_text.word[][]
+---@param settings rich_text.settings
+function M.is_fit_info_area(lines, settings)
+ local lines_metrics = M._get_lines_metrics(lines, settings)
+ local area_size = gui.get_size(settings.parent)
+ return lines_metrics.text_width <= area_size.x and lines_metrics.text_height <= area_size.y
end
@@ -575,7 +597,7 @@ end
-- @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
+ 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
@@ -590,6 +612,7 @@ function M.on_click(words, action)
end
end
end
+
return false
end
@@ -600,7 +623,7 @@ end
-- @return Words matching the tag
function M.tagged(words, tag)
local tagged = {}
- for i=1,#words do
+ for i = 1, #words do
local word = words[i]
if not tag and not word.tags then
tagged[#tagged + 1] = word
@@ -612,54 +635,6 @@ function M.tagged(words, tag)
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
@@ -705,12 +680,12 @@ function M.characters(word)
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
+ for i = 1, #words do
gui.delete_node(words[i].node)
end
end
diff --git a/druid/custom/rich_text/rich_text/tags.lua b/druid/custom/rich_text/rich_text/tags.lua
index eba23a0..64ce43b 100644
--- a/druid/custom/rich_text/rich_text/tags.lua
+++ b/druid/custom/rich_text/rich_text/tags.lua
@@ -27,6 +27,20 @@ function M.register(tag, fn)
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("color", function(params, settings)
settings.color = color.parse(params)
end)
@@ -48,17 +62,7 @@ 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
+ settings.relative_scale = tonumber(params)
end)
@@ -68,7 +72,7 @@ end)
M.register("br", function(params, settings)
- settings.linebreak = true
+ settings.br = true
end)
@@ -77,20 +81,6 @@ M.register("nobr", function(params, settings)
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
@@ -105,6 +95,11 @@ M.register("img", function(params, settings)
width = width and tonumber(width)
height = height and tonumber(height)
+ ---@class rich_text.word.image
+ ---@field texture string
+ ---@field anim string
+ ---@field width number
+ ---@field height number
settings.image = {
texture = texture,
anim = anim,
@@ -114,18 +109,4 @@ M.register("img", function(params, settings)
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/example/examples/custom/rich_text/rich_text.gui b/example/examples/custom/rich_text/rich_text.gui
index 4e70236..b637c59 100644
--- a/example/examples/custom/rich_text/rich_text.gui
+++ b/example/examples/custom/rich_text/rich_text.gui
@@ -70,6 +70,64 @@ nodes {
enabled: true
visible: true
}
+nodes {
+ position {
+ x: 0.0
+ y: 300.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: 400.0
+ y: 150.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 0.101960786
+ y: 0.3019608
+ z: 0.3019608
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: ""
+ id: "case1"
+ 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_MANUAL
+ custom_type: 0
+ enabled: true
+ visible: true
+}
nodes {
position {
x: 0.0
@@ -84,8 +142,8 @@ nodes {
w: 1.0
}
scale {
- x: 1.3
- y: 1.3
+ x: 1.0
+ y: 1.0
z: 1.0
w: 1.0
}
@@ -102,8 +160,8 @@ nodes {
w: 1.0
}
type: TYPE_TEMPLATE
- id: "rich_text"
- parent: "root"
+ id: "rich_text_1"
+ parent: "case1"
layer: ""
inherit_alpha: true
alpha: 1.0
@@ -126,32 +184,32 @@ nodes {
w: 1.0
}
scale {
- x: 1.2
- y: 1.2
+ x: 1.0
+ y: 1.0
z: 1.0
w: 1.0
}
size {
- x: 300.0
- y: 200.0
+ x: 400.0
+ y: 150.0
z: 0.0
w: 1.0
}
color {
- x: 0.4
- y: 0.6
- z: 0.6
+ x: 1.0
+ y: 1.0
+ z: 1.0
w: 1.0
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: ""
- id: "rich_text/root"
+ id: "rich_text_1/root"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
- pivot: PIVOT_N
+ pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
- parent: "rich_text"
+ parent: "rich_text_1"
layer: ""
inherit_alpha: true
slice9 {
@@ -164,15 +222,12 @@ nodes {
clipping_visible: true
clipping_inverted: false
alpha: 1.0
- overridden_fields: 3
- overridden_fields: 5
- overridden_fields: 14
- overridden_fields: 46
+ overridden_fields: 4
template_node_child: true
size_mode: SIZE_MODE_MANUAL
custom_type: 0
enabled: true
- visible: true
+ visible: false
}
nodes {
position {
@@ -188,14 +243,14 @@ nodes {
w: 1.0
}
scale {
- x: 0.8
- y: 0.8
+ x: 1.0
+ y: 1.0
z: 1.0
w: 1.0
}
size {
- x: 200.0
- y: 100.0
+ x: 400.0
+ y: 150.0
z: 0.0
w: 1.0
}
@@ -207,9 +262,9 @@ nodes {
}
type: TYPE_TEXT
blend_mode: BLEND_MODE_ALPHA
- text: "Text"
+ text: "Rich text"
font: "game"
- id: "rich_text/text_prefab"
+ id: "rich_text_1/text_prefab"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
@@ -220,21 +275,22 @@ nodes {
w: 1.0
}
shadow {
- x: 1.0
- y: 1.0
- z: 1.0
+ x: 0.0
+ y: 0.0
+ z: 0.0
w: 1.0
}
adjust_mode: ADJUST_MODE_FIT
line_break: true
- parent: "rich_text/root"
+ parent: "rich_text_1/root"
layer: ""
inherit_alpha: true
alpha: 1.0
- outline_alpha: 1.0
+ outline_alpha: 0.75
shadow_alpha: 0.0
- overridden_fields: 3
+ overridden_fields: 4
overridden_fields: 18
+ overridden_fields: 31
template_node_child: true
text_leading: 1.0
text_tracking: 0.0
@@ -244,7 +300,7 @@ nodes {
}
nodes {
position {
- x: 77.0
+ x: 110.0
y: 0.0
z: 0.0
w: 1.0
@@ -262,8 +318,8 @@ nodes {
w: 1.0
}
size {
- x: 36.0
- y: 36.0
+ x: 21.0
+ y: 20.0
z: 0.0
w: 1.0
}
@@ -275,13 +331,13 @@ nodes {
}
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
- texture: "kenney/slider_move"
- id: "rich_text/icon_prefab"
+ texture: "items/checkmark"
+ id: "rich_text_1/icon_prefab"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
adjust_mode: ADJUST_MODE_FIT
- parent: "rich_text/root"
+ parent: "rich_text_1/root"
layer: ""
inherit_alpha: true
slice9 {
@@ -300,6 +356,64 @@ nodes {
enabled: true
visible: true
}
+nodes {
+ position {
+ x: 0.0
+ y: 180.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: 400.0
+ y: 50.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 0.101960786
+ y: 0.3019608
+ z: 0.3019608
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: ""
+ id: "case2"
+ 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_MANUAL
+ custom_type: 0
+ enabled: true
+ visible: true
+}
nodes {
position {
x: 0.0
@@ -320,8 +434,50 @@ nodes {
w: 1.0
}
size {
- x: 150.0
- y: 4.0
+ 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_2"
+ parent: "case2"
+ 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: 400.0
+ y: 50.0
z: 0.0
w: 1.0
}
@@ -334,7 +490,197 @@ nodes {
type: TYPE_BOX
blend_mode: BLEND_MODE_ALPHA
texture: ""
- id: "middle_line"
+ id: "rich_text_2/root"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "rich_text_2"
+ 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: 4
+ template_node_child: true
+ 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: 0.75
+ y: 0.75
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 550.0
+ y: 50.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 0.9019608
+ z: 0.6
+ w: 1.0
+ }
+ type: TYPE_TEXT
+ blend_mode: BLEND_MODE_ALPHA
+ text: "Rich text"
+ font: "game"
+ id: "rich_text_2/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: 0.5019608
+ y: 0.7019608
+ z: 0.7019608
+ w: 1.0
+ }
+ adjust_mode: ADJUST_MODE_FIT
+ line_break: true
+ parent: "rich_text_2/root"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ outline_alpha: 0.0
+ shadow_alpha: 0.0
+ overridden_fields: 3
+ overridden_fields: 4
+ overridden_fields: 5
+ overridden_fields: 16
+ overridden_fields: 18
+ overridden_fields: 32
+ template_node_child: true
+ text_leading: 1.0
+ text_tracking: 0.0
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 110.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: 0.75
+ y: 0.75
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 21.0
+ y: 20.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: "items/checkmark"
+ id: "rich_text_2/icon_prefab"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "rich_text_2/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
+ overridden_fields: 3
+ template_node_child: true
+ size_mode: SIZE_MODE_AUTO
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 0.0
+ y: 110.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: 400.0
+ y: 50.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 0.101960786
+ y: 0.3019608
+ z: 0.3019608
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: ""
+ id: "case3"
xanchor: XANCHOR_NONE
yanchor: YANCHOR_NONE
pivot: PIVOT_CENTER
@@ -351,13 +697,369 @@ nodes {
clipping_mode: CLIPPING_MODE_NONE
clipping_visible: true
clipping_inverted: false
- alpha: 0.56
+ 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_3"
+ parent: "case3"
+ 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: 400.0
+ y: 50.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: "rich_text_3/root"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "rich_text_3"
+ 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: 4
+ template_node_child: true
+ 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: 0.75
+ y: 0.75
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 550.0
+ y: 50.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 0.9019608
+ z: 0.6
+ w: 1.0
+ }
+ type: TYPE_TEXT
+ blend_mode: BLEND_MODE_ALPHA
+ text: "Rich text"
+ font: "game"
+ id: "rich_text_3/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: 0.5019608
+ y: 0.7019608
+ z: 0.7019608
+ w: 1.0
+ }
+ adjust_mode: ADJUST_MODE_FIT
+ line_break: true
+ parent: "rich_text_3/root"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ outline_alpha: 0.0
+ shadow_alpha: 0.0
+ overridden_fields: 3
+ overridden_fields: 4
+ overridden_fields: 5
+ overridden_fields: 16
+ overridden_fields: 18
+ overridden_fields: 32
+ template_node_child: true
+ text_leading: 1.0
+ text_tracking: 0.0
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 110.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: 0.75
+ y: 0.75
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 21.0
+ y: 20.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: "items/checkmark"
+ id: "rich_text_3/icon_prefab"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ adjust_mode: ADJUST_MODE_FIT
+ parent: "rich_text_3/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
+ overridden_fields: 3
+ template_node_child: true
+ size_mode: SIZE_MODE_AUTO
+ custom_type: 0
+ enabled: true
+ visible: true
+}
+nodes {
+ position {
+ x: 0.0
+ y: 40.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: 400.0
+ y: 50.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 0.101960786
+ y: 0.3019608
+ z: 0.3019608
+ w: 1.0
+ }
+ type: TYPE_BOX
+ blend_mode: BLEND_MODE_ALPHA
+ texture: ""
+ id: "case4"
+ 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_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: 0.75
+ y: 0.75
+ z: 1.0
+ w: 1.0
+ }
+ size {
+ x: 550.0
+ y: 50.0
+ z: 0.0
+ w: 1.0
+ }
+ color {
+ x: 1.0
+ y: 0.9019608
+ z: 0.6
+ w: 1.0
+ }
+ type: TYPE_TEXT
+ blend_mode: BLEND_MODE_ALPHA
+ text: "Energy is full to restore"
+ font: "game"
+ id: "usual_text"
+ xanchor: XANCHOR_NONE
+ yanchor: YANCHOR_NONE
+ pivot: PIVOT_CENTER
+ outline {
+ x: 0.0
+ y: 0.0
+ z: 0.0
+ w: 1.0
+ }
+ shadow {
+ x: 0.5019608
+ y: 0.7019608
+ z: 0.7019608
+ w: 1.0
+ }
+ adjust_mode: ADJUST_MODE_FIT
+ line_break: true
+ parent: "case4"
+ layer: ""
+ inherit_alpha: true
+ alpha: 1.0
+ outline_alpha: 0.0
+ shadow_alpha: 0.0
+ template_node_child: false
+ text_leading: 1.0
+ text_tracking: 0.0
+ custom_type: 0
+ enabled: true
+ visible: true
+}
layers {
name: "image"
}
diff --git a/example/examples/custom/rich_text/rich_text.gui_script b/example/examples/custom/rich_text/rich_text.gui_script
index c6f6234..7383218 100644
--- a/example/examples/custom/rich_text/rich_text.gui_script
+++ b/example/examples/custom/rich_text/rich_text.gui_script
@@ -6,11 +6,16 @@ local RichText = require("druid.custom.rich_text.rich_text")
function init(self)
self.druid = druid.new(self)
- self.rich_text = self.druid:new(RichText, "rich_text")
+ self.rich_text = self.druid:new(RichText, "rich_text_1")
+ self.rich_text:set_text("Lorem long text with differrent placeholder
here to check")
-- self.rich_text:set_text("Lorem long text with differrent placeholder or just text without any sense here to check multiline without long words")
- -- self.rich_text:set_text("Lorem long text with differrent placeholder or just text without any sense here to check multiline without long wordswordwordwrodwrodwrodswrodword he")
- self.rich_text:set_text("Some text with image
in the middle")
- self.rich_text:set_text("Some text with image
in the middle")
+ --self.rich_text:set_text("Some text with image
in the middle")
+ --self.rich_text:set_text("Some text with image
in the middle")
+ self.rich_text_2 = self.druid:new(RichText, "rich_text_2")
+ self.rich_text_2:set_text("Energy is full to restore")
+
+ self.rich_text_3 = self.druid:new(RichText, "rich_text_3")
+ self.rich_text_3:set_text("Energy is full to restore")
end