reversion

This commit is contained in:
Michael Freno
2025-09-21 11:15:16 -04:00
parent b2aff5c38c
commit 4909e03a2c

View File

@@ -337,7 +337,6 @@ end
-- Element Object -- Element Object
-- ==================== -- ====================
---@class Element ---@class Element
---@field id string?
---@field autosizing {width:boolean, height:boolean} -- Whether the element should automatically size to fit its children ---@field autosizing {width:boolean, height:boolean} -- Whether the element should automatically size to fit its children
---@field x number -- X coordinate of the element ---@field x number -- X coordinate of the element
---@field y number -- Y coordinate of the element ---@field y number -- Y coordinate of the element
@@ -369,229 +368,11 @@ end
---@field transform TransformProps -- Transform properties for animations and styling ---@field transform TransformProps -- Transform properties for animations and styling
---@field transition TransitionProps -- Transition settings for animations ---@field transition TransitionProps -- Transition settings for animations
---@field callback function? -- Callback function for click events ---@field callback function? -- Callback function for click events
-- Unit parsing and viewport calculations
local Units = {}
--- Parse a unit value (string or number) into value and unit type
---@param value string|number
---@return number, string -- Returns numeric value and unit type ("px", "%", "vw", "vh", "vmin", "vmax")
function Units.parse(value)
if type(value) == "number" then
return value, "px"
end
if type(value) ~= "string" then
error("Unit value must be a string or number, got " .. type(value))
end
-- Match number followed by optional unit
local numStr, unit = value:match("^([%-]?[%d%.]+)(.*)$")
if not numStr then
error("Invalid unit format: " .. value)
end
local num = tonumber(numStr)
if not num then
error("Invalid numeric value: " .. numStr)
end
-- Default to pixels if no unit specified
if unit == "" then
unit = "px"
end
-- Validate unit type
local validUnits = { px = true, ["%"] = true, vw = true, vh = true, vmin = true, vmax = true }
if not validUnits[unit] then
error("Unsupported unit type: " .. unit)
end
return num, unit
end
--- Convert relative units to pixels based on viewport and parent dimensions
---@param value number
---@param unit string
---@param viewportWidth number
---@param viewportHeight number
---@param parentSize number? -- Required for percentage units
---@return number -- Pixel value
function Units.resolve(value, unit, viewportWidth, viewportHeight, parentSize)
if unit == "px" then
return value
elseif unit == "%" then
if not parentSize then
error("Percentage units require parent dimension")
end
return (value / 100) * parentSize
elseif unit == "vw" then
return (value / 100) * viewportWidth
elseif unit == "vh" then
return (value / 100) * viewportHeight
elseif unit == "vmin" then
return (value / 100) * math.min(viewportWidth, viewportHeight)
elseif unit == "vmax" then
return (value / 100) * math.max(viewportWidth, viewportHeight)
else
error("Unknown unit type: " .. unit)
end
end
--- Cache viewport dimensions for performance
local ViewportCache = {
width = 0,
height = 0,
lastUpdate = 0,
}
-- Performance optimization: Resize state management
local ResizeState = {
isResizing = false,
batchedElements = {},
resizeDebounceTime = 0.016, -- 16ms for 60fps
lastResizeTime = 0,
resizeTimer = nil,
}
--- Get current viewport dimensions (cached for performance)
---@return number, number -- width, height
function Units.getViewport()
-- Update cache every frame to detect window resize
local currentTime = love.timer.getTime()
if currentTime ~= ViewportCache.lastUpdate then
ViewportCache.width, ViewportCache.height = love.window.getMode()
ViewportCache.lastUpdate = currentTime
end
return ViewportCache.width, ViewportCache.height
end
--- Performance: Start batch resize operation
---@param newGameWidth number
---@param newGameHeight number
function Units.startBatchResize(newGameWidth, newGameHeight)
ResizeState.isResizing = true
ResizeState.batchedElements = {}
ResizeState.lastResizeTime = love.timer.getTime()
-- Update viewport cache once for the entire batch
ViewportCache.width = newGameWidth
ViewportCache.height = newGameHeight
ViewportCache.lastUpdate = ResizeState.lastResizeTime
end
--- Performance: Add element to batch resize
---@param element table
function Units.addToBatchResize(element)
if ResizeState.isResizing then
table.insert(ResizeState.batchedElements, element)
element.isDirty = true
end
end
--- Performance: Complete batch resize operation
function Units.completeBatchResize()
if not ResizeState.isResizing then
return
end
-- Process all batched elements in a single pass
for _, element in ipairs(ResizeState.batchedElements) do
if element.isDirty then
element:recalculateUnits()
element.needsLayout = true
element.isDirty = false
end
end
-- Perform layout updates in a second pass to avoid redundant calculations
for _, element in ipairs(ResizeState.batchedElements) do
if element.needsLayout then
element:layoutChildren()
element.needsLayout = false
end
end
-- Reset batch state
ResizeState.isResizing = false
ResizeState.batchedElements = {}
end
--- Performance: Check if resize should be debounced
---@return boolean
function Units.shouldDebounceResize()
local currentTime = love.timer.getTime()
return (currentTime - ResizeState.lastResizeTime) < ResizeState.resizeDebounceTime
end
--- Resolve units for spacing properties (padding, margin) that can have top, right, bottom, left, vertical, horizontal
---@param spacingProps table?
---@param parentWidth number
---@param parentHeight number
---@return table -- Resolved spacing with top, right, bottom, left in pixels
function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
if not spacingProps then
return { top = 0, right = 0, bottom = 0, left = 0 }
end
local viewportWidth, viewportHeight = Units.getViewport()
local result = {}
-- Handle shorthand properties first
local vertical = spacingProps.vertical
local horizontal = spacingProps.horizontal
if vertical then
if type(vertical) == "string" then
local value, unit = Units.parse(vertical)
vertical = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
else
-- Numeric value, use as-is
vertical = vertical
end
end
if horizontal then
if type(horizontal) == "string" then
local value, unit = Units.parse(horizontal)
horizontal = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
else
-- Numeric value, use as-is
horizontal = horizontal
end
end
-- Handle individual sides
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
local value = spacingProps[side]
if value then
if type(value) == "string" then
local numValue, unit = Units.parse(value)
local parentSize = (side == "top" or side == "bottom") and parentHeight or parentWidth
result[side] = Units.resolve(numValue, unit, viewportWidth, viewportHeight, parentSize)
else
-- Numeric value, use as-is
result[side] = value
end
else
-- Use fallbacks
if side == "top" or side == "bottom" then
result[side] = vertical or 0
else
result[side] = horizontal or 0
end
end
end
return result
end
local Element = {} local Element = {}
Element.__index = Element Element.__index = Element
---@class ElementProps ---@class ElementProps
---@field parent Element? -- Parent element for hierarchical structure ---@field parent Element? -- Parent element for hierarchical structure
---@field id string?
---@field x number? -- X coordinate of the element (default: 0) ---@field x number? -- X coordinate of the element (default: 0)
---@field y number? -- Y coordinate of the element (default: 0) ---@field y number? -- Y coordinate of the element (default: 0)
---@field z number? -- Z-index for layering (default: 0) ---@field z number? -- Z-index for layering (default: 0)
@@ -627,7 +408,6 @@ local ElementProps = {}
function Element.new(props) function Element.new(props)
local self = setmetatable({}, Element) local self = setmetatable({}, Element)
self.children = {} self.children = {}
self.id = props.id or ""
self.callback = props.callback self.callback = props.callback
------ add non-hereditary ------ ------ add non-hereditary ------
@@ -650,294 +430,111 @@ function Element.new(props)
self.opacity = props.opacity or 1 self.opacity = props.opacity or 1
self.text = props.text self.text = props.text
self.textSize = props.textSize or 12 self.textSize = props.textSize
self.textAlign = props.textAlign or TextAlign.START self.textAlign = props.textAlign or TextAlign.START
--- self positioning --- --- self positioning ---
local viewportWidth, viewportHeight = Units.getViewport() self.padding = props.padding
local containerWidth = self.parent and self.parent.width or viewportWidth and {
local containerHeight = self.parent and self.parent.height or viewportHeight top = props.padding.top or props.padding.vertical or 0,
right = props.padding.right or props.padding.horizontal or 0,
bottom = props.padding.bottom or props.padding.vertical or 0,
left = props.padding.left or props.padding.horizontal or 0,
}
or {
top = 0,
right = 0,
bottom = 0,
left = 0,
}
self.padding = Units.resolveSpacing(props.padding, containerWidth, containerHeight) self.margin = props.margin
self.margin = Units.resolveSpacing(props.margin, containerWidth, containerHeight) and {
top = props.margin.top or props.margin.vertical or 0,
right = props.margin.right or props.margin.horizontal or 0,
bottom = props.margin.bottom or props.margin.vertical or 0,
left = props.margin.left or props.margin.horizontal or 0,
}
or {
top = 0,
right = 0,
bottom = 0,
left = 0,
}
---- Sizing ---- ---- Sizing ----
local gw, gh = love.window.getMode() local gw, gh = love.window.getMode()
self.prevGameSize = { width = gw, height = gh } self.prevGameSize = { width = gw, height = gh }
self.autosizing = { width = false, height = false } self.autosizing = { width = false, height = false }
-- Store unit specifications for responsive behavior
self.units = {
width = { value = nil, unit = "px" },
height = { value = nil, unit = "px" },
x = { value = nil, unit = "px" },
y = { value = nil, unit = "px" },
textSize = { value = nil, unit = "px" },
padding = {
top = { value = nil, unit = "px" },
right = { value = nil, unit = "px" },
bottom = { value = nil, unit = "px" },
left = { value = nil, unit = "px" },
},
margin = {
top = { value = nil, unit = "px" },
right = { value = nil, unit = "px" },
bottom = { value = nil, unit = "px" },
left = { value = nil, unit = "px" },
},
}
-- Performance optimization: dirty flag for resize operations
self.isDirty = false
self.needsLayout = false
if props.w then if props.w then
if type(props.w) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.w)
self.units.width = { value = value, unit = unit }
-- Resolve to pixels immediately
local viewportWidth, viewportHeight = Units.getViewport()
local parentWidth = self.parent and self.parent.width or viewportWidth
self.width = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
else
-- Handle numeric values (backward compatibility)
self.width = props.w self.width = props.w
self.units.width = { value = props.w, unit = "px" }
end
else else
self.autosizing.width = true self.autosizing.width = true
self.width = self:calculateAutoWidth() self.width = self:calculateAutoWidth()
end end
if props.h then if props.h then
if type(props.h) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.h)
self.units.height = { value = value, unit = unit }
-- Resolve to pixels immediately
local viewportWidth, viewportHeight = Units.getViewport()
local parentHeight = self.parent and self.parent.height or viewportHeight
self.height = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
else
-- Handle numeric values (backward compatibility)
self.height = props.h self.height = props.h
self.units.height = { value = props.h, unit = "px" }
end
else else
self.autosizing.height = true self.autosizing.height = true
self.height = self:calculateAutoHeight() self.height = self:calculateAutoHeight()
end end
--- child positioning --- --- child positioning ---
if props.gap then self.gap = props.gap or 10
if type(props.gap) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.gap)
self.units.gap = { value = value, unit = unit }
local viewportWidth, viewportHeight = Units.getViewport()
local containerSize = (self.flexDirection == FlexDirection.HORIZONTAL)
and (self.parent and self.parent.width or viewportWidth)
or (self.parent and self.parent.height or viewportHeight)
self.gap = Units.resolve(value, unit, viewportWidth, viewportHeight, containerSize)
else
-- Handle numeric values (backward compatibility)
self.gap = props.gap
self.units.gap = { value = props.gap, unit = "px" }
end
else
self.gap = 10
self.units.gap = { value = 10, unit = "px" }
end
-- Store original values for responsive scaling
if props.textSize then
if type(props.textSize) == "string" then
local value, unit = Units.parse(props.textSize)
self.units.textSize = { value = value, unit = unit }
else
self.units.textSize = { value = props.textSize, unit = "px" }
end
end
-- Store original spacing values for scaling
if props.padding then
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
if props.padding[side] then
if type(props.padding[side]) == "string" then
local value, unit = Units.parse(props.padding[side])
self.units.padding[side] = { value = value, unit = unit }
else
self.units.padding[side] = { value = props.padding[side], unit = "px" }
end
end
end
end
if props.margin then
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
if props.margin[side] then
if type(props.margin[side]) == "string" then
local value, unit = Units.parse(props.margin[side])
self.units.margin[side] = { value = value, unit = unit }
else
self.units.margin[side] = { value = props.margin[side], unit = "px" }
end
end
end
end
------ add hereditary ------ ------ add hereditary ------
if props.parent == nil then if props.parent == nil then
table.insert(Gui.topElements, self) table.insert(Gui.topElements, self)
-- Handle x position with units self.x = props.x or 0
if props.x then self.y = props.y or 0
if type(props.x) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.x)
self.units.x = { value = value, unit = unit }
local viewportWidth, viewportHeight = Units.getViewport()
self.x = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
else
-- Handle numeric values (backward compatibility)
self.x = props.x
self.units.x = { value = props.x, unit = "px" }
end
else
self.x = 0
end
-- Handle y position with units
if props.y then
if type(props.y) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.y)
self.units.y = { value = value, unit = unit }
local viewportWidth, viewportHeight = Units.getViewport()
self.y = Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
else
-- Handle numeric values (backward compatibility)
self.y = props.y
self.units.y = { value = props.y, unit = "px" }
end
else
self.y = 0
end
self.z = props.z or 0 self.z = props.z or 0
self.textColor = props.textColor or Color.new(0, 0, 0, 1) self.textColor = props.textColor or Color.new(0, 0, 0, 1)
-- Set positioning - top level elements are always absolute -- Track if positioning was explicitly set
self.positioning = props.positioning or Positioning.ABSOLUTE if props.positioning then
-- Set explicitlyAbsolute flag for top-level elements self.positioning = props.positioning
if props.positioning == Positioning.ABSOLUTE then self._originalPositioning = props.positioning
self.explicitlyAbsolute = true -- User explicitly requested absolute positioning self._explicitlyAbsolute = (props.positioning == Positioning.ABSOLUTE)
elseif props.positioning == Positioning.FLEX then
self.explicitlyAbsolute = false -- User explicitly requested flex container
else else
self.explicitlyAbsolute = false -- Default positioning, not explicitly requested self.positioning = Positioning.ABSOLUTE
self._originalPositioning = nil -- No explicit positioning
self._explicitlyAbsolute = false
end end
else else
self.parent = props.parent self.parent = props.parent
-- Set positioning based on user's explicit choice or default -- Set positioning first and track if explicitly set
self._originalPositioning = props.positioning -- Track original intent
if props.positioning == Positioning.ABSOLUTE then if props.positioning == Positioning.ABSOLUTE then
self.positioning = Positioning.ABSOLUTE self.positioning = Positioning.ABSOLUTE
self.explicitlyAbsolute = true -- User explicitly requested absolute positioning self._explicitlyAbsolute = true -- Explicitly set to absolute by user
elseif props.positioning == Positioning.FLEX then elseif props.positioning == Positioning.FLEX then
self.positioning = Positioning.FLEX self.positioning = Positioning.FLEX
self.explicitlyAbsolute = false -- User explicitly requested flex container self._explicitlyAbsolute = false
else
-- Default: children in flex containers participate in flex layout
-- children in absolute containers default to absolute
if self.parent.positioning == Positioning.FLEX then
self.positioning = Positioning.ABSOLUTE -- They are positioned BY flex, not AS flex
self._explicitlyAbsolute = false -- Participate in parent's flex layout
else else
-- Default: absolute positioning (most common case)
self.positioning = Positioning.ABSOLUTE self.positioning = Positioning.ABSOLUTE
self.explicitlyAbsolute = false -- Default positioning, not explicitly requested self._explicitlyAbsolute = false -- Default for absolute containers
end
end end
-- Set initial position -- Set initial position
if self.positioning == Positioning.ABSOLUTE then if self.positioning == Positioning.ABSOLUTE then
-- Handle x position with units self.x = props.x or 0
if props.x then self.y = props.y or 0
if type(props.x) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.x)
self.units.x = { value = value, unit = unit }
local viewportWidth, viewportHeight = Units.getViewport()
local parentWidth = self.parent.width
self.x = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
else
-- Handle numeric values (backward compatibility)
self.x = props.x
self.units.x = { value = props.x, unit = "px" }
end
else
self.x = 0
end
-- Handle y position with units
if props.y then
if type(props.y) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.y)
self.units.y = { value = value, unit = unit }
local viewportWidth, viewportHeight = Units.getViewport()
local parentHeight = self.parent.height
self.y = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
else
-- Handle numeric values (backward compatibility)
self.y = props.y
self.units.y = { value = props.y, unit = "px" }
end
else
self.y = 0
end
self.z = props.z or 0 self.z = props.z or 0
else else
-- Children in flex containers start at parent position but will be repositioned by layoutChildren -- Children in flex containers start at parent position but will be repositioned by layoutChildren
-- For flex children, relative units are resolved relative to parent self.x = self.parent.x + (props.x or 0)
local baseX = self.parent.x self.y = self.parent.y + (props.y or 0)
local baseY = self.parent.y
if props.x then
if type(props.x) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.x)
self.units.x = { value = value, unit = unit }
local viewportWidth, viewportHeight = Units.getViewport()
local parentWidth = self.parent.width
local offsetX = Units.resolve(value, unit, viewportWidth, viewportHeight, parentWidth)
self.x = baseX + offsetX
else
-- Handle numeric values (backward compatibility)
self.x = baseX + props.x
self.units.x = { value = props.x, unit = "px" }
end
else
self.x = baseX
end
if props.y then
if type(props.y) == "string" then
-- Handle units for string values
local value, unit = Units.parse(props.y)
self.units.y = { value = value, unit = unit }
local viewportWidth, viewportHeight = Units.getViewport()
local parentHeight = self.parent.height
local offsetY = Units.resolve(value, unit, viewportWidth, viewportHeight, parentHeight)
self.y = baseY + offsetY
else
-- Handle numeric values (backward compatibility)
self.y = baseY + props.y
self.units.y = { value = props.y, unit = "px" }
end
else
self.y = baseY
end
self.z = props.z or self.parent.z or 0 self.z = props.z or self.parent.z or 0
end end
@@ -961,72 +558,9 @@ function Element.new(props)
self.transform = props.transform or {} self.transform = props.transform or {}
self.transition = props.transition or {} self.transition = props.transition or {}
-- Interactive state for callbacks
self.pressed = false
self.touchPressed = {}
return self return self
end end
--- Recalculate all unit-based dimensions and positions
--- Should be called when viewport changes or parent dimensions change
function Element:recalculateUnits()
-- Use cached viewport if available and current, otherwise get fresh values
local viewportWidth, viewportHeight
if ViewportCache.lastUpdate == love.timer.getTime() then
viewportWidth, viewportHeight = ViewportCache.width, ViewportCache.height
else
viewportWidth, viewportHeight = Units.getViewport()
end
-- Recalculate width if it uses units
if not self.autosizing.width and self.units.width.value then
local parentWidth = self.parent and self.parent.width or viewportWidth
self.width =
Units.resolve(self.units.width.value, self.units.width.unit, viewportWidth, viewportHeight, parentWidth)
end
-- Recalculate height if it uses units
if not self.autosizing.height and self.units.height.value then
local parentHeight = self.parent and self.parent.height or viewportHeight
self.height =
Units.resolve(self.units.height.value, self.units.height.unit, viewportWidth, viewportHeight, parentHeight)
end
-- Recalculate position if it uses units
if self.units.x.value then
local parentWidth = self.parent and self.parent.width or viewportWidth
local baseX = (self.parent and self.positioning ~= Positioning.ABSOLUTE) and self.parent.x or 0
local offsetX = Units.resolve(self.units.x.value, self.units.x.unit, viewportWidth, viewportHeight, parentWidth)
self.x = baseX + offsetX
end
if self.units.y.value then
local parentHeight = self.parent and self.parent.height or viewportHeight
local baseY = (self.parent and self.positioning ~= Positioning.ABSOLUTE) and self.parent.y or 0
local offsetY = Units.resolve(self.units.y.value, self.units.y.unit, viewportWidth, viewportHeight, parentHeight)
self.y = baseY + offsetY
end
-- Recalculate gap if it uses units
if self.units.gap and self.units.gap.value then
local containerSize = (self.flexDirection == FlexDirection.HORIZONTAL)
and (self.parent and self.parent.width or viewportWidth)
or (self.parent and self.parent.height or viewportHeight)
self.gap = Units.resolve(self.units.gap.value, self.units.gap.unit, viewportWidth, viewportHeight, containerSize)
end
-- Recalculate textSize if it uses units
if self.units.textSize and self.units.textSize.value then
-- For textSize, we don't need a parent size context - use viewport directly
self.textSize =
Units.resolve(self.units.textSize.value, self.units.textSize.unit, viewportWidth, viewportHeight, nil)
end
-- NOTE: Children recalculation is now handled by the batch resize system
-- This avoids recursive calls during resize operations for better performance
end
--- Get element bounds --- Get element bounds
---@return { x:number, y:number, width:number, height:number } ---@return { x:number, y:number, width:number, height:number }
function Element:getBounds() function Element:getBounds()
@@ -1038,11 +572,22 @@ end
function Element:addChild(child) function Element:addChild(child)
child.parent = self child.parent = self
table.insert(self.children, child) -- Re-evaluate positioning now that we have a parent
-- If child was created without explicit positioning, inherit from parent
if child._originalPositioning == nil then
-- No explicit positioning was set during construction
if self.positioning == Positioning.FLEX then
child.positioning = Positioning.ABSOLUTE -- They are positioned BY flex, not AS flex
child._explicitlyAbsolute = false -- Participate in parent's flex layout
else
child.positioning = Positioning.ABSOLUTE
child._explicitlyAbsolute = false -- Default for absolute containers
end
end
-- If child._originalPositioning is set, it means explicit positioning was provided
-- and _explicitlyAbsolute was already set correctly during construction
-- Recalculate child dimensions now that parent relationship is established table.insert(self.children, child)
-- This is important for percentage-based units that depend on parent dimensions
child:recalculateUnits()
if self.autosizing.height then if self.autosizing.height then
self.height = self:calculateAutoHeight() self.height = self:calculateAutoHeight()
@@ -1069,11 +614,8 @@ function Element:layoutChildren()
-- Get flex children (children that participate in flex layout) -- Get flex children (children that participate in flex layout)
local flexChildren = {} local flexChildren = {}
for _, child in ipairs(self.children) do for _, child in ipairs(self.children) do
-- Children participate in flex layout if: local isFlexChild = not (child.positioning == Positioning.ABSOLUTE and child._explicitlyAbsolute)
-- 1. Parent is a flex container AND if isFlexChild then
-- 2. Child is NOT explicitly positioned absolute
local shouldParticipateInFlex = (self.positioning == Positioning.FLEX) and not child.explicitlyAbsolute
if shouldParticipateInFlex then
table.insert(flexChildren, child) table.insert(flexChildren, child)
end end
end end
@@ -1277,8 +819,6 @@ function Element:layoutChildren()
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == AlignItems.FLEX_END then
child.y = self.y + self.padding.top + currentCrossPos + lineHeight - (child.height or 0) child.y = self.y + self.padding.top + currentCrossPos + lineHeight - (child.height or 0)
elseif effectiveAlign == AlignItems.STRETCH then elseif effectiveAlign == AlignItems.STRETCH then
-- In horizontal layout, cross-axis is height
-- CSS flexbox stretch behavior: always stretch unless explicitly marked as non-stretchable
child.height = lineHeight child.height = lineHeight
child.y = self.y + self.padding.top + currentCrossPos child.y = self.y + self.padding.top + currentCrossPos
end end
@@ -1300,8 +840,6 @@ function Element:layoutChildren()
elseif effectiveAlign == AlignItems.FLEX_END then elseif effectiveAlign == AlignItems.FLEX_END then
child.x = self.x + self.padding.left + currentCrossPos + lineHeight - (child.width or 0) child.x = self.x + self.padding.left + currentCrossPos + lineHeight - (child.width or 0)
elseif effectiveAlign == AlignItems.STRETCH then elseif effectiveAlign == AlignItems.STRETCH then
-- In vertical layout, cross-axis is width
-- CSS flexbox stretch behavior: always stretch unless explicitly marked as non-stretchable
child.width = lineHeight child.width = lineHeight
child.x = self.x + self.padding.left + currentCrossPos child.x = self.x + self.padding.left + currentCrossPos
end end
@@ -1310,14 +848,6 @@ function Element:layoutChildren()
end end
end end
-- After positioning all children in this line, recursively layout their children
for _, child in ipairs(line) do
-- Only layout children of flex containers to update positions relative to new parent position
if child.positioning == Positioning.FLEX and #child.children > 0 then
child:layoutChildren()
end
end
-- Move to next line position -- Move to next line position
currentCrossPos = currentCrossPos + lineHeight + lineSpacing currentCrossPos = currentCrossPos + lineHeight + lineSpacing
end end
@@ -1456,7 +986,7 @@ function Element:draw()
end end
-- Draw visual feedback when element is pressed (if it has a callback) -- Draw visual feedback when element is pressed (if it has a callback)
if self.callback and self.pressed then if self.callback and self._pressed then
love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * self.opacity) -- Semi-transparent gray for pressed state with opacity love.graphics.setColor(0.5, 0.5, 0.5, 0.3 * self.opacity) -- Semi-transparent gray for pressed state with opacity
love.graphics.rectangle( love.graphics.rectangle(
"fill", "fill",
@@ -1504,122 +1034,50 @@ function Element:update(dt)
local by = self.y local by = self.y
if mx >= bx and mx <= bx + self.width and my >= by and my <= by + self.height then if mx >= bx and mx <= bx + self.width and my >= by and my <= by + self.height then
if love.mouse.isDown(1) then if love.mouse.isDown(1) then
self.pressed = true -- set pressed flag
self._pressed = true
elseif not love.mouse.isDown(1) and self._pressed then elseif not love.mouse.isDown(1) and self._pressed then
self.callback(self) self.callback(self)
self.pressed = false self._pressed = false
end end
else else
self.pressed = false self._pressed = false
end end
local touches = love.touch.getTouches() local touches = love.touch.getTouches()
for _, id in ipairs(touches) do for _, id in ipairs(touches) do
local tx, ty = love.touch.getPosition(id) local tx, ty = love.touch.getPosition(id)
if tx >= bx and tx <= bx + self.width and ty >= by and ty <= by + self.height then if tx >= bx and tx <= bx + self.width and ty >= by and ty <= by + self.height then
self.touchPressed[id] = true self._touchPressed[id] = true
elseif self.touchPressed[id] then elseif self._touchPressed[id] then
self.callback(self) self.callback(self)
self.touchPressed[id] = false self._touchPressed[id] = false
end end
end end
end end
end end
--- Resize element and its children based on game window size change (Performance Optimized) --- Resize element and its children based on game window size change
---@param newGameWidth number ---@param newGameWidth number
---@param newGameHeight number ---@param newGameHeight number
function Element:resize(newGameWidth, newGameHeight) function Element:resize(newGameWidth, newGameHeight)
-- Early return if dimensions haven't changed local prevW = self.prevGameSize.width
if self.prevGameSize.width == newGameWidth and self.prevGameSize.height == newGameHeight then local prevH = self.prevGameSize.height
return local ratioW = newGameWidth / prevW
end local ratioH = newGameHeight / prevH
-- Update element size
-- Performance: batch operations at root level self.width = self.width * ratioW
local isRootResize = not self.parent self.height = self.height * ratioH
if isRootResize then self.x = self.x * ratioW
-- Update viewport cache once for entire operation self.y = self.y * ratioH
ViewportCache.width = newGameWidth -- Update children positions and sizes
ViewportCache.height = newGameHeight
ViewportCache.lastUpdate = love.timer.getTime()
end
-- Calculate scale factors for proportional scaling
local scaleX = newGameWidth / self.prevGameSize.width
local scaleY = newGameHeight / self.prevGameSize.height
-- Apply proportional scaling to pixel-based properties
if self.units.width.unit == "px" and not self.autosizing.width then
self.width = self.width * scaleX
self.units.width.value = self.width
end
if self.units.height.unit == "px" and not self.autosizing.height then
self.height = self.height * scaleY
self.units.height.value = self.height
end
if self.units.x.unit == "px" then
self.x = self.x * scaleX
self.units.x.value = self.x
end
if self.units.y.unit == "px" then
self.y = self.y * scaleY
self.units.y.value = self.y
end
if self.units.gap and self.units.gap.unit == "px" then
self.gap = self.gap * ((scaleX + scaleY) / 2)
self.units.gap.value = self.gap
end
if self.units.textSize and self.units.textSize.unit == "px" and self.textSize then
local avgScale = (scaleX + scaleY) / 2
self.textSize = self.textSize * avgScale
self.units.textSize.value = self.textSize
end
-- Scale padding and margin
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
if self.units.padding[side] and self.units.padding[side].unit == "px" and self.padding[side] then
local scale = (side == "top" or side == "bottom") and scaleY or scaleX
self.padding[side] = self.padding[side] * scale
self.units.padding[side].value = self.padding[side]
end
if self.units.margin[side] and self.units.margin[side].unit == "px" and self.margin[side] then
local scale = (side == "top" or side == "bottom") and scaleY or scaleX
self.margin[side] = self.margin[side] * scale
self.units.margin[side].value = self.margin[side]
end
end
-- Update stored game size
self.prevGameSize.width = newGameWidth
self.prevGameSize.height = newGameHeight
-- Recalculate units for viewport/percentage units
self:recalculateUnits()
-- Recursively resize children
for _, child in ipairs(self.children) do for _, child in ipairs(self.children) do
child:resize(ratioW, ratioH) child:resize(ratioW, ratioH)
end end
-- Re-layout children after resizing
self:layoutChildren() self:layoutChildren()
end self.prevGameSize.width = newGameWidth
self.prevGameSize.height = newGameHeight
--- Check if element uses viewport units (performance helper)
---@return boolean
function Element:hasViewportUnits()
return (self.units.width.unit == "vw" or self.units.width.unit == "vh")
or (self.units.height.unit == "vw" or self.units.height.unit == "vh")
or (self.units.x.unit == "vw" or self.units.x.unit == "vh")
or (self.units.y.unit == "vw" or self.units.y.unit == "vh")
or (self.units.gap and (self.units.gap.unit == "vw" or self.units.gap.unit == "vh"))
or (self.units.textSize and (self.units.textSize.unit == "vw" or self.units.textSize.unit == "vh"))
end end
--- Calculate text width for button --- Calculate text width for button
@@ -1663,8 +1121,7 @@ function Element:calculateAutoWidth()
local participatingChildren = 0 local participatingChildren = 0
for _, child in ipairs(self.children) do for _, child in ipairs(self.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing -- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
local participatesInLayout = (self.positioning == Positioning.FLEX) and not child.explicitlyAbsolute if not child._explicitlyAbsolute then
if participatesInLayout then
local paddingAdjustment = (child.padding.left or 0) + (child.padding.right or 0) local paddingAdjustment = (child.padding.left or 0) + (child.padding.right or 0)
local childWidth = child.width or child:calculateAutoWidth() local childWidth = child.width or child:calculateAutoWidth()
local childOffset = childWidth + paddingAdjustment local childOffset = childWidth + paddingAdjustment
@@ -1688,8 +1145,7 @@ function Element:calculateAutoHeight()
local participatingChildren = 0 local participatingChildren = 0
for _, child in ipairs(self.children) do for _, child in ipairs(self.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing -- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
local participatesInLayout = (self.positioning == Positioning.FLEX) and not child.explicitlyAbsolute if not child._explicitlyAbsolute then
if participatesInLayout then
local paddingAdjustment = (child.padding.top or 0) + (child.padding.bottom or 0) local paddingAdjustment = (child.padding.top or 0) + (child.padding.bottom or 0)
local childOffset = child.height + paddingAdjustment local childOffset = child.height + paddingAdjustment