Fix a few bugs and add automatic sizing for layout containers.

This commit is contained in:
Signal 2026-03-07 16:30:53 -05:00
parent be73993547
commit 969030a026
2 changed files with 153 additions and 48 deletions

197
init.lua
View file

@ -62,7 +62,26 @@ local function add_elem_style(e, state, props)
props = state
state = "default"
end
e._styles[state] = imfs.style(e.__id..":"..state, props, true)
-- Permit targeting pseudo-elements as well.
if state:sub(1, 1) == "." then
e._styles[state] = imfs.style(e.__id..state, props, true)
else
e._styles[state] = imfs.style(e.__id..":"..state, props, true)
end
return e
end
-- Bulk version of `add_elem_style`.
local function add_elem_styles(e, states)
for state, props in pairs(states) do
if state:sub(1, 1) == "." then
e._styles[state] = imfs.style(e.__id..state, props, true)
else
e._styles[state] = imfs.style(e.__id..":"..state, props, true)
end
end
return e
end
@ -75,16 +94,22 @@ local function add_elem_tooltip(e, text, enabled, bgcolor, txtcolor)
txtcolor = bgcolor
bgcolor = enabled
end
-- We must overload `render()` both to ensure that tooltips don't clutter the element tree and mess with layout containers
-- and (for area tooltips) in order to ensure the tooltip's position and size match the parent's exactly in all cases.
-- For named elements, prefer name-associated tooltips.
if e.__id then
imfs._named_tooltip(e.__id, text, bgcolor, txtcolor)
-- For other elements, if they have a size, use an area tooltip instead.
elseif e.w then
-- We must overload `render()` in order to ensure the tooltip's position matches the parent's exactly in all cases.
local render = e.render
e.render = function(e, x, y, w, h)
local out = render(e, x, y, w, h)
return table.concat{out, imfs.tooltip.init(x or e.x, y or e.y, w or e.w, h or e.h, text, bgcolor, txtcolor, true):render()}
return out..imfs.tooltip.named(e.__id, text, bgcolor, txtcolor):render()
end
-- For other elements, if they have a size, use an area tooltip instead.
elseif e.w then
local render = e.render
e.render = function(e, x, y, w, h)
local out = render(e, x, y, w, h)
return out..imfs.tooltip.init(x or e.x, y or e.y, w or e.w, h or e.h, text, bgcolor, txtcolor):render()
end
end
return e
@ -248,6 +273,11 @@ local fs_tooltip = {
setmetatable(e, imfs.tooltip)
return e
end,
named = function(name, text, bgcolor, txtcolor)
local e = {_name = name, text = text, bgcolor = bgcolor or imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"}
setmetatable(e, imfs.tooltip)
return e
end,
render = function(e, x, y, w, h)
if e._name then
return string.format("tooltip[%s;%s;%s;%s]", e._name, get(e, "text"), get(e, "bgcolor"), get(e, "txtcolor"))
@ -273,13 +303,6 @@ setmetatable(fs_tooltip, {
end
})
local function fs_named_tooltip(name, text, bgcolor, txtcolor)
local e = {_name = name, text = text, bgcolor = bgcolor or imfs.theme.tooltip_bg or "#444", txtcolor = txtcolor or imfs.theme.tooltip_text or "#aaa"}
setmetatable(e, fs_tooltip)
table.insert(ctx, e)
return e
end
local fs_label = {
render = function(e, x, y)
return string.format("label[%f,%f;%s]", x or get(e, "x"), y or get(e, "y"), get(e, "txt"))
@ -326,19 +349,31 @@ setmetatable(fs_arealabel, {
local fs_hypertext = {
render = function(e, x, y, w, h)
return string.format("hypertext[%f,%f;%f,%f;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), e.__id, get(e, "txt"))
local out = {}
for _, x in pairs(e._styles) do
out[#out +1] = x:render()
end
out[#out +1] = string.format("hypertext[%f,%f;%f,%f;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), e.__id, get(e, "txt"))
return table.concat(out)
end,
onaction = function(e, fn)
ctx._events.on_click[e.__id] = fn
return e
end,
named = function(e, name)
e.__id = name
return e
end,
styles = add_elem_styles,
style = add_elem_style,
tooltip = add_elem_tooltip,
}
fs_hypertext.__index = fs_hypertext
setmetatable(fs_hypertext, {
__call = function(_, x, y, w, h, txt)
local e = {x = x, y = y, w = w, h = h, txt = txt}
e.__id = "_"..minetest.get_us_time().."_"..math.random(1, 100000)
local e = {x = x, y = y, w = w, h = h, txt = txt, _styles = {}}
e.__id = new_id()
setmetatable(e, fs_hypertext)
table.insert(ctx, e)
return e
@ -470,6 +505,7 @@ local fs_model = {
return e
end,
style = add_elem_style,
styles = add_elem_styles,
tooltip = add_elem_tooltip,
}
fs_model.__index = fs_model
@ -521,6 +557,7 @@ local fs_button = {
return e
end,
style = add_elem_style,
styles = add_elem_styles,
tooltip = add_elem_tooltip,
}
fs_button.__index = fs_button
@ -557,16 +594,32 @@ setmetatable(fs_checkbox, {
local fs_list = {
render = function(e, x, y, w, h)
x = x or get(e, "x")
y = y or get(e, "y")
w = w or get(e, "w")
h = h or get(e, "h")
return string.format("list[%s;%s;%f,%f;%d,%d;%s]", get(e, "location"), get(e, "list"), x or get(e, "x"), y or get(e, "y"), w, h, get(e, "start"))
local width = (4 *w +1) /5
local height = (4 *h +1) /5
local padding_x = (w -(width *1.25) +0.25) /2
local padding_y = (h -(height *1.25) +0.25) /2
return string.format("list[%s;%s;%f,%f;%d,%d;%s]", get(e, "location"), get(e, "list"), x +padding_x, y +padding_y, width, height, get(e, "start"))
end
}
fs_list.__index = fs_list
setmetatable(fs_list, {
__call = function(_, x, y, w, h, location, list, start)
local e = {x = x, y = y, w = type(w == "string") and w or w +((w -1) /4), h = type(h) == "string" and h or h +((h -1) /4), location = location or "current_player", list = list or "main", start = start or ""}
local e = {
x = x,
y = y,
w = type(w) == "string" and w or w +((w -1) /4),
h = type(h) == "string" and h or h +((h -1) /4),
location = location or "current_player",
list = list or "main",
start = start or ""
}
e.__id = new_id()
setmetatable(e, fs_list)
table.insert(ctx, e)
@ -644,6 +697,7 @@ local fs_field = {
return e
end,
style = add_elem_style,
styles = add_elem_styles,
tooltip = add_elem_tooltip,
}
fs_field.__index = fs_field
@ -665,6 +719,10 @@ end
local fs_scrollbar = {
render = function(e, x, y, w, h)
local out = {}
for _, x in pairs(e._styles) do
out[#out +1] = x:render()
end
if e._options then
out[#out +1] = "scrollbaroptions["
local first = true
@ -682,6 +740,11 @@ local fs_scrollbar = {
out[#out +1] = string.format("scrollbar[%f,%f;%f,%f;%s;%s;%s]", x or get(e, "x"), y or get(e, "y"), w or get(e, "w"), h or get(e, "h"), get(e, "orientation"), e.__id, get(e, "value"))
-- If we were given custom options, reset them all so they don't interfere with future scrollbars (particularly those automatically configured by scroll containers).
if e._options then
out[#out +1] = "scrollbaroptions[min=-1;max=-1;smallstep=-1;largestep=-1;thumbsize=-1;arrows=default]"
end
return table.concat(out)
end,
options = function(e, props)
@ -693,13 +756,14 @@ local fs_scrollbar = {
return e
end,
style = add_elem_style,
styles = add_elem_styles,
tooltip = add_elem_tooltip,
}
fs_scrollbar.__index = fs_scrollbar
setmetatable(fs_scrollbar, {
__call = function(_, x, y, w, h, orientation, value)
local e = {x = x, y = y, w = w, h = h, orientation = orientation or "vertical", value = value or "", _styles = {}}
e.__id = "_"..minetest.get_us_time().."_"..math.random(1, 100000)
e.__id = new_id()
setmetatable(e, fs_scrollbar)
ctx[#ctx +1] = e
return e
@ -741,9 +805,7 @@ local fs_scroll_container = {
end
out[#out +1] = "scroll_container_end[]"
if e._scrollbar then
out[#out +1] = e._scrollbar:render()
else
if not e._scrollbar then
local v = e.__fs._ctx.fields[e.__id]
out[#out +1] = string.format("scrollbar[-800,-800;0,0;%s;%s;%s]", get(e, "orientation"), e.__id, v and v:sub(5) or "")
end
@ -769,6 +831,7 @@ local fs_scroll_container = {
fn()
else
fs_scrollbar(fn, ...)
:options({arrows = "hide"})
end
e._scrollbar = ctx[1]
ctx = ctx.__parent
@ -806,7 +869,7 @@ setmetatable(fs_scroll_container, {
__index = ctx._events,
__newindex = ctx._events
}),
__fs = ctx.__parent or ctx
__fs = ctx.__fs or ctx
}
e.__id = new_id()
setmetatable(e, fs_scroll_container)
@ -821,6 +884,10 @@ local function fs_scroll_container_end()
minetest.log("warn", "`scroll_container_end` has no scroll container to end; it will be ignored.")
return
end
-- If the container has a custom scrollbar, we add it _after_ the container's contents.
if ctx._scrollbar then
table.insert(ctx.__parent, ctx._scrollbar)
end
ctx = ctx.__parent
end
@ -858,7 +925,7 @@ local fs_group = {
fs_group.__index = fs_group
setmetatable(fs_group, {
__call = function(_, x, y, w, h)
local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__parent or ctx}
local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__fs or ctx}
setmetatable(e, fs_group)
table.insert(ctx, e)
ctx = e
@ -871,6 +938,19 @@ local function fs_group_end()
minetest.log("warn", "`group_end` has no group to end; it will be ignored.")
return
end
for i = 0, 1 do
local axis = i == 0 and "w" or "h"
if ctx[axis] == "auto" then
local size = 0
for i = 1, #ctx do
local child_size = tonumber(ctx[i][axis]) or 0
if child_size > size then
size = child_size
end
end
ctx[axis] = size
end
end
ctx = ctx.__parent
end
@ -894,16 +974,14 @@ local fs_row = {
-- Pass 1: Collect total sizing information to allow layout computation.
for i = 1, #e do
local c = e[i]
if not c._no_layout then
if c.w and not c._no_layout then
local ca, ca_flex = resolve_flex_layout_units(get(c, e._direction == "column" and "h" or "w"), axis_size)
if c.w then
if ca_flex then
flex_found = true
c.__flex = ca
total_grow = total_grow +ca
else
used_space = used_space +(ca or 0)
end
if ca_flex then
flex_found = true
c.__flex = ca
total_grow = total_grow +ca
else
used_space = used_space +(ca or 0)
end
end
end
@ -924,8 +1002,10 @@ local fs_row = {
if c.__flex then
c.__flex = c.__flex *grow_basis
current = current +c.__flex +e._gap
else
elseif c.w then
current = current +resolve_layout_units(get(c, e._direction == "column" and "h" or "w"), axis_size) +e._gap
else
current = current +e._gap
end
end
end
@ -956,9 +1036,9 @@ local fs_row = {
out[#out +1] = c:render(cx, cy, cw, ch)
elseif c.w then
if e._direction == "column" then
out[#out +1] = c:render(resolve_layout_units(get(c, "x"), w) +x, base +c.__flex_offset, resolve_layout_units(get(c, "w"), w), c.__flex)
out[#out +1] = c:render(resolve_layout_units(get(c, "x"), w) +x, base +c.__flex_offset, resolve_layout_units(get(c, "w"), w), c.__flex or resolve_layout_units(get(c, "h"), h))
else
out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y"), h) +y, c.__flex, resolve_layout_units(get(c, "h"), h))
out[#out +1] = c:render(base +c.__flex_offset, resolve_layout_units(get(c, "y"), h) +y, c.__flex or resolve_layout_units(get(c, "w"), w), resolve_layout_units(get(c, "h"), h))
end
elseif c.x then
if e._direction == "column" then
@ -987,7 +1067,7 @@ local fs_row = {
fs_row.__index = fs_row
setmetatable(fs_row, {
__call = function(_, x, y, w, h)
local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__parent or ctx}
local e = {x = x, y = y, w = w, h = h, _gap = 0, __parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__fs or ctx}
setmetatable(e, fs_row)
table.insert(ctx, e)
ctx = e
@ -1001,6 +1081,17 @@ local function fs_row_end()
minetest.log("warn", "`row_end` has no row to end; it will be ignored.")
return
end
local axis = ctx._direction == "column" and "w" or "h"
if ctx[axis] == "auto" then
local size = 0
for i = 1, #ctx do
local child_size = tonumber(ctx[i][axis]) or 0
if child_size > size then
size = child_size
end
end
ctx[axis] = size
end
ctx = ctx.__parent
end
@ -1157,15 +1248,7 @@ local Context = {
fs._ctx = e
for _, el in ipairs(fs) do
for _, x in pairs(el) do
local mt = getmetatable(x)
if mt == state or mt == DerivedState then
x._getters[e.id] = e
e._linked_states[x] = true
end
end
end
e:collect_state_depends()
e._events = fs._events
@ -1178,6 +1261,22 @@ local Context = {
minetest.show_formspec(e.target, e.id, str)
end
end,
get_states_from_elem = function(e, el)
for k, x in pairs(el) do
local mt = getmetatable(x)
if mt == state or mt == DerivedState then
x._getters[e.id] = e
e._linked_states[x] = true
elseif tonumber(k) then
e:get_states_from_elem(x)
end
end
end,
collect_state_depends = function(e)
for _, el in ipairs(e._window) do
e:get_states_from_elem(el)
end
end,
clear_state_bindings = function(e)
for x in pairs(e._linked_states) do
x._getters[e.id] = nil
@ -1361,6 +1460,9 @@ imfs = {
_contexts = contexts,
_inventories = inventories,
_new_id = new_id,
_unique_id = unique_id,
state = state,
derive = derive,
get_field = get,
@ -1374,7 +1476,7 @@ imfs = {
resolve_layout_units = resolve_layout_units,
resolve_flex_layout_units = resolve_flex_layout_units,
container_start = function()
local container = {__parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__parent or ctx}
local container = {__parent = ctx, _events = setmetatable({}, {__index = ctx._events, __newindex = ctx._events}), __fs = ctx.__fs or ctx}
table.insert(ctx, container)
ctx = container
end,
@ -1389,7 +1491,6 @@ imfs = {
style = fs_style,
tooltip = fs_tooltip,
_named_tooltip = fs_named_tooltip,
label = fs_label,
arealabel = fs_arealabel,
hypertext = fs_hypertext,