diff --git a/FlexLove.lua b/FlexLove.lua index a1fadea..98785fa 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -1083,7 +1083,7 @@ end --- If called before FlexLove.init(), the element creation will be automatically queued and executed after initialization ---@param props ElementProps ---@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) props = props or {} @@ -1102,8 +1102,7 @@ function flexlove.new(props, callback) ) end end - - return nil -- Element will be created later + return nil end -- Determine effective mode: props.mode takes precedence over global mode diff --git a/flexlove-0.7.1-1.rockspec b/flexlove-0.7.1-1.rockspec deleted file mode 100644 index b7c29c7..0000000 --- a/flexlove-0.7.1-1.rockspec +++ /dev/null @@ -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//.luarocks/share/lua//?.lua" - package.path = package.path .. ";/Users//.luarocks/share/lua//?/init.lua" - package.cpath = package.cpath .. ";/Users//.luarocks/lib/lua//?.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", - --}, -} diff --git a/modules/Element.lua b/modules/Element.lua index e4bd459..db2613e 100644 --- a/modules/Element.lua +++ b/modules/Element.lua @@ -1308,6 +1308,91 @@ function Element.new(props) self._originalPositioning = nil -- No explicit positioning self._explicitlyAbsolute = false 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 -- Set positioning first and track if explicitly set self._originalPositioning = props.positioning -- Track original intent @@ -1468,106 +1553,94 @@ function Element.new(props) 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) 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 -- Validate enum properties if props.flexDirection then @@ -2135,6 +2208,9 @@ function Element:addChild(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 -- (CSS: absolutely positioned children don't affect parent auto-sizing) if not child._explicitlyAbsolute then diff --git a/modules/LayoutEngine.lua b/modules/LayoutEngine.lua index 9515352..d220b24 100644 --- a/modules/LayoutEngine.lua +++ b/modules/LayoutEngine.lua @@ -123,6 +123,8 @@ end --- Apply CSS positioning offsets (top, right, bottom, left) to a child element ---@param child Element The element to apply offsets to function LayoutEngine:applyPositioningOffsets(child) + + if not child then return end @@ -139,7 +141,7 @@ function LayoutEngine:applyPositioningOffsets(child) or child.positioning == self._Positioning.GRID 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 top offset (distance from parent's content box top edge) if child.top then @@ -224,6 +226,7 @@ end --- Layout children within this element according to positioning mode function LayoutEngine:layoutChildren() + -- Start performance timing first (before any early returns) local timerName = nil 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 -- Absolute/Relative positioned containers don't layout their children according to flex rules, -- 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 + -- Apply CSS positioning offsets to children with absolute positioning if child.top or child.right or child.bottom or child.left then 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 @@ -316,81 +333,45 @@ function LayoutEngine:layoutChildren() 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 - return - end - - -- Calculate space reserved by absolutely positioned siblings with explicit positioning - local reservedMainStart = 0 -- Space reserved at the start of main axis (left for horizontal, top for vertical) - local reservedMainEnd = 0 -- Space reserved at the end of main axis (right for horizontal, bottom for vertical) - 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) + -- Position absolutely positioned children even when there are no flex children + for i, child in ipairs(self.element.children) do + if child.positioning == self._Positioning.ABSOLUTE and child._explicitlyAbsolute then + self:applyPositioningOffsets(child) + + -- If child has children, layout them after position change + if #child.children > 0 then + child:layoutChildren() 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 - - -- 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) local availableMainSize = 0 local availableCrossSize = 0 if self.flexDirection == self._FlexDirection.HORIZONTAL then - availableMainSize = self.element.width - reservedMainStart - reservedMainEnd - availableCrossSize = self.element.height - reservedCrossStart - reservedCrossEnd + availableMainSize = self.element.width + availableCrossSize = self.element.height else - availableMainSize = self.element.height - reservedMainStart - reservedMainEnd - availableCrossSize = self.element.width - reservedCrossStart - reservedCrossEnd + availableMainSize = self.element.height + availableCrossSize = self.element.width end -- Handle flex wrap: create lines of children @@ -614,9 +595,9 @@ function LayoutEngine:layoutChildren() if self.flexDirection == self._FlexDirection.HORIZONTAL then -- Horizontal layout: main axis is X, cross axis is Y -- 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 - child.x = elementX + elementPaddingLeft + reservedMainStart + currentMainPos + childMarginLeft + child.x = elementX + elementPaddingLeft + currentMainPos + childMarginLeft -- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations local childBorderBoxHeight = child:getBorderBoxHeight() @@ -625,16 +606,15 @@ function LayoutEngine:layoutChildren() local childTotalCrossSize = childBorderBoxHeight + childMarginTop + childMarginBottom if effectiveAlign == alignItems_FLEX_START then - child.y = elementY + elementPaddingTop + reservedCrossStart + currentCrossPos + childMarginTop + child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop elseif effectiveAlign == alignItems_CENTER then child.y = elementY + elementPaddingTop - + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + childMarginTop 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 -- STRETCH: Only apply if height was not explicitly set if childAutosizing and childAutosizing.height then @@ -643,7 +623,7 @@ function LayoutEngine:layoutChildren() child._borderBoxHeight = availableHeight child.height = math.max(0, availableHeight - childPadding.top - childPadding.bottom) end - child.y = elementY + elementPaddingTop + reservedCrossStart + currentCrossPos + childMarginTop + child.y = elementY + elementPaddingTop + currentCrossPos + childMarginTop end -- Apply positioning offsets (top, right, bottom, left) @@ -659,9 +639,9 @@ function LayoutEngine:layoutChildren() else -- Vertical layout: main axis is Y, cross axis is X -- 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 - child.y = elementY + elementPaddingTop + reservedMainStart + currentMainPos + childMarginTop + child.y = elementY + elementPaddingTop + currentMainPos + childMarginTop -- BORDER-BOX MODEL: Use border-box dimensions for alignment calculations local childBorderBoxWidth = child:getBorderBoxWidth() @@ -671,16 +651,15 @@ function LayoutEngine:layoutChildren() local elementPaddingLeft = elementPadding.left if effectiveAlign == alignItems_FLEX_START then - child.x = elementX + elementPaddingLeft + reservedCrossStart + currentCrossPos + childMarginLeft + child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft elseif effectiveAlign == alignItems_CENTER then child.x = elementX + elementPaddingLeft - + reservedCrossStart + currentCrossPos + ((lineHeight - childTotalCrossSize) / 2) + childMarginLeft 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 -- STRETCH: Only apply if width was not explicitly set if childAutosizing and childAutosizing.width then @@ -689,7 +668,7 @@ function LayoutEngine:layoutChildren() child._borderBoxWidth = availableWidth child.width = math.max(0, availableWidth - childPadding.left - childPadding.right) end - child.x = elementX + elementPaddingLeft + reservedCrossStart + currentCrossPos + childMarginLeft + child.x = elementX + elementPaddingLeft + currentCrossPos + childMarginLeft end -- Apply positioning offsets (top, right, bottom, left) @@ -710,7 +689,7 @@ function LayoutEngine:layoutChildren() end -- 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 -- Apply positioning offsets (top, right, bottom, left) self:applyPositioningOffsets(child) diff --git a/testing/__tests__/absolute_positioning_test.lua b/testing/__tests__/absolute_positioning_test.lua new file mode 100644 index 0000000..da7b747 --- /dev/null +++ b/testing/__tests__/absolute_positioning_test.lua @@ -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 diff --git a/testing/runAll.lua b/testing/runAll.lua index ce57c80..df4b71d 100644 --- a/testing/runAll.lua +++ b/testing/runAll.lua @@ -36,6 +36,7 @@ end local luaunit = require("testing.luaunit") local testFiles = { + "testing/__tests__/absolute_positioning_test.lua", "testing/__tests__/animation_test.lua", "testing/__tests__/blur_test.lua", "testing/__tests__/calc_test.lua",