|
|
|
|
@@ -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"})
|
|
|
|
|
@@ -55,189 +68,650 @@ local lineBreakElement = {
|
|
|
|
|
getVisible = function(self) return true end,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
local function sortElements(self, direction, spacing, wrap)
|
|
|
|
|
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
|
|
|
|
|
return sortedElements
|
|
|
|
|
local visibleElements = {}
|
|
|
|
|
local childCount = 0
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if childCount == 0 then
|
|
|
|
|
return sortedElements
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if not wrap then
|
|
|
|
|
sortedElements[1] = {offset=1}
|
|
|
|
|
|
|
|
|
|
for _, elem in ipairs(visibleElements) do
|
|
|
|
|
if elem == lineBreakElement then
|
|
|
|
|
local nextIndex = #sortedElements + 1
|
|
|
|
|
if sortedElements[nextIndex] == nil then
|
|
|
|
|
sortedElements[nextIndex] = {offset=1}
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
table.insert(sortedElements[#sortedElements], elem)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
local containerSize = direction == "row" and self.get("width") or self.get("height")
|
|
|
|
|
|
|
|
|
|
local segments = {{}}
|
|
|
|
|
local currentSegment = 1
|
|
|
|
|
|
|
|
|
|
for _, elem in ipairs(visibleElements) do
|
|
|
|
|
if elem == lineBreakElement then
|
|
|
|
|
currentSegment = currentSegment + 1
|
|
|
|
|
segments[currentSegment] = {}
|
|
|
|
|
else
|
|
|
|
|
table.insert(segments[currentSegment], elem)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
for segmentIndex, segment in ipairs(segments) do
|
|
|
|
|
if #segment == 0 then
|
|
|
|
|
sortedElements[#sortedElements + 1] = {offset=1}
|
|
|
|
|
else
|
|
|
|
|
local rows = {}
|
|
|
|
|
local currentRow = {}
|
|
|
|
|
local currentWidth = 0
|
|
|
|
|
|
|
|
|
|
for _, elem in ipairs(segment) do
|
|
|
|
|
local intrinsicSize = 0
|
|
|
|
|
local currentSize = direction == "row" and elem.get("width") or elem.get("height")
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
local elemSize = hasIntrinsic and intrinsicSize or currentSize
|
|
|
|
|
|
|
|
|
|
local spaceNeeded = elemSize
|
|
|
|
|
|
|
|
|
|
if #currentRow > 0 then
|
|
|
|
|
spaceNeeded = spaceNeeded + spacing
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if currentWidth + spaceNeeded <= containerSize or #currentRow == 0 then
|
|
|
|
|
table.insert(currentRow, elem)
|
|
|
|
|
currentWidth = currentWidth + spaceNeeded
|
|
|
|
|
else
|
|
|
|
|
table.insert(rows, currentRow)
|
|
|
|
|
currentRow = {elem}
|
|
|
|
|
currentWidth = elemSize
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
if #currentRow > 0 then
|
|
|
|
|
table.insert(rows, currentRow)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
local containerWidth = self.get("width")
|
|
|
|
|
|
|
|
|
|
local usedSpace = spacing * (#children - 1)
|
|
|
|
|
local totalFlexGrow = 0
|
|
|
|
|
|
|
|
|
|
-- Make a copy of children that filters out lineBreak elements
|
|
|
|
|
local filteredChildren = {}
|
|
|
|
|
for _, child in ipairs(children) do
|
|
|
|
|
if child ~= lineBreakElement then
|
|
|
|
|
usedSpace = usedSpace + child.get("width")
|
|
|
|
|
totalFlexGrow = totalFlexGrow + child.get("flexGrow")
|
|
|
|
|
table.insert(filteredChildren, child)
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
-- Categorize elements and calculate their minimal widths and flexibilities
|
|
|
|
|
local totalFixedWidth = 0
|
|
|
|
|
local totalFlexGrow = 0
|
|
|
|
|
local minWidths = {}
|
|
|
|
|
local flexGrows = {}
|
|
|
|
|
local flexShrinks = {}
|
|
|
|
|
|
|
|
|
|
-- 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")
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
child.set("x", currentX)
|
|
|
|
|
child.set("y", children.offset or 1)
|
|
|
|
|
child.set("width", childWidth)
|
|
|
|
|
currentX = currentX + childWidth + spacing
|
|
|
|
|
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 3: Position elements (never allow overlapping)
|
|
|
|
|
local currentX = 1
|
|
|
|
|
|
|
|
|
|
-- 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)
|
|
|
|
|
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
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
local remainingSpace = containerWidth - usedWidth
|
|
|
|
|
|
|
|
|
|
if remainingSpace > 0 then
|
|
|
|
|
if justifyContent == "flex-end" then
|
|
|
|
|
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(filteredChildren) do
|
|
|
|
|
child.set("x", child.get("x") + offset)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local function calculateColumn(self, children, spacing, justifyContent)
|
|
|
|
|
local containerHeight = self.get("height")
|
|
|
|
|
|
|
|
|
|
local usedSpace = spacing * (#children - 1)
|
|
|
|
|
local totalFlexGrow = 0
|
|
|
|
|
|
|
|
|
|
-- Make a copy of children that filters out lineBreak elements
|
|
|
|
|
local filteredChildren = {}
|
|
|
|
|
for _, child in ipairs(children) do
|
|
|
|
|
if child ~= lineBreakElement then
|
|
|
|
|
usedSpace = usedSpace + child.get("height")
|
|
|
|
|
totalFlexGrow = totalFlexGrow + child.get("flexGrow")
|
|
|
|
|
table.insert(filteredChildren, child)
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
-- Categorize elements and calculate their minimal heights and flexibilities
|
|
|
|
|
local totalFixedHeight = 0
|
|
|
|
|
local totalFlexGrow = 0
|
|
|
|
|
local minHeights = {}
|
|
|
|
|
local flexGrows = {}
|
|
|
|
|
local flexShrinks = {}
|
|
|
|
|
|
|
|
|
|
-- 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")
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
child.set("x", children.offset or 1)
|
|
|
|
|
child.set("y", currentY)
|
|
|
|
|
child.set("height", childHeight)
|
|
|
|
|
currentY = currentY + childHeight + spacing
|
|
|
|
|
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 3: Position elements (never allow overlapping)
|
|
|
|
|
local currentY = 1
|
|
|
|
|
|
|
|
|
|
-- 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)
|
|
|
|
|
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
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
-- 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
|
|
|
|
|
|
|
|
|
|
local remainingSpace = containerHeight - usedHeight
|
|
|
|
|
|
|
|
|
|
if remainingSpace > 0 then
|
|
|
|
|
if justifyContent == "flex-end" then
|
|
|
|
|
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(filteredChildren) do
|
|
|
|
|
child.set("y", child.get("y") + offset)
|
|
|
|
|
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)
|
|
|
|
|
if self.get("width") <= 0 or self.get("height") <= 0 then
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
direction = (direction == "row" or direction == "column") and direction or "row"
|
|
|
|
|
|
|
|
|
|
local currentWidth, currentHeight = self.get("width"), self.get("height")
|
|
|
|
|
local sizeChanged = currentWidth ~= self._lastLayoutWidth or currentHeight ~= self._lastLayoutHeight
|
|
|
|
|
|
|
|
|
|
self._lastLayoutWidth = currentWidth
|
|
|
|
|
self._lastLayoutHeight = currentHeight
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
local ok, value = pcall(function() return child.get("intrinsicWidth") end)
|
|
|
|
|
if ok and value then
|
|
|
|
|
child.set("width", value)
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
local ok, value = pcall(function() return child.get("intrinsicHeight") end)
|
|
|
|
|
if ok and value then
|
|
|
|
|
child.set("height", value)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local elements = sortElements(self, direction, spacing, wrap)
|
|
|
|
|
if #elements == 0 then return end
|
|
|
|
|
|
|
|
|
|
local layoutFunction = direction == "row" and calculateRow or calculateColumn
|
|
|
|
|
|
|
|
|
|
if direction == "row" and wrap then
|
|
|
|
|
local currentY = 1
|
|
|
|
|
for i, rowOrColumn in ipairs(elements) do
|
|
|
|
|
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
|
|
|
|
|
end
|
|
|
|
|
elseif direction == "column" and wrap then
|
|
|
|
|
local currentX = 1
|
|
|
|
|
for i, rowOrColumn in ipairs(elements) do
|
|
|
|
|
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
|
|
|
|
|
end
|
|
|
|
|
else
|
|
|
|
|
for _, rowOrColumn in ipairs(elements) do
|
|
|
|
|
layoutFunction(self, rowOrColumn, spacing, justifyContent)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
self:sortChildren()
|
|
|
|
|
self.set("childrenEventsSorted", false)
|
|
|
|
|
self.set("flexUpdateLayout", false)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
@@ -251,8 +725,19 @@ function Flexbox.new()
|
|
|
|
|
self.set("height", 6)
|
|
|
|
|
self.set("background", colors.blue)
|
|
|
|
|
self.set("z", 10)
|
|
|
|
|
|
|
|
|
|
self._lastLayoutWidth = 0
|
|
|
|
|
self._lastLayoutHeight = 0
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
@@ -278,6 +763,24 @@ 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"})
|
|
|
|
|
|
|
|
|
|
element:observe("flexGrow", function() self.set("flexUpdateLayout", true) end)
|
|
|
|
|
element:observe("flexShrink", function() self.set("flexUpdateLayout", true) end)
|
|
|
|
|
|
|
|
|
|
element:observe("width", function(_, newValue, oldValue)
|
|
|
|
|
if element.get("flexGrow") == 0 then
|
|
|
|
|
element.set("intrinsicWidth", newValue)
|
|
|
|
|
end
|
|
|
|
|
self.set("flexUpdateLayout", true)
|
|
|
|
|
end)
|
|
|
|
|
element:observe("height", function(_, newValue, oldValue)
|
|
|
|
|
if element.get("flexGrow") == 0 then
|
|
|
|
|
element.set("intrinsicHeight", newValue)
|
|
|
|
|
end
|
|
|
|
|
self.set("flexUpdateLayout", true)
|
|
|
|
|
end)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
self.set("flexUpdateLayout", true)
|
|
|
|
|
|