diff --git a/layouts/flow.lua b/layouts/flow.lua new file mode 100644 index 0000000..4122d2d --- /dev/null +++ b/layouts/flow.lua @@ -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 diff --git a/layouts/grid.lua b/layouts/grid.lua index 3c8077e..b0d8be7 100644 --- a/layouts/grid.lua +++ b/layouts/grid.lua @@ -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 diff --git a/src/elements/Container.lua b/src/elements/Container.lua index e917f96..ce73a4f 100644 --- a/src/elements/Container.lua +++ b/src/elements/Container.lua @@ -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)) diff --git a/src/plugins/reactive.lua b/src/plugins/reactive.lua index afea0e2..2fb70f2 100644 --- a/src/plugins/reactive.lua +++ b/src/plugins/reactive.lua @@ -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