From 858d3faa30a3ecc97b31e16f4d0a2e130f1568fd Mon Sep 17 00:00:00 2001 From: megaSukura <1390953859@qq.com> Date: Sat, 19 Apr 2025 18:32:56 +0800 Subject: [PATCH 1/5] Fixed flexbox layout bug when container size changes Added cross-axis alignment functionality --- src/elements/Flexbox.lua | 682 +++++++++++++++++++++++++++++++-------- 1 file changed, 542 insertions(+), 140 deletions(-) diff --git a/src/elements/Flexbox.lua b/src/elements/Flexbox.lua index b73751a..f8a5030 100644 --- a/src/elements/Flexbox.lua +++ b/src/elements/Flexbox.lua @@ -31,6 +31,19 @@ Flexbox.defineProperty(Flexbox, "flexJustifyContent", { return value end }) +---@property flexAlignItems string "flex-start" The alignment of flex items along the cross axis +Flexbox.defineProperty(Flexbox, "flexAlignItems", { + default = "flex-start", + type = "string", + setter = function(self, value) + if not value:match("^flex%-") and value ~= "stretch" then + value = "flex-" .. value + end + return value + end +}) +---@property flexCrossPadding number 0 The padding on both sides of the cross axis +Flexbox.defineProperty(Flexbox, "flexCrossPadding", {default = 0, type = "number"}) ---@property flexWrap boolean false Whether to wrap flex items onto multiple lines ---@property flexUpdateLayout boolean false Whether to update the layout of the flexbox Flexbox.defineProperty(Flexbox, "flexWrap", {default = false, type = "boolean"}) @@ -57,187 +70,576 @@ local lineBreakElement = { local function sortElements(self, direction, spacing, wrap) + -- Pre-allocate tables to reduce dynamic expansion local elements = self.get("children") local sortedElements = {} - if not(wrap)then - local index = 1 - local lineSize = 1 - local lineOffset = 1 - for _,v in pairs(elements)do - if(sortedElements[index]==nil)then sortedElements[index]={offset=1} end - - local childHeight = direction == "row" and v.get("height") or v.get("width") - if childHeight > lineSize then - lineSize = childHeight - end - if(v == lineBreakElement)then - lineOffset = lineOffset + lineSize + spacing - lineSize = 1 - index = index + 1 - sortedElements[index] = {offset=lineOffset} - else - table.insert(sortedElements[index], v) - end - end - elseif(wrap)then - local lineSize = 1 - local lineOffset = 1 - - local maxSize = direction == "row" and self.get("width") or self.get("height") - local usedSize = 0 - local index = 1 - - for _,v in pairs(elements) do - if(sortedElements[index]==nil) then sortedElements[index]={offset=1} end - - if v:getType() == "lineBreak" then - lineOffset = lineOffset + lineSize + spacing - usedSize = 0 - lineSize = 1 - index = index + 1 - sortedElements[index] = {offset=lineOffset} - else - local objSize = direction == "row" and v.get("width") or v.get("height") - if(objSize+usedSize<=maxSize) then - table.insert(sortedElements[index], v) - usedSize = usedSize + objSize + spacing - else - lineOffset = lineOffset + lineSize + spacing - lineSize = direction == "row" and v.get("height") or v.get("width") - index = index + 1 - usedSize = objSize + spacing - sortedElements[index] = {offset=lineOffset, v} - end - - local childHeight = direction == "row" and v.get("height") or v.get("width") - if childHeight > lineSize then - lineSize = childHeight - end - end - end - end + local visibleElements = {} + local childCount = 0 + + -- First calculate the number of visible elements, pre-allocate space + for _, elem in pairs(elements) do + if elem ~= lineBreakElement and elem:getVisible() then + childCount = childCount + 1 + end + end + + -- Use known size to pre-allocate array + if not wrap then + -- No-wrap mode, all elements in one row/column + sortedElements[1] = {offset=1} + + for _, elem in pairs(elements) do + if elem == lineBreakElement then + -- Create new line + local nextIndex = #sortedElements + 1 + if sortedElements[nextIndex] == nil then + sortedElements[nextIndex] = {offset=1} + end + else + table.insert(sortedElements[#sortedElements], elem) + end + end + else + -- Wrap mode, need to calculate rows/columns + local index = 1 + local containerSize = direction == "row" and self.get("width") or self.get("height") + local remainingSpace = containerSize + sortedElements[index] = {offset=1} + + for _, elem in pairs(elements) do + if elem == lineBreakElement then + -- Create new line + index = index + 1 + sortedElements[index] = {offset=1} + remainingSpace = containerSize + else + local elemSize = direction == "row" and elem.get("width") or elem.get("height") + if elemSize + spacing <= remainingSpace then + -- Element fits in current line + table.insert(sortedElements[index], elem) + remainingSpace = remainingSpace - elemSize - spacing + else + -- Need new line + index = index + 1 + sortedElements[index] = {offset=1, elem} + remainingSpace = containerSize - elemSize - spacing + end + end + end + end + return sortedElements end local function calculateRow(self, children, spacing, justifyContent) local containerWidth = self.get("width") - - local usedSpace = spacing * (#children - 1) + local containerHeight = self.get("height") + local alignItems = self.get("flexAlignItems") + local crossPadding = self.get("flexCrossPadding") + + -- Safety check + if containerWidth <= 0 then return end + + -- Calculate available cross axis space (considering padding) + local availableCrossAxisSpace = containerHeight - (crossPadding * 2) + if availableCrossAxisSpace < 1 then + availableCrossAxisSpace = containerHeight + crossPadding = 0 + end + + -- Cache local variables to reduce function calls + local max = math.max + local min = math.min + local floor = math.floor + local ceil = math.ceil + + -- Fixed elements + local fixedElements = {} + -- Flexible elements + local flexElements = {} + -- Total flex coefficient local totalFlexGrow = 0 - + -- Pre-allocate capacity + local fixedCount = 0 + local flexCount = 0 + + -- First calculate element counts to pre-allocate space for _, child in ipairs(children) do if child ~= lineBreakElement then - usedSpace = usedSpace + child.get("width") - totalFlexGrow = totalFlexGrow + child.get("flexGrow") + local grow = child.get("flexGrow") or 0 + if grow > 0 then + flexCount = flexCount + 1 + else + fixedCount = fixedCount + 1 + end end end - - local remainingSpace = containerWidth - usedSpace - local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 - local distributedSpace = 0 - - local currentX = 1 - for i, child in ipairs(children) do + + -- Pre-allocate table space + for i = 1, fixedCount do fixedElements[i] = nil end + + -- Step 1: Categorize elements and collect information + for _, child in ipairs(children) do if child ~= lineBreakElement then - local childWidth = child.get("width") - - if child.get("flexGrow") > 0 then - - if i == #children then - local extraSpace = remainingSpace - distributedSpace - childWidth = childWidth + extraSpace - else - local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) - childWidth = childWidth + extraSpace - distributedSpace = distributedSpace + extraSpace + local grow = child.get("flexGrow") or 0 + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + table.insert(flexElements, {element = child, grow = grow}) + else + table.insert(fixedElements, child) + end + end + end + + -- Step 2: Pre-processing before layout + -- First calculate the total width needed for all fixed elements + local fixedWidthSum = 0 + for _, element in ipairs(fixedElements) do + fixedWidthSum = fixedWidthSum + element.get("width") + end + + -- Calculate total width of gaps + local totalElements = #fixedElements + #flexElements + local totalGaps = totalElements > 1 and (totalElements - 1) or 0 + local gapsWidth = spacing * totalGaps + + -- Calculate total available space for flexible elements + local flexAvailableSpace = max(0, containerWidth - fixedWidthSum - gapsWidth) + + -- Safety check: If not enough space, force compress fixed elements + if flexAvailableSpace < 0 then + -- Set gaps to zero + gapsWidth = 0 + flexAvailableSpace = containerWidth - fixedWidthSum + + -- If still not enough, need to shrink fixed elements + if flexAvailableSpace < 0 and #fixedElements > 0 then + local reductionPerElement = ceil(-flexAvailableSpace / #fixedElements) + for _, element in ipairs(fixedElements) do + local currentWidth = element.get("width") + local newWidth = max(1, currentWidth - reductionPerElement) + element.set("width", newWidth) + flexAvailableSpace = flexAvailableSpace + (currentWidth - newWidth) + if flexAvailableSpace >= 0 then + break end end - - child.set("x", currentX) - child.set("y", children.offset or 1) - child.set("width", childWidth) - currentX = currentX + childWidth + spacing + end + + -- If still not enough, may need to set minimum width + flexAvailableSpace = max(0, flexAvailableSpace) + end + + -- Step 3: Allocate space for flexible elements + -- Pre-allocate table to avoid dynamic expansion + local allocatedWidths = {} + for i = 1, flexCount do + allocatedWidths[flexElements[i].element] = nil + end + + -- If there are flexible elements and available space + if #flexElements > 0 and flexAvailableSpace > 0 and totalFlexGrow > 0 then + -- Reserve some safety margin (e.g., 5% of space) to ensure no overflow due to rounding + local safeFlexSpace = floor(flexAvailableSpace * 0.95) + + -- Allocate base width for each element (conservative strategy) + for _, item in ipairs(flexElements) do + -- Determine this element's share + local proportion = item.grow / totalFlexGrow + -- Determine width to allocate (floor to ensure safety) + local extraWidth = floor(safeFlexSpace * proportion) + -- Set final width + allocatedWidths[item.element] = item.element.get("width") + extraWidth end end - - if justifyContent == "flex-end" then - local offset = containerWidth - (currentX - spacing - 1) - for _, child in ipairs(children) do - child.set("x", child.get("x") + offset) + + -- Step 4: Strictly validate final widths + -- Calculate total width after allocation (including gaps) + local finalTotalWidth = gapsWidth + for _, element in ipairs(fixedElements) do + finalTotalWidth = finalTotalWidth + element.get("width") + end + for _, item in ipairs(flexElements) do + local width = allocatedWidths[item.element] or item.element.get("width") + finalTotalWidth = finalTotalWidth + width + end + + -- If total width exceeds container, proportionally reduce all elements + if finalTotalWidth > containerWidth then + local excessWidth = finalTotalWidth - containerWidth + local reductionFactor = excessWidth / (finalTotalWidth - gapsWidth) + + -- First reduce flexible elements + if #flexElements > 0 then + for _, item in ipairs(flexElements) do + local width = allocatedWidths[item.element] or item.element.get("width") + local reduction = ceil(width * reductionFactor) + allocatedWidths[item.element] = max(1, width - reduction) + end end - elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate - local offset = math.floor((containerWidth - (currentX - spacing - 1)) / 2) - for _, child in ipairs(children) do - child.set("x", child.get("x") + offset) + + -- If still not enough, reduce fixed elements + finalTotalWidth = gapsWidth + for _, element in ipairs(fixedElements) do + finalTotalWidth = finalTotalWidth + element.get("width") + end + for _, item in ipairs(flexElements) do + finalTotalWidth = finalTotalWidth + (allocatedWidths[item.element] or item.element.get("width")) + end + + if finalTotalWidth > containerWidth and #fixedElements > 0 then + excessWidth = finalTotalWidth - containerWidth + reductionFactor = excessWidth / (finalTotalWidth - gapsWidth) + + for _, element in ipairs(fixedElements) do + local width = element.get("width") + local reduction = ceil(width * reductionFactor) + element.set("width", max(1, width - reduction)) + end + end + end + + -- Step 5: Apply layout + local currentX = 1 + + -- Place all elements + for _, child in ipairs(children) do + if child ~= lineBreakElement then + -- Apply X coordinate + child.set("x", currentX) + + -- Apply Y coordinate (based on vertical alignment) + if alignItems == "stretch" then + -- Vertical stretch to fill container, considering padding + child.set("height", availableCrossAxisSpace) + child.set("y", 1 + crossPadding) + else + local childHeight = child.get("height") + local y = 1 + + if alignItems == "flex-end" then + -- Bottom align + y = containerHeight - childHeight + 1 + elseif alignItems == "flex-center" or alignItems == "center" then + -- Center align + y = floor((containerHeight - childHeight) / 2) + 1 + end + + -- Ensure Y value is not less than 1 + child.set("y", max(1, y)) + end + + -- If flexible element, apply calculated width + if allocatedWidths[child] then + child.set("width", allocatedWidths[child]) + end + + -- Final safety check (using cached math functions) + local rightEdge = currentX + child.get("width") - 1 + if rightEdge > containerWidth then + child.set("width", max(1, containerWidth - currentX + 1)) + end + + -- Final safety check height doesn't exceed container + local bottomEdge = child.get("y") + child.get("height") - 1 + if bottomEdge > containerHeight then + child.set("height", max(1, containerHeight - child.get("y") + 1)) + end + + -- Update position for next element + currentX = currentX + child.get("width") + spacing + + -- Ensure won't exceed container right edge + if currentX > containerWidth + 1 then + currentX = containerWidth + 1 + end + end + end + + -- Apply alignment (only when remaining space is positive) + local usedWidth = min(containerWidth, currentX - spacing - 1) + local remainingSpace = containerWidth - usedWidth + + if remainingSpace > 0 then + if justifyContent == "flex-end" then + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("x", child.get("x") + remainingSpace) + end + end + elseif justifyContent == "flex-center" or justifyContent == "center" then + local offset = floor(remainingSpace / 2) + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("x", child.get("x") + offset) + end + end end end end local function calculateColumn(self, children, spacing, justifyContent) + local containerWidth = self.get("width") local containerHeight = self.get("height") - - local usedSpace = spacing * (#children - 1) + local alignItems = self.get("flexAlignItems") + local crossPadding = self.get("flexCrossPadding") + + -- Safety check + if containerHeight <= 0 then return end + + -- Calculate available cross axis space (considering padding) + local availableCrossAxisSpace = containerWidth - (crossPadding * 2) + if availableCrossAxisSpace < 1 then + availableCrossAxisSpace = containerWidth + crossPadding = 0 + end + + -- Cache local variables to reduce function calls + local max = math.max + local min = math.min + local floor = math.floor + local ceil = math.ceil + + -- Fixed elements + local fixedElements = {} + -- Flexible elements + local flexElements = {} + -- Total flex coefficient local totalFlexGrow = 0 - + -- Pre-allocate capacity + local fixedCount = 0 + local flexCount = 0 + + -- First calculate element counts to pre-allocate space for _, child in ipairs(children) do if child ~= lineBreakElement then - usedSpace = usedSpace + child.get("height") - totalFlexGrow = totalFlexGrow + child.get("flexGrow") + local grow = child.get("flexGrow") or 0 + if grow > 0 then + flexCount = flexCount + 1 + else + fixedCount = fixedCount + 1 + end end end - - local remainingSpace = containerHeight - usedSpace - local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 - local distributedSpace = 0 - - local currentY = 1 - for i, child in ipairs(children) do + + -- Pre-allocate table space + for i = 1, fixedCount do fixedElements[i] = nil end + + -- Step 1: Categorize elements and collect information + for _, child in ipairs(children) do if child ~= lineBreakElement then - local childHeight = child.get("height") - - if child.get("flexGrow") > 0 then - - if i == #children then - local extraSpace = remainingSpace - distributedSpace - childHeight = childHeight + extraSpace - else - local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) - childHeight = childHeight + extraSpace - distributedSpace = distributedSpace + extraSpace + local grow = child.get("flexGrow") or 0 + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + table.insert(flexElements, {element = child, grow = grow}) + else + table.insert(fixedElements, child) + end + end + end + + -- Step 2: Pre-processing before layout + + -- First calculate the total height needed for all fixed elements + local fixedHeightSum = 0 + for _, element in ipairs(fixedElements) do + fixedHeightSum = fixedHeightSum + element.get("height") + end + + -- Calculate total height of gaps + local totalElements = #fixedElements + #flexElements + local totalGaps = totalElements > 1 and (totalElements - 1) or 0 + local gapsHeight = spacing * totalGaps + + -- Calculate total available space for flexible elements + local flexAvailableSpace = max(0, containerHeight - fixedHeightSum - gapsHeight) + + -- Safety check: If not enough space, force compress fixed elements + if flexAvailableSpace < 0 then + -- Set gaps to zero + gapsHeight = 0 + flexAvailableSpace = containerHeight - fixedHeightSum + + -- If still not enough, need to shrink fixed elements + if flexAvailableSpace < 0 and #fixedElements > 0 then + local reductionPerElement = ceil(-flexAvailableSpace / #fixedElements) + for _, element in ipairs(fixedElements) do + local currentHeight = element.get("height") + local newHeight = max(1, currentHeight - reductionPerElement) + element.set("height", newHeight) + flexAvailableSpace = flexAvailableSpace + (currentHeight - newHeight) + if flexAvailableSpace >= 0 then + break end end - - child.set("x", children.offset or 1) - child.set("y", currentY) - child.set("height", childHeight) - currentY = currentY + childHeight + spacing + end + + -- If still not enough, may need to set minimum height + flexAvailableSpace = max(0, flexAvailableSpace) + end + + -- Step 3: Allocate space for flexible elements + -- Pre-allocate table to avoid dynamic expansion + local allocatedHeights = {} + for i = 1, flexCount do + allocatedHeights[flexElements[i].element] = nil + end + + -- If there are flexible elements and available space + if #flexElements > 0 and flexAvailableSpace > 0 and totalFlexGrow > 0 then + -- Reserve some safety margin (e.g., 5% of space) to ensure no overflow due to rounding + local safeFlexSpace = floor(flexAvailableSpace * 0.95) + + -- Allocate base height for each element (conservative strategy) + for _, item in ipairs(flexElements) do + -- Determine this element's share + local proportion = item.grow / totalFlexGrow + -- Determine height to allocate (floor to ensure safety) + local extraHeight = floor(safeFlexSpace * proportion) + -- Set final height + allocatedHeights[item.element] = item.element.get("height") + extraHeight end end - - if justifyContent == "flex-end" then - local offset = containerHeight - (currentY - spacing - 1) - for _, child in ipairs(children) do - child.set("y", child.get("y") + offset) + + -- Step 4: Strictly validate final heights + -- Calculate total height after allocation (including gaps) + local finalTotalHeight = gapsHeight + for _, element in ipairs(fixedElements) do + finalTotalHeight = finalTotalHeight + element.get("height") + end + for _, item in ipairs(flexElements) do + local height = allocatedHeights[item.element] or item.element.get("height") + finalTotalHeight = finalTotalHeight + height + end + + -- If total height exceeds container, proportionally reduce all elements + if finalTotalHeight > containerHeight then + local excessHeight = finalTotalHeight - containerHeight + local reductionFactor = excessHeight / (finalTotalHeight - gapsHeight) + + -- First reduce flexible elements + if #flexElements > 0 then + for _, item in ipairs(flexElements) do + local height = allocatedHeights[item.element] or item.element.get("height") + local reduction = ceil(height * reductionFactor) + allocatedHeights[item.element] = max(1, height - reduction) + end end - elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate - local offset = math.floor((containerHeight - (currentY - spacing - 1)) / 2) - for _, child in ipairs(children) do - child.set("y", child.get("y") + offset) + + -- If still not enough, reduce fixed elements + finalTotalHeight = gapsHeight + for _, element in ipairs(fixedElements) do + finalTotalHeight = finalTotalHeight + element.get("height") + end + for _, item in ipairs(flexElements) do + finalTotalHeight = finalTotalHeight + (allocatedHeights[item.element] or item.element.get("height")) + end + + if finalTotalHeight > containerHeight and #fixedElements > 0 then + excessHeight = finalTotalHeight - containerHeight + reductionFactor = excessHeight / (finalTotalHeight - gapsHeight) + + for _, element in ipairs(fixedElements) do + local height = element.get("height") + local reduction = ceil(height * reductionFactor) + element.set("height", max(1, height - reduction)) + end + end + end + + -- Step 5: Apply layout + local currentY = 1 + + -- Place all elements + for _, child in ipairs(children) do + if child ~= lineBreakElement then + -- Apply Y coordinate + child.set("y", currentY) + + -- Apply X coordinate (based on horizontal alignment) + if alignItems == "stretch" then + -- Horizontal stretch to fill container, considering padding + child.set("width", availableCrossAxisSpace) + child.set("x", 1 + crossPadding) + else + local childWidth = child.get("width") + local x = 1 + + if alignItems == "flex-end" then + -- Right align + x = containerWidth - childWidth + 1 + elseif alignItems == "flex-center" or alignItems == "center" then + -- Center align + x = floor((containerWidth - childWidth) / 2) + 1 + end + + -- Ensure X value is not less than 1 + child.set("x", max(1, x)) + end + + -- If flexible element, apply calculated height + if allocatedHeights[child] then + child.set("height", allocatedHeights[child]) + end + + -- Final safety check (using cached math functions) + local bottomEdge = currentY + child.get("height") - 1 + if bottomEdge > containerHeight then + child.set("height", max(1, containerHeight - currentY + 1)) + end + + -- Final safety check width doesn't exceed container + local rightEdge = child.get("x") + child.get("width") - 1 + if rightEdge > containerWidth then + child.set("width", max(1, containerWidth - child.get("x") + 1)) + end + + -- Update position for next element + currentY = currentY + child.get("height") + spacing + + -- Ensure won't exceed container bottom edge + if currentY > containerHeight + 1 then + currentY = containerHeight + 1 + end + end + end + + -- Apply alignment (only when remaining space is positive) + local usedHeight = min(containerHeight, currentY - spacing - 1) + local remainingSpace = containerHeight - usedHeight + + if remainingSpace > 0 then + if justifyContent == "flex-end" then + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("y", child.get("y") + remainingSpace) + end + end + elseif justifyContent == "flex-center" or justifyContent == "center" then + local offset = floor(remainingSpace / 2) + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("y", child.get("y") + offset) + end + end end end end +-- Optimize updateLayout function local function updateLayout(self, direction, spacing, justifyContent, wrap) - local elements = sortElements(self, direction, spacing, wrap) - if direction == "row" then - for _,v in pairs(elements)do - calculateRow(self, v, spacing, justifyContent) - end - else - for _,v in pairs(elements)do - calculateColumn(self, v, spacing, justifyContent) - end + -- Get all elements that need layout + local elements = sortElements(self, direction, spacing, wrap) + + -- Based on direction, select layout function, avoid checking every iteration + local layoutFunction = direction == "row" and calculateRow or calculateColumn + + -- Apply layout calculation + for _, rowOrColumn in pairs(elements) do + layoutFunction(self, rowOrColumn, spacing, justifyContent) end + + -- Reset layout update flag self.set("flexUpdateLayout", false) end From 521e75b97c53b55dba56fb867d10a02efc365f27 Mon Sep 17 00:00:00 2001 From: megaSukura <1390953859@qq.com> Date: Sat, 19 Apr 2025 18:32:56 +0800 Subject: [PATCH 2/5] Fixed flexbox layout bug when container size changes Added cross-axis alignment functionality --- src/elements/Flexbox.lua | 682 +++++++++++++++++++++++++++++++-------- 1 file changed, 542 insertions(+), 140 deletions(-) diff --git a/src/elements/Flexbox.lua b/src/elements/Flexbox.lua index b73751a..f8a5030 100644 --- a/src/elements/Flexbox.lua +++ b/src/elements/Flexbox.lua @@ -31,6 +31,19 @@ Flexbox.defineProperty(Flexbox, "flexJustifyContent", { return value end }) +---@property flexAlignItems string "flex-start" The alignment of flex items along the cross axis +Flexbox.defineProperty(Flexbox, "flexAlignItems", { + default = "flex-start", + type = "string", + setter = function(self, value) + if not value:match("^flex%-") and value ~= "stretch" then + value = "flex-" .. value + end + return value + end +}) +---@property flexCrossPadding number 0 The padding on both sides of the cross axis +Flexbox.defineProperty(Flexbox, "flexCrossPadding", {default = 0, type = "number"}) ---@property flexWrap boolean false Whether to wrap flex items onto multiple lines ---@property flexUpdateLayout boolean false Whether to update the layout of the flexbox Flexbox.defineProperty(Flexbox, "flexWrap", {default = false, type = "boolean"}) @@ -57,187 +70,576 @@ local lineBreakElement = { local function sortElements(self, direction, spacing, wrap) + -- Pre-allocate tables to reduce dynamic expansion local elements = self.get("children") local sortedElements = {} - if not(wrap)then - local index = 1 - local lineSize = 1 - local lineOffset = 1 - for _,v in pairs(elements)do - if(sortedElements[index]==nil)then sortedElements[index]={offset=1} end - - local childHeight = direction == "row" and v.get("height") or v.get("width") - if childHeight > lineSize then - lineSize = childHeight - end - if(v == lineBreakElement)then - lineOffset = lineOffset + lineSize + spacing - lineSize = 1 - index = index + 1 - sortedElements[index] = {offset=lineOffset} - else - table.insert(sortedElements[index], v) - end - end - elseif(wrap)then - local lineSize = 1 - local lineOffset = 1 - - local maxSize = direction == "row" and self.get("width") or self.get("height") - local usedSize = 0 - local index = 1 - - for _,v in pairs(elements) do - if(sortedElements[index]==nil) then sortedElements[index]={offset=1} end - - if v:getType() == "lineBreak" then - lineOffset = lineOffset + lineSize + spacing - usedSize = 0 - lineSize = 1 - index = index + 1 - sortedElements[index] = {offset=lineOffset} - else - local objSize = direction == "row" and v.get("width") or v.get("height") - if(objSize+usedSize<=maxSize) then - table.insert(sortedElements[index], v) - usedSize = usedSize + objSize + spacing - else - lineOffset = lineOffset + lineSize + spacing - lineSize = direction == "row" and v.get("height") or v.get("width") - index = index + 1 - usedSize = objSize + spacing - sortedElements[index] = {offset=lineOffset, v} - end - - local childHeight = direction == "row" and v.get("height") or v.get("width") - if childHeight > lineSize then - lineSize = childHeight - end - end - end - end + local visibleElements = {} + local childCount = 0 + + -- First calculate the number of visible elements, pre-allocate space + for _, elem in pairs(elements) do + if elem ~= lineBreakElement and elem:getVisible() then + childCount = childCount + 1 + end + end + + -- Use known size to pre-allocate array + if not wrap then + -- No-wrap mode, all elements in one row/column + sortedElements[1] = {offset=1} + + for _, elem in pairs(elements) do + if elem == lineBreakElement then + -- Create new line + local nextIndex = #sortedElements + 1 + if sortedElements[nextIndex] == nil then + sortedElements[nextIndex] = {offset=1} + end + else + table.insert(sortedElements[#sortedElements], elem) + end + end + else + -- Wrap mode, need to calculate rows/columns + local index = 1 + local containerSize = direction == "row" and self.get("width") or self.get("height") + local remainingSpace = containerSize + sortedElements[index] = {offset=1} + + for _, elem in pairs(elements) do + if elem == lineBreakElement then + -- Create new line + index = index + 1 + sortedElements[index] = {offset=1} + remainingSpace = containerSize + else + local elemSize = direction == "row" and elem.get("width") or elem.get("height") + if elemSize + spacing <= remainingSpace then + -- Element fits in current line + table.insert(sortedElements[index], elem) + remainingSpace = remainingSpace - elemSize - spacing + else + -- Need new line + index = index + 1 + sortedElements[index] = {offset=1, elem} + remainingSpace = containerSize - elemSize - spacing + end + end + end + end + return sortedElements end local function calculateRow(self, children, spacing, justifyContent) local containerWidth = self.get("width") - - local usedSpace = spacing * (#children - 1) + local containerHeight = self.get("height") + local alignItems = self.get("flexAlignItems") + local crossPadding = self.get("flexCrossPadding") + + -- Safety check + if containerWidth <= 0 then return end + + -- Calculate available cross axis space (considering padding) + local availableCrossAxisSpace = containerHeight - (crossPadding * 2) + if availableCrossAxisSpace < 1 then + availableCrossAxisSpace = containerHeight + crossPadding = 0 + end + + -- Cache local variables to reduce function calls + local max = math.max + local min = math.min + local floor = math.floor + local ceil = math.ceil + + -- Fixed elements + local fixedElements = {} + -- Flexible elements + local flexElements = {} + -- Total flex coefficient local totalFlexGrow = 0 - + -- Pre-allocate capacity + local fixedCount = 0 + local flexCount = 0 + + -- First calculate element counts to pre-allocate space for _, child in ipairs(children) do if child ~= lineBreakElement then - usedSpace = usedSpace + child.get("width") - totalFlexGrow = totalFlexGrow + child.get("flexGrow") + local grow = child.get("flexGrow") or 0 + if grow > 0 then + flexCount = flexCount + 1 + else + fixedCount = fixedCount + 1 + end end end - - local remainingSpace = containerWidth - usedSpace - local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 - local distributedSpace = 0 - - local currentX = 1 - for i, child in ipairs(children) do + + -- Pre-allocate table space + for i = 1, fixedCount do fixedElements[i] = nil end + + -- Step 1: Categorize elements and collect information + for _, child in ipairs(children) do if child ~= lineBreakElement then - local childWidth = child.get("width") - - if child.get("flexGrow") > 0 then - - if i == #children then - local extraSpace = remainingSpace - distributedSpace - childWidth = childWidth + extraSpace - else - local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) - childWidth = childWidth + extraSpace - distributedSpace = distributedSpace + extraSpace + local grow = child.get("flexGrow") or 0 + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + table.insert(flexElements, {element = child, grow = grow}) + else + table.insert(fixedElements, child) + end + end + end + + -- Step 2: Pre-processing before layout + -- First calculate the total width needed for all fixed elements + local fixedWidthSum = 0 + for _, element in ipairs(fixedElements) do + fixedWidthSum = fixedWidthSum + element.get("width") + end + + -- Calculate total width of gaps + local totalElements = #fixedElements + #flexElements + local totalGaps = totalElements > 1 and (totalElements - 1) or 0 + local gapsWidth = spacing * totalGaps + + -- Calculate total available space for flexible elements + local flexAvailableSpace = max(0, containerWidth - fixedWidthSum - gapsWidth) + + -- Safety check: If not enough space, force compress fixed elements + if flexAvailableSpace < 0 then + -- Set gaps to zero + gapsWidth = 0 + flexAvailableSpace = containerWidth - fixedWidthSum + + -- If still not enough, need to shrink fixed elements + if flexAvailableSpace < 0 and #fixedElements > 0 then + local reductionPerElement = ceil(-flexAvailableSpace / #fixedElements) + for _, element in ipairs(fixedElements) do + local currentWidth = element.get("width") + local newWidth = max(1, currentWidth - reductionPerElement) + element.set("width", newWidth) + flexAvailableSpace = flexAvailableSpace + (currentWidth - newWidth) + if flexAvailableSpace >= 0 then + break end end - - child.set("x", currentX) - child.set("y", children.offset or 1) - child.set("width", childWidth) - currentX = currentX + childWidth + spacing + end + + -- If still not enough, may need to set minimum width + flexAvailableSpace = max(0, flexAvailableSpace) + end + + -- Step 3: Allocate space for flexible elements + -- Pre-allocate table to avoid dynamic expansion + local allocatedWidths = {} + for i = 1, flexCount do + allocatedWidths[flexElements[i].element] = nil + end + + -- If there are flexible elements and available space + if #flexElements > 0 and flexAvailableSpace > 0 and totalFlexGrow > 0 then + -- Reserve some safety margin (e.g., 5% of space) to ensure no overflow due to rounding + local safeFlexSpace = floor(flexAvailableSpace * 0.95) + + -- Allocate base width for each element (conservative strategy) + for _, item in ipairs(flexElements) do + -- Determine this element's share + local proportion = item.grow / totalFlexGrow + -- Determine width to allocate (floor to ensure safety) + local extraWidth = floor(safeFlexSpace * proportion) + -- Set final width + allocatedWidths[item.element] = item.element.get("width") + extraWidth end end - - if justifyContent == "flex-end" then - local offset = containerWidth - (currentX - spacing - 1) - for _, child in ipairs(children) do - child.set("x", child.get("x") + offset) + + -- Step 4: Strictly validate final widths + -- Calculate total width after allocation (including gaps) + local finalTotalWidth = gapsWidth + for _, element in ipairs(fixedElements) do + finalTotalWidth = finalTotalWidth + element.get("width") + end + for _, item in ipairs(flexElements) do + local width = allocatedWidths[item.element] or item.element.get("width") + finalTotalWidth = finalTotalWidth + width + end + + -- If total width exceeds container, proportionally reduce all elements + if finalTotalWidth > containerWidth then + local excessWidth = finalTotalWidth - containerWidth + local reductionFactor = excessWidth / (finalTotalWidth - gapsWidth) + + -- First reduce flexible elements + if #flexElements > 0 then + for _, item in ipairs(flexElements) do + local width = allocatedWidths[item.element] or item.element.get("width") + local reduction = ceil(width * reductionFactor) + allocatedWidths[item.element] = max(1, width - reduction) + end end - elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate - local offset = math.floor((containerWidth - (currentX - spacing - 1)) / 2) - for _, child in ipairs(children) do - child.set("x", child.get("x") + offset) + + -- If still not enough, reduce fixed elements + finalTotalWidth = gapsWidth + for _, element in ipairs(fixedElements) do + finalTotalWidth = finalTotalWidth + element.get("width") + end + for _, item in ipairs(flexElements) do + finalTotalWidth = finalTotalWidth + (allocatedWidths[item.element] or item.element.get("width")) + end + + if finalTotalWidth > containerWidth and #fixedElements > 0 then + excessWidth = finalTotalWidth - containerWidth + reductionFactor = excessWidth / (finalTotalWidth - gapsWidth) + + for _, element in ipairs(fixedElements) do + local width = element.get("width") + local reduction = ceil(width * reductionFactor) + element.set("width", max(1, width - reduction)) + end + end + end + + -- Step 5: Apply layout + local currentX = 1 + + -- Place all elements + for _, child in ipairs(children) do + if child ~= lineBreakElement then + -- Apply X coordinate + child.set("x", currentX) + + -- Apply Y coordinate (based on vertical alignment) + if alignItems == "stretch" then + -- Vertical stretch to fill container, considering padding + child.set("height", availableCrossAxisSpace) + child.set("y", 1 + crossPadding) + else + local childHeight = child.get("height") + local y = 1 + + if alignItems == "flex-end" then + -- Bottom align + y = containerHeight - childHeight + 1 + elseif alignItems == "flex-center" or alignItems == "center" then + -- Center align + y = floor((containerHeight - childHeight) / 2) + 1 + end + + -- Ensure Y value is not less than 1 + child.set("y", max(1, y)) + end + + -- If flexible element, apply calculated width + if allocatedWidths[child] then + child.set("width", allocatedWidths[child]) + end + + -- Final safety check (using cached math functions) + local rightEdge = currentX + child.get("width") - 1 + if rightEdge > containerWidth then + child.set("width", max(1, containerWidth - currentX + 1)) + end + + -- Final safety check height doesn't exceed container + local bottomEdge = child.get("y") + child.get("height") - 1 + if bottomEdge > containerHeight then + child.set("height", max(1, containerHeight - child.get("y") + 1)) + end + + -- Update position for next element + currentX = currentX + child.get("width") + spacing + + -- Ensure won't exceed container right edge + if currentX > containerWidth + 1 then + currentX = containerWidth + 1 + end + end + end + + -- Apply alignment (only when remaining space is positive) + local usedWidth = min(containerWidth, currentX - spacing - 1) + local remainingSpace = containerWidth - usedWidth + + if remainingSpace > 0 then + if justifyContent == "flex-end" then + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("x", child.get("x") + remainingSpace) + end + end + elseif justifyContent == "flex-center" or justifyContent == "center" then + local offset = floor(remainingSpace / 2) + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("x", child.get("x") + offset) + end + end end end end local function calculateColumn(self, children, spacing, justifyContent) + local containerWidth = self.get("width") local containerHeight = self.get("height") - - local usedSpace = spacing * (#children - 1) + local alignItems = self.get("flexAlignItems") + local crossPadding = self.get("flexCrossPadding") + + -- Safety check + if containerHeight <= 0 then return end + + -- Calculate available cross axis space (considering padding) + local availableCrossAxisSpace = containerWidth - (crossPadding * 2) + if availableCrossAxisSpace < 1 then + availableCrossAxisSpace = containerWidth + crossPadding = 0 + end + + -- Cache local variables to reduce function calls + local max = math.max + local min = math.min + local floor = math.floor + local ceil = math.ceil + + -- Fixed elements + local fixedElements = {} + -- Flexible elements + local flexElements = {} + -- Total flex coefficient local totalFlexGrow = 0 - + -- Pre-allocate capacity + local fixedCount = 0 + local flexCount = 0 + + -- First calculate element counts to pre-allocate space for _, child in ipairs(children) do if child ~= lineBreakElement then - usedSpace = usedSpace + child.get("height") - totalFlexGrow = totalFlexGrow + child.get("flexGrow") + local grow = child.get("flexGrow") or 0 + if grow > 0 then + flexCount = flexCount + 1 + else + fixedCount = fixedCount + 1 + end end end - - local remainingSpace = containerHeight - usedSpace - local extraSpacePerUnit = totalFlexGrow > 0 and (remainingSpace / totalFlexGrow) or 0 - local distributedSpace = 0 - - local currentY = 1 - for i, child in ipairs(children) do + + -- Pre-allocate table space + for i = 1, fixedCount do fixedElements[i] = nil end + + -- Step 1: Categorize elements and collect information + for _, child in ipairs(children) do if child ~= lineBreakElement then - local childHeight = child.get("height") - - if child.get("flexGrow") > 0 then - - if i == #children then - local extraSpace = remainingSpace - distributedSpace - childHeight = childHeight + extraSpace - else - local extraSpace = math.floor(extraSpacePerUnit * child.get("flexGrow")) - childHeight = childHeight + extraSpace - distributedSpace = distributedSpace + extraSpace + local grow = child.get("flexGrow") or 0 + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + table.insert(flexElements, {element = child, grow = grow}) + else + table.insert(fixedElements, child) + end + end + end + + -- Step 2: Pre-processing before layout + + -- First calculate the total height needed for all fixed elements + local fixedHeightSum = 0 + for _, element in ipairs(fixedElements) do + fixedHeightSum = fixedHeightSum + element.get("height") + end + + -- Calculate total height of gaps + local totalElements = #fixedElements + #flexElements + local totalGaps = totalElements > 1 and (totalElements - 1) or 0 + local gapsHeight = spacing * totalGaps + + -- Calculate total available space for flexible elements + local flexAvailableSpace = max(0, containerHeight - fixedHeightSum - gapsHeight) + + -- Safety check: If not enough space, force compress fixed elements + if flexAvailableSpace < 0 then + -- Set gaps to zero + gapsHeight = 0 + flexAvailableSpace = containerHeight - fixedHeightSum + + -- If still not enough, need to shrink fixed elements + if flexAvailableSpace < 0 and #fixedElements > 0 then + local reductionPerElement = ceil(-flexAvailableSpace / #fixedElements) + for _, element in ipairs(fixedElements) do + local currentHeight = element.get("height") + local newHeight = max(1, currentHeight - reductionPerElement) + element.set("height", newHeight) + flexAvailableSpace = flexAvailableSpace + (currentHeight - newHeight) + if flexAvailableSpace >= 0 then + break end end - - child.set("x", children.offset or 1) - child.set("y", currentY) - child.set("height", childHeight) - currentY = currentY + childHeight + spacing + end + + -- If still not enough, may need to set minimum height + flexAvailableSpace = max(0, flexAvailableSpace) + end + + -- Step 3: Allocate space for flexible elements + -- Pre-allocate table to avoid dynamic expansion + local allocatedHeights = {} + for i = 1, flexCount do + allocatedHeights[flexElements[i].element] = nil + end + + -- If there are flexible elements and available space + if #flexElements > 0 and flexAvailableSpace > 0 and totalFlexGrow > 0 then + -- Reserve some safety margin (e.g., 5% of space) to ensure no overflow due to rounding + local safeFlexSpace = floor(flexAvailableSpace * 0.95) + + -- Allocate base height for each element (conservative strategy) + for _, item in ipairs(flexElements) do + -- Determine this element's share + local proportion = item.grow / totalFlexGrow + -- Determine height to allocate (floor to ensure safety) + local extraHeight = floor(safeFlexSpace * proportion) + -- Set final height + allocatedHeights[item.element] = item.element.get("height") + extraHeight end end - - if justifyContent == "flex-end" then - local offset = containerHeight - (currentY - spacing - 1) - for _, child in ipairs(children) do - child.set("y", child.get("y") + offset) + + -- Step 4: Strictly validate final heights + -- Calculate total height after allocation (including gaps) + local finalTotalHeight = gapsHeight + for _, element in ipairs(fixedElements) do + finalTotalHeight = finalTotalHeight + element.get("height") + end + for _, item in ipairs(flexElements) do + local height = allocatedHeights[item.element] or item.element.get("height") + finalTotalHeight = finalTotalHeight + height + end + + -- If total height exceeds container, proportionally reduce all elements + if finalTotalHeight > containerHeight then + local excessHeight = finalTotalHeight - containerHeight + local reductionFactor = excessHeight / (finalTotalHeight - gapsHeight) + + -- First reduce flexible elements + if #flexElements > 0 then + for _, item in ipairs(flexElements) do + local height = allocatedHeights[item.element] or item.element.get("height") + local reduction = ceil(height * reductionFactor) + allocatedHeights[item.element] = max(1, height - reduction) + end end - elseif justifyContent == "flex-center" or justifyContent == "center" then -- Akzeptiere beide Formate - local offset = math.floor((containerHeight - (currentY - spacing - 1)) / 2) - for _, child in ipairs(children) do - child.set("y", child.get("y") + offset) + + -- If still not enough, reduce fixed elements + finalTotalHeight = gapsHeight + for _, element in ipairs(fixedElements) do + finalTotalHeight = finalTotalHeight + element.get("height") + end + for _, item in ipairs(flexElements) do + finalTotalHeight = finalTotalHeight + (allocatedHeights[item.element] or item.element.get("height")) + end + + if finalTotalHeight > containerHeight and #fixedElements > 0 then + excessHeight = finalTotalHeight - containerHeight + reductionFactor = excessHeight / (finalTotalHeight - gapsHeight) + + for _, element in ipairs(fixedElements) do + local height = element.get("height") + local reduction = ceil(height * reductionFactor) + element.set("height", max(1, height - reduction)) + end + end + end + + -- Step 5: Apply layout + local currentY = 1 + + -- Place all elements + for _, child in ipairs(children) do + if child ~= lineBreakElement then + -- Apply Y coordinate + child.set("y", currentY) + + -- Apply X coordinate (based on horizontal alignment) + if alignItems == "stretch" then + -- Horizontal stretch to fill container, considering padding + child.set("width", availableCrossAxisSpace) + child.set("x", 1 + crossPadding) + else + local childWidth = child.get("width") + local x = 1 + + if alignItems == "flex-end" then + -- Right align + x = containerWidth - childWidth + 1 + elseif alignItems == "flex-center" or alignItems == "center" then + -- Center align + x = floor((containerWidth - childWidth) / 2) + 1 + end + + -- Ensure X value is not less than 1 + child.set("x", max(1, x)) + end + + -- If flexible element, apply calculated height + if allocatedHeights[child] then + child.set("height", allocatedHeights[child]) + end + + -- Final safety check (using cached math functions) + local bottomEdge = currentY + child.get("height") - 1 + if bottomEdge > containerHeight then + child.set("height", max(1, containerHeight - currentY + 1)) + end + + -- Final safety check width doesn't exceed container + local rightEdge = child.get("x") + child.get("width") - 1 + if rightEdge > containerWidth then + child.set("width", max(1, containerWidth - child.get("x") + 1)) + end + + -- Update position for next element + currentY = currentY + child.get("height") + spacing + + -- Ensure won't exceed container bottom edge + if currentY > containerHeight + 1 then + currentY = containerHeight + 1 + end + end + end + + -- Apply alignment (only when remaining space is positive) + local usedHeight = min(containerHeight, currentY - spacing - 1) + local remainingSpace = containerHeight - usedHeight + + if remainingSpace > 0 then + if justifyContent == "flex-end" then + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("y", child.get("y") + remainingSpace) + end + end + elseif justifyContent == "flex-center" or justifyContent == "center" then + local offset = floor(remainingSpace / 2) + for _, child in ipairs(children) do + if child ~= lineBreakElement then + child.set("y", child.get("y") + offset) + end + end end end end +-- Optimize updateLayout function local function updateLayout(self, direction, spacing, justifyContent, wrap) - local elements = sortElements(self, direction, spacing, wrap) - if direction == "row" then - for _,v in pairs(elements)do - calculateRow(self, v, spacing, justifyContent) - end - else - for _,v in pairs(elements)do - calculateColumn(self, v, spacing, justifyContent) - end + -- Get all elements that need layout + local elements = sortElements(self, direction, spacing, wrap) + + -- Based on direction, select layout function, avoid checking every iteration + local layoutFunction = direction == "row" and calculateRow or calculateColumn + + -- Apply layout calculation + for _, rowOrColumn in pairs(elements) do + layoutFunction(self, rowOrColumn, spacing, justifyContent) end + + -- Reset layout update flag self.set("flexUpdateLayout", false) end From 7df5380d18169f37f274d0f0bb975a157ae689d3 Mon Sep 17 00:00:00 2001 From: megaSukura <1390953859@qq.com> Date: Sun, 20 Apr 2025 02:50:01 +0800 Subject: [PATCH 3/5] fix FlexWrap --- src/elements/Flexbox.lua | 948 +++++++++++++++++++++++---------------- 1 file changed, 558 insertions(+), 390 deletions(-) diff --git a/src/elements/Flexbox.lua b/src/elements/Flexbox.lua index f8a5030..6eda187 100644 --- a/src/elements/Flexbox.lua +++ b/src/elements/Flexbox.lua @@ -76,19 +76,27 @@ local function sortElements(self, direction, spacing, wrap) local visibleElements = {} local childCount = 0 - -- First calculate the number of visible elements, pre-allocate space + -- First gather all visible elements for _, elem in pairs(elements) do - if elem ~= lineBreakElement and elem:getVisible() then - childCount = childCount + 1 + if elem:getVisible() then + table.insert(visibleElements, elem) + if elem ~= lineBreakElement then + childCount = childCount + 1 + end end end + -- No visible elements, nothing to layout + if childCount == 0 then + return sortedElements + end + -- Use known size to pre-allocate array if not wrap then -- No-wrap mode, all elements in one row/column sortedElements[1] = {offset=1} - for _, elem in pairs(elements) do + for _, elem in ipairs(visibleElements) do if elem == lineBreakElement then -- Create new line local nextIndex = #sortedElements + 1 @@ -100,42 +108,125 @@ local function sortElements(self, direction, spacing, wrap) end end else - -- Wrap mode, need to calculate rows/columns - local index = 1 + -- Wrap mode, need to calculate rows/columns more optimally local containerSize = direction == "row" and self.get("width") or self.get("height") - local remainingSpace = containerSize - sortedElements[index] = {offset=1} - for _, elem in pairs(elements) do + -- First split elements by line breaks + local segments = {{}} + local currentSegment = 1 + + for _, elem in ipairs(visibleElements) do if elem == lineBreakElement then - -- Create new line - index = index + 1 - sortedElements[index] = {offset=1} - remainingSpace = containerSize + -- Start a new segment + currentSegment = currentSegment + 1 + segments[currentSegment] = {} else - local elemSize = direction == "row" and elem.get("width") or elem.get("height") - if elemSize + spacing <= remainingSpace then - -- Element fits in current line - table.insert(sortedElements[index], elem) - remainingSpace = remainingSpace - elemSize - spacing - else - -- Need new line - index = index + 1 - sortedElements[index] = {offset=1, elem} - remainingSpace = containerSize - elemSize - spacing + -- Add to current segment + table.insert(segments[currentSegment], elem) + end + end + + -- Now process each segment optimally + for segmentIndex, segment in ipairs(segments) do + if #segment == 0 then + -- Empty segment (consecutive line breaks) + sortedElements[#sortedElements + 1] = {offset=1} + else + -- Try to pack elements optimally within this segment + local rows = {} + local currentRow = {} + local currentWidth = 0 + + for _, elem in ipairs(segment) do + -- Get intrinsic size if available, otherwise use current size + local intrinsicSize = 0 + local currentSize = direction == "row" and elem.get("width") or elem.get("height") + + -- Try to get intrinsic size, safely + local hasIntrinsic = false + if direction == "row" then + local ok, intrinsicWidth = pcall(function() return elem.get("intrinsicWidth") end) + if ok and intrinsicWidth then + intrinsicSize = intrinsicWidth + hasIntrinsic = true + end + else + local ok, intrinsicHeight = pcall(function() return elem.get("intrinsicHeight") end) + if ok and intrinsicHeight then + intrinsicSize = intrinsicHeight + hasIntrinsic = true + end + end + + -- Fall back to current size if no intrinsic size + local elemSize = hasIntrinsic and intrinsicSize or currentSize + + local spaceNeeded = elemSize + + -- Add spacing if not first element in row + if #currentRow > 0 then + spaceNeeded = spaceNeeded + spacing + end + + -- Check if element fits in current row + if currentWidth + spaceNeeded <= containerSize or #currentRow == 0 then + -- Element fits or it's first element (must place even if too large) + table.insert(currentRow, elem) + currentWidth = currentWidth + spaceNeeded + else + -- Element doesn't fit, start new row + table.insert(rows, currentRow) + currentRow = {elem} + currentWidth = elemSize + end + end + + -- Don't forget the last row + if #currentRow > 0 then + table.insert(rows, currentRow) + end + + -- Add rows to sorted elements + for _, row in ipairs(rows) do + sortedElements[#sortedElements + 1] = {offset=1} + for _, elem in ipairs(row) do + table.insert(sortedElements[#sortedElements], elem) + end end end end end - return sortedElements + -- Filter out empty rows/columns + local filteredElements = {} + for i, rowOrColumn in ipairs(sortedElements) do + if #rowOrColumn > 0 then + table.insert(filteredElements, rowOrColumn) + end + end + + return filteredElements end local function calculateRow(self, children, spacing, justifyContent) + -- Make a copy of children that filters out lineBreak elements + local filteredChildren = {} + for _, child in ipairs(children) do + if child ~= lineBreakElement then + table.insert(filteredChildren, child) + end + end + + -- Skip processing if no children + if #filteredChildren == 0 then + return + end + local containerWidth = self.get("width") local containerHeight = self.get("height") local alignItems = self.get("flexAlignItems") local crossPadding = self.get("flexCrossPadding") + local wrap = self.get("flexWrap") -- Safety check if containerWidth <= 0 then return end @@ -153,162 +244,131 @@ local function calculateRow(self, children, spacing, justifyContent) local floor = math.floor local ceil = math.ceil - -- Fixed elements - local fixedElements = {} - -- Flexible elements - local flexElements = {} - -- Total flex coefficient + -- Categorize elements and calculate their minimal widths and flexibilities + local totalFixedWidth = 0 local totalFlexGrow = 0 - -- Pre-allocate capacity - local fixedCount = 0 - local flexCount = 0 + local minWidths = {} + local flexGrows = {} + local flexShrinks = {} - -- First calculate element counts to pre-allocate space - for _, child in ipairs(children) do - if child ~= lineBreakElement then - local grow = child.get("flexGrow") or 0 - if grow > 0 then - flexCount = flexCount + 1 - else - fixedCount = fixedCount + 1 - end - end - end - - -- Pre-allocate table space - for i = 1, fixedCount do fixedElements[i] = nil end - - -- Step 1: Categorize elements and collect information - for _, child in ipairs(children) do - if child ~= lineBreakElement then - local grow = child.get("flexGrow") or 0 - if grow > 0 then - totalFlexGrow = totalFlexGrow + grow - table.insert(flexElements, {element = child, grow = grow}) - else - table.insert(fixedElements, child) - end - end - end - - -- Step 2: Pre-processing before layout - -- First calculate the total width needed for all fixed elements - local fixedWidthSum = 0 - for _, element in ipairs(fixedElements) do - fixedWidthSum = fixedWidthSum + element.get("width") - end - - -- Calculate total width of gaps - local totalElements = #fixedElements + #flexElements - local totalGaps = totalElements > 1 and (totalElements - 1) or 0 - local gapsWidth = spacing * totalGaps - - -- Calculate total available space for flexible elements - local flexAvailableSpace = max(0, containerWidth - fixedWidthSum - gapsWidth) - - -- Safety check: If not enough space, force compress fixed elements - if flexAvailableSpace < 0 then - -- Set gaps to zero - gapsWidth = 0 - flexAvailableSpace = containerWidth - fixedWidthSum + -- First pass: collect fixed widths and flex properties + for _, child in ipairs(filteredChildren) do + local grow = child.get("flexGrow") or 0 + local shrink = child.get("flexShrink") or 0 + local width = child.get("width") - -- If still not enough, need to shrink fixed elements - if flexAvailableSpace < 0 and #fixedElements > 0 then - local reductionPerElement = ceil(-flexAvailableSpace / #fixedElements) - for _, element in ipairs(fixedElements) do - local currentWidth = element.get("width") - local newWidth = max(1, currentWidth - reductionPerElement) - element.set("width", newWidth) - flexAvailableSpace = flexAvailableSpace + (currentWidth - newWidth) - if flexAvailableSpace >= 0 then - break + -- Track element properties + flexGrows[child] = grow + flexShrinks[child] = shrink + minWidths[child] = width + + -- Calculate total flex grow factor + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + else + -- If not flex grow, it's a fixed element + totalFixedWidth = totalFixedWidth + width + end + end + + -- Calculate total spacing + local elementsCount = #filteredChildren + local totalSpacing = (elementsCount > 1) and ((elementsCount - 1) * spacing) or 0 + + -- Calculate available space for flex items + local availableSpace = containerWidth - totalFixedWidth - totalSpacing + + -- Second pass: distribute available space to flex-grow items + if availableSpace > 0 and totalFlexGrow > 0 then + -- Container has extra space - distribute according to flex-grow + for _, child in ipairs(filteredChildren) do + local grow = flexGrows[child] + if grow > 0 then + -- Calculate flex basis (never less than minWidth) + local minWidth = minWidths[child] + local flexWidth = floor((grow / totalFlexGrow) * availableSpace) + + -- Set calculated width, ensure it's at least 1 + child.set("width", max(flexWidth, 1)) + end + end + elseif availableSpace < 0 then + -- Container doesn't have enough space - check for shrinkable items + local totalFlexShrink = 0 + local shrinkableItems = {} + + -- Find shrinkable items + for _, child in ipairs(filteredChildren) do + local shrink = flexShrinks[child] + if shrink > 0 then + totalFlexShrink = totalFlexShrink + shrink + table.insert(shrinkableItems, child) + end + end + + -- If we have shrinkable items, shrink them proportionally + if totalFlexShrink > 0 and #shrinkableItems > 0 then + local excessWidth = -availableSpace + + for _, child in ipairs(shrinkableItems) do + local width = child.get("width") + local shrink = flexShrinks[child] + local proportion = shrink / totalFlexShrink + local reduction = ceil(excessWidth * proportion) + + -- Ensure width doesn't go below 1 + child.set("width", max(1, width - reduction)) + end + end + + -- Recalculate fixed widths after shrinking + totalFixedWidth = 0 + for _, child in ipairs(filteredChildren) do + totalFixedWidth = totalFixedWidth + child.get("width") + end + + -- If we still have flex-grow items, ensure they have proportional space + if totalFlexGrow > 0 then + local growableItems = {} + local totalGrowableInitialWidth = 0 + + -- Find growable items + for _, child in ipairs(filteredChildren) do + if flexGrows[child] > 0 then + table.insert(growableItems, child) + totalGrowableInitialWidth = totalGrowableInitialWidth + child.get("width") + end + end + + -- Ensure flexGrow items get at least some width, even if space is tight + if #growableItems > 0 and totalGrowableInitialWidth > 0 then + -- Minimum guaranteed width for flex items (at least 20% of container) + local minFlexSpace = max(floor(containerWidth * 0.2), #growableItems) + + -- Reserve space for flex items + local reservedFlexSpace = min(minFlexSpace, containerWidth - totalSpacing) + + -- Distribute among flex items + for _, child in ipairs(growableItems) do + local grow = flexGrows[child] + local proportion = grow / totalFlexGrow + local flexWidth = max(1, floor(reservedFlexSpace * proportion)) + child.set("width", flexWidth) end end end - - -- If still not enough, may need to set minimum width - flexAvailableSpace = max(0, flexAvailableSpace) end - -- Step 3: Allocate space for flexible elements - -- Pre-allocate table to avoid dynamic expansion - local allocatedWidths = {} - for i = 1, flexCount do - allocatedWidths[flexElements[i].element] = nil - end - - -- If there are flexible elements and available space - if #flexElements > 0 and flexAvailableSpace > 0 and totalFlexGrow > 0 then - -- Reserve some safety margin (e.g., 5% of space) to ensure no overflow due to rounding - local safeFlexSpace = floor(flexAvailableSpace * 0.95) - - -- Allocate base width for each element (conservative strategy) - for _, item in ipairs(flexElements) do - -- Determine this element's share - local proportion = item.grow / totalFlexGrow - -- Determine width to allocate (floor to ensure safety) - local extraWidth = floor(safeFlexSpace * proportion) - -- Set final width - allocatedWidths[item.element] = item.element.get("width") + extraWidth - end - end - - -- Step 4: Strictly validate final widths - -- Calculate total width after allocation (including gaps) - local finalTotalWidth = gapsWidth - for _, element in ipairs(fixedElements) do - finalTotalWidth = finalTotalWidth + element.get("width") - end - for _, item in ipairs(flexElements) do - local width = allocatedWidths[item.element] or item.element.get("width") - finalTotalWidth = finalTotalWidth + width - end - - -- If total width exceeds container, proportionally reduce all elements - if finalTotalWidth > containerWidth then - local excessWidth = finalTotalWidth - containerWidth - local reductionFactor = excessWidth / (finalTotalWidth - gapsWidth) - - -- First reduce flexible elements - if #flexElements > 0 then - for _, item in ipairs(flexElements) do - local width = allocatedWidths[item.element] or item.element.get("width") - local reduction = ceil(width * reductionFactor) - allocatedWidths[item.element] = max(1, width - reduction) - end - end - - -- If still not enough, reduce fixed elements - finalTotalWidth = gapsWidth - for _, element in ipairs(fixedElements) do - finalTotalWidth = finalTotalWidth + element.get("width") - end - for _, item in ipairs(flexElements) do - finalTotalWidth = finalTotalWidth + (allocatedWidths[item.element] or item.element.get("width")) - end - - if finalTotalWidth > containerWidth and #fixedElements > 0 then - excessWidth = finalTotalWidth - containerWidth - reductionFactor = excessWidth / (finalTotalWidth - gapsWidth) - - for _, element in ipairs(fixedElements) do - local width = element.get("width") - local reduction = ceil(width * reductionFactor) - element.set("width", max(1, width - reduction)) - end - end - end - - -- Step 5: Apply layout + -- Step 3: Position elements (never allow overlapping) local currentX = 1 - -- Place all elements - for _, child in ipairs(children) do - if child ~= lineBreakElement then - -- Apply X coordinate - child.set("x", currentX) - - -- Apply Y coordinate (based on vertical alignment) + -- Place all elements sequentially + for _, child in ipairs(filteredChildren) do + -- Apply X coordinate + child.set("x", currentX) + + -- Apply Y coordinate (based on vertical alignment) ONLY if not in wrapped mode + if not wrap then if alignItems == "stretch" then -- Vertical stretch to fill container, considering padding child.set("height", availableCrossAxisSpace) @@ -328,61 +388,60 @@ local function calculateRow(self, children, spacing, justifyContent) -- Ensure Y value is not less than 1 child.set("y", max(1, y)) end - - -- If flexible element, apply calculated width - if allocatedWidths[child] then - child.set("width", allocatedWidths[child]) - end - - -- Final safety check (using cached math functions) - local rightEdge = currentX + child.get("width") - 1 - if rightEdge > containerWidth then - child.set("width", max(1, containerWidth - currentX + 1)) - end - - -- Final safety check height doesn't exceed container - local bottomEdge = child.get("y") + child.get("height") - 1 - if bottomEdge > containerHeight then - child.set("height", max(1, containerHeight - child.get("y") + 1)) - end - - -- Update position for next element - currentX = currentX + child.get("width") + spacing - - -- Ensure won't exceed container right edge - if currentX > containerWidth + 1 then - currentX = containerWidth + 1 - end end + + -- Final safety check height doesn't exceed container - only for elements with flexShrink + local bottomEdge = child.get("y") + child.get("height") - 1 + if bottomEdge > containerHeight and (child.get("flexShrink") or 0) > 0 then + child.set("height", max(1, containerHeight - child.get("y") + 1)) + end + + -- Update position for next element - advance by element width + spacing + currentX = currentX + child.get("width") + spacing + end + + -- Apply justifyContent only if there's remaining space + local lastChild = filteredChildren[#filteredChildren] + local usedWidth = 0 + if lastChild then + usedWidth = lastChild.get("x") + lastChild.get("width") - 1 end - -- Apply alignment (only when remaining space is positive) - local usedWidth = min(containerWidth, currentX - spacing - 1) local remainingSpace = containerWidth - usedWidth if remainingSpace > 0 then if justifyContent == "flex-end" then - for _, child in ipairs(children) do - if child ~= lineBreakElement then - child.set("x", child.get("x") + remainingSpace) - end + for _, child in ipairs(filteredChildren) do + child.set("x", child.get("x") + remainingSpace) end elseif justifyContent == "flex-center" or justifyContent == "center" then local offset = floor(remainingSpace / 2) - for _, child in ipairs(children) do - if child ~= lineBreakElement then - child.set("x", child.get("x") + offset) - end + for _, child in ipairs(filteredChildren) do + child.set("x", child.get("x") + offset) end end end end local function calculateColumn(self, children, spacing, justifyContent) + -- Make a copy of children that filters out lineBreak elements + local filteredChildren = {} + for _, child in ipairs(children) do + if child ~= lineBreakElement then + table.insert(filteredChildren, child) + end + end + + -- Skip processing if no children + if #filteredChildren == 0 then + return + end + local containerWidth = self.get("width") local containerHeight = self.get("height") local alignItems = self.get("flexAlignItems") local crossPadding = self.get("flexCrossPadding") + local wrap = self.get("flexWrap") -- Safety check if containerHeight <= 0 then return end @@ -400,163 +459,131 @@ local function calculateColumn(self, children, spacing, justifyContent) local floor = math.floor local ceil = math.ceil - -- Fixed elements - local fixedElements = {} - -- Flexible elements - local flexElements = {} - -- Total flex coefficient + -- Categorize elements and calculate their minimal heights and flexibilities + local totalFixedHeight = 0 local totalFlexGrow = 0 - -- Pre-allocate capacity - local fixedCount = 0 - local flexCount = 0 + local minHeights = {} + local flexGrows = {} + local flexShrinks = {} - -- First calculate element counts to pre-allocate space - for _, child in ipairs(children) do - if child ~= lineBreakElement then - local grow = child.get("flexGrow") or 0 - if grow > 0 then - flexCount = flexCount + 1 - else - fixedCount = fixedCount + 1 - end - end - end - - -- Pre-allocate table space - for i = 1, fixedCount do fixedElements[i] = nil end - - -- Step 1: Categorize elements and collect information - for _, child in ipairs(children) do - if child ~= lineBreakElement then - local grow = child.get("flexGrow") or 0 - if grow > 0 then - totalFlexGrow = totalFlexGrow + grow - table.insert(flexElements, {element = child, grow = grow}) - else - table.insert(fixedElements, child) - end - end - end - - -- Step 2: Pre-processing before layout - - -- First calculate the total height needed for all fixed elements - local fixedHeightSum = 0 - for _, element in ipairs(fixedElements) do - fixedHeightSum = fixedHeightSum + element.get("height") - end - - -- Calculate total height of gaps - local totalElements = #fixedElements + #flexElements - local totalGaps = totalElements > 1 and (totalElements - 1) or 0 - local gapsHeight = spacing * totalGaps - - -- Calculate total available space for flexible elements - local flexAvailableSpace = max(0, containerHeight - fixedHeightSum - gapsHeight) - - -- Safety check: If not enough space, force compress fixed elements - if flexAvailableSpace < 0 then - -- Set gaps to zero - gapsHeight = 0 - flexAvailableSpace = containerHeight - fixedHeightSum + -- First pass: collect fixed heights and flex properties + for _, child in ipairs(filteredChildren) do + local grow = child.get("flexGrow") or 0 + local shrink = child.get("flexShrink") or 0 + local height = child.get("height") - -- If still not enough, need to shrink fixed elements - if flexAvailableSpace < 0 and #fixedElements > 0 then - local reductionPerElement = ceil(-flexAvailableSpace / #fixedElements) - for _, element in ipairs(fixedElements) do - local currentHeight = element.get("height") - local newHeight = max(1, currentHeight - reductionPerElement) - element.set("height", newHeight) - flexAvailableSpace = flexAvailableSpace + (currentHeight - newHeight) - if flexAvailableSpace >= 0 then - break + -- Track element properties + flexGrows[child] = grow + flexShrinks[child] = shrink + minHeights[child] = height + + -- Calculate total flex grow factor + if grow > 0 then + totalFlexGrow = totalFlexGrow + grow + else + -- If not flex grow, it's a fixed element + totalFixedHeight = totalFixedHeight + height + end + end + + -- Calculate total spacing + local elementsCount = #filteredChildren + local totalSpacing = (elementsCount > 1) and ((elementsCount - 1) * spacing) or 0 + + -- Calculate available space for flex items + local availableSpace = containerHeight - totalFixedHeight - totalSpacing + + -- Second pass: distribute available space to flex-grow items + if availableSpace > 0 and totalFlexGrow > 0 then + -- Container has extra space - distribute according to flex-grow + for _, child in ipairs(filteredChildren) do + local grow = flexGrows[child] + if grow > 0 then + -- Calculate flex basis (never less than minHeight) + local minHeight = minHeights[child] + local flexHeight = floor((grow / totalFlexGrow) * availableSpace) + + -- Set calculated height, ensure it's at least 1 + child.set("height", max(flexHeight, 1)) + end + end + elseif availableSpace < 0 then + -- Container doesn't have enough space - check for shrinkable items + local totalFlexShrink = 0 + local shrinkableItems = {} + + -- Find shrinkable items + for _, child in ipairs(filteredChildren) do + local shrink = flexShrinks[child] + if shrink > 0 then + totalFlexShrink = totalFlexShrink + shrink + table.insert(shrinkableItems, child) + end + end + + -- If we have shrinkable items, shrink them proportionally + if totalFlexShrink > 0 and #shrinkableItems > 0 then + local excessHeight = -availableSpace + + for _, child in ipairs(shrinkableItems) do + local height = child.get("height") + local shrink = flexShrinks[child] + local proportion = shrink / totalFlexShrink + local reduction = ceil(excessHeight * proportion) + + -- Ensure height doesn't go below 1 + child.set("height", max(1, height - reduction)) + end + end + + -- Recalculate fixed heights after shrinking + totalFixedHeight = 0 + for _, child in ipairs(filteredChildren) do + totalFixedHeight = totalFixedHeight + child.get("height") + end + + -- If we still have flex-grow items, ensure they have proportional space + if totalFlexGrow > 0 then + local growableItems = {} + local totalGrowableInitialHeight = 0 + + -- Find growable items + for _, child in ipairs(filteredChildren) do + if flexGrows[child] > 0 then + table.insert(growableItems, child) + totalGrowableInitialHeight = totalGrowableInitialHeight + child.get("height") + end + end + + -- Ensure flexGrow items get at least some height, even if space is tight + if #growableItems > 0 and totalGrowableInitialHeight > 0 then + -- Minimum guaranteed height for flex items (at least 20% of container) + local minFlexSpace = max(floor(containerHeight * 0.2), #growableItems) + + -- Reserve space for flex items + local reservedFlexSpace = min(minFlexSpace, containerHeight - totalSpacing) + + -- Distribute among flex items + for _, child in ipairs(growableItems) do + local grow = flexGrows[child] + local proportion = grow / totalFlexGrow + local flexHeight = max(1, floor(reservedFlexSpace * proportion)) + child.set("height", flexHeight) end end end - - -- If still not enough, may need to set minimum height - flexAvailableSpace = max(0, flexAvailableSpace) end - -- Step 3: Allocate space for flexible elements - -- Pre-allocate table to avoid dynamic expansion - local allocatedHeights = {} - for i = 1, flexCount do - allocatedHeights[flexElements[i].element] = nil - end - - -- If there are flexible elements and available space - if #flexElements > 0 and flexAvailableSpace > 0 and totalFlexGrow > 0 then - -- Reserve some safety margin (e.g., 5% of space) to ensure no overflow due to rounding - local safeFlexSpace = floor(flexAvailableSpace * 0.95) - - -- Allocate base height for each element (conservative strategy) - for _, item in ipairs(flexElements) do - -- Determine this element's share - local proportion = item.grow / totalFlexGrow - -- Determine height to allocate (floor to ensure safety) - local extraHeight = floor(safeFlexSpace * proportion) - -- Set final height - allocatedHeights[item.element] = item.element.get("height") + extraHeight - end - end - - -- Step 4: Strictly validate final heights - -- Calculate total height after allocation (including gaps) - local finalTotalHeight = gapsHeight - for _, element in ipairs(fixedElements) do - finalTotalHeight = finalTotalHeight + element.get("height") - end - for _, item in ipairs(flexElements) do - local height = allocatedHeights[item.element] or item.element.get("height") - finalTotalHeight = finalTotalHeight + height - end - - -- If total height exceeds container, proportionally reduce all elements - if finalTotalHeight > containerHeight then - local excessHeight = finalTotalHeight - containerHeight - local reductionFactor = excessHeight / (finalTotalHeight - gapsHeight) - - -- First reduce flexible elements - if #flexElements > 0 then - for _, item in ipairs(flexElements) do - local height = allocatedHeights[item.element] or item.element.get("height") - local reduction = ceil(height * reductionFactor) - allocatedHeights[item.element] = max(1, height - reduction) - end - end - - -- If still not enough, reduce fixed elements - finalTotalHeight = gapsHeight - for _, element in ipairs(fixedElements) do - finalTotalHeight = finalTotalHeight + element.get("height") - end - for _, item in ipairs(flexElements) do - finalTotalHeight = finalTotalHeight + (allocatedHeights[item.element] or item.element.get("height")) - end - - if finalTotalHeight > containerHeight and #fixedElements > 0 then - excessHeight = finalTotalHeight - containerHeight - reductionFactor = excessHeight / (finalTotalHeight - gapsHeight) - - for _, element in ipairs(fixedElements) do - local height = element.get("height") - local reduction = ceil(height * reductionFactor) - element.set("height", max(1, height - reduction)) - end - end - end - - -- Step 5: Apply layout + -- Step 3: Position elements (never allow overlapping) local currentY = 1 - -- Place all elements - for _, child in ipairs(children) do - if child ~= lineBreakElement then - -- Apply Y coordinate - child.set("y", currentY) - - -- Apply X coordinate (based on horizontal alignment) + -- Place all elements sequentially + for _, child in ipairs(filteredChildren) do + -- Apply Y coordinate + child.set("y", currentY) + + -- Apply X coordinate (based on horizontal alignment) + if not wrap then if alignItems == "stretch" then -- Horizontal stretch to fill container, considering padding child.set("width", availableCrossAxisSpace) @@ -576,51 +603,36 @@ local function calculateColumn(self, children, spacing, justifyContent) -- Ensure X value is not less than 1 child.set("x", max(1, x)) end - - -- If flexible element, apply calculated height - if allocatedHeights[child] then - child.set("height", allocatedHeights[child]) - end - - -- Final safety check (using cached math functions) - local bottomEdge = currentY + child.get("height") - 1 - if bottomEdge > containerHeight then - child.set("height", max(1, containerHeight - currentY + 1)) - end - - -- Final safety check width doesn't exceed container - local rightEdge = child.get("x") + child.get("width") - 1 - if rightEdge > containerWidth then - child.set("width", max(1, containerWidth - child.get("x") + 1)) - end - - -- Update position for next element - currentY = currentY + child.get("height") + spacing - - -- Ensure won't exceed container bottom edge - if currentY > containerHeight + 1 then - currentY = containerHeight + 1 - end end + + -- Final safety check width doesn't exceed container - only for elements with flexShrink + local rightEdge = child.get("x") + child.get("width") - 1 + if rightEdge > containerWidth and (child.get("flexShrink") or 0) > 0 then + child.set("width", max(1, containerWidth - child.get("x") + 1)) + end + + -- Update position for next element - advance by element height + spacing + currentY = currentY + child.get("height") + spacing + end + + -- Apply justifyContent only if there's remaining space + local lastChild = filteredChildren[#filteredChildren] + local usedHeight = 0 + if lastChild then + usedHeight = lastChild.get("y") + lastChild.get("height") - 1 end - -- Apply alignment (only when remaining space is positive) - local usedHeight = min(containerHeight, currentY - spacing - 1) local remainingSpace = containerHeight - usedHeight if remainingSpace > 0 then if justifyContent == "flex-end" then - for _, child in ipairs(children) do - if child ~= lineBreakElement then - child.set("y", child.get("y") + remainingSpace) - end + for _, child in ipairs(filteredChildren) do + child.set("y", child.get("y") + remainingSpace) end elseif justifyContent == "flex-center" or justifyContent == "center" then local offset = floor(remainingSpace / 2) - for _, child in ipairs(children) do - if child ~= lineBreakElement then - child.set("y", child.get("y") + offset) - end + for _, child in ipairs(filteredChildren) do + child.set("y", child.get("y") + offset) end end end @@ -628,15 +640,138 @@ end -- Optimize updateLayout function local function updateLayout(self, direction, spacing, justifyContent, wrap) + -- Check essential properties for layout + if self.get("width") <= 0 or self.get("height") <= 0 then + return + end + + -- Force direction to be valid + direction = (direction == "row" or direction == "column") and direction or "row" + + -- Check if container size has changed since last layout + local currentWidth, currentHeight = self.get("width"), self.get("height") + local lastWidth = self.get("_lastLayoutWidth") or 0 + local lastHeight = self.get("_lastLayoutHeight") or 0 + local sizeChanged = currentWidth ~= lastWidth or currentHeight ~= lastHeight + + -- Store current size for next comparison + self.set("_lastLayoutWidth", currentWidth) + self.set("_lastLayoutHeight", currentHeight) + + -- If container size increased, we might need to reset flexGrow items to recalculate + if wrap and sizeChanged and (currentWidth > lastWidth or currentHeight > lastHeight) then + -- Get reference to all children + local allChildren = self.get("children") + + -- Reset flex items to intrinsic size temporarily to allow reflow + for _, child in pairs(allChildren) do + if child ~= lineBreakElement and child:getVisible() and child.get("flexGrow") and child.get("flexGrow") > 0 then + if direction == "row" then + -- Store the actual width temporarily + local actualWidth = child.get("width") + -- Reset to intrinsic width for layout calculation + local ok, value = pcall(function() return child.get("intrinsicWidth") end) + if ok and value then + child.set("width", value) + end + else + -- Store the actual height temporarily + local actualHeight = child.get("height") + -- Reset to intrinsic height for layout calculation + local ok, value = pcall(function() return child.get("intrinsicHeight") end) + if ok and value then + child.set("height", value) + end + end + end + end + end + -- Get all elements that need layout local elements = sortElements(self, direction, spacing, wrap) + -- Debug: Check what elements were found + if #elements == 0 then + return -- No elements to layout + end + -- Based on direction, select layout function, avoid checking every iteration local layoutFunction = direction == "row" and calculateRow or calculateColumn - -- Apply layout calculation - for _, rowOrColumn in pairs(elements) do - layoutFunction(self, rowOrColumn, spacing, justifyContent) + -- Apply layout calculation with vertical offset + if direction == "row" and wrap then + -- In row direction with wrap, we need to offset each row vertically + local currentY = 1 + for i, rowOrColumn in ipairs(elements) do + -- Skip empty rows + if #rowOrColumn == 0 then goto continue end + + -- First, set the vertical offset for this row + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + element.set("y", currentY) + end + end + + -- Apply the row layout + layoutFunction(self, rowOrColumn, spacing, justifyContent) + + -- Calculate height for this row (maximum element height) + local rowHeight = 0 + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + rowHeight = math.max(rowHeight, element.get("height")) + end + end + + -- Move to next row (add spacing only if not the last row) + if i < #elements then + currentY = currentY + rowHeight + spacing + else + currentY = currentY + rowHeight + end + + ::continue:: + end + elseif direction == "column" and wrap then + -- In column direction with wrap, we need to offset each column horizontally + local currentX = 1 + for i, rowOrColumn in ipairs(elements) do + -- Skip empty columns + if #rowOrColumn == 0 then goto continue end + + -- First, set the horizontal offset for this column + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + element.set("x", currentX) + end + end + + -- Apply the column layout + layoutFunction(self, rowOrColumn, spacing, justifyContent) + + -- Calculate width for this column (maximum element width) + local columnWidth = 0 + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + columnWidth = math.max(columnWidth, element.get("width")) + end + end + + -- Move to next column (add spacing only if not the last column) + if i < #elements then + currentX = currentX + columnWidth + spacing + else + currentX = currentX + columnWidth + end + + ::continue:: + end + else + -- Simple case: no wrapping + for i, rowOrColumn in ipairs(elements) do + layoutFunction(self, rowOrColumn, spacing, justifyContent) + end end -- Reset layout update flag @@ -653,8 +788,21 @@ function Flexbox.new() self.set("height", 6) self.set("background", colors.blue) self.set("z", 10) + + -- Add instance properties for layout tracking + self:instanceProperty("_lastLayoutWidth", {default = 0, type = "number"}) + self:instanceProperty("_lastLayoutHeight", {default = 0, type = "number"}) + + -- Add observers for properties that affect layout self:observe("width", function() self.set("flexUpdateLayout", true) end) self:observe("height", function() self.set("flexUpdateLayout", true) end) + self:observe("flexDirection", function() self.set("flexUpdateLayout", true) end) + self:observe("flexSpacing", function() self.set("flexUpdateLayout", true) end) + self:observe("flexWrap", function() self.set("flexUpdateLayout", true) end) + self:observe("flexJustifyContent", function() self.set("flexUpdateLayout", true) end) + self:observe("flexAlignItems", function() self.set("flexUpdateLayout", true) end) + self:observe("flexCrossPadding", function() self.set("flexUpdateLayout", true) end) + return self end @@ -680,6 +828,26 @@ function Flexbox:addChild(element) element:instanceProperty("flexGrow", {default = 0, type = "number"}) element:instanceProperty("flexShrink", {default = 0, type = "number"}) element:instanceProperty("flexBasis", {default = 0, type = "number"}) + element:instanceProperty("intrinsicWidth", {default = element.get("width"), type = "number"}) + element:instanceProperty("intrinsicHeight", {default = element.get("height"), type = "number"}) + + -- Add observer to child element's flexGrow and flexShrink properties + element:observe("flexGrow", function() self.set("flexUpdateLayout", true) end) + element:observe("flexShrink", function() self.set("flexUpdateLayout", true) end) + + -- Add observer for size changes to track intrinsic size + element:observe("width", function(_, oldW, newW) + if element.get("flexGrow") == 0 then + element.set("intrinsicWidth", newW) + end + self.set("flexUpdateLayout", true) + end) + element:observe("height", function(_, oldH, newH) + if element.get("flexGrow") == 0 then + element.set("intrinsicHeight", newH) + end + self.set("flexUpdateLayout", true) + end) end self.set("flexUpdateLayout", true) From 2fc72a6a135f1a771061d2476e03cbb2c862d13d Mon Sep 17 00:00:00 2001 From: megaSukura <1390953859@qq.com> Date: Sun, 20 Apr 2025 05:40:24 +0800 Subject: [PATCH 4/5] Removed all goto statements Fixed the parameter order in observe Used the correct internal property approach Did not use the built-in visibleChildren, as we need to handle elements that are obscured or completely removed --- src/elements/Flexbox.lua | 187 +++++++++++++-------------------------- 1 file changed, 60 insertions(+), 127 deletions(-) diff --git a/src/elements/Flexbox.lua b/src/elements/Flexbox.lua index 6eda187..c953241 100644 --- a/src/elements/Flexbox.lua +++ b/src/elements/Flexbox.lua @@ -68,17 +68,15 @@ local lineBreakElement = { getVisible = function(self) return true end, } - local function sortElements(self, direction, spacing, wrap) - -- Pre-allocate tables to reduce dynamic expansion - local elements = self.get("children") local sortedElements = {} local visibleElements = {} local childCount = 0 - -- First gather all visible elements - for _, elem in pairs(elements) do - if elem:getVisible() then + -- We can't use self.get("visibleChildren") here + --because it would exclude elements that are obscured + for _, elem in pairs(self.get("children")) do + if elem.get("visible") then table.insert(visibleElements, elem) if elem ~= lineBreakElement then childCount = childCount + 1 @@ -86,19 +84,16 @@ local function sortElements(self, direction, spacing, wrap) end end - -- No visible elements, nothing to layout + if childCount == 0 then return sortedElements end - -- Use known size to pre-allocate array if not wrap then - -- No-wrap mode, all elements in one row/column sortedElements[1] = {offset=1} for _, elem in ipairs(visibleElements) do if elem == lineBreakElement then - -- Create new line local nextIndex = #sortedElements + 1 if sortedElements[nextIndex] == nil then sortedElements[nextIndex] = {offset=1} @@ -108,41 +103,32 @@ local function sortElements(self, direction, spacing, wrap) end end else - -- Wrap mode, need to calculate rows/columns more optimally local containerSize = direction == "row" and self.get("width") or self.get("height") - -- First split elements by line breaks local segments = {{}} local currentSegment = 1 for _, elem in ipairs(visibleElements) do if elem == lineBreakElement then - -- Start a new segment currentSegment = currentSegment + 1 segments[currentSegment] = {} else - -- Add to current segment table.insert(segments[currentSegment], elem) end end - -- Now process each segment optimally for segmentIndex, segment in ipairs(segments) do if #segment == 0 then - -- Empty segment (consecutive line breaks) sortedElements[#sortedElements + 1] = {offset=1} else - -- Try to pack elements optimally within this segment local rows = {} local currentRow = {} local currentWidth = 0 for _, elem in ipairs(segment) do - -- Get intrinsic size if available, otherwise use current size local intrinsicSize = 0 local currentSize = direction == "row" and elem.get("width") or elem.get("height") - -- Try to get intrinsic size, safely local hasIntrinsic = false if direction == "row" then local ok, intrinsicWidth = pcall(function() return elem.get("intrinsicWidth") end) @@ -158,35 +144,28 @@ local function sortElements(self, direction, spacing, wrap) end end - -- Fall back to current size if no intrinsic size local elemSize = hasIntrinsic and intrinsicSize or currentSize local spaceNeeded = elemSize - -- Add spacing if not first element in row if #currentRow > 0 then spaceNeeded = spaceNeeded + spacing end - -- Check if element fits in current row if currentWidth + spaceNeeded <= containerSize or #currentRow == 0 then - -- Element fits or it's first element (must place even if too large) table.insert(currentRow, elem) currentWidth = currentWidth + spaceNeeded else - -- Element doesn't fit, start new row table.insert(rows, currentRow) currentRow = {elem} currentWidth = elemSize end end - -- Don't forget the last row if #currentRow > 0 then table.insert(rows, currentRow) end - -- Add rows to sorted elements for _, row in ipairs(rows) do sortedElements[#sortedElements + 1] = {offset=1} for _, elem in ipairs(row) do @@ -197,7 +176,6 @@ local function sortElements(self, direction, spacing, wrap) end end - -- Filter out empty rows/columns local filteredElements = {} for i, rowOrColumn in ipairs(sortedElements) do if #rowOrColumn > 0 then @@ -640,44 +618,27 @@ end -- Optimize updateLayout function local function updateLayout(self, direction, spacing, justifyContent, wrap) - -- Check essential properties for layout if self.get("width") <= 0 or self.get("height") <= 0 then return end - -- Force direction to be valid direction = (direction == "row" or direction == "column") and direction or "row" - -- Check if container size has changed since last layout local currentWidth, currentHeight = self.get("width"), self.get("height") - local lastWidth = self.get("_lastLayoutWidth") or 0 - local lastHeight = self.get("_lastLayoutHeight") or 0 - local sizeChanged = currentWidth ~= lastWidth or currentHeight ~= lastHeight + local sizeChanged = currentWidth ~= self._lastLayoutWidth or currentHeight ~= self._lastLayoutHeight - -- Store current size for next comparison - self.set("_lastLayoutWidth", currentWidth) - self.set("_lastLayoutHeight", currentHeight) + self._lastLayoutWidth = currentWidth + self._lastLayoutHeight = currentHeight - -- If container size increased, we might need to reset flexGrow items to recalculate - if wrap and sizeChanged and (currentWidth > lastWidth or currentHeight > lastHeight) then - -- Get reference to all children - local allChildren = self.get("children") - - -- Reset flex items to intrinsic size temporarily to allow reflow - for _, child in pairs(allChildren) do + if wrap and sizeChanged and (currentWidth > self._lastLayoutWidth or currentHeight > self._lastLayoutHeight) then + for _, child in pairs(self.get("children")) do if child ~= lineBreakElement and child:getVisible() and child.get("flexGrow") and child.get("flexGrow") > 0 then if direction == "row" then - -- Store the actual width temporarily - local actualWidth = child.get("width") - -- Reset to intrinsic width for layout calculation local ok, value = pcall(function() return child.get("intrinsicWidth") end) if ok and value then child.set("width", value) end else - -- Store the actual height temporarily - local actualHeight = child.get("height") - -- Reset to intrinsic height for layout calculation local ok, value = pcall(function() return child.get("intrinsicHeight") end) if ok and value then child.set("height", value) @@ -687,94 +648,70 @@ local function updateLayout(self, direction, spacing, justifyContent, wrap) end end - -- Get all elements that need layout local elements = sortElements(self, direction, spacing, wrap) + if #elements == 0 then return end - -- Debug: Check what elements were found - if #elements == 0 then - return -- No elements to layout - end - - -- Based on direction, select layout function, avoid checking every iteration local layoutFunction = direction == "row" and calculateRow or calculateColumn - -- Apply layout calculation with vertical offset if direction == "row" and wrap then - -- In row direction with wrap, we need to offset each row vertically local currentY = 1 for i, rowOrColumn in ipairs(elements) do - -- Skip empty rows - if #rowOrColumn == 0 then goto continue end - - -- First, set the vertical offset for this row - for _, element in ipairs(rowOrColumn) do - if element ~= lineBreakElement then - element.set("y", currentY) + if #rowOrColumn > 0 then + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + element.set("y", currentY) + end + end + + layoutFunction(self, rowOrColumn, spacing, justifyContent) + + local rowHeight = 0 + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + rowHeight = math.max(rowHeight, element.get("height")) + end + end + + if i < #elements then + currentY = currentY + rowHeight + spacing + else + currentY = currentY + rowHeight end end - - -- Apply the row layout - layoutFunction(self, rowOrColumn, spacing, justifyContent) - - -- Calculate height for this row (maximum element height) - local rowHeight = 0 - for _, element in ipairs(rowOrColumn) do - if element ~= lineBreakElement then - rowHeight = math.max(rowHeight, element.get("height")) - end - end - - -- Move to next row (add spacing only if not the last row) - if i < #elements then - currentY = currentY + rowHeight + spacing - else - currentY = currentY + rowHeight - end - - ::continue:: end elseif direction == "column" and wrap then - -- In column direction with wrap, we need to offset each column horizontally local currentX = 1 for i, rowOrColumn in ipairs(elements) do - -- Skip empty columns - if #rowOrColumn == 0 then goto continue end - - -- First, set the horizontal offset for this column - for _, element in ipairs(rowOrColumn) do - if element ~= lineBreakElement then - element.set("x", currentX) + if #rowOrColumn > 0 then + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + element.set("x", currentX) + end + end + + layoutFunction(self, rowOrColumn, spacing, justifyContent) + + local columnWidth = 0 + for _, element in ipairs(rowOrColumn) do + if element ~= lineBreakElement then + columnWidth = math.max(columnWidth, element.get("width")) + end + end + + if i < #elements then + currentX = currentX + columnWidth + spacing + else + currentX = currentX + columnWidth end end - - -- Apply the column layout - layoutFunction(self, rowOrColumn, spacing, justifyContent) - - -- Calculate width for this column (maximum element width) - local columnWidth = 0 - for _, element in ipairs(rowOrColumn) do - if element ~= lineBreakElement then - columnWidth = math.max(columnWidth, element.get("width")) - end - end - - -- Move to next column (add spacing only if not the last column) - if i < #elements then - currentX = currentX + columnWidth + spacing - else - currentX = currentX + columnWidth - end - - ::continue:: end else - -- Simple case: no wrapping - for i, rowOrColumn in ipairs(elements) do + for _, rowOrColumn in ipairs(elements) do layoutFunction(self, rowOrColumn, spacing, justifyContent) end end - - -- Reset layout update flag + self:sortChildren() + self.set("childrenEventsSorted", false) self.set("flexUpdateLayout", false) end @@ -789,11 +726,9 @@ function Flexbox.new() self.set("background", colors.blue) self.set("z", 10) - -- Add instance properties for layout tracking - self:instanceProperty("_lastLayoutWidth", {default = 0, type = "number"}) - self:instanceProperty("_lastLayoutHeight", {default = 0, type = "number"}) + self._lastLayoutWidth = 0 + self._lastLayoutHeight = 0 - -- Add observers for properties that affect layout self:observe("width", function() self.set("flexUpdateLayout", true) end) self:observe("height", function() self.set("flexUpdateLayout", true) end) self:observe("flexDirection", function() self.set("flexUpdateLayout", true) end) @@ -831,20 +766,18 @@ function Flexbox:addChild(element) element:instanceProperty("intrinsicWidth", {default = element.get("width"), type = "number"}) element:instanceProperty("intrinsicHeight", {default = element.get("height"), type = "number"}) - -- Add observer to child element's flexGrow and flexShrink properties element:observe("flexGrow", function() self.set("flexUpdateLayout", true) end) element:observe("flexShrink", function() self.set("flexUpdateLayout", true) end) - -- Add observer for size changes to track intrinsic size - element:observe("width", function(_, oldW, newW) + element:observe("width", function(_, newValue, oldValue) if element.get("flexGrow") == 0 then - element.set("intrinsicWidth", newW) + element.set("intrinsicWidth", newValue) end self.set("flexUpdateLayout", true) end) - element:observe("height", function(_, oldH, newH) + element:observe("height", function(_, newValue, oldValue) if element.get("flexGrow") == 0 then - element.set("intrinsicHeight", newH) + element.set("intrinsicHeight", newValue) end self.set("flexUpdateLayout", true) end) From 853600c70e8d28c904baa9e3893d70882fd892e6 Mon Sep 17 00:00:00 2001 From: Robert Jelic <36573031+NoryiE@users.noreply.github.com> Date: Sun, 20 Apr 2025 10:17:49 +0200 Subject: [PATCH 5/5] Update main.yml --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 736cde3..9781144 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -66,11 +66,11 @@ jobs: keep_files: true # Step 6: Generate Changelog - - name: Generate Changelog - id: changelog - uses: heinrichreimer/github-changelog-generator-action@v2.3 - with: - token: ${{ secrets.GITHUB_TOKEN }} + #- name: Generate Changelog + # id: changelog + # uses: heinrichreimer/github-changelog-generator-action@v2.3 + # with: + # token: ${{ secrets.GITHUB_TOKEN }} # Step 7: Commit all changes - name: Commit Changes @@ -79,4 +79,4 @@ jobs: git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' git add config.lua BasaltLS.lua release/basalt.lua CHANGELOG.md git commit -m "Update config, BasaltLS definitions, bundle and changelog" || exit 0 - git push \ No newline at end of file + git push