fixes for the absolute positioning bug

This commit is contained in:
Michael Freno
2025-12-14 11:46:48 -05:00
parent 5b5de4e5c0
commit a3de78a343
6 changed files with 501 additions and 264 deletions

View File

@@ -1083,7 +1083,7 @@ end
--- If called before FlexLove.init(), the element creation will be automatically queued and executed after initialization --- If called before FlexLove.init(), the element creation will be automatically queued and executed after initialization
---@param props ElementProps ---@param props ElementProps
---@param callback? function Optional callback function(element) that will be called with the created element (useful when queued) ---@param callback? function Optional callback function(element) that will be called with the created element (useful when queued)
---@return Element|nil element Returns element if initialized, nil if queued for later creation ---@return Element -- Returns element if initialized, nil if queued for later creation
function flexlove.new(props, callback) function flexlove.new(props, callback)
props = props or {} props = props or {}
@@ -1102,8 +1102,7 @@ function flexlove.new(props, callback)
) )
end end
end end
return nil
return nil -- Element will be created later
end end
-- Determine effective mode: props.mode takes precedence over global mode -- Determine effective mode: props.mode takes precedence over global mode

View File

@@ -1,85 +0,0 @@
package = "flexlove"
version = "0.7.1-1"
source = {
url = "git+https://github.com/mikefreno/FlexLove.git",
tag = "v0.7.1",
}
description = {
summary = "A comprehensive UI library providing flexbox/grid layouts, theming, animations, and event handling for LÖVE2D games",
detailed = [[
FlexLöve is a lightweight, flexible GUI library for LÖVE2D that implements a
flexbox-based layout system. The goals of this project are two-fold: first,
anyone with basic CSS knowledge should be able to use this library with minimal
learning curve. Second, this library should take you from early prototyping to
production.
Features:
- Flexbox and Grid Layout systems
- Modern theming with 9-patch support
- Animations and transitions
- Image rendering with CSS-like object-fit
- Touch events and gesture recognition
- Text input with rich editing features
- Responsive design with viewport units
- Both immediate and retained rendering modes
Going this route, you will need to link the luarocks path to your project:
(for mac/linux)
```lua
package.path = package.path .. ";/Users/<username>/.luarocks/share/lua/<version>/?.lua"
package.path = package.path .. ";/Users/<username>/.luarocks/share/lua/<version>/?/init.lua"
package.cpath = package.cpath .. ";/Users/<username>/.luarocks/lib/lua/<version>/?.so"
```
]],
homepage = "https://mikefreno.github.io/FlexLove/",
license = "MIT",
maintainer = "Mike Freno",
}
dependencies = {
"lua >= 5.1",
"luautf8 >= 0.1.3",
}
build = {
type = "builtin",
modules = {
["FlexLove"] = "FlexLove.lua",
["FlexLove.modules.Animation"] = "modules/Animation.lua",
["FlexLove.modules.Blur"] = "modules/Blur.lua",
["FlexLove.modules.Calc"] = "modules/Calc.lua",
["FlexLove.modules.Color"] = "modules/Color.lua",
["FlexLove.modules.Context"] = "modules/Context.lua",
["FlexLove.modules.Element"] = "modules/Element.lua",
["FlexLove.modules.ErrorHandler"] = "modules/ErrorHandler.lua",
["FlexLove.modules.EventHandler"] = "modules/EventHandler.lua",
["FlexLove.modules.FFI"] = "modules/FFI.lua",
["FlexLove.modules.GestureRecognizer"] = "modules/GestureRecognizer.lua",
["FlexLove.modules.Grid"] = "modules/Grid.lua",
["FlexLove.modules.ImageCache"] = "modules/ImageCache.lua",
["FlexLove.modules.ImageRenderer"] = "modules/ImageRenderer.lua",
["FlexLove.modules.ImageScaler"] = "modules/ImageScaler.lua",
["FlexLove.modules.InputEvent"] = "modules/InputEvent.lua",
["FlexLove.modules.LayoutEngine"] = "modules/LayoutEngine.lua",
["FlexLove.modules.MemoryScanner"] = "modules/MemoryScanner.lua",
["FlexLove.modules.ModuleLoader"] = "modules/ModuleLoader.lua",
["FlexLove.modules.NinePatch"] = "modules/NinePatch.lua",
["FlexLove.modules.Performance"] = "modules/Performance.lua",
["FlexLove.modules.Renderer"] = "modules/Renderer.lua",
["FlexLove.modules.RoundedRect"] = "modules/RoundedRect.lua",
["FlexLove.modules.ScrollManager"] = "modules/ScrollManager.lua",
["FlexLove.modules.StateManager"] = "modules/StateManager.lua",
["FlexLove.modules.TextEditor"] = "modules/TextEditor.lua",
["FlexLove.modules.Theme"] = "modules/Theme.lua",
["FlexLove.modules.types"] = "modules/types.lua",
["FlexLove.modules.Units"] = "modules/Units.lua",
["FlexLove.modules.UTF8"] = "modules/UTF8.lua",
["FlexLove.modules.utils"] = "modules/utils.lua",
},
--copy_directories = {
--"docs",
--"examples",
--},
}

View File

@@ -1308,6 +1308,91 @@ function Element.new(props)
self._originalPositioning = nil -- No explicit positioning self._originalPositioning = nil -- No explicit positioning
self._explicitlyAbsolute = false self._explicitlyAbsolute = false
end end
-- Handle positioning properties for elements without parent
-- Handle top positioning with units
if props.top then
local isCalc = Element._Calc and Element._Calc.isCalc(props.top)
if type(props.top) == "string" or isCalc then
local value, unit = Element._Units.parse(props.top)
self.units.top = { value = value, unit = unit }
local resolvedTop = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
if type(resolvedTop) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "top resolution returned non-number value",
type = type(resolvedTop),
})
resolvedTop = 0
end
self.top = resolvedTop
else
self.top = props.top
self.units.top = { value = props.top, unit = "px" }
end
end
-- Handle right positioning with units
if props.right then
local isCalc = Element._Calc and Element._Calc.isCalc(props.right)
if type(props.right) == "string" or isCalc then
local value, unit = Element._Units.parse(props.right)
self.units.right = { value = value, unit = unit }
local resolvedRight = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
if type(resolvedRight) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "right resolution returned non-number value",
type = type(resolvedRight),
})
resolvedRight = 0
end
self.right = resolvedRight
else
self.right = props.right
self.units.right = { value = props.right, unit = "px" }
end
end
-- Handle bottom positioning with units
if props.bottom then
local isCalc = Element._Calc and Element._Calc.isCalc(props.bottom)
if type(props.bottom) == "string" or isCalc then
local value, unit = Element._Units.parse(props.bottom)
self.units.bottom = { value = value, unit = unit }
local resolvedBottom = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
if type(resolvedBottom) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "bottom resolution returned non-number value",
type = type(resolvedBottom),
})
resolvedBottom = 0
end
self.bottom = resolvedBottom
else
self.bottom = props.bottom
self.units.bottom = { value = props.bottom, unit = "px" }
end
end
-- Handle left positioning with units
if props.left then
local isCalc = Element._Calc and Element._Calc.isCalc(props.left)
if type(props.left) == "string" or isCalc then
local value, unit = Element._Units.parse(props.left)
self.units.left = { value = value, unit = unit }
local resolvedLeft = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
if type(resolvedLeft) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "left resolution returned non-number value",
type = type(resolvedLeft),
})
resolvedLeft = 0
end
self.left = resolvedLeft
else
self.left = props.left
self.units.left = { value = props.left, unit = "px" }
end
end
else else
-- Set positioning first and track if explicitly set -- Set positioning first and track if explicitly set
self._originalPositioning = props.positioning -- Track original intent self._originalPositioning = props.positioning -- Track original intent
@@ -1468,106 +1553,94 @@ function Element.new(props)
end end
end end
-- Handle positioning properties BEFORE adding to parent (so they're available during layout)
-- Handle top positioning with units
if props.top then
local isCalc = Element._Calc and Element._Calc.isCalc(props.top)
if type(props.top) == "string" or isCalc then
local value, unit = Element._Units.parse(props.top)
self.units.top = { value = value, unit = unit }
local resolvedTop = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
if type(resolvedTop) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "top resolution returned non-number value",
type = type(resolvedTop),
})
resolvedTop = 0
end
self.top = resolvedTop
else
self.top = props.top
self.units.top = { value = props.top, unit = "px" }
end
end
-- Handle right positioning with units
if props.right then
local isCalc = Element._Calc and Element._Calc.isCalc(props.right)
if type(props.right) == "string" or isCalc then
local value, unit = Element._Units.parse(props.right)
self.units.right = { value = value, unit = unit }
local resolvedRight = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
if type(resolvedRight) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "right resolution returned non-number value",
type = type(resolvedRight),
})
resolvedRight = 0
end
self.right = resolvedRight
else
self.right = props.right
self.units.right = { value = props.right, unit = "px" }
end
end
-- Handle bottom positioning with units
if props.bottom then
local isCalc = Element._Calc and Element._Calc.isCalc(props.bottom)
if type(props.bottom) == "string" or isCalc then
local value, unit = Element._Units.parse(props.bottom)
self.units.bottom = { value = value, unit = unit }
local resolvedBottom = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
if type(resolvedBottom) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "bottom resolution returned non-number value",
type = type(resolvedBottom),
})
resolvedBottom = 0
end
self.bottom = resolvedBottom
else
self.bottom = props.bottom
self.units.bottom = { value = props.bottom, unit = "px" }
end
end
-- Handle left positioning with units
if props.left then
local isCalc = Element._Calc and Element._Calc.isCalc(props.left)
if type(props.left) == "string" or isCalc then
local value, unit = Element._Units.parse(props.left)
self.units.left = { value = value, unit = unit }
local resolvedLeft = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
if type(resolvedLeft) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "left resolution returned non-number value",
type = type(resolvedLeft),
})
resolvedLeft = 0
end
self.left = resolvedLeft
else
self.left = props.left
self.units.left = { value = props.left, unit = "px" }
end
end
props.parent:addChild(self) props.parent:addChild(self)
end end
-- Handle positioning properties for ALL elements (with or without parent)
-- Handle top positioning with units
if props.top then
local isCalc = Element._Calc and Element._Calc.isCalc(props.top)
if type(props.top) == "string" or isCalc then
local value, unit = Element._Units.parse(props.top)
self.units.top = { value = value, unit = unit }
local resolvedTop = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
if type(resolvedTop) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "top resolution returned non-number value",
type = type(resolvedTop),
})
resolvedTop = 0
end
self.top = resolvedTop
else
self.top = props.top
self.units.top = { value = props.top, unit = "px" }
end
else
self.top = nil
self.units.top = nil
end
-- Handle right positioning with units
if props.right then
local isCalc = Element._Calc and Element._Calc.isCalc(props.right)
if type(props.right) == "string" or isCalc then
local value, unit = Element._Units.parse(props.right)
self.units.right = { value = value, unit = unit }
local resolvedRight = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
if type(resolvedRight) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "right resolution returned non-number value",
type = type(resolvedRight),
})
resolvedRight = 0
end
self.right = resolvedRight
else
self.right = props.right
self.units.right = { value = props.right, unit = "px" }
end
else
self.right = nil
self.units.right = nil
end
-- Handle bottom positioning with units
if props.bottom then
local isCalc = Element._Calc and Element._Calc.isCalc(props.bottom)
if type(props.bottom) == "string" or isCalc then
local value, unit = Element._Units.parse(props.bottom)
self.units.bottom = { value = value, unit = unit }
local resolvedBottom = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportHeight)
if type(resolvedBottom) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "bottom resolution returned non-number value",
type = type(resolvedBottom),
})
resolvedBottom = 0
end
self.bottom = resolvedBottom
else
self.bottom = props.bottom
self.units.bottom = { value = props.bottom, unit = "px" }
end
else
self.bottom = nil
self.units.bottom = nil
end
-- Handle left positioning with units
if props.left then
local isCalc = Element._Calc and Element._Calc.isCalc(props.left)
if type(props.left) == "string" or isCalc then
local value, unit = Element._Units.parse(props.left)
self.units.left = { value = value, unit = unit }
local resolvedLeft = Element._Units.resolve(value, unit, viewportWidth, viewportHeight, viewportWidth)
if type(resolvedLeft) ~= "number" then
Element._ErrorHandler:warn("Element", "LAY_003", {
issue = "left resolution returned non-number value",
type = type(resolvedLeft),
})
resolvedLeft = 0
end
self.left = resolvedLeft
else
self.left = props.left
self.units.left = { value = props.left, unit = "px" }
end
else
self.left = nil
self.units.left = nil
end
if self.positioning == Element._utils.enums.Positioning.FLEX then if self.positioning == Element._utils.enums.Positioning.FLEX then
-- Validate enum properties -- Validate enum properties
if props.flexDirection then if props.flexDirection then
@@ -2135,6 +2208,9 @@ function Element:addChild(child)
table.insert(self.children, child) table.insert(self.children, child)
-- Mark parent as having dirty children to trigger layout recalculation
self._childrenDirty = true
-- Only recalculate auto-sizing if the child participates in layout -- Only recalculate auto-sizing if the child participates in layout
-- (CSS: absolutely positioned children don't affect parent auto-sizing) -- (CSS: absolutely positioned children don't affect parent auto-sizing)
if not child._explicitlyAbsolute then if not child._explicitlyAbsolute then

View File

@@ -123,6 +123,8 @@ end
--- Apply CSS positioning offsets (top, right, bottom, left) to a child element --- Apply CSS positioning offsets (top, right, bottom, left) to a child element
---@param child Element The element to apply offsets to ---@param child Element The element to apply offsets to
function LayoutEngine:applyPositioningOffsets(child) function LayoutEngine:applyPositioningOffsets(child)
if not child then if not child then
return return
end end
@@ -139,7 +141,7 @@ function LayoutEngine:applyPositioningOffsets(child)
or child.positioning == self._Positioning.GRID or child.positioning == self._Positioning.GRID
or (child.positioning == self._Positioning.ABSOLUTE and not child._explicitlyAbsolute) or (child.positioning == self._Positioning.ABSOLUTE and not child._explicitlyAbsolute)
if not isFlexChild then if not isFlexChild and child._explicitlyAbsolute then
-- Apply absolute positioning for explicitly absolute children -- Apply absolute positioning for explicitly absolute children
-- Apply top offset (distance from parent's content box top edge) -- Apply top offset (distance from parent's content box top edge)
if child.top then if child.top then
@@ -224,6 +226,7 @@ end
--- Layout children within this element according to positioning mode --- Layout children within this element according to positioning mode
function LayoutEngine:layoutChildren() function LayoutEngine:layoutChildren()
-- Start performance timing first (before any early returns) -- Start performance timing first (before any early returns)
local timerName = nil local timerName = nil
if LayoutEngine._Performance and LayoutEngine._Performance.enabled and self.element then if LayoutEngine._Performance and LayoutEngine._Performance.enabled and self.element then
@@ -250,9 +253,23 @@ function LayoutEngine:layoutChildren()
if self.positioning == self._Positioning.ABSOLUTE or self.positioning == self._Positioning.RELATIVE then if self.positioning == self._Positioning.ABSOLUTE or self.positioning == self._Positioning.RELATIVE then
-- Absolute/Relative positioned containers don't layout their children according to flex rules, -- Absolute/Relative positioned containers don't layout their children according to flex rules,
-- but they should still apply CSS positioning offsets to their children -- but they should still apply CSS positioning offsets to their children
local baseX = (self.element.x or 0) + self.element.padding.left
local baseY = (self.element.y or 0) + self.element.padding.top
for _, child in ipairs(self.element.children) do for _, child in ipairs(self.element.children) do
-- Apply CSS positioning offsets to children with absolute positioning
if child.top or child.right or child.bottom or child.left then if child.top or child.right or child.bottom or child.left then
self:applyPositioningOffsets(child) self:applyPositioningOffsets(child)
elseif child.positioning == self._Positioning.RELATIVE then
-- Reposition relative children to match parent's new position
-- This is needed when the parent (absolute container) moves after children are created
child.x = baseX
child.y = baseY
-- If child has children, recursively layout them
if #child.children > 0 then
child:layoutChildren()
end
end end
end end
@@ -316,81 +333,45 @@ function LayoutEngine:layoutChildren()
end end
end end
-- CSS-compliant behavior: absolutely positioned elements are completely removed from normal flow
-- They do NOT reserve space or affect flex layout calculations at all
-- If no flex children, skip flex layout but still position absolute children
if #flexChildren == 0 then if #flexChildren == 0 then
return -- Position absolutely positioned children even when there are no flex children
end for i, child in ipairs(self.element.children) do
if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then
self:applyPositioningOffsets(child)
-- Calculate space reserved by absolutely positioned siblings with explicit positioning -- If child has children, layout them after position change
local reservedMainStart = 0 -- Space reserved at the start of main axis (left for horizontal, top for vertical) if #child.children > 0 then
local reservedMainEnd = 0 -- Space reserved at the end of main axis (right for horizontal, bottom for vertical) child:layoutChildren()
local reservedCrossStart = 0 -- Space reserved at the start of cross axis (top for horizontal, left for vertical)
local reservedCrossEnd = 0 -- Space reserved at the end of cross axis (bottom for horizontal, right for vertical)
for _, child in ipairs(self.element.children) do
-- Only consider absolutely positioned children with explicit positioning
if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box dimensions for space calculations
local childBorderBoxWidth = child:getBorderBoxWidth()
local childBorderBoxHeight = child:getBorderBoxHeight()
if self.flexDirection == self._FlexDirection.HORIZONTAL then
-- Horizontal layout: main axis is X, cross axis is Y
-- Check for left positioning (reserves space at main axis start)
if child.left then
local spaceNeeded = child.left + childBorderBoxWidth
reservedMainStart = math.max(reservedMainStart, spaceNeeded)
end
-- Check for right positioning (reserves space at main axis end)
if child.right then
local spaceNeeded = child.right + childBorderBoxWidth
reservedMainEnd = math.max(reservedMainEnd, spaceNeeded)
end
-- Check for top positioning (reserves space at cross axis start)
if child.top then
local spaceNeeded = child.top + childBorderBoxHeight
reservedCrossStart = math.max(reservedCrossStart, spaceNeeded)
end
-- Check for bottom positioning (reserves space at cross axis end)
if child.bottom then
local spaceNeeded = child.bottom + childBorderBoxHeight
reservedCrossEnd = math.max(reservedCrossEnd, spaceNeeded)
end
else
-- Vertical layout: main axis is Y, cross axis is X
-- Check for top positioning (reserves space at main axis start)
if child.top then
local spaceNeeded = child.top + childBorderBoxHeight
reservedMainStart = math.max(reservedMainStart, spaceNeeded)
end
-- Check for bottom positioning (reserves space at main axis end)
if child.bottom then
local spaceNeeded = child.bottom + childBorderBoxHeight
reservedMainEnd = math.max(reservedMainEnd, spaceNeeded)
end
-- Check for left positioning (reserves space at cross axis start)
if child.left then
local spaceNeeded = child.left + childBorderBoxWidth
reservedCrossStart = math.max(reservedCrossStart, spaceNeeded)
end
-- Check for right positioning (reserves space at cross axis end)
if child.right then
local spaceNeeded = child.right + childBorderBoxWidth
reservedCrossEnd = math.max(reservedCrossEnd, spaceNeeded)
end end
end end
end end
-- Detect overflow after children positioning
if self.element._detectOverflow then
self.element:_detectOverflow()
end
-- Stop performance timing
if timerName and LayoutEngine._Performance then
LayoutEngine._Performance:stopTimer(timerName)
end
return
end end
-- Calculate available space (accounting for padding and reserved space) -- Calculate available space (accounting for padding only, NOT absolute children)
-- BORDER-BOX MODEL: element.width and element.height are already content dimensions (padding subtracted) -- BORDER-BOX MODEL: element.width and element.height are already content dimensions (padding subtracted)
local availableMainSize = 0 local availableMainSize = 0
local availableCrossSize = 0 local availableCrossSize = 0
if self.flexDirection == self._FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
availableMainSize = self.element.width - reservedMainStart - reservedMainEnd availableMainSize = self.element.width
availableCrossSize = self.element.height - reservedCrossStart - reservedCrossEnd availableCrossSize = self.element.height
else else
availableMainSize = self.element.height - reservedMainStart - reservedMainEnd availableMainSize = self.element.height
availableCrossSize = self.element.width - reservedCrossStart - reservedCrossEnd availableCrossSize = self.element.width
end end
-- Handle flex wrap: create lines of children -- Handle flex wrap: create lines of children
@@ -614,9 +595,9 @@ function LayoutEngine:layoutChildren()
if self.flexDirection == self._FlexDirection.HORIZONTAL then if self.flexDirection == self._FlexDirection.HORIZONTAL then
-- Horizontal layout: main axis is X, cross axis is Y -- Horizontal layout: main axis is X, cross axis is Y
-- Position child at border box (x, y represents top-left including padding) -- Position child at border box (x, y represents top-left including padding)
-- Add reservedMainStart and left margin to account for absolutely positioned siblings and margins -- CSS-compliant: absolute children don't affect flex positioning, so no reserved space offset
local childMarginLeft = childMargin.left local childMarginLeft = childMargin.left
child.x = elementX + elementPaddingLeft + reservedMainStart + currentMainPos + childMarginLeft child.x = elementX + elementPaddingLeft + currentMainPos + childMarginLeft
-- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations -- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations
local childBorderBoxHeight = child:getBorderBoxHeight() local childBorderBoxHeight = child:getBorderBoxHeight()
@@ -625,16 +606,15 @@ function LayoutEngine:layoutChildren()
local childTotalCrossSize = childBorderBoxHeight + childMarginTop + childMarginBottom local childTotalCrossSize = childBorderBoxHeight + childMarginTop + childMarginBottom
if effectiveAlign == alignItems_FLEX_START then if effectiveAlign == alignItems_FLEX_START then
child.y = elementY + elementPaddingTop + reservedCrossStart + currentCrossPos + childMarginTop child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop
elseif effectiveAlign == alignItems_CENTER then elseif effectiveAlign == alignItems_CENTER then
child.y = elementY child.y = elementY
+ elementPaddingTop + elementPaddingTop
+ reservedCrossStart
+ currentCrossPos + currentCrossPos
+ ((lineHeight - childTotalCrossSize) / 2) + ((lineHeight - childTotalCrossSize) / 2)
+ childMarginTop + childMarginTop
elseif effectiveAlign == alignItems_FLEX_END then elseif effectiveAlign == alignItems_FLEX_END then
child.y = elementY + elementPaddingTop + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + childMarginTop child.y = elementY + elementPaddingTop + currentCrossPos + lineHeight - childTotalCrossSize + childMarginTop
elseif effectiveAlign == alignItems_STRETCH then elseif effectiveAlign == alignItems_STRETCH then
-- STRETCH: Only apply if height was not explicitly set -- STRETCH: Only apply if height was not explicitly set
if childAutosizing and childAutosizing.height then if childAutosizing and childAutosizing.height then
@@ -643,7 +623,7 @@ function LayoutEngine:layoutChildren()
child._borderBoxHeight = availableHeight child._borderBoxHeight = availableHeight
child.height = math.max(0, availableHeight - childPadding.top - childPadding.bottom) child.height = math.max(0, availableHeight - childPadding.top - childPadding.bottom)
end end
child.y = elementY + elementPaddingTop + reservedCrossStart + currentCrossPos + childMarginTop child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop
end end
-- Apply positioning offsets (top, right, bottom, left) -- Apply positioning offsets (top, right, bottom, left)
@@ -659,9 +639,9 @@ function LayoutEngine:layoutChildren()
else else
-- Vertical layout: main axis is Y, cross axis is X -- Vertical layout: main axis is Y, cross axis is X
-- Position child at border box (x, y represents top-left including padding) -- Position child at border box (x, y represents top-left including padding)
-- Add reservedMainStart and top margin to account for absolutely positioned siblings and margins -- CSS-compliant: absolute children don't affect flex positioning, so no reserved space offset
local childMarginTop = childMargin.top local childMarginTop = childMargin.top
child.y = elementY + elementPaddingTop + reservedMainStart + currentMainPos + childMarginTop child.y = elementY + elementPaddingTop + currentMainPos + childMarginTop
-- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations -- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations
local childBorderBoxWidth = child:getBorderBoxWidth() local childBorderBoxWidth = child:getBorderBoxWidth()
@@ -671,16 +651,15 @@ function LayoutEngine:layoutChildren()
local elementPaddingLeft = elementPadding.left local elementPaddingLeft = elementPadding.left
if effectiveAlign == alignItems_FLEX_START then if effectiveAlign == alignItems_FLEX_START then
child.x = elementX + elementPaddingLeft + reservedCrossStart + currentCrossPos + childMarginLeft child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft
elseif effectiveAlign == alignItems_CENTER then elseif effectiveAlign == alignItems_CENTER then
child.x = elementX child.x = elementX
+ elementPaddingLeft + elementPaddingLeft
+ reservedCrossStart
+ currentCrossPos + currentCrossPos
+ ((lineHeight - childTotalCrossSize) / 2) + ((lineHeight - childTotalCrossSize) / 2)
+ childMarginLeft + childMarginLeft
elseif effectiveAlign == alignItems_FLEX_END then elseif effectiveAlign == alignItems_FLEX_END then
child.x = elementX + elementPaddingLeft + reservedCrossStart + currentCrossPos + lineHeight - childTotalCrossSize + childMarginLeft child.x = elementX + elementPaddingLeft + currentCrossPos + lineHeight - childTotalCrossSize + childMarginLeft
elseif effectiveAlign == alignItems_STRETCH then elseif effectiveAlign == alignItems_STRETCH then
-- STRETCH: Only apply if width was not explicitly set -- STRETCH: Only apply if width was not explicitly set
if childAutosizing and childAutosizing.width then if childAutosizing and childAutosizing.width then
@@ -689,7 +668,7 @@ function LayoutEngine:layoutChildren()
child._borderBoxWidth = availableWidth child._borderBoxWidth = availableWidth
child.width = math.max(0, availableWidth - childPadding.left - childPadding.right) child.width = math.max(0, availableWidth - childPadding.left - childPadding.right)
end end
child.x = elementX + elementPaddingLeft + reservedCrossStart + currentCrossPos + childMarginLeft child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft
end end
-- Apply positioning offsets (top, right, bottom, left) -- Apply positioning offsets (top, right, bottom, left)
@@ -710,7 +689,7 @@ function LayoutEngine:layoutChildren()
end end
-- Position explicitly absolute children after flex layout -- Position explicitly absolute children after flex layout
for _, child in ipairs(self.element.children) do for i, child in ipairs(self.element.children) do
if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then
-- Apply positioning offsets (top, right, bottom, left) -- Apply positioning offsets (top, right, bottom, left)
self:applyPositioningOffsets(child) self:applyPositioningOffsets(child)

View File

@@ -0,0 +1,267 @@
package.path = package.path .. ";./?.lua;./modules/?.lua"
local originalSearchers = package.searchers or package.loaders
table.insert(originalSearchers, 2, function(modname)
if modname:match("^FlexLove%.modules%.") then
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
return function()
return require("modules." .. moduleName)
end
end
end)
require("testing.loveStub")
local luaunit = require("testing.luaunit")
-- Load FlexLove
local FlexLove = require("FlexLove")
TestAbsolutePositioning = {}
function TestAbsolutePositioning:setUp()
FlexLove.init()
end
function TestAbsolutePositioning:testAbsoluteBottomRightInFlexParent()
-- Create a flex parent
local parent = FlexLove.new({
positioning = "flex",
width = 400,
height = 400,
})
-- Create an absolutely positioned child with bottom and right offsets
local child = FlexLove.new({
parent = parent,
positioning = "absolute",
bottom = 0,
right = 0,
width = 100,
height = 100,
})
-- Child should be positioned at bottom-right
luaunit.assertEquals(child.x, 300, "Child x should be 300 (400 - 100)")
luaunit.assertEquals(child.y, 300, "Child y should be 300 (400 - 100)")
end
function TestAbsolutePositioning:testAbsoluteTopLeftInFlexParent()
-- Create a flex parent
local parent = FlexLove.new({
positioning = "flex",
width = 400,
height = 400,
})
-- Create an absolutely positioned child with top and left offsets
local child = FlexLove.new({
parent = parent,
positioning = "absolute",
top = 10,
left = 10,
width = 100,
height = 100,
})
-- Child should be positioned at top-left with 10px offset
luaunit.assertEquals(child.x, 10, "Child x should be 10")
luaunit.assertEquals(child.y, 10, "Child y should be 10")
end
function TestAbsolutePositioning:testAbsoluteWithPaddingParent()
-- Create a flex parent with padding
local parent = FlexLove.new({
positioning = "flex",
width = 400,
height = 400,
padding = 20,
})
-- Create an absolutely positioned child
local child = FlexLove.new({
parent = parent,
positioning = "absolute",
bottom = 0,
right = 0,
width = 100,
height = 100,
})
-- Absolute positioning is relative to parent's padding box
-- Parent content box: 400 - 20 (left padding) - 20 (right padding) = 360
-- Child x: parent.x (0) + padding.left (20) + content.width (360) - right (0) - child.width (100) = 280
luaunit.assertEquals(child.x, 280, "Child x should account for parent padding")
luaunit.assertEquals(child.y, 280, "Child y should account for parent padding")
end
function TestAbsolutePositioning:testAbsoluteDoesNotAffectFlexLayout()
-- Create a flex parent
local parent = FlexLove.new({
positioning = "flex",
flexDirection = "horizontal",
width = 400,
height = 400,
})
-- Add flex children
local flexChild1 = FlexLove.new({
parent = parent,
width = 100,
height = 100,
})
local flexChild2 = FlexLove.new({
parent = parent,
width = 100,
height = 100,
})
-- Add absolutely positioned child
local absChild = FlexLove.new({
parent = parent,
positioning = "absolute",
top = 0,
left = 0,
width = 50,
height = 50,
})
-- Flex children should be positioned normally (absolute child doesn't affect layout)
luaunit.assertEquals(flexChild1.x, 0, "First flex child at x=0")
luaunit.assertEquals(flexChild2.x, 100, "Second flex child at x=100")
-- Absolute child should be at top-left
luaunit.assertEquals(absChild.x, 0, "Absolute child at x=0")
luaunit.assertEquals(absChild.y, 0, "Absolute child at y=0")
end
function TestAbsolutePositioning:testMultipleAbsoluteChildren()
-- Create a flex parent
local parent = FlexLove.new({
positioning = "flex",
width = 400,
height = 400,
})
-- Create multiple absolutely positioned children
local topLeft = FlexLove.new({
parent = parent,
positioning = "absolute",
top = 0,
left = 0,
width = 50,
height = 50,
})
local topRight = FlexLove.new({
parent = parent,
positioning = "absolute",
top = 0,
right = 0,
width = 50,
height = 50,
})
local bottomLeft = FlexLove.new({
parent = parent,
positioning = "absolute",
bottom = 0,
left = 0,
width = 50,
height = 50,
})
local bottomRight = FlexLove.new({
parent = parent,
positioning = "absolute",
bottom = 0,
right = 0,
width = 50,
height = 50,
})
-- Verify positions
luaunit.assertEquals(topLeft.x, 0, "Top-left x")
luaunit.assertEquals(topLeft.y, 0, "Top-left y")
luaunit.assertEquals(topRight.x, 350, "Top-right x")
luaunit.assertEquals(topRight.y, 0, "Top-right y")
luaunit.assertEquals(bottomLeft.x, 0, "Bottom-left x")
luaunit.assertEquals(bottomLeft.y, 350, "Bottom-left y")
luaunit.assertEquals(bottomRight.x, 350, "Bottom-right x")
luaunit.assertEquals(bottomRight.y, 350, "Bottom-right y")
end
function TestAbsolutePositioning:testAbsoluteInImmediateMode()
FlexLove.setMode("immediate")
local parent, child
local function createUI()
parent = FlexLove.new({
positioning = "flex",
width = 400,
height = 400,
})
child = FlexLove.new({
parent = parent,
positioning = "absolute",
bottom = 0,
right = 0,
width = 100,
height = 100,
})
end
-- First frame
FlexLove.beginFrame()
createUI()
FlexLove.endFrame()
luaunit.assertEquals(child.x, 300, "Frame 1: Child x should be 300")
luaunit.assertEquals(child.y, 300, "Frame 1: Child y should be 300")
-- Second frame (recreate UI)
FlexLove.beginFrame()
createUI()
FlexLove.endFrame()
luaunit.assertEquals(child.x, 300, "Frame 2: Child x should be 300")
luaunit.assertEquals(child.y, 300, "Frame 2: Child y should be 300")
FlexLove.setMode("retained")
end
function TestAbsolutePositioning:testExplicitlyAbsoluteFlagIsSet()
local parent = FlexLove.new({
positioning = "flex",
width = 400,
height = 400,
})
-- Child with explicit absolute positioning
local absoluteChild = FlexLove.new({
parent = parent,
positioning = "absolute",
width = 100,
height = 100,
})
-- Child without explicit positioning (participates in flex)
local flexChild = FlexLove.new({
parent = parent,
width = 100,
height = 100,
})
luaunit.assertEquals(absoluteChild._explicitlyAbsolute, true, "Explicitly absolute child should have _explicitlyAbsolute = true")
luaunit.assertEquals(absoluteChild._originalPositioning, "absolute", "Absolute child should have _originalPositioning = 'absolute'")
luaunit.assertEquals(flexChild._explicitlyAbsolute, false, "Flex child should have _explicitlyAbsolute = false")
luaunit.assertEquals(flexChild._originalPositioning, nil, "Flex child should have _originalPositioning = nil")
end
if not _G.RUNNING_ALL_TESTS then
os.exit(luaunit.LuaUnit.run())
end

View File

@@ -36,6 +36,7 @@ end
local luaunit = require("testing.luaunit") local luaunit = require("testing.luaunit")
local testFiles = { local testFiles = {
"testing/__tests__/absolute_positioning_test.lua",
"testing/__tests__/animation_test.lua", "testing/__tests__/animation_test.lua",
"testing/__tests__/blur_test.lua", "testing/__tests__/blur_test.lua",
"testing/__tests__/calc_test.lua", "testing/__tests__/calc_test.lua",