Fixed a bug in reactive not calling the observers

Fixed a layout issue (not updating properly)
added the flow layout
This commit is contained in:
Robert Jelic
2025-11-02 18:36:55 +01:00
parent 7375c33bbb
commit dc51a73749
4 changed files with 239 additions and 4 deletions

193
layouts/flow.lua Normal file
View File

@@ -0,0 +1,193 @@
local flow = {}
--- Calculates positions for all children in a flow layout
--- @param instance table The layout instance
--- - container: the container to layout
--- - options: layout options
--- - direction: "horizontal" or "vertical" (default: "horizontal")
--- - spacing: gap between elements (default: 0)
--- - padding: padding around the flow (default: 0)
--- - align: "start", "center", or "end" (default: "start")
function flow.calculate(instance)
local container = instance.container
local options = instance.options or {}
local children = container.get("children")
local containerWidth = container.get("width")
local containerHeight = container.get("height")
local direction = options.direction or "horizontal"
local spacing = options.spacing or 0
local padding = options.padding or 0
local align = options.align or "start"
local childCount = #children
if childCount == 0 then
instance._positions = {}
return
end
local positions = {}
if direction == "horizontal" then
local rows = {}
local currentRow = {}
local currentX = padding + 1
local currentY = padding + 1
local maxHeightInRow = 0
for i, child in ipairs(children) do
local childWidth = child.get("width")
local childHeight = child.get("height")
if currentX + childWidth - 1 > containerWidth - padding and currentX > padding + 1 then
table.insert(rows, {
children = currentRow,
y = currentY,
height = maxHeightInRow
})
currentRow = {}
currentX = padding + 1
currentY = currentY + maxHeightInRow + spacing
maxHeightInRow = 0
end
table.insert(currentRow, {
child = child,
width = childWidth,
height = childHeight
})
currentX = currentX + childWidth + spacing
maxHeightInRow = math.max(maxHeightInRow, childHeight)
end
if #currentRow > 0 then
table.insert(rows, {
children = currentRow,
y = currentY,
height = maxHeightInRow
})
end
for _, row in ipairs(rows) do
local rowWidth = 0
for j, item in ipairs(row.children) do
rowWidth = rowWidth + item.width
if j < #row.children then
rowWidth = rowWidth + spacing
end
end
local startX = padding + 1
if align == "center" then
startX = padding + 1 + math.floor((containerWidth - 2 * padding - rowWidth) / 2)
elseif align == "end" then
startX = containerWidth - padding - rowWidth + 1
end
local x = startX
for _, item in ipairs(row.children) do
local y = row.y
if align == "center" then
y = row.y + math.floor((row.height - item.height) / 2)
elseif align == "end" then
y = row.y + row.height - item.height
end
positions[item.child] = {
x = x,
y = y,
width = item.width,
height = item.height
}
x = x + item.width + spacing
end
end
else
local columns = {}
local currentColumn = {}
local currentX = padding + 1
local currentY = padding + 1
local maxWidthInColumn = 0
for i, child in ipairs(children) do
local childWidth = child.get("width")
local childHeight = child.get("height")
if currentY + childHeight - 1 > containerHeight - padding and currentY > padding + 1 then
table.insert(columns, {
children = currentColumn,
x = currentX,
width = maxWidthInColumn
})
currentColumn = {}
currentY = padding + 1
currentX = currentX + maxWidthInColumn + spacing
maxWidthInColumn = 0
end
table.insert(currentColumn, {
child = child,
width = childWidth,
height = childHeight
})
currentY = currentY + childHeight + spacing
maxWidthInColumn = math.max(maxWidthInColumn, childWidth)
end
if #currentColumn > 0 then
table.insert(columns, {
children = currentColumn,
x = currentX,
width = maxWidthInColumn
})
end
for _, column in ipairs(columns) do
local columnHeight = 0
for j, item in ipairs(column.children) do
columnHeight = columnHeight + item.height
if j < #column.children then
columnHeight = columnHeight + spacing
end
end
local startY = padding + 1
if align == "center" then
startY = padding + 1 + math.floor((containerHeight - 2 * padding - columnHeight) / 2)
elseif align == "end" then
startY = containerHeight - padding - columnHeight + 1
end
local y = startY
for _, item in ipairs(column.children) do
local x = column.x
if align == "center" then
x = column.x + math.floor((column.width - item.width) / 2)
elseif align == "end" then
x = column.x + column.width - item.width
end
positions[item.child] = {
x = x,
y = y,
width = item.width,
height = item.height
}
y = y + item.height + spacing
end
end
end
instance._positions = positions
end
return flow

View File

@@ -22,6 +22,11 @@ function grid.calculate(instance)
local columns = options.columns
local childCount = #children
if childCount == 0 then
instance._positions = {}
return
end
if not rows and not columns then
columns = math.ceil(math.sqrt(childCount))
rows = math.ceil(childCount / columns)
@@ -31,11 +36,21 @@ function grid.calculate(instance)
rows = math.ceil(childCount / columns)
end
if columns <= 0 then columns = 1 end
if rows <= 0 then rows = 1 end
local availableWidth = containerWidth - (2 * padding) - ((columns - 1) * spacing)
local availableHeight = containerHeight - (2 * padding) - ((rows - 1) * spacing)
if availableWidth < 1 then availableWidth = 1 end
if availableHeight < 1 then availableHeight = 1 end
local cellWidth = math.floor(availableWidth / columns)
local cellHeight = math.floor(availableHeight / rows)
if cellWidth < 1 then cellWidth = 1 end
if cellHeight < 1 then cellHeight = 1 end
local positions = {}
for i, child in ipairs(children) do

View File

@@ -103,10 +103,12 @@ function Container:init(props, basalt)
self:observe("width", function()
self.set("childrenSorted", false)
self.set("childrenEventsSorted", false)
self:updateRender()
end)
self:observe("height", function()
self.set("childrenSorted", false)
self.set("childrenEventsSorted", false)
self:updateRender()
end)
end
@@ -203,11 +205,12 @@ end
--- @shortDescription Updates child element ordering
--- @return Container self For method chaining
function Container:sortChildren()
self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
self.set("childrenSorted", true)
if self._layoutInstance then
self:updateLayout()
end
self.set("visibleChildren", sortAndFilterChildren(self, self._values.children))
return self
end
@@ -546,7 +549,7 @@ end
--- @protected
function Container:multiBlit(x, y, width, height, text, fg, bg)
local w, h = self.get("width"), self.get("height")
width = x < 1 and math.min(width + x - 1, w) or math.min(width, math.max(0, w - x + 1))
height = y < 1 and math.min(height + y - 1, h) or math.min(height, math.max(0, h - y + 1))

View File

@@ -191,9 +191,17 @@ local observerCache = setmetatable({}, {
end
})
local valueCache = setmetatable({}, {
__mode = "k",
__index = function(t, k)
t[k] = {}
return t[k]
end
})
local function setupObservers(element, expr, propertyName)
local deps = analyzeDependencies(expr)
if observerCache[element][propertyName] then
for _, observer in ipairs(observerCache[element][propertyName]) do
observer.target:removeObserver(observer.property, observer.callback)
@@ -229,7 +237,20 @@ local function setupObservers(element, expr, propertyName)
target = target,
property = isState and "states" or prop,
callback = function()
element:updateRender()
local oldValue = valueCache[element][propertyName]
local newValue = element.get(propertyName)
if oldValue ~= newValue then
valueCache[element][propertyName] = newValue
if element._observers and element._observers[propertyName] then
for _, obs in ipairs(element._observers[propertyName]) do
obs()
end
end
element:updateRender()
end
end
}
target:observe(observer.property, observer.callback)
@@ -281,6 +302,8 @@ PropertySystem.addSetterHook(function(element, propertyName, value, config)
end
return config.default
end
valueCache[element][propertyName] = result
return result
end
end
@@ -304,6 +327,7 @@ BaseElement.hooks = {
end
end
observerCache[self] = nil
valueCache[self] = nil
functionCache[self] = nil
end
end