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,