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
This commit is contained in:
Robert Jelic
2025-11-03 13:29:11 +01:00
parent 250ce886ca
commit c723c66004
5 changed files with 221 additions and 31 deletions

View File

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

View File

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

View File

@@ -320,20 +320,40 @@ end
--- @return boolean Whether the event was handled
--- @protected
function ScrollFrame:mouse_scroll(direction, x, y)
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 success, child = self:callChildrenEvent(true, "mouse_scroll", direction, relX, relY)
if success then
return true
end
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")
local needsHorizontalScrollBar = self.get("showScrollBar") and contentWidth > width
local viewportHeight = needsHorizontalScrollBar and height - 1 or height
local maxScroll = math.max(0, contentHeight - viewportHeight)
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

View File

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

View File

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