Skip to content

Commit

Permalink
Added support for splitting a word into it's characters
Browse files Browse the repository at this point in the history
  • Loading branch information
britzl committed Mar 2, 2018
1 parent 082120e commit f8b0d4b
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 47 deletions.
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ The following tags are supported:
| i | The text should be italic | `<i>Foobar</i>` |
| size | Change text size, relative to default size | `<size="2">Twice as large</size>` |
| color | Change text color | `<color=red>Foobar</color>` |
| | | `<color=1.0,0.5,0,1.0>Foobar</color>` |
| | | `<color=#ff00ffff>Foobar</color>` |
| | | `<color=1.0,0,0,1.0>Foobar</color>` |
| | | `<color=#ff0000>Foobar</color>` |
| | | `<color=#ff0000ff>Foobar</color>` |
| font | Change font | `<font=MyCoolFont>Foobar</font>` |
| img | Display image | `<img=texture:image/>` |
| br | Insert a line break (see notes on linebreak) | `<br/>` |
Expand Down Expand Up @@ -79,6 +80,7 @@ The following named colors are supported:
| white | `#ffffffff` | ![](https://placehold.it/15/ffffff/000000?text=+) |
| yellow | `#ffff00ff` | ![](https://placehold.it/15/ffff00/000000?text=+) |


# Usage
The RichText library will create gui text nodes representing the markup in the text passed to the library. It will search for tags and split the entire text into words, where each word contains additional meta-data that is used to create and configure text nodes. This means that the library will create as many text nodes as there are words in the text.

Expand Down Expand Up @@ -116,7 +118,6 @@ A more complex example with different fonts, colors, inline images and automatic
![](docs/example.png)



# API
### richtext.create(text, font, settings)
Creates rich text gui nodes from a text containing markup.
Expand Down Expand Up @@ -174,6 +175,16 @@ Get the length of a text ignoring any tags except image tags which are treated a
* `length` (number) - The length of the provided text.


### richtext.characters(word)
Split a word into it's characters, including the creation of the gui nodes. Each of the characters will be given the same attributes as the word, and they will be positioned correctly within the word.

**PARAMETERS**
* `word` (table) - The word to split, as received from a call to `richtext.create()` or `richtext.tagged()`.

**RETURNS**
* `characters` (table) - The individual characters of the word.


### richtext.ALIGN_LEFT
Left-align text. The words of a line starts at the specified position (see `richtext.create` settings above).

Expand Down
20 changes: 19 additions & 1 deletion example/example.gui_script
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ end
local function create_truncate_example()
local settings = { position = vmath.vector3(0, 260, 0) }
local words, metrics = richtext.create("This text should be shown one <img=smileys:cyclops/> at a time...", "Roboto-Regular", settings)
pprint(metrics)
local length = 0
richtext.truncate(words, length)
timer.repeating(0.1, function()
Expand All @@ -61,8 +60,27 @@ local function create_truncate_example()
end



local function create_characters_example()
local settings = { position = vmath.vector3(0, 330, 0) }
local words, metrics = richtext.create("Our <wave><color=#ff69b4>princess</color></wave> is in another <wave><color=red>castle</color></wave>", "Roboto-Regular", settings)

local waves = richtext.tagged(words, "wave")
for _,wave in pairs(waves) do
local chars = richtext.characters(wave)
gui.delete_node(wave.node)
for i,char in ipairs(chars) do
local pos = gui.get_position(char.node)
gui.animate(char.node, gui.PROP_POSITION, pos + vmath.vector3(0, 15, 0), gui.EASING_INOUTSINE, 2, i * 0.3, nil, gui.PLAYBACK_LOOP_PINGPONG)
end
end
end



function init(self)
create_complex_example()
create_align_example()
create_truncate_example()
create_characters_example()
end
10 changes: 10 additions & 0 deletions richtext/parse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ local function parse_tag(tag, params)
return settings
end


-- add a single word to the list of words
local function add_word(text, settings, words)
local data = { text = text }
Expand All @@ -36,6 +37,7 @@ local function add_word(text, settings, words)
words[#words + 1] = data
end


-- split a line into words
local function split_line(line, settings, words)
assert(line)
Expand All @@ -56,6 +58,7 @@ local function split_line(line, settings, words)
end
end


-- split text
-- split by lines first
local function split_text(text, settings, words)
Expand Down Expand Up @@ -91,6 +94,7 @@ local function split_text(text, settings, words)
end
end


-- find tag in text
-- return the tag, tag params and any text before and after the tag
local function find_tag(text)
Expand Down Expand Up @@ -121,6 +125,11 @@ local function find_tag(text)
end
end


--- Parse the text into individual words
-- @param text The text to parse
-- @param word_settings Default settings for each word
-- @return List of all words
function M.parse(text, word_settings)
assert(text)
assert(word_settings)
Expand Down Expand Up @@ -161,6 +170,7 @@ end

--- Get the length of a text, excluding any tags (except image tags)
function M.length(text)
-- treat image tags as a single character
return #text:gsub("<img.-/>", " "):gsub("<.->", "")
end

Expand Down
159 changes: 116 additions & 43 deletions richtext/richtext.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,31 @@ M.ALIGN_RIGHT = hash("ALIGN_RIGHT")
local V3_ZERO = vmath.vector3(0)
local V3_ONE = vmath.vector3(1)

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_trailing_whitespace(text)
return text:match("^.-(%s*)$") or ""
end


local function get_space_width(font)
return gui.get_text_metrics(font, " _").width - gui.get_text_metrics(font, "_").width
end


local function get_font(word, fonts)
local font_settings = fonts[word.font]
local font = nil
Expand Down Expand Up @@ -76,6 +96,58 @@ function M.length(text)
end


local function create_box_node(word)
local node = gui.new_box_node(V3_ZERO, V3_ZERO)
gui.set_size_mode(node, gui.SIZE_MODE_AUTO)
gui.set_texture(node, word.image.texture)
gui.play_flipbook(node, hash(word.image.anim))

-- get metrics of node based on image size
local image_size = gui.get_size(node)
local metrics = {}
metrics.total_width = image_size.x
metrics.width = image_size.x
metrics.height = image_size.y
return node, metrics
end


local function create_text_node(word, font, font_cache)
local node = gui.new_text_node(V3_ZERO, word.text)
gui.set_font(node, font)
gui.set_color(node, word.color)
gui.set_scale(node, V3_ONE * (word.size or 1))

-- get metrics of node with and without trailing whitespace
local metrics = gui.get_text_metrics(font, word.text)
metrics.width = metrics.width * word.size
metrics.height = metrics.height * word.size

-- get width of text with trailing whitespace included
local trailing_whitespace = get_trailing_whitespace(word.text)
if #trailing_whitespace > 0 then
metrics.total_width = (metrics.width + #trailing_whitespace * get_space_width(font)) * word.size
else
metrics.total_width = metrics.width
end
return node, metrics
end


local function create_node(word, parent, font)
local node, metrics
if word.image then
node, metrics = create_box_node(word)
else
node, metrics = create_text_node(word, font)
end
gui.set_pivot(node, gui.PIVOT_NW)
gui.set_parent(node, parent)
return node, metrics
end



--- Create rich text gui nodes from text
-- @param text The text to create rich text nodes from
-- @param font The default font
Expand All @@ -92,9 +164,6 @@ function M.create(text, font, settings)
settings.color = settings.color or V3_ONE
settings.position = settings.position or V3_ZERO

-- cache length fonr metrics such as space width and height
local font_sizes = {}

-- default settings for a word
-- will be assigned to each word unless tags override the values
local word_settings = {
Expand All @@ -118,47 +187,9 @@ function M.create(text, font, settings)

-- get font to use based on word tags
local font = get_font(word, settings.fonts)
-- cache some font measurements for the current font
if not font_sizes[font] then
font_sizes[font] = {
space = gui.get_text_metrics(font, " _").width - gui.get_text_metrics(font, "_").width,
height = gui.get_text_metrics(font, "Ij").height,
}
end

-- create and configure node
local node
if word.image then
node = gui.new_box_node(V3_ZERO, V3_ZERO)
gui.set_size_mode(node, gui.SIZE_MODE_AUTO)
gui.set_texture(node, word.image.texture)
gui.play_flipbook(node, hash(word.image.anim))

-- get metrics of node based on image size
local image_size = gui.get_size(node)
word.metrics = {}
word.metrics.total_width = image_size.x
word.metrics.width = image_size.x
word.metrics.height = image_size.y
else
node = gui.new_text_node(V3_ZERO, word.text)
gui.set_font(node, font)
gui.set_color(node, word.color)
gui.set_scale(node, V3_ONE * (word.size or 1))

-- get metrics of node with and without trailing whitespace
word.metrics = gui.get_text_metrics_from_node(node)
word.metrics.total_width = (word.metrics.width + #get_trailing_whitespace(word.text) * font_sizes[font].space) * word.size
word.metrics.width = word.metrics.width * word.size
word.metrics.height = word.metrics.height * word.size
end

-- store node on word and set it's parent and pivot
word.node = node
if settings.parent then
gui.set_parent(node, settings.parent)
end
gui.set_pivot(node, gui.PIVOT_NW)
-- create node and get metrics
word.node, word.metrics = create_node(word, settings.parent, font)

-- does the word fit on the line or does it overflow?
local overflow = (settings.width and (line_width + word.metrics.width) > settings.width)
Expand Down Expand Up @@ -256,4 +287,46 @@ function M.truncate(words, length)
end


--- Split a word into it's characters
-- @param word The word to split
-- @return The individual characters
function M.characters(word)
assert(word)

-- exit early if word is a single character or empty
if #word.text <= 1 then
local char = deepcopy(word)
char.node, char.metrics = create_node(char, parent, font)
ui.set_position(char.node, gui.get_position(word.node))
return { char }
end

-- split word into characters
local parent = gui.get_parent(word.node)
local font = gui.get_font(word.node)
local chars = {}
local chars_width = 0
for i=1,#word.text do
local char = deepcopy(word)
chars[#chars + 1] = char
char.text = word.text:sub(i,i)
char.node, char.metrics = create_node(char, parent, font)
chars_width = chars_width + char.metrics.width
end

-- position each character
-- take into account that the sum of the width of the individual
-- characters differ from the width of the entire word
local position = gui.get_position(word.node)
local spacing = (word.metrics.width - chars_width) / (#chars - 1)
for i=1,#chars do
local char = chars[i]
gui.set_position(char.node, position)
position.x = position.x + char.metrics.width + spacing
end

return chars
end


return M

0 comments on commit f8b0d4b

Please sign in to comment.