Fixed flexbox layout bug when container size changes #1

Merged
megaSukura merged 8 commits from fix-flexbox into main 2025-04-20 16:08:06 +08:00
6 changed files with 4135 additions and 3627 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ tests
testWorkflows
.vscode
todo.txt
Flexbox2.lua

File diff suppressed because it is too large Load Diff

View File

@@ -1,413 +1,413 @@
return {
["metadata"] = {
["version"] = "2.0",
["generated"] = "Sat Apr 19 16:22:32 2025",
},
["categories"] = {
["core"] = {
["description"] = "Core Files",
["files"] = {
["main"] = {
["path"] = "main.lua",
["default"] = true,
["size"] = 10456,
["description"] = "",
["requires"] = {
},
},
["elementManager"] = {
["path"] = "elementManager.lua",
["default"] = true,
["size"] = 6297,
["description"] = "",
["requires"] = {
},
},
["propertySystem"] = {
["path"] = "propertySystem.lua",
["default"] = true,
["size"] = 15524,
["description"] = "",
["requires"] = {
},
},
["render"] = {
["size"] = 12422,
["path"] = "render.lua",
["default"] = true,
["size"] = 12422,
["description"] = "",
["requires"] = {
},
["description"] = "",
},
["errorManager"] = {
["size"] = 3789,
["path"] = "errorManager.lua",
["default"] = true,
["size"] = 3789,
["description"] = "",
["requires"] = {
},
["description"] = "",
},
["log"] = {
["path"] = "log.lua",
["main"] = {
["size"] = 10456,
["path"] = "main.lua",
["default"] = true,
["size"] = 3142,
["description"] = "",
["requires"] = {
},
["description"] = "",
},
["init"] = {
["size"] = 583,
["path"] = "init.lua",
["default"] = true,
["size"] = 583,
["description"] = "",
["requires"] = {
},
["description"] = "",
},
},
},
["plugins"] = {
["description"] = "Plugins",
["files"] = {
["animation"] = {
["path"] = "plugins/animation.lua",
["elementManager"] = {
["size"] = 6297,
["path"] = "elementManager.lua",
["default"] = true,
["size"] = 14731,
["description"] = "",
["requires"] = {
},
["description"] = "",
},
["debug"] = {
["path"] = "plugins/debug.lua",
["log"] = {
["size"] = 3142,
["path"] = "log.lua",
["default"] = true,
["size"] = 6250,
["description"] = "",
["requires"] = {
},
["description"] = "",
},
["state"] = {
["path"] = "plugins/state.lua",
["propertySystem"] = {
["size"] = 15524,
["path"] = "propertySystem.lua",
["default"] = true,
["size"] = 6896,
["description"] = "",
["requires"] = {
[1] = "Container",
},
},
["canvas"] = {
["path"] = "plugins/canvas.lua",
["default"] = true,
["size"] = 7873,
["description"] = "",
["requires"] = {
},
},
["reactive"] = {
["path"] = "plugins/reactive.lua",
["default"] = true,
["size"] = 7193,
["description"] = "",
["requires"] = {
},
},
["xml"] = {
["path"] = "plugins/xml.lua",
["default"] = true,
["size"] = 9901,
["description"] = "",
["requires"] = {
},
},
["benchmark"] = {
["path"] = "plugins/benchmark.lua",
["default"] = true,
["size"] = 12581,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["theme"] = {
["path"] = "plugins/theme.lua",
["default"] = true,
["size"] = 7166,
["description"] = "",
["requires"] = {
},
},
},
},
["libraries"] = {
["description"] = "Libraries",
["files"] = {
["colorHex"] = {
["path"] = "libraries/colorHex.lua",
["default"] = true,
["size"] = 132,
["description"] = "",
["requires"] = {
},
},
["expect"] = {
["path"] = "libraries/expect.lua",
["default"] = true,
["size"] = 846,
["description"] = "",
["requires"] = {
},
},
["utils"] = {
["path"] = "libraries/utils.lua",
["default"] = true,
["size"] = 2661,
["description"] = "",
["requires"] = {
},
},
},
},
["elements"] = {
["description"] = "UI Elements",
["files"] = {
["List"] = {
["path"] = "elements/List.lua",
["Scrollbar"] = {
["size"] = 9191,
["path"] = "elements/Scrollbar.lua",
["default"] = true,
["size"] = 8702,
["description"] = "A scrollable list of selectable items",
["requires"] = {
[1] = "VisualElement",
},
},
["Switch"] = {
["path"] = "elements/Switch.lua",
["default"] = true,
["size"] = 1378,
["description"] = "",
},
["Label"] = {
["size"] = 3092,
["path"] = "elements/Label.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "A simple text display element that automatically resizes its width based on the text content.",
},
["Frame"] = {
["path"] = "elements/Frame.lua",
["Slider"] = {
["size"] = 4766,
["path"] = "elements/Slider.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["LineChart"] = {
["size"] = 3227,
["path"] = "elements/LineChart.lua",
["default"] = false,
["requires"] = {
},
["description"] = "",
},
["BaseFrame"] = {
["size"] = 8466,
["path"] = "elements/BaseFrame.lua",
["default"] = true,
["size"] = 4458,
["description"] = "A frame element that serves as a grouping container for other elements.",
["requires"] = {
[1] = "Container",
},
["description"] = "This is the base frame class. It is the root element of all elements and the only element without a parent.",
},
["TextBox"] = {
["path"] = "elements/TextBox.lua",
["default"] = false,
["size"] = 10929,
["description"] = "A multi-line text editor component with cursor support and text manipulation features",
["Checkbox"] = {
["size"] = 2928,
["path"] = "elements/Checkbox.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["ProgressBar"] = {
["size"] = 3397,
["path"] = "elements/ProgressBar.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["VisualElement"] = {
["size"] = 17775,
["path"] = "elements/VisualElement.lua",
["default"] = true,
["size"] = 17775,
["description"] = "The Visual Element class which is the base class for all visual UI elements",
["requires"] = {
[1] = "BaseElement",
},
["description"] = "The Visual Element class which is the base class for all visual UI elements",
},
["Label"] = {
["path"] = "elements/Label.lua",
["Switch"] = {
["size"] = 1378,
["path"] = "elements/Switch.lua",
["default"] = true,
["size"] = 3092,
["description"] = "A simple text display element that automatically resizes its width based on the text content.",
["requires"] = {
[1] = "VisualElement",
},
},
["BaseFrame"] = {
["path"] = "elements/BaseFrame.lua",
["default"] = true,
["size"] = 8466,
["description"] = "This is the base frame class. It is the root element of all elements and the only element without a parent.",
["requires"] = {
[1] = "Container",
},
},
["Flexbox"] = {
["path"] = "elements/Flexbox.lua",
["default"] = true,
["size"] = 12215,
["description"] = "A flexbox container that arranges its children in a flexible layout.",
["requires"] = {
[1] = "Container",
},
},
["Input"] = {
["path"] = "elements/Input.lua",
["default"] = true,
["size"] = 8876,
["description"] = "A text input field with various features",
["requires"] = {
[1] = "VisualElement",
},
},
["Container"] = {
["path"] = "elements/Container.lua",
["default"] = true,
["size"] = 25030,
["description"] = "The container class. It is a visual element that can contain other elements. It is the base class for all containers",
["requires"] = {
[1] = "VisualElement",
},
},
["Tree"] = {
["path"] = "elements/Tree.lua",
["default"] = true,
["size"] = 7941,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["Slider"] = {
["path"] = "elements/Slider.lua",
["default"] = true,
["size"] = 4766,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["Image"] = {
["path"] = "elements/Image.lua",
["default"] = false,
["size"] = 15125,
["description"] = "An element that displays an image in bimg format",
["requires"] = {
[1] = "VisualElement",
},
},
["Graph"] = {
["path"] = "elements/Graph.lua",
["default"] = false,
["size"] = 6990,
["description"] = "A point based graph element",
["requires"] = {
},
},
["ProgressBar"] = {
["path"] = "elements/ProgressBar.lua",
["default"] = true,
["size"] = 3397,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["BigFont"] = {
["size"] = 20951,
["path"] = "elements/BigFont.lua",
["default"] = false,
["size"] = 20951,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["Program"] = {
["path"] = "elements/Program.lua",
["default"] = true,
["size"] = 7733,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["BaseElement"] = {
["path"] = "elements/BaseElement.lua",
["default"] = true,
["size"] = 9544,
["description"] = "The base class for all UI elements in Basalt.",
["requires"] = {
},
},
["Dropdown"] = {
["path"] = "elements/Dropdown.lua",
["default"] = false,
["size"] = 6359,
["description"] = "A dropdown menu that shows a list of selectable items",
["requires"] = {
[1] = "List",
},
},
["Button"] = {
["path"] = "elements/Button.lua",
["default"] = true,
["size"] = 1656,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["Checkbox"] = {
["path"] = "elements/Checkbox.lua",
["default"] = true,
["size"] = 2928,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["BarChart"] = {
["size"] = 3190,
["path"] = "elements/BarChart.lua",
["default"] = false,
["size"] = 3190,
["description"] = "",
["requires"] = {
},
["description"] = "",
},
["Display"] = {
["path"] = "elements/Display.lua",
["default"] = false,
["size"] = 4243,
["description"] = "The Display is a special element which uses the cc window API which you can use.",
["Program"] = {
["size"] = 7733,
["path"] = "elements/Program.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["Table"] = {
["size"] = 8811,
["path"] = "elements/Table.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["Button"] = {
["size"] = 1656,
["path"] = "elements/Button.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["Menu"] = {
["size"] = 4679,
["path"] = "elements/Menu.lua",
["default"] = true,
["size"] = 4679,
["description"] = "A horizontal menu bar with selectable items.",
["requires"] = {
[1] = "List",
},
["description"] = "A horizontal menu bar with selectable items.",
},
["Scrollbar"] = {
["path"] = "elements/Scrollbar.lua",
["default"] = true,
["size"] = 9191,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
},
["LineChart"] = {
["path"] = "elements/LineChart.lua",
["Display"] = {
["size"] = 4243,
["path"] = "elements/Display.lua",
["default"] = false,
["size"] = 3227,
["description"] = "",
["requires"] = {
},
},
["Table"] = {
["path"] = "elements/Table.lua",
["default"] = true,
["size"] = 8680,
["description"] = "",
["requires"] = {
[1] = "VisualElement",
},
["description"] = "The Display is a special element which uses the cc window API which you can use.",
},
["BaseElement"] = {
["size"] = 9544,
["path"] = "elements/BaseElement.lua",
["default"] = true,
["requires"] = {
},
["description"] = "The base class for all UI elements in Basalt.",
},
["Frame"] = {
["size"] = 4458,
["path"] = "elements/Frame.lua",
["default"] = true,
["requires"] = {
[1] = "Container",
},
["description"] = "A frame element that serves as a grouping container for other elements.",
},
["Container"] = {
["size"] = 25030,
["path"] = "elements/Container.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "The container class. It is a visual element that can contain other elements. It is the base class for all containers",
},
["Graph"] = {
["size"] = 6990,
["path"] = "elements/Graph.lua",
["default"] = false,
["requires"] = {
},
["description"] = "A point based graph element",
},
["Flexbox"] = {
["size"] = 12215,
["path"] = "elements/Flexbox.lua",
["default"] = true,
["requires"] = {
[1] = "Container",
},
["description"] = "A flexbox container that arranges its children in a flexible layout.",
},
["Tree"] = {
["size"] = 7941,
["path"] = "elements/Tree.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["Input"] = {
["size"] = 8876,
["path"] = "elements/Input.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "A text input field with various features",
},
["Dropdown"] = {
["size"] = 6359,
["path"] = "elements/Dropdown.lua",
["default"] = false,
["requires"] = {
[1] = "List",
},
["description"] = "A dropdown menu that shows a list of selectable items",
},
["TextBox"] = {
["size"] = 10929,
["path"] = "elements/TextBox.lua",
["default"] = false,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "A multi-line text editor component with cursor support and text manipulation features",
},
["List"] = {
["size"] = 8702,
["path"] = "elements/List.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "A scrollable list of selectable items",
},
["Image"] = {
["size"] = 15125,
["path"] = "elements/Image.lua",
["default"] = false,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "An element that displays an image in bimg format",
},
},
},
["plugins"] = {
["description"] = "Plugins",
["files"] = {
["xml"] = {
["size"] = 9901,
["path"] = "plugins/xml.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
["animation"] = {
["size"] = 14731,
["path"] = "plugins/animation.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
["state"] = {
["size"] = 6896,
["path"] = "plugins/state.lua",
["default"] = true,
["requires"] = {
[1] = "Container",
},
["description"] = "",
},
["reactive"] = {
["size"] = 7193,
["path"] = "plugins/reactive.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
["theme"] = {
["size"] = 7166,
["path"] = "plugins/theme.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
["benchmark"] = {
["size"] = 12581,
["path"] = "plugins/benchmark.lua",
["default"] = true,
["requires"] = {
[1] = "VisualElement",
},
["description"] = "",
},
["canvas"] = {
["size"] = 7873,
["path"] = "plugins/canvas.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
["debug"] = {
["size"] = 6250,
["path"] = "plugins/debug.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
},
},
["libraries"] = {
["description"] = "Libraries",
["files"] = {
["expect"] = {
["size"] = 846,
["path"] = "libraries/expect.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
["colorHex"] = {
["size"] = 132,
["path"] = "libraries/colorHex.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
["utils"] = {
["size"] = 2661,
["path"] = "libraries/utils.lua",
["default"] = true,
["requires"] = {
},
["description"] = "",
},
},
},
},
["metadata"] = {
["version"] = "2.0",
["generated"] = "Sat Apr 19 07:21:46 2025",
},
}

View File

@@ -1538,8 +1538,9 @@ if bc and(ac+1)<=#ca then db=1
local cc=
(ac+1)==da and self.get("selectedColor")or self.get("background")
for dc,_d in ipairs(ba)do local ad=tostring(bc[dc]or"")local bd=ad..
string.rep(" ",_d.width-#ad)
self:blit(db,y,string.sub(bd,1,cb-db+1),string.sub(string.rep(_a[self.get("foreground")],_d.width),1,
string.rep(" ",_d.width-#ad)if dc<#ba then bd=
string.sub(bd,1,_d.width-1).." "end
self:blit(db,y,string.sub(bd,1,_d.width),string.sub(string.rep(_a[self.get("foreground")],_d.width),1,
cb-db+1),string.sub(string.rep(_a[cc],_d.width),1,
cb-db+1))db=db+_d.width end else
self:blit(1,y,string.rep(" ",self.get("width")),string.rep(_a[self.get("foreground")],self.get("width")),string.rep(_a[self.get("background")],self.get("width")))end end

View File

@@ -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)

View File

@@ -183,7 +183,10 @@ function Table:render()
for i, col in ipairs(columns) do
local cellText = tostring(rowData[i] or "")
local paddedText = cellText .. string.rep(" ", col.width - #cellText)
self:blit(currentX, y, string.sub(paddedText, 1, width-currentX+1),
if i < #columns then
paddedText = string.sub(paddedText, 1, col.width - 1) .. " "
end
self:blit(currentX, y, string.sub(paddedText, 1, col.width),
string.sub(string.rep(tHex[self.get("foreground")], col.width), 1, width-currentX+1),
string.sub(string.rep(tHex[bg], col.width), 1, width-currentX+1))
currentX = currentX + col.width