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