---@alias RuntimeLayoutAlign 'start'|'center'|'end' ---@class (exact) RuntimeLayoutItem ---@field node RuntimeNode ---@field marginLeft? number ---@field marginRight? number ---@field marginTop? number ---@field marginBottom? number ---@class RuntimeLayoutItemOpts ---@field margin? number ---@field mx? number ---@field my? number ---@field ml? number ---@field mr? number ---@field mt? number ---@field mb? number ---@field marginLeft? number ---@field marginRight? number ---@field marginTop? number ---@field marginBottom? number ---@class RuntimeLinearLayoutOpts ---@field x? number ---@field y? number ---@field width? number ---@field height? number ---@field gap? number ---@field align? RuntimeLayoutAlign ---@field padding? number ---@field paddingX? number ---@field paddingY? number ---@field px? number ---@field py? number ---@field paddingLeft? number ---@field paddingTop? number ---@class RuntimeBoxLayoutOpts: RuntimeLinearLayoutOpts ---@field rows? integer ---@field columns? integer ---@field cols? integer ---@field cellWidth? number ---@field cellHeight? number ---@field cellW? number ---@field cellH? number ---@field gapX? number ---@field gapY? number ---@field valign? RuntimeLayoutAlign ---@class RuntimeLayout local layout = {} ---@param value any ---@param fallback number ---@return number local function read_number(value, fallback) if type(value) == "number" then return value end return fallback end ---@param opts? table ---@param key string ---@return number local function spacing(opts, key) opts = opts or {} if key == "left" then return read_number(opts.paddingLeft, read_number(opts.paddingX, read_number(opts.px, read_number(opts.padding, 0)))) end if key == "top" then return read_number(opts.paddingTop, read_number(opts.paddingY, read_number(opts.py, read_number(opts.padding, 0)))) end return 0 end ---@param node RuntimeNode ---@param parent? string local function apply_parent(node, parent) if parent ~= nil and parent ~= "" then node.parent = parent elseif parent == "" then node.parent = nil end end ---@param axis_size number ---@param item_size number ---@param align RuntimeLayoutAlign ---@return number local function align_cross(axis_size, item_size, align) if align == "center" then return (axis_size - item_size) / 2 end if align == "end" then return axis_size - item_size end return 0 end ---@param value any ---@return RuntimeNode local function node_of(value) return value.node or value end ---@param value any ---@param key string ---@return number local function margin_of(value, key) local all = read_number(value.margin, 0) if key == "marginLeft" then return read_number(value.marginLeft, read_number(value.ml, read_number(value.mx, all))) end if key == "marginRight" then return read_number(value.marginRight, read_number(value.mr, read_number(value.mx, all))) end if key == "marginTop" then return read_number(value.marginTop, read_number(value.mt, read_number(value.my, all))) end if key == "marginBottom" then return read_number(value.marginBottom, read_number(value.mb, read_number(value.my, all))) end return 0 end ---@param node RuntimeNode ---@param opts? RuntimeLayoutItemOpts ---@return RuntimeLayoutItem function layout.item(node, opts) opts = opts or {} return { node = node, marginLeft = margin_of(opts, "marginLeft"), marginRight = margin_of(opts, "marginRight"), marginTop = margin_of(opts, "marginTop"), marginBottom = margin_of(opts, "marginBottom") } end ---@param origin RuntimePoint ---@param position RuntimePoint ---@return RuntimePoint function layout.local_position(origin, position) return { x = position.x - origin.x, y = position.y - origin.y } end ---@param parent? string ---@param items (RuntimeNode|RuntimeLayoutItem)[] ---@param opts? RuntimeLinearLayoutOpts ---@return RuntimeNode[] function layout.row(parent, items, opts) opts = opts or {} local cursor = read_number(opts.x, 0) + spacing(opts, "left") local base_y = read_number(opts.y, 0) + spacing(opts, "top") local gap = read_number(opts.gap, 0) local height = read_number(opts.height, 0) local align = opts.align or "start" local nodes = {} for index, item in ipairs(items) do local node = node_of(item) cursor = cursor + margin_of(item, "marginLeft") node.x = cursor local item_height = read_number(node.height, 0) node.y = base_y + align_cross(height, item_height, align) apply_parent(node, parent) cursor = cursor + read_number(node.width, 0) + margin_of(item, "marginRight") + gap nodes[index] = node end return nodes end ---@param parent? string ---@param items (RuntimeNode|RuntimeLayoutItem)[] ---@param opts? RuntimeLinearLayoutOpts ---@return RuntimeNode[] function layout.column(parent, items, opts) opts = opts or {} local cursor = read_number(opts.y, 0) + spacing(opts, "top") local base_x = read_number(opts.x, 0) + spacing(opts, "left") local gap = read_number(opts.gap, 0) local width = read_number(opts.width, 0) local align = opts.align or "start" local nodes = {} for index, item in ipairs(items) do local node = node_of(item) cursor = cursor + margin_of(item, "marginTop") node.y = cursor local item_width = read_number(node.width, 0) node.x = base_x + align_cross(width, item_width, align) apply_parent(node, parent) cursor = cursor + read_number(node.height, 0) + margin_of(item, "marginBottom") + gap nodes[index] = node end return nodes end ---@param parent? string ---@param items (RuntimeNode|RuntimeLayoutItem)[] ---@param opts? RuntimeLinearLayoutOpts ---@return RuntimeNode[] function layout.stack(parent, items, opts) opts = opts or {} local offset_x = read_number(opts.x, 0) + spacing(opts, "left") local offset_y = read_number(opts.y, 0) + spacing(opts, "top") local nodes = {} for index, item in ipairs(items) do local node = node_of(item) node.x = read_number(node.x, 0) + offset_x node.y = read_number(node.y, 0) + offset_y apply_parent(node, parent) nodes[index] = node end return nodes end ---@param parent? string ---@param items (RuntimeNode|RuntimeLayoutItem)[] ---@param opts? RuntimeBoxLayoutOpts ---@return RuntimeNode[] function layout.box(parent, items, opts) opts = opts or {} local count = #items local rows = math.floor(read_number(opts.rows, 0)) local columns = math.floor(read_number(opts.columns, read_number(opts.cols, 0))) if columns <= 0 and rows > 0 then columns = math.ceil(count / rows) end if columns <= 0 then columns = count > 0 and count or 1 end if rows <= 0 then rows = math.ceil(count / columns) end local gap_x = read_number(opts.gapX, read_number(opts.gap, 0)) local gap_y = read_number(opts.gapY, read_number(opts.gap, 0)) local origin_x = read_number(opts.x, 0) + spacing(opts, "left") local origin_y = read_number(opts.y, 0) + spacing(opts, "top") local cell_w = read_number(opts.cellWidth, read_number(opts.cellW, 0)) local cell_h = read_number(opts.cellHeight, read_number(opts.cellH, 0)) local width = read_number(opts.width, 0) local height = read_number(opts.height, 0) if cell_w <= 0 and width > 0 then cell_w = (width - gap_x * (columns - 1)) / columns end if cell_h <= 0 and height > 0 then cell_h = (height - gap_y * (rows - 1)) / rows end local align = opts.align or "start" local valign = opts.valign or "start" local nodes = {} for index, item in ipairs(items) do local node = node_of(item) local item_width = read_number(node.width, cell_w) local item_height = read_number(node.height, cell_h) local effective_cell_w = cell_w > 0 and cell_w or item_width local effective_cell_h = cell_h > 0 and cell_h or item_height local col = (index - 1) % columns local row = math.floor((index - 1) / columns) local margin_left = margin_of(item, "marginLeft") local margin_right = margin_of(item, "marginRight") local margin_top = margin_of(item, "marginTop") local margin_bottom = margin_of(item, "marginBottom") local inner_w = effective_cell_w - margin_left - margin_right local inner_h = effective_cell_h - margin_top - margin_bottom node.x = origin_x + col * (effective_cell_w + gap_x) + margin_left + align_cross(inner_w, item_width, align) node.y = origin_y + row * (effective_cell_h + gap_y) + margin_top + align_cross(inner_h, item_height, valign) apply_parent(node, parent) nodes[index] = node end return nodes end return layout