Translate labels before calculating their size

I've made a new naive_str_width function that tries to estimate the
width of UTF-8 text and is faster than strip_escape_sequences on LuaJIT
(I guess because there isn't an intermediate string being created).

Closes #4
This commit is contained in:
luk3yx 2023-06-16 14:04:05 +12:00
parent 5bb5dba625
commit 4046fed55f
2 changed files with 123 additions and 11 deletions

View File

@ -21,28 +21,90 @@ local DEBUG_MODE = false
flow = {}
local S = minetest.get_translator("flow")
local Form = {}
local min, max = math.min, math.max
local function strip_escape_sequences(str)
return (str:gsub("\27%([^)]+%)", ""):gsub("\27.", ""))
-- Estimates the width of a valid UTF-8 string, ignoring any escape sequences.
-- This function hopefully works with most (but not all) scripts, maybe it
-- could still be improved.
local byte = string.byte
local LPAREN = byte("(")
local function naive_str_width(str)
local w = 0
local prev_w = 0
local line_count = 1
local i = 1
local str_length = #str
while i <= str_length do
local char = byte(str, i)
if char == 0x1b then
-- Ignore escape sequences
i = i + 1
if byte(str, i) == LPAREN then
i = str:find(")", i + 1, true) or str_length
end
elseif char == 0xe1 then
if (byte(str, i + 1) or 0) < 0x84 then
-- U+1000 - U+10FF
w = w + 1
else
-- U+1100 - U+2000
w = w + 2
end
i = i + 2
elseif char > 0xe1 and char < 0xf5 then
-- U+2000 - U+10FFFF
w = w + 2
i = i + 2
elseif char == 0x0a then
-- Newlines: Reset the width and increase the line count
prev_w = max(prev_w, w)
w = 0
line_count = line_count + 1
elseif char < 0x80 or char > 0xbf then
-- Everything except UTF-8 continuation sequences
w = w + 1
end
i = i + 1
end
return max(w, prev_w), line_count
end
local LABEL_HEIGHT = 0.4
local LABEL_OFFSET = LABEL_HEIGHT / 2
local CHAR_WIDTH = 0.21 -- 0.2
local CHAR_WIDTH = 0.21
-- The "current_lang" variable isn't ideal but means that the language will be
-- known inside ScrollableVBox etc
local current_lang
-- get_translated_string doesn't exist in MT 5.2.0 and older
local get_translated_string = minetest.get_translated_string or function(_, s)
return s
end
local function get_lines_size(lines)
local w = 0
for _, line in ipairs(lines) do
w = max(w, #strip_escape_sequences(line) * CHAR_WIDTH)
-- Translate the string if necessary
if current_lang and current_lang ~= "" and current_lang ~= "en" then
line = get_translated_string(current_lang, line)
end
w = max(w, naive_str_width(line) * CHAR_WIDTH)
end
return w, LABEL_HEIGHT * #lines
end
local function get_label_size(label)
return get_lines_size((label or ""):split("\n", true))
label = label or ""
if current_lang and current_lang ~= "" and current_lang ~= "en" then
label = get_translated_string(current_lang, label)
end
local longest_line_width, line_count = naive_str_width(label)
return longest_line_width * CHAR_WIDTH, line_count * LABEL_HEIGHT
end
local size_getters = {}
@ -733,8 +795,9 @@ end
-- Renders a GUI into a formspec_ast tree and a table with callbacks.
function Form:_render(player, ctx, formspec_version, id1, embedded)
function Form:_render(player, ctx, formspec_version, id1, embedded, lang_code)
local used_ctx_vars = {}
current_lang = lang_code
-- Wrap ctx.form
local orig_form = ctx.form or {}
@ -777,6 +840,8 @@ function Form:_render(player, ctx, formspec_version, id1, embedded)
end
end
current_lang = nil
return tree, {
self = self,
callbacks = callbacks,
@ -792,7 +857,8 @@ local function prepare_form(self, player, formname, ctx, auto_name_id)
-- local t = DEBUG_MODE and minetest.get_us_time()
local info = minetest.get_player_information(name)
local tree, form_info = self:_render(player, ctx,
info and info.formspec_version, auto_name_id)
info and info.formspec_version, auto_name_id, false,
info and info.lang_code)
-- local t2 = DEBUG_MODE and minetest.get_us_time()
local fs = assert(formspec_ast.unparse(tree))
@ -833,7 +899,9 @@ function Form:show(player, ctx)
end
function Form:show_hud(player, ctx)
local tree = self:_render(player, ctx or {})
local info = minetest.get_player_information(name)
local tree = self:_render(player, ctx or {}, nil, nil, nil,
info and info.lang_code)
hud_fs.show_hud(player, self, tree)
end
@ -869,7 +937,7 @@ function Form:render_to_formspec_string(player, ctx, standalone)
local info = minetest.get_player_information(name)
local tree, form_info = self:_render(player, ctx or {},
info and info.formspec_version, render_to_formspec_auto_name_ids[name],
not standalone)
not standalone, info and info.lang_code)
local public_form_info
if not standalone then
local size = table.remove(tree, 1)

View File

@ -70,7 +70,11 @@ string.split = string.split or function(str, chr)
end
-- Load flow
dofile('init.lua')
local f = assert(io.open("init.lua"))
local code = f:read("*a") .. "\nreturn naive_str_width"
f:close()
local naive_str_width = assert((loadstring or load)(code))()
local gui = flow.widgets
-- "Normalise" the AST by flattening then parsing/unparsing to remove extra
@ -438,4 +442,44 @@ describe("Flow", function()
minetest.get_player_by_name = nil
end)
end)
describe("naive_str_width", function()
it("works in a simple string", function()
local w, h = naive_str_width("Hello world!")
assert.equals(w, 12)
assert.equals(h, 1)
end)
it("works with multi-line strings", function()
local w, h = naive_str_width("Hello world!\nLine 2")
assert.equals(w, 12)
assert.equals(h, 2)
w, h = naive_str_width("Hello world!\nThis is a test")
assert.equals(w, 14)
assert.equals(h, 2)
end)
it("works with Cyrillic script", function()
local w, h = naive_str_width("Привіт Світ")
assert.equals(w, 11)
assert.equals(h, 1)
end)
it("works with full width characters", function()
local w, h = naive_str_width("你好世界\n123456")
assert.equals(w, 8)
assert.equals(h, 2)
end)
it("strips escape codes", function()
local w, h = naive_str_width("\27(T@test)Hello \27Fworld\27E!\27E")
assert.equals(w, 12)
assert.equals(h, 1)
w, h = naive_str_width("\27(c@blue)Test\27(c@#ffffff)\n123")
assert.equals(w, 4)
assert.equals(h, 2)
end)
end)
end)