293 lines
8.4 KiB
Lua
293 lines
8.4 KiB
Lua
---@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
|