sync
This commit is contained in:
331
FlexLove.lua
331
FlexLove.lua
@@ -445,6 +445,15 @@ local ViewportCache = {
|
||||
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()
|
||||
@@ -457,6 +466,64 @@ function Units.getViewport()
|
||||
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
|
||||
@@ -495,7 +562,7 @@ function Units.resolveSpacing(spacingProps, parentWidth, parentHeight)
|
||||
end
|
||||
|
||||
-- Handle individual sides
|
||||
for _, side in ipairs({"top", "right", "bottom", "left"}) do
|
||||
for _, side in ipairs({ "top", "right", "bottom", "left" }) do
|
||||
local value = spacingProps[side]
|
||||
if value then
|
||||
if type(value) == "string" then
|
||||
@@ -583,7 +650,7 @@ function Element.new(props)
|
||||
self.opacity = props.opacity or 1
|
||||
|
||||
self.text = props.text
|
||||
self.textSize = props.textSize
|
||||
self.textSize = props.textSize or 12
|
||||
self.textAlign = props.textAlign or TextAlign.START
|
||||
|
||||
--- self positioning ---
|
||||
@@ -605,8 +672,25 @@ function Element.new(props)
|
||||
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 type(props.w) == "string" then
|
||||
-- Handle units for string values
|
||||
@@ -654,9 +738,9 @@ function Element.new(props)
|
||||
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)
|
||||
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)
|
||||
@@ -668,6 +752,43 @@ function Element.new(props)
|
||||
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 ------
|
||||
if props.parent == nil then
|
||||
table.insert(Gui.topElements, self)
|
||||
@@ -847,87 +968,29 @@ function Element.new(props)
|
||||
return self
|
||||
end
|
||||
|
||||
--- Apply proportional scaling to pixel-based properties
|
||||
---@param scaleX number
|
||||
---@param scaleY number
|
||||
function Element:applyProportionalScaling(scaleX, scaleY)
|
||||
-- Scale pixel-based dimensions (only if not using viewport units)
|
||||
if not self.autosizing.width and self.units.width.unit == "px" then
|
||||
self.units.width.value = self.units.width.value * scaleX
|
||||
self.width = self.units.width.value
|
||||
end
|
||||
|
||||
if not self.autosizing.height and self.units.height.unit == "px" then
|
||||
self.units.height.value = self.units.height.value * scaleY
|
||||
self.height = self.units.height.value
|
||||
end
|
||||
|
||||
-- Scale pixel-based positions (only if using pixel units)
|
||||
if self.units.x and self.units.x.unit == "px" then
|
||||
self.units.x.value = self.units.x.value * scaleX
|
||||
-- Position will be recalculated in recalculateUnits()
|
||||
end
|
||||
|
||||
if self.units.y and self.units.y.unit == "px" then
|
||||
self.units.y.value = self.units.y.value * scaleY
|
||||
-- Position will be recalculated in recalculateUnits()
|
||||
end
|
||||
|
||||
-- Scale font size proportionally (use average scale to maintain readability)
|
||||
if self.textSize then
|
||||
local fontScale = math.sqrt(scaleX * scaleY) -- Geometric mean for balanced scaling
|
||||
self.textSize = math.max(1, math.floor(self.textSize * fontScale + 0.5)) -- Round and ensure minimum size
|
||||
end
|
||||
|
||||
-- Scale gap if using pixel units
|
||||
if self.units.gap and self.units.gap.unit == "px" then
|
||||
self.units.gap.value = self.units.gap.value * math.sqrt(scaleX * scaleY) -- Use geometric mean for gap
|
||||
-- Gap will be recalculated in recalculateUnits()
|
||||
end
|
||||
|
||||
-- Scale padding and margin values
|
||||
self:scaleSpacing(self.padding, scaleX, scaleY)
|
||||
self:scaleSpacing(self.margin, scaleX, scaleY)
|
||||
|
||||
-- Recursively apply scaling to children
|
||||
for _, child in ipairs(self.children) do
|
||||
child:applyProportionalScaling(scaleX, scaleY)
|
||||
end
|
||||
end
|
||||
|
||||
--- Scale spacing properties (padding/margin) proportionally
|
||||
---@param spacing table
|
||||
---@param scaleX number
|
||||
---@param scaleY number
|
||||
function Element:scaleSpacing(spacing, scaleX, scaleY)
|
||||
if spacing.top then
|
||||
spacing.top = spacing.top * scaleY
|
||||
end
|
||||
if spacing.bottom then
|
||||
spacing.bottom = spacing.bottom * scaleY
|
||||
end
|
||||
if spacing.left then
|
||||
spacing.left = spacing.left * scaleX
|
||||
end
|
||||
if spacing.right then
|
||||
spacing.right = spacing.right * scaleX
|
||||
end
|
||||
end
|
||||
|
||||
--- Recalculate all unit-based dimensions and positions
|
||||
--- Should be called when viewport changes or parent dimensions change
|
||||
function Element:recalculateUnits()
|
||||
local viewportWidth, viewportHeight = Units.getViewport()
|
||||
-- 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)
|
||||
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)
|
||||
self.height =
|
||||
Units.resolve(self.units.height.value, self.units.height.unit, viewportWidth, viewportHeight, parentHeight)
|
||||
end
|
||||
|
||||
-- Recalculate position if it uses units
|
||||
@@ -947,16 +1010,21 @@ function Element:recalculateUnits()
|
||||
|
||||
-- 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)
|
||||
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 children
|
||||
for _, child in ipairs(self.children) do
|
||||
child:recalculateUnits()
|
||||
-- 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
|
||||
@@ -1004,7 +1072,7 @@ function Element:layoutChildren()
|
||||
-- Children participate in flex layout if:
|
||||
-- 1. Parent is a flex container AND
|
||||
-- 2. Child is NOT explicitly positioned absolute
|
||||
local shouldParticipateInFlex = (self.positioning == Positioning.FLEX) and (not child.explicitlyAbsolute)
|
||||
local shouldParticipateInFlex = (self.positioning == Positioning.FLEX) and not child.explicitlyAbsolute
|
||||
if shouldParticipateInFlex then
|
||||
table.insert(flexChildren, child)
|
||||
end
|
||||
@@ -1209,6 +1277,8 @@ function Element:layoutChildren()
|
||||
elseif effectiveAlign == AlignItems.FLEX_END then
|
||||
child.y = self.y + self.padding.top + currentCrossPos + lineHeight - (child.height or 0)
|
||||
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.y = self.y + self.padding.top + currentCrossPos
|
||||
end
|
||||
@@ -1230,6 +1300,8 @@ function Element:layoutChildren()
|
||||
elseif effectiveAlign == AlignItems.FLEX_END then
|
||||
child.x = self.x + self.padding.left + currentCrossPos + lineHeight - (child.width or 0)
|
||||
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.x = self.x + self.padding.left + currentCrossPos
|
||||
end
|
||||
@@ -1456,31 +1528,100 @@ function Element:update(dt)
|
||||
end
|
||||
end
|
||||
|
||||
--- Resize element and its children based on game window size change
|
||||
--- Resize element and its children based on game window size change (Performance Optimized)
|
||||
---@param newGameWidth number
|
||||
---@param newGameHeight number
|
||||
function Element:resize(newGameWidth, newGameHeight)
|
||||
-- Update viewport cache
|
||||
ViewportCache.width = newGameWidth
|
||||
ViewportCache.height = newGameHeight
|
||||
ViewportCache.lastUpdate = love.timer.getTime()
|
||||
-- Early return if dimensions haven't changed
|
||||
if self.prevGameSize.width == newGameWidth and self.prevGameSize.height == newGameHeight then
|
||||
return
|
||||
end
|
||||
|
||||
-- Calculate scale factors from original size to new size
|
||||
-- Performance: batch operations at root level
|
||||
local isRootResize = not self.parent
|
||||
if isRootResize then
|
||||
-- Update viewport cache once for entire operation
|
||||
ViewportCache.width = newGameWidth
|
||||
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
|
||||
self:applyProportionalScaling(scaleX, scaleY)
|
||||
if self.units.width.unit == "px" and not self.autosizing.width then
|
||||
self.width = self.width * scaleX
|
||||
self.units.width.value = self.width
|
||||
end
|
||||
|
||||
-- Recalculate all unit-based dimensions and positions (for viewport units)
|
||||
self:recalculateUnits()
|
||||
if self.units.height.unit == "px" and not self.autosizing.height then
|
||||
self.height = self.height * scaleY
|
||||
self.units.height.value = self.height
|
||||
end
|
||||
|
||||
-- Update layout
|
||||
self:layoutChildren()
|
||||
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
|
||||
child:resize(newGameWidth, newGameHeight)
|
||||
end
|
||||
|
||||
-- Re-layout children after resizing
|
||||
self:layoutChildren()
|
||||
end
|
||||
|
||||
--- 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
|
||||
|
||||
--- Calculate text width for button
|
||||
@@ -1524,7 +1665,7 @@ function Element:calculateAutoWidth()
|
||||
local participatingChildren = 0
|
||||
for _, child in ipairs(self.children) do
|
||||
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
|
||||
local participatesInLayout = (self.positioning == Positioning.FLEX) and (not child.explicitlyAbsolute)
|
||||
local participatesInLayout = (self.positioning == Positioning.FLEX) and not child.explicitlyAbsolute
|
||||
if participatesInLayout then
|
||||
local paddingAdjustment = (child.padding.left or 0) + (child.padding.right or 0)
|
||||
local childWidth = child.width or child:calculateAutoWidth()
|
||||
@@ -1549,7 +1690,7 @@ function Element:calculateAutoHeight()
|
||||
local participatingChildren = 0
|
||||
for _, child in ipairs(self.children) do
|
||||
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
|
||||
local participatesInLayout = (self.positioning == Positioning.FLEX) and (not child.explicitlyAbsolute)
|
||||
local participatesInLayout = (self.positioning == Positioning.FLEX) and not child.explicitlyAbsolute
|
||||
if participatesInLayout then
|
||||
local paddingAdjustment = (child.padding.top or 0) + (child.padding.bottom or 0)
|
||||
local childOffset = child.height + paddingAdjustment
|
||||
|
||||
Reference in New Issue
Block a user