forked from your-land-mirror/minetest-flow
These are only supported in inline styles so that flow can calculate the size of the elements without having to look through the rest of the tree.
325 lines
9.4 KiB
Lua
325 lines
9.4 KiB
Lua
--
|
|
-- Flow: Layouting (initial pass)
|
|
--
|
|
-- Copyright © 2022-2025 by luk3yx
|
|
--
|
|
-- This program is free software: you can redistribute it and/or modify
|
|
-- it under the terms of the GNU Lesser General Public License as published by
|
|
-- the Free Software Foundation, either version 2.1 of the License, or
|
|
-- (at your option) any later version.
|
|
--
|
|
-- This program is distributed in the hope that it will be useful,
|
|
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
-- GNU Lesser General Public License for more details.
|
|
--
|
|
-- You should have received a copy of the GNU Lesser General Public License
|
|
-- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
--
|
|
|
|
local max = math.max
|
|
|
|
-- 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, strlen = string.byte, string.len
|
|
local LPAREN = byte("(")
|
|
local function naive_str_width(str)
|
|
local w = 0
|
|
local prev_w = 0
|
|
local line_count = 1
|
|
local i = 1
|
|
-- string.len() is used so that numbers are coerced to strings without any
|
|
-- extra checking
|
|
local str_length = strlen(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
|
|
|
|
-- 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 = core.get_translated_string or function(_, s)
|
|
return s
|
|
end
|
|
|
|
local function get_lines_size(lines)
|
|
local w = 0
|
|
for _, line in ipairs(lines) do
|
|
-- 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)
|
|
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 = {}
|
|
|
|
local function parse_v2f(str, default_x, default_y)
|
|
if str and type(str) == "string" then
|
|
local x, y = str:match("^%s-(%d+)%s-,%s-(%d+)%s-$")
|
|
return tonumber(x) or default_x, tonumber(y) or default_y
|
|
end
|
|
|
|
return default_x, default_y
|
|
end
|
|
|
|
local function get_and_fill_in_sizes(node)
|
|
if node.type == "list" then
|
|
if node._flow_w and node._flow_h then
|
|
return node._flow_w, node._flow_h
|
|
end
|
|
|
|
local style = node.style
|
|
local slot_w, slot_h = parse_v2f(style and style.size, 1, 1)
|
|
local spacing_w, spacing_h = parse_v2f(style and style.spacing, 0.25, 0.25)
|
|
|
|
local w = node.w * (slot_w + spacing_w) - spacing_w
|
|
local h = node.h * (slot_h + spacing_h) - spacing_h
|
|
|
|
-- Cache calculated size so we don't have to parse the list style again
|
|
node._flow_w, node._flow_h = w, h
|
|
|
|
return w, h
|
|
end
|
|
|
|
if node.w and node.h then
|
|
return node.w, node.h
|
|
end
|
|
|
|
local f = size_getters[node.type]
|
|
if not f then return 0, 0 end
|
|
|
|
local w, h = f(node)
|
|
node.w = node.w or max(w, node.min_w or 0)
|
|
node.h = node.h or max(h, node.min_h or 0)
|
|
return node.w, node.h
|
|
end
|
|
|
|
function size_getters.container(node)
|
|
local w, h = 0, 0
|
|
for _, n in ipairs(node) do
|
|
local w2, h2 = get_and_fill_in_sizes(n)
|
|
w = max(w, (n.x or 0) + w2)
|
|
h = max(h, (n.y or 0) + h2)
|
|
end
|
|
return w, h
|
|
end
|
|
size_getters.scroll_container = size_getters.container
|
|
|
|
function size_getters.label(node)
|
|
local w, h = get_label_size(node.label)
|
|
return w, LABEL_HEIGHT + (h - LABEL_HEIGHT) * 1.25
|
|
end
|
|
|
|
local MIN_BUTTON_HEIGHT = 0.8
|
|
function size_getters.button(node)
|
|
local x, y = get_label_size(node.label)
|
|
return max(x, MIN_BUTTON_HEIGHT * 2), max(y, MIN_BUTTON_HEIGHT)
|
|
end
|
|
|
|
size_getters.button_exit = size_getters.button
|
|
size_getters.image_button = size_getters.button
|
|
size_getters.image_button_exit = size_getters.button
|
|
size_getters.item_image_button = size_getters.button
|
|
size_getters.button_url = size_getters.button
|
|
|
|
function size_getters.field(node)
|
|
local label_w, label_h = get_label_size(node.label)
|
|
|
|
-- This is done in apply_padding as well but the label size has already
|
|
-- been calculated here
|
|
if not node._padding_top and node.label and #node.label > 0 then
|
|
node._padding_top = label_h
|
|
end
|
|
|
|
local w, h = get_label_size(node.default)
|
|
return max(w, label_w, 3), max(h, MIN_BUTTON_HEIGHT)
|
|
end
|
|
size_getters.pwdfield = size_getters.field
|
|
size_getters.textarea = size_getters.field
|
|
|
|
function size_getters.vertlabel(node)
|
|
return CHAR_WIDTH, #node.label * LABEL_HEIGHT
|
|
end
|
|
|
|
function size_getters.textlist(node)
|
|
local w, h = get_lines_size(node.listelems)
|
|
return w, h * 1.1
|
|
end
|
|
|
|
function size_getters.dropdown(node)
|
|
return max(get_lines_size(node.items) + 0.3, 2), MIN_BUTTON_HEIGHT
|
|
end
|
|
|
|
function size_getters.checkbox(node)
|
|
local w, h = get_label_size(node.label)
|
|
return w + 0.4, h
|
|
end
|
|
|
|
local field_elems = {field = true, pwdfield = true, textarea = true}
|
|
|
|
local function apply_padding(node, x, y)
|
|
local w, h = get_and_fill_in_sizes(node)
|
|
|
|
-- Labels are positioned from the centre of the first line and checkboxes
|
|
-- are positioned from the centre.
|
|
if node.type == "label" then
|
|
y = y + LABEL_OFFSET
|
|
elseif node.type == "checkbox" then
|
|
y = y + h / 2
|
|
elseif field_elems[node.type] and not node._padding_top and node.label and
|
|
#node.label > 0 then
|
|
-- Add _padding_top to fields with labels that have a fixed size set
|
|
local _, label_h = get_label_size(node.label)
|
|
node._padding_top = label_h
|
|
elseif node.type == "tabheader" and w > 0 and h > 0 then
|
|
-- Handle tabheader if the width and height are set
|
|
-- I'm not sure what to do with tabheaders that don't have a width or
|
|
-- height set.
|
|
y = y + h
|
|
end
|
|
|
|
if node._padding_top then
|
|
y = y + node._padding_top
|
|
h = h + node._padding_top
|
|
end
|
|
|
|
local padding = node.padding
|
|
if padding then
|
|
x = x + padding
|
|
y = y + padding
|
|
w = w + padding * 2
|
|
h = h + padding * 2
|
|
end
|
|
|
|
node.x, node.y = x, y
|
|
return w, h
|
|
end
|
|
|
|
local invisible_elems = {
|
|
style = true, listring = true, scrollbaroptions = true, tableoptions = true,
|
|
tablecolumns = true, tooltip = true, style_type = true, set_focus = true,
|
|
listcolors = true
|
|
}
|
|
|
|
local DEFAULT_SPACING = 0.2
|
|
function size_getters.vbox(vbox)
|
|
local spacing = vbox.spacing or DEFAULT_SPACING
|
|
local width = 0
|
|
local y = 0
|
|
for _, node in ipairs(vbox) do
|
|
if not invisible_elems[node.type] then
|
|
if y > 0 then
|
|
y = y + spacing
|
|
end
|
|
|
|
local w, h = apply_padding(node, 0, y)
|
|
width = max(width, w)
|
|
y = y + h
|
|
end
|
|
end
|
|
|
|
return width, y
|
|
end
|
|
|
|
function size_getters.hbox(hbox)
|
|
local spacing = hbox.spacing or DEFAULT_SPACING
|
|
local x = 0
|
|
local height = 0
|
|
for _, node in ipairs(hbox) do
|
|
if not invisible_elems[node.type] then
|
|
if x > 0 then
|
|
x = x + spacing
|
|
end
|
|
|
|
local w, h = apply_padding(node, x, 0)
|
|
height = max(height, h)
|
|
x = x + w
|
|
end
|
|
end
|
|
|
|
return x, height
|
|
end
|
|
|
|
function size_getters.stack(stack)
|
|
local width, height = 0, 0
|
|
for _, node in ipairs(stack) do
|
|
if not invisible_elems[node.type] then
|
|
local w, h = apply_padding(node, 0, 0)
|
|
width = max(width, w)
|
|
height = max(height, h)
|
|
end
|
|
end
|
|
|
|
return width, height
|
|
end
|
|
|
|
function size_getters.padding(node)
|
|
core.log("warning", "[flow] The gui.Padding element is deprecated")
|
|
assert(#node == 1, "Padding can only have one element inside.")
|
|
local n = node[1]
|
|
local x, y = apply_padding(n, 0, 0)
|
|
if node.expand == nil then
|
|
node.expand = n.expand
|
|
end
|
|
return x, y
|
|
end
|
|
|
|
local function set_current_lang(lang)
|
|
current_lang = lang
|
|
end
|
|
|
|
return apply_padding, get_and_fill_in_sizes, set_current_lang,
|
|
DEFAULT_SPACING, LABEL_OFFSET, invisible_elems
|