xurfer/src/lovr/lib/iui/layout.lua

453 lines
12 KiB
Lua
Raw Normal View History

2026-06-06 12:35:31 +02:00
local currentPath = (...):match('(.-)[^%./]+$')
--- @class IUILib
local iui = require(currentPath .. "iui")
--- @alias IUILayoutRowKind "fixed" | "dynamic" | "intrinsic" | "mixed"
--- @class (exact) IUILayoutPanel
--- @field x number
--- @field y number
--- @field w number
--- @field h number
--- @field margin number
--- @field rowHeight number
--- @field rowKind IUILayoutRowKind
--- @field rowData any
--- @field intrinsicDefault? number
--- @field intrinsicLimit? number
--- @field rowY number
--- @field columnX number
--- @field wantsIntrinsicWidth boolean
--- @field wantsIntrinsicHeight boolean
--- @field intrinsicWidth? number
--- @field intrinsicHeight? number
--- @field columnIndex number
--- @field columnCountCache? number
--- @field columnBoundsCache? IUIColumnXBounds[]
--- @field zStackCount? number
--- @field contentWidth number
--- @field contentHeight number
--- @class (exact) IUIColumnXBounds
--- @field x number
--- @field w number
--- @class (exact) IUILayoutColumn
--- @field kind "fixed" | "dynamic"
--- @field size number
--- @class IUILayout
--- @field windowWidth number
--- @field windowHeight number
local layout = {}
--- @type IUILayoutPanel[]
local panels = {}
function layout.beginFrame()
end
function layout.endFrame()
if #panels ~= 0 then
error("Unbalanced panel stack: expected 0, got " .. #panels)
end
end
--- @return number rowHeight
function layout.getDefaultRowHeight()
local padding = iui.style["padding"]
local fontHeight = math.floor(iui.style["font"]:getHeight())
local rowHeight = fontHeight + padding * 2
return rowHeight
end
--- @param panel IUILayoutPanel
local function resetColumnBoundsCache(panel)
if panel.columnBoundsCache ~= nil then
iui.pool.put(panel.columnBoundsCache)
panel.columnBoundsCache = nil
end
end
--- @param x number
--- @param y number
--- @param w number
--- @param h number
--- @param margin? number
--- @return IUILayoutPanel
function layout.beginPanel(x, y, w, h, margin)
margin = margin or iui.style["margin"]
local rowHeight = layout.getDefaultRowHeight()
--- @type IUILayoutPanel
local panel = iui.pool.get("layout_panel")
panel.x = x
panel.y = y
panel.w = w
panel.h = h
panel.margin = margin
panel.rowHeight = rowHeight
panel.rowY = margin
panel.columnX = margin
panel.wantsIntrinsicWidth = false
panel.wantsIntrinsicHeight = false
panel.columnIndex = 1
panel.rowKind = "dynamic"
panel.rowData = 1
panel.intrinsicDefault = nil
panel.intrinsicLimit = nil
panel.contentWidth = 0
panel.contentHeight = 0
panel.intrinsicWidth = nil
panel.intrinsicHeight = nil
panel.columnCountCache = nil
panel.zStackCount = nil
resetColumnBoundsCache(panel)
table.insert(panels, panel)
return panel
end
--- @param advance? boolean
function layout.endPanel(advance)
local panel = layout.getPanel()
if panel.zStackCount ~= nil and panel.zStackCount ~= 0 then
error(
"Unbalanced panel zstack count: expected 0, got " ..
panel.zStackCount
)
end
table.remove(panels)
iui.pool.put(panel)
if advance == true or (#panels > 0 and advance ~= false) then
iui.layout.advance()
end
end
--- @param index? number
function layout.getPanel(index)
return panels[index or #panels]
end
--- @return number w, number h
function layout.getContentSize()
local panel = layout.getPanel()
return panel.contentWidth, panel.contentHeight
end
--- @return boolean wantsIntrinsicWidth, boolean wantsIntrinsicHeight
function layout.getWantsIntrinsic()
local panel = layout.getPanel()
return panel.wantsIntrinsicWidth, panel.wantsIntrinsicHeight
end
--- @param w number
function layout.setIntrinsicWidth(w)
local panel = layout.getPanel()
panel.intrinsicWidth = w
end
--- @param h number
function layout.setIntrinsicHeight(h)
local panel = layout.getPanel()
panel.intrinsicHeight = h
end
--- @param rowHeight? number
--- @return IUILayoutPanel
local function beginRowCommon(rowHeight)
if rowHeight == nil then
rowHeight = layout.getDefaultRowHeight()
end
local panel = layout.getPanel()
if panel.columnIndex ~= 1 then
panel.columnIndex = 1
panel.rowY = panel.rowY + panel.rowHeight + iui.style["spacing"]
end
panel.wantsIntrinsicHeight = false
panel.wantsIntrinsicWidth = false
panel.intrinsicWidth = nil
panel.intrinsicHeight = nil
panel.rowHeight = rowHeight
panel.columnCountCache = nil
resetColumnBoundsCache(panel)
return panel
end
--- @param count? number
--- @param rowHeight? number
function layout.beginDynamicRow(count, rowHeight)
local panel = beginRowCommon(rowHeight)
panel.rowKind = "dynamic"
panel.rowData = count or 1
end
--- @param size number
--- @param rowHeight? number
function layout.beginFixedRow(size, rowHeight)
local panel = beginRowCommon(rowHeight)
panel.rowKind = "fixed"
panel.rowData = size
end
--- @param default? number
--- @param limit? number
--- @param rowHeight? number
function layout.beginIntrinsicRow(default, limit, rowHeight)
local panel = beginRowCommon(rowHeight)
panel.rowKind = "intrinsic"
panel.wantsIntrinsicWidth = true
panel.intrinsicDefault = default
panel.intrinsicLimit = limit
end
--- @param columns IUILayoutColumn[]
--- @param rowHeight? number
function layout.beginMixedRow(columns, rowHeight)
local panel = beginRowCommon(rowHeight)
panel.rowKind = "mixed"
panel.rowData = columns
end
--- Begins a row that causes the next widget to fill the remainder of the
--- current panel.
function layout.fillPanel()
local panel = layout.getPanel()
if panel.columnIndex ~= 1 then
panel.columnIndex = 1
panel.rowY = panel.rowY + panel.rowHeight + iui.style["spacing"]
end
local h = panel.h - (panel.rowY + panel.margin)
layout.beginDynamicRow(1, h)
end
function layout.beginZStack()
local panel = layout.getPanel()
panel.zStackCount = (panel.zStackCount or 0) + 1
end
--- @param advance? boolean
function layout.endZStack(advance)
local panel = layout.getPanel()
if panel.zStackCount == nil or panel.zStackCount == 0 then
error("Attempt to end ZStack without one on panel")
end
panel.zStackCount = panel.zStackCount - 1
if advance then
iui.layout.advance()
end
end
--- @return number x, number y, number w, number h
function layout.getBounds()
local panel = layout.getPanel()
local y = panel.rowY
local h = panel.rowHeight
local margin = panel.margin
local spacing = iui.style["spacing"]
local x, w = 0, 0
local rowKind = panel.rowKind
if rowKind == "fixed" then
w = panel.rowData
x = margin + (panel.columnIndex - 1) * (w + spacing)
elseif rowKind == "dynamic" then
local count = panel.rowData
w = (panel.w - (2 * margin + (count - 1) * spacing)) / count
x = margin + (panel.columnIndex - 1) * (w + spacing)
local maxX = iui.utils.round(x + w)
x = iui.utils.round(x)
w = maxX - x
elseif rowKind == "intrinsic" then
local edge = panel.w - panel.margin
x = panel.columnX
w = panel.intrinsicWidth or panel.intrinsicDefault or (edge - x)
if x + w > edge and panel.columnIndex ~= 1 then
x = panel.margin
panel.columnIndex = 1
panel.rowY = panel.rowY + panel.rowHeight + spacing
y = panel.rowY
end
panel.intrinsicWidth = w
elseif rowKind == "mixed" then
if panel.columnBoundsCache == nil then
--- The sum of dynamic column `size` values.
local dynamicSum = 0
--- The amount of space available to dynamic columns. This is
--- whatever's left over after the margin, spacing, and fixed
--- columns are accounted for.
local dynamicSpace = panel.w - margin * 2
--- @type IUILayoutColumn[]
local columns = panel.rowData
-- Do a first pass through the columns, calculating the dynamic sum
-- and space values.
for idx = 1, #columns do
if idx > 1 then
dynamicSpace = dynamicSpace - spacing
end
local column = columns[idx]
if column.kind == "dynamic" then
dynamicSum = dynamicSum + column.size
elseif column.kind == "fixed" then
dynamicSpace = dynamicSpace - column.size
end
end
--- @type IUIColumnXBounds[]
local bounds = iui.pool.get("layout_column_bounds_cache")
for index, entry in ipairs(bounds) do
iui.pool.put(entry)
bounds[index] = nil
end
local head = margin
for idx = 1, #columns do
local column = columns[idx]
local colX = head
local colW = 0
if column.kind == "fixed" then
colW = column.size
elseif column.kind == "dynamic" then
colW = dynamicSpace * (column.size / dynamicSum)
end
head = head + colW + spacing
local maxX = colX + colW
colX = iui.utils.round(colX)
maxX = iui.utils.round(maxX)
colW = maxX - colX
--- @type IUIColumnXBounds
local value = iui.pool.get("layout_column_bounds_entry")
value.x = colX
value.w = colW
table.insert(bounds, value)
end
panel.columnBoundsCache = bounds
end
local bounds = panel.columnBoundsCache[panel.columnIndex]
x = bounds.x
w = bounds.w
end
x = panel.x + x
y = panel.y + y
panel.contentWidth = math.max(panel.contentWidth, x + w + panel.margin - panel.x)
panel.contentHeight = math.max(panel.contentHeight, y + h + panel.margin - panel.y)
return x, y, w, h
end
--- @param index? number
--- @return number x, number y, number w, number h
function layout.getPanelBounds(index)
local panel = layout.getPanel(index)
return panel.x, panel.y, panel.w, panel.h
end
--- @param x? number
--- @param y? number
--- @return boolean
function layout.containsPoint(x, y)
x = x or iui.input.mouse.x
y = y or iui.input.mouse.y
local bx, by, bw, bh = layout.getBounds()
if not iui.utils.rectContains(bx, by, bw, bh, x, y) then
return false
end
local cx, cy, cw, ch = iui.draw.getClipBounds()
if cx then
if not iui.utils.rectContains(
cx, cy --[[@as any]], cw --[[@as any]], ch --[[@as any]], x, y
) then
return false
end
end
return true
end
function layout.spacer()
layout.advance()
end
function layout.advance()
local panel = layout.getPanel()
if panel.zStackCount ~= nil and panel.zStackCount ~= 0 then
return
end
local spacing = iui.style["spacing"]
local rowKind = panel.rowKind
local count = panel.columnCountCache
if count == nil then
if rowKind == "fixed" then
local num = panel.w + spacing - panel.margin * 2
local den = panel.rowData + spacing
count = math.floor(num / den)
if count < 1 then
count = 1
end
elseif rowKind == "dynamic" then
count = panel.rowData
elseif rowKind == "mixed" then
count = #panel.rowData
end
panel.columnCountCache = count
end
if rowKind == "intrinsic" then
panel.columnIndex = panel.columnIndex + 1
panel.columnX = panel.columnX + spacing + (panel.intrinsicWidth or 0)
panel.intrinsicWidth = nil
panel.intrinsicHeight = nil
if panel.intrinsicLimit and panel.columnIndex >= panel.intrinsicLimit then
panel.columnIndex = 1
panel.columnX = panel.margin
panel.rowY = panel.rowY + panel.rowHeight + spacing
end
elseif panel.columnIndex == count then
panel.columnIndex = 1
panel.rowY = panel.rowY + panel.rowHeight + spacing
else
panel.columnIndex = panel.columnIndex + 1
end
end
iui.layout = layout