-- -- Flow: Layout expansion/stretching 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 . -- local DEFAULT_SPACING, LABEL_HEIGHT, get_and_fill_in_sizes, invisible_elems = ... local align_types = {} function align_types.fill(node, x, w, extra_space) -- Special cases if node.type == "list" or node.type == "checkbox" or node._label_hack then return align_types.centre(node, x, w, extra_space) elseif node.type == "label" then if x == "y" then node.y = node.y + extra_space / 2 return end -- Hack node.type = "container" -- Reset bgimg, some games apply styling to all image_buttons inside -- the formspec prepend node[1] = { type = "style", -- MT 5.1.0 only supports one style selector selectors = {"_#"}, -- bgimg_pressed is included for 5.1.0 support -- bgimg_hovered is unnecessary as it was added in 5.2.0 (which -- also adds support for :hovered and :pressed) props = {bgimg = "", bgimg_pressed = ""}, } -- Use the newer pressed selector as well in case the deprecated one is -- removed node[2] = { type = "style", selectors = {"_#:hovered", "_#:pressed"}, props = {bgimg = ""}, } node[3] = { type = "image_button", texture_name = "blank.png", drawborder = false, x = 0, y = 0, w = node.w + extra_space, h = node.h, name = "_#", label = node.label, style = node.style, } -- Overlay button to prevent clicks from doing anything node[4] = { type = "image_button", texture_name = "blank.png", drawborder = false, x = 0, y = 0, w = node.w + extra_space, h = node.h, name = "_#", label = "", } node.y = node.y - (node._flow_font_height or LABEL_HEIGHT) / 2 node.label = nil node.style = nil node._label_hack = true assert(#node == 4) end if node[w] then node[w] = node[w] + extra_space else core.log("warning", "[flow] Unknown element: \"" .. tostring(node.type) .. "\". Please make sure that flow is " .. "up-to-date and the element has a size set (if required).") node[w] = extra_space end end function align_types.start() -- No alterations required end -- "end" is a Lua keyword align_types["end"] = function(node, x, _, extra_space) node[x] = node[x] + extra_space end -- Aliases for convenience align_types.top, align_types.bottom = align_types.start, align_types["end"] align_types.left, align_types.right = align_types.start, align_types["end"] function align_types.centre(node, x, w, extra_space) if node.type == "label" then return align_types.fill(node, x, w, extra_space) elseif node.type == "checkbox" and x == "y" then node.y = (node.h + extra_space) / 2 return end node[x] = node[x] + extra_space / 2 end align_types.center = align_types.centre -- Try to guess at what the best expansion setting is local auto_align_centre = { image = true, animated_image = true, model = true, item_image_button = true } function align_types.auto(node, x, w, extra_space, cross) if auto_align_centre[node.type] then return align_types.centre(node, x, w, extra_space) end if x == "y" or (node.type ~= "label" and node.type ~= "checkbox") or (node.expand and not cross) then return align_types.fill(node, x, w, extra_space) end end local expand_child_boxes local function expand(box) local x, w, align_h, y, h, align_v local box_type = box.type if box_type == "hbox" then x, w, align_h, y, h, align_v = "x", "w", "align_h", "y", "h", "align_v" elseif box_type == "vbox" then x, w, align_h, y, h, align_v = "y", "h", "align_v", "x", "w", "align_h" elseif box_type == "stack" or (box_type == "padding" and box[1].expand) then box.type = "container" box._enable_bgimg_hack = true for _, node in ipairs(box) do if not invisible_elems[node.type] then local width, height = node.w or 0, node.h or 0 if node.type == "list" then width, height = get_and_fill_in_sizes(node) end local padding_x2 = (node.padding or 0) * 2 align_types[node.align_h or "auto"](node, "x", "w", box.w - width - padding_x2) align_types[node.align_v or "auto"](node, "y", "h", box.h - height - padding_x2 - (node._padding_top or 0)) end end return expand_child_boxes(box) elseif box_type == "container" or box_type == "scroll_container" then for _, node in ipairs(box) do if node.x == 0 and node.expand and box.w then node.w = box.w end expand(node) end return elseif box_type == "padding" then box.type = "container" return expand_child_boxes(box) else return end box.type = "container" -- Calculate the amount of free space and put expand nodes into a table local box_h = box[h] local free_space = box[w] local expandable = {} local expand_count = 0 local first = true for i, node in ipairs(box) do local width, height = node[w] or 0, node[h] or 0 if not invisible_elems[node.type] then if first then first = false else free_space = free_space - (box.spacing or DEFAULT_SPACING) end if node.type == "list" then width, height = get_and_fill_in_sizes(node) if y == "x" then width, height = height, width end end free_space = free_space - width - (node.padding or 0) * 2 - (y == "x" and node._padding_top or 0) if node.expand then expandable[node] = i expand_count = expand_count + 1 elseif node.type == "label" and align_h == "align_h" then -- Use the image_button hack even if the label isn't expanded align_types[node.align_h or "auto"](node, "x", "w", 0) end -- Nodes are expanded in the other direction no matter what their -- expand setting is if box_h > height or (node.type == "label" and align_v == "align_h") then align_types[node[align_v] or "auto"](node, y, h, box_h - height - (node.padding or 0) * 2 - (y == "y" and node._padding_top or 0), true) end end end -- If there's any free space then expand the nodes to fit if free_space > 0 then local extra_space = free_space / expand_count for node, node_idx in pairs(expandable) do align_types[node[align_h] or "auto"](node, x, w, extra_space) -- Shift other elements along for j = node_idx + 1, #box do if box[j][x] then box[j][x] = box[j][x] + extra_space end end end elseif align_h == "align_h" then -- Use the image_button hack on labels regardless of the amount of free -- space if this is in a horizontal box. for node in pairs(expandable) do if node.type == "label" then align_types[node.align_h or "auto"](node, "x", "w", 0) end end end expand_child_boxes(box) end function expand_child_boxes(box) -- Recursively expand and remove any invisible nodes for i = #box, 1, -1 do local node = box[i] -- node.visible ~= nil and not node.visible if node.visible == false then -- There's no need to try and expand anything inside invisible -- nodes since it won't affect the overall size. table.remove(box, i) else expand(node) end end end return expand