From 969030a0263d0593a8fa7ffc5cd7a23b9ad6d426 Mon Sep 17 00:00:00 2001 From: Signal Date: Sat, 7 Mar 2026 16:30:53 -0500 Subject: [PATCH] Fix a few bugs and add automatic sizing for layout containers. --- README.md | 4 ++ init.lua | 197 +++++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 153 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index a115f0e..0f6a93f 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ Besides these basic containers, however, imfs also provides several layouting co * `imfs.row`: A flex row. This automatically positions elements within it based on the provided gap and alignment. Child elements may specify their width as a grow ratio like `"1x"` to dynamically resize to fill any empty space in the row. Higher grow ratios will cause the element to take up a larger portion of the available space compared to other dynamically sized elements. * `imfs.column`: The same as `imfs.row`, but in the vertical direction. +Inside all containers (including the window), it is possible to specify properties of an element in terms of its parent's size, rather than with absolute formspec units. This is done by passing a percent string (like `"100%"`) instead of a number to the element's constructor. Percentage units may also optionally include a fixed offset to be applied to the computed percentage by following the percent with `+` or `-` and a number (like `"100% - 0.5"`). + +Groups and flex containers may specify their width or height as `"auto"` to automatically compute their width or height based on the accumulated size of their children (note that for flex containers this only works on the secondary axis). Using percentage units relative to such a computed dimension, while technically possible, will likely cause problems, as doing so results in a circular dpendency (which is broken internally by having the container interpret relative sizes as zero for the purposes of `"auto"`). + To create a custom layouting container, the procedure is essentially as follows: 1. Create a Lua class for the container. 2. In the class constructor, call `imfs.container_start(container)` with the newly created container instance. diff --git a/init.lua b/init.lua index d5c3b1e..e805d9d 100644 --- a/init.lua +++ b/init.lua @@ -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,