From c723c66004f2c7eecc2cbf3cb6c9adb9cf176aed Mon Sep 17 00:00:00 2001 From: Robert Jelic <36573031+NoryiE@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:29:11 +0100 Subject: [PATCH] fixed layout manager so that we know if size got manually changed fixed scrollframe not sending scroll events to its children fixed scrollframe scrolling even if mouse is not hovering over the element improved the behaviour of the flow layout --- layouts/flow.lua | 125 +++++++++++++++++++++++++++++---- src/elements/Container.lua | 3 +- src/elements/ScrollFrame.lua | 42 ++++++++--- src/elements/VisualElement.lua | 50 +++++++++++-- src/layoutManager.lua | 32 +++++++++ 5 files changed, 221 insertions(+), 31 deletions(-) diff --git a/layouts/flow.lua b/layouts/flow.lua index 4122d2d..5ade09d 100644 --- a/layouts/flow.lua +++ b/layouts/flow.lua @@ -39,8 +39,24 @@ function flow.calculate(instance) for i, child in ipairs(children) do local childWidth = child.get("width") local childHeight = child.get("height") + local layoutConfig = child.get("layoutConfig") or {} - if currentX + childWidth - 1 > containerWidth - padding and currentX > padding + 1 then + if child._userModified then + child._originalWidth = childWidth + child._originalHeight = childHeight + end + + local basis = layoutConfig.basis + if not basis then + if not child._originalWidth then + child._originalWidth = childWidth + end + basis = child._originalWidth + end + + local hasFlexConfig = layoutConfig.grow or layoutConfig.shrink or layoutConfig.basis + + if currentX + basis - 1 > containerWidth - padding and currentX > padding + 1 then table.insert(rows, { children = currentRow, @@ -57,10 +73,14 @@ function flow.calculate(instance) table.insert(currentRow, { child = child, width = childWidth, - height = childHeight + height = childHeight, + basis = basis, + grow = layoutConfig.grow or 0, + shrink = layoutConfig.shrink or 1, + hasFlexConfig = hasFlexConfig }) - currentX = currentX + childWidth + spacing + currentX = currentX + basis + spacing maxHeightInRow = math.max(maxHeightInRow, childHeight) end @@ -73,9 +93,39 @@ function flow.calculate(instance) end for _, row in ipairs(rows) do + local totalBasis = 0 + local totalSpacing = (#row.children - 1) * spacing + local totalGrow = 0 + local totalShrink = 0 + + for _, item in ipairs(row.children) do + totalBasis = totalBasis + item.basis + totalGrow = totalGrow + item.grow + totalShrink = totalShrink + item.shrink + end + + local availableWidth = containerWidth - 2 * padding + local remainingSpace = availableWidth - totalBasis - totalSpacing + + for _, item in ipairs(row.children) do + if remainingSpace > 0 and totalGrow > 0 then + local extraSpace = (remainingSpace * item.grow) / totalGrow + item.finalWidth = item.basis + extraSpace + elseif remainingSpace < 0 and totalShrink > 0 then + local reduceSpace = (-remainingSpace * item.shrink) / totalShrink + item.finalWidth = math.max(1, item.basis - reduceSpace) + else + item.finalWidth = item.basis + end + + if not item.hasFlexConfig then + item.finalWidth = item.basis + end + end + local rowWidth = 0 for j, item in ipairs(row.children) do - rowWidth = rowWidth + item.width + rowWidth = rowWidth + item.finalWidth if j < #row.children then rowWidth = rowWidth + spacing end @@ -100,11 +150,11 @@ function flow.calculate(instance) positions[item.child] = { x = x, y = y, - width = item.width, + width = math.floor(item.finalWidth), height = item.height } - x = x + item.width + spacing + x = x + math.floor(item.finalWidth) + spacing end end @@ -118,8 +168,25 @@ function flow.calculate(instance) for i, child in ipairs(children) do local childWidth = child.get("width") local childHeight = child.get("height") + local layoutConfig = child.get("layoutConfig") or {} - if currentY + childHeight - 1 > containerHeight - padding and currentY > padding + 1 then + -- If user modified the element, update the original size + if child._userModified then + child._originalWidth = childWidth + child._originalHeight = childHeight + end + + local basis = layoutConfig.basis + if not basis then + if not child._originalHeight then + child._originalHeight = childHeight + end + basis = child._originalHeight + end + + local hasFlexConfig = layoutConfig.grow or layoutConfig.shrink or layoutConfig.basis + + if currentY + basis - 1 > containerHeight - padding and currentY > padding + 1 then table.insert(columns, { children = currentColumn, x = currentX, @@ -135,10 +202,14 @@ function flow.calculate(instance) table.insert(currentColumn, { child = child, width = childWidth, - height = childHeight + height = childHeight, + basis = basis, + grow = layoutConfig.grow or 0, + shrink = layoutConfig.shrink or 1, + hasFlexConfig = hasFlexConfig }) - currentY = currentY + childHeight + spacing + currentY = currentY + basis + spacing maxWidthInColumn = math.max(maxWidthInColumn, childWidth) end @@ -151,9 +222,39 @@ function flow.calculate(instance) end for _, column in ipairs(columns) do + local totalBasis = 0 + local totalSpacing = (#column.children - 1) * spacing + local totalGrow = 0 + local totalShrink = 0 + + for _, item in ipairs(column.children) do + totalBasis = totalBasis + item.basis + totalGrow = totalGrow + item.grow + totalShrink = totalShrink + item.shrink + end + + local availableHeight = containerHeight - 2 * padding + local remainingSpace = availableHeight - totalBasis - totalSpacing + + for _, item in ipairs(column.children) do + if remainingSpace > 0 and totalGrow > 0 then + local extraSpace = (remainingSpace * item.grow) / totalGrow + item.finalHeight = item.basis + extraSpace + elseif remainingSpace < 0 and totalShrink > 0 then + local reduceSpace = (-remainingSpace * item.shrink) / totalShrink + item.finalHeight = math.max(1, item.basis - reduceSpace) + else + item.finalHeight = item.basis + end + + if not item.hasFlexConfig then + item.finalHeight = item.basis + end + end + local columnHeight = 0 for j, item in ipairs(column.children) do - columnHeight = columnHeight + item.height + columnHeight = columnHeight + item.finalHeight if j < #column.children then columnHeight = columnHeight + spacing end @@ -179,10 +280,10 @@ function flow.calculate(instance) x = x, y = y, width = item.width, - height = item.height + height = math.floor(item.finalHeight) } - y = y + item.height + spacing + y = y + math.floor(item.finalHeight) + spacing end end end diff --git a/src/elements/Container.lua b/src/elements/Container.lua index ce73a4f..1910f3b 100644 --- a/src/elements/Container.lua +++ b/src/elements/Container.lua @@ -1,6 +1,7 @@ local elementManager = require("elementManager") local errorManager = require("errorManager") local VisualElement = elementManager.getElement("VisualElement") +local LayoutManager = require("layoutManager") local expect = require("libraries/expect") local split = require("libraries/utils").split ---@configDescription The container class. It is a visual element that can contain other elements. It is the base class for all containers @@ -697,7 +698,6 @@ end --- @param options? table Optional layout-specific options --- @return Container self For method chaining function Container:applyLayout(layoutPath, options) - local LayoutManager = require("layoutManager") if self._layoutInstance then LayoutManager.destroy(self._layoutInstance) @@ -716,7 +716,6 @@ end --- @return Container self For method chaining function Container:updateLayout() if self._layoutInstance then - local LayoutManager = require("layoutManager") LayoutManager.update(self._layoutInstance) end return self diff --git a/src/elements/ScrollFrame.lua b/src/elements/ScrollFrame.lua index 4f3c482..c9ece6b 100644 --- a/src/elements/ScrollFrame.lua +++ b/src/elements/ScrollFrame.lua @@ -320,20 +320,40 @@ end --- @return boolean Whether the event was handled --- @protected function ScrollFrame:mouse_scroll(direction, x, y) - local height = self.get("height") - local width = self.get("width") - local offsetY = self.get("offsetY") - local contentWidth = self.get("contentWidth") - local contentHeight = self.get("contentHeight") + if self:isInBounds(x, y) then + local xOffset, yOffset = self.get("offsetX"), self.get("offsetY") + local relX, relY = self:getRelativePosition(x + xOffset, y + yOffset) - local needsHorizontalScrollBar = self.get("showScrollBar") and contentWidth > width - local viewportHeight = needsHorizontalScrollBar and height - 1 or height - local maxScroll = math.max(0, contentHeight - viewportHeight) + local success, child = self:callChildrenEvent(true, "mouse_scroll", direction, relX, relY) + if success then + return true + end - local newScroll = math.min(maxScroll, math.max(0, offsetY + direction)) - self.set("offsetY", newScroll) + local height = self.get("height") + local width = self.get("width") + local offsetY = self.get("offsetY") + local offsetX = self.get("offsetX") + local contentWidth = self.get("contentWidth") + local contentHeight = self.get("contentHeight") - return true + local needsHorizontalScrollBar = self.get("showScrollBar") and contentWidth > width + local viewportHeight = needsHorizontalScrollBar and height - 1 or height + local needsVerticalScrollBar = self.get("showScrollBar") and contentHeight > viewportHeight + local viewportWidth = needsVerticalScrollBar and width - 1 or width + + if needsVerticalScrollBar then + local maxScroll = math.max(0, contentHeight - viewportHeight) + local newScroll = math.min(maxScroll, math.max(0, offsetY + direction)) + self.set("offsetY", newScroll) + elseif needsHorizontalScrollBar then + local maxScroll = math.max(0, contentWidth - viewportWidth) + local newScroll = math.min(maxScroll, math.max(0, offsetX + direction)) + self.set("offsetX", newScroll) + end + + return true + end + return false end --- Renders the ScrollFrame and its scrollbars diff --git a/src/elements/VisualElement.lua b/src/elements/VisualElement.lua index de6b636..ff7c165 100644 --- a/src/elements/VisualElement.lua +++ b/src/elements/VisualElement.lua @@ -64,6 +64,9 @@ end}) ---@property ignoreOffset boolean false Whether to ignore the parent's offset VisualElement.defineProperty(VisualElement, "ignoreOffset", {default = false, type = "boolean"}) +---@property layoutConfig table {} Configuration for layout systems (grow, shrink, alignSelf, etc.) +VisualElement.defineProperty(VisualElement, "layoutConfig", {default = {}, type = "table"}) + ---@combinedProperty position {x number, y number} Combined x, y position VisualElement.combineProperties(VisualElement, "position", "x", "y") ---@combinedProperty size {width number, height number} Combined width, height @@ -178,6 +181,27 @@ function VisualElement:setConstraint(property, targetElement, targetProperty, of return self end +--- Updates a single property in the layoutConfig table +--- @shortDescription Updates a single layout config property without replacing the entire table +--- @param key string The layout config property to update (grow, shrink, basis, alignSelf, order, etc.) +--- @param value any The value to set for the property +--- @return VisualElement self The element instance +function VisualElement:setLayoutConfigProperty(key, value) + local layoutConfig = self.get("layoutConfig") + layoutConfig[key] = value + self.set("layoutConfig", layoutConfig) + return self +end + +--- Gets a single property from the layoutConfig table +--- @shortDescription Gets a single layout config property +--- @param key string The layout config property to get +--- @return any value The value of the property, or nil if not set +function VisualElement:getLayoutConfigProperty(key) + local layoutConfig = self.get("layoutConfig") + return layoutConfig[key] +end + --- Resolves all constraints for the element --- @shortDescription Resolves all constraints for the element --- @return VisualElement self The element instance @@ -191,7 +215,7 @@ function VisualElement:resolveAllConstraints() for _, property in ipairs(order) do if constraints[property] then local value = self:_resolveConstraint(property, constraints[property]) - self:_applyConstraintValue(property, value) + self:_applyConstraintValue(property, value, constraints) end end self._constraintsDirty = false @@ -200,17 +224,31 @@ end --- Applies a resolved constraint value to the appropriate property --- @private -function VisualElement:_applyConstraintValue(property, value) +function VisualElement:_applyConstraintValue(property, value, constraints) if property == "x" or property == "left" then self.set("x", value) elseif property == "y" or property == "top" then self.set("y", value) elseif property == "right" then - local width = self.get("width") - self.set("x", value - width + 1) + if constraints.left then + local leftValue = self:_resolveConstraint("left", constraints.left) + local width = value - leftValue + 1 + self.set("width", width) + self.set("x", leftValue) + else + local width = self.get("width") + self.set("x", value - width + 1) + end elseif property == "bottom" then - local height = self.get("height") - self.set("y", value - height + 1) + if constraints.top then + local topValue = self:_resolveConstraint("top", constraints.top) + local height = value - topValue + 1 + self.set("height", height) + self.set("y", topValue) + else + local height = self.get("height") + self.set("y", value - height + 1) + end elseif property == "centerX" then local width = self.get("width") self.set("x", value - math.floor(width / 2)) diff --git a/src/layoutManager.lua b/src/layoutManager.lua index 55eed4d..34ba231 100644 --- a/src/layoutManager.lua +++ b/src/layoutManager.lua @@ -58,14 +58,46 @@ function LayoutManager._applyPositions(instance) child.set("y", pos.y) child.set("width", pos.width) child.set("height", pos.height) + child._layoutValues = { + x = pos.x, + y = pos.y, + width = pos.width, + height = pos.height + } end end end +--- Checks if a child's properties were changed by the user since last layout +--- @param child BaseElement The child element to check +--- @return boolean changed Whether the user changed x, y, width, or height +--- @private +function LayoutManager._wasChangedByUser(child) + if not child._layoutValues then return false end + + local currentX = child.get("x") + local currentY = child.get("y") + local currentWidth = child.get("width") + local currentHeight = child.get("height") + + return currentX ~= child._layoutValues.x or + currentY ~= child._layoutValues.y or + currentWidth ~= child._layoutValues.width or + currentHeight ~= child._layoutValues.height +end + --- Updates a layout instance (recalculates positions) --- @param instance table The layout instance function LayoutManager.update(instance) if instance and instance.layout and instance.layout.calculate then + if instance._positions then + for child, pos in pairs(instance._positions) do + if not child._destroyed then + child._userModified = LayoutManager._wasChangedByUser(child) + end + end + end + instance.layout.calculate(instance) LayoutManager._applyPositions(instance) end