Fix a few bugs and add automatic sizing for layout containers.
This commit is contained in:
parent
be73993547
commit
969030a026
2 changed files with 153 additions and 48 deletions
|
|
@ -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.
|
||||
|
|
|
|||
197
init.lua
197
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue