Adjust autosizing to better account for text wrapping.

This commit is contained in:
Michael Freno
2025-12-05 11:51:26 -05:00
parent 873299833b
commit ae2d28f6b1
2 changed files with 200 additions and 41 deletions

View File

@@ -634,6 +634,55 @@ function LayoutEngine:layoutChildren()
end end
end end
--- Simulate wrapping children into lines for auto-sizing calculations
---@param children table Array of child elements
---@param availableSize number Available space in main axis
---@param isHorizontal boolean True if flex direction is horizontal
---@return table Array of lines, where each line is an array of children
function LayoutEngine:_simulateWrap(children, availableSize, isHorizontal)
local lines = {}
local currentLine = {}
local currentLineSize = 0
for _, child in ipairs(children) do
-- Calculate child size in main axis (including margins)
local childMainSize = 0
local childMainMargin = 0
if isHorizontal then
childMainSize = child:getBorderBoxWidth()
if child.margin then
childMainMargin = child.margin.left + child.margin.right
end
else
childMainSize = child:getBorderBoxHeight()
if child.margin then
childMainMargin = child.margin.top + child.margin.bottom
end
end
local childTotalMainSize = childMainSize + childMainMargin
-- Check if adding this child would exceed the available space
local lineSpacing = #currentLine > 0 and self.gap or 0
if #currentLine > 0 and currentLineSize + lineSpacing + childTotalMainSize > availableSize then
-- Start a new line
table.insert(lines, currentLine)
currentLine = { child }
currentLineSize = childTotalMainSize
else
-- Add to current line
table.insert(currentLine, child)
currentLineSize = currentLineSize + lineSpacing + childTotalMainSize
end
end
-- Add the last line if it has children
if #currentLine > 0 then
table.insert(lines, currentLine)
end
return lines
end
--- Calculate auto width based on children --- Calculate auto width based on children
---@return number ---@return number
function LayoutEngine:calculateAutoWidth() function LayoutEngine:calculateAutoWidth()
@@ -647,32 +696,72 @@ function LayoutEngine:calculateAutoWidth()
return contentWidth return contentWidth
end end
-- For HORIZONTAL flex: sum children widths + gaps -- Get flex children (children that participate in flex layout)
-- For VERTICAL flex: max of children widths local flexChildren = {}
local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
local totalWidth = contentWidth
local maxWidth = contentWidth
local participatingChildren = 0
for _, child in ipairs(self.element.children) do for _, child in ipairs(self.element.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
if not child._explicitlyAbsolute then if not child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box width for auto-sizing calculations table.insert(flexChildren, child)
local childBorderBoxWidth = child:getBorderBoxWidth()
if isHorizontal then
totalWidth = totalWidth + childBorderBoxWidth
else
maxWidth = math.max(maxWidth, childBorderBoxWidth)
end
participatingChildren = participatingChildren + 1
end end
end end
if #flexChildren == 0 then
return contentWidth
end
local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
if isHorizontal then if isHorizontal then
-- Add gaps between children (n-1 gaps for n children) -- HORIZONTAL flex with potential wrapping
local gapCount = math.max(0, participatingChildren - 1) if self.flexWrap ~= self._FlexWrap.NOWRAP and self.element.width and self.element.width > 0 then
return totalWidth + (self.gap * gapCount) -- Container has explicit width and wrapping enabled - calculate based on wrapped lines
local availableWidth = self.element.width
local lines = self:_simulateWrap(flexChildren, availableWidth, true)
-- Find the widest line
local maxLineWidth = contentWidth
for _, line in ipairs(lines) do
local lineWidth = 0
for i, child in ipairs(line) do
local childBorderBoxWidth = child:getBorderBoxWidth()
local childMarginH = 0
if child.margin then
childMarginH = child.margin.left + child.margin.right
end
lineWidth = lineWidth + childBorderBoxWidth + childMarginH
if i < #line then
lineWidth = lineWidth + self.gap
end
end
maxLineWidth = math.max(maxLineWidth, lineWidth)
end
return maxLineWidth
else else
-- No wrapping or no explicit width - sum all children on one line
local totalWidth = contentWidth
for i, child in ipairs(flexChildren) do
local childBorderBoxWidth = child:getBorderBoxWidth()
local childMarginH = 0
if child.margin then
childMarginH = child.margin.left + child.margin.right
end
totalWidth = totalWidth + childBorderBoxWidth + childMarginH
if i < #flexChildren then
totalWidth = totalWidth + self.gap
end
end
return totalWidth
end
else
-- VERTICAL flex - return max child width (including margins)
local maxWidth = contentWidth
for _, child in ipairs(flexChildren) do
local childBorderBoxWidth = child:getBorderBoxWidth()
local childMarginH = 0
if child.margin then
childMarginH = child.margin.left + child.margin.right
end
maxWidth = math.max(maxWidth, childBorderBoxWidth + childMarginH)
end
return maxWidth return maxWidth
end end
end end
@@ -688,34 +777,100 @@ function LayoutEngine:calculateAutoHeight()
return height return height
end end
-- For VERTICAL flex: sum children heights + gaps -- Get flex children (children that participate in flex layout)
-- For HORIZONTAL flex: max of children heights local flexChildren = {}
local isVertical = self.flexDirection == self._FlexDirection.VERTICAL
local totalHeight = height
local maxHeight = height
local participatingChildren = 0
for _, child in ipairs(self.element.children) do for _, child in ipairs(self.element.children) do
-- Skip explicitly absolute positioned children as they don't affect parent auto-sizing
if not child._explicitlyAbsolute then if not child._explicitlyAbsolute then
-- BORDER-BOX MODEL: Use border-box height for auto-sizing calculations table.insert(flexChildren, child)
local childBorderBoxHeight = child:getBorderBoxHeight()
if isVertical then
totalHeight = totalHeight + childBorderBoxHeight
else
maxHeight = math.max(maxHeight, childBorderBoxHeight)
end
participatingChildren = participatingChildren + 1
end end
end end
if #flexChildren == 0 then
return height
end
local isVertical = self.flexDirection == self._FlexDirection.VERTICAL
if isVertical then if isVertical then
-- Add gaps between children (n-1 gaps for n children) -- VERTICAL flex with potential wrapping
local gapCount = math.max(0, participatingChildren - 1) if self.flexWrap ~= self._FlexWrap.NOWRAP and self.element.height and self.element.height > 0 then
return totalHeight + (self.gap * gapCount) -- Container has explicit height and wrapping enabled - calculate based on wrapped lines
local availableHeight = self.element.height
local lines = self:_simulateWrap(flexChildren, availableHeight, false)
-- Sum all line heights
local totalLinesHeight = height
for i, line in ipairs(lines) do
local lineHeight = 0
for _, child in ipairs(line) do
local childBorderBoxHeight = child:getBorderBoxHeight()
local childMarginV = 0
if child.margin then
childMarginV = child.margin.top + child.margin.bottom
end
lineHeight = math.max(lineHeight, childBorderBoxHeight + childMarginV)
end
totalLinesHeight = totalLinesHeight + lineHeight
if i < #lines then
totalLinesHeight = totalLinesHeight + self.gap
end
end
return totalLinesHeight
else else
-- No wrapping or no explicit height - sum all children on one line
local totalHeight = height
for i, child in ipairs(flexChildren) do
local childBorderBoxHeight = child:getBorderBoxHeight()
local childMarginV = 0
if child.margin then
childMarginV = child.margin.top + child.margin.bottom
end
totalHeight = totalHeight + childBorderBoxHeight + childMarginV
if i < #flexChildren then
totalHeight = totalHeight + self.gap
end
end
return totalHeight
end
else
-- HORIZONTAL flex with potential wrapping
if self.flexWrap ~= self._FlexWrap.NOWRAP and self.element.width and self.element.width > 0 then
-- Container has explicit width and wrapping enabled - calculate based on wrapped lines
local availableWidth = self.element.width
local lines = self:_simulateWrap(flexChildren, availableWidth, true)
-- Sum all line heights (cross-axis for horizontal flex)
local totalLinesHeight = height
for i, line in ipairs(lines) do
local lineHeight = 0
for _, child in ipairs(line) do
local childBorderBoxHeight = child:getBorderBoxHeight()
local childMarginV = 0
if child.margin then
childMarginV = child.margin.top + child.margin.bottom
end
lineHeight = math.max(lineHeight, childBorderBoxHeight + childMarginV)
end
totalLinesHeight = totalLinesHeight + lineHeight
if i < #lines then
totalLinesHeight = totalLinesHeight + self.gap
end
end
return totalLinesHeight
else
-- No wrapping or no explicit width - return max child height (including margins)
local maxHeight = height
for _, child in ipairs(flexChildren) do
local childBorderBoxHeight = child:getBorderBoxHeight()
local childMarginV = 0
if child.margin then
childMarginV = child.margin.top + child.margin.bottom
end
maxHeight = math.max(maxHeight, childBorderBoxHeight + childMarginV)
end
return maxHeight return maxHeight
end end
end
end end
--- Recalculate units based on new viewport dimensions (for vw, vh, % units) --- Recalculate units based on new viewport dimensions (for vw, vh, % units)

View File

@@ -1737,8 +1737,12 @@ function TestElementAutoSizing:test_autosize_with_margin()
parent = parent, parent = parent,
}) })
-- Parent should size to children (margins don't add to content size in flex layout) -- Parent should size to children including margins (flexbox includes margins in sizing)
luaunit.assertEquals(parent.width, 200) -- Child1: 100px + 20px right margin = 120px
-- Child2: 20px left margin + 100px = 120px
-- Total width: 240px
-- Max height: 100px (no vertical margins)
luaunit.assertEquals(parent.width, 240)
luaunit.assertEquals(parent.height, 100) luaunit.assertEquals(parent.height, 100)
end end