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 for i, child in ipairs(children) do
local childWidth = child.get("width") local childWidth = child.get("width")
local childHeight = child.get("height") 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, { table.insert(rows, {
children = currentRow, children = currentRow,
@@ -57,10 +73,14 @@ function flow.calculate(instance)
table.insert(currentRow, { table.insert(currentRow, {
child = child, child = child,
width = childWidth, 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) maxHeightInRow = math.max(maxHeightInRow, childHeight)
end end
@@ -73,9 +93,39 @@ function flow.calculate(instance)
end end
for _, row in ipairs(rows) do 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 local rowWidth = 0
for j, item in ipairs(row.children) do for j, item in ipairs(row.children) do
rowWidth = rowWidth + item.width rowWidth = rowWidth + item.finalWidth
if j < #row.children then if j < #row.children then
rowWidth = rowWidth + spacing rowWidth = rowWidth + spacing
end end
@@ -100,11 +150,11 @@ function flow.calculate(instance)
positions[item.child] = { positions[item.child] = {
x = x, x = x,
y = y, y = y,
width = item.width, width = math.floor(item.finalWidth),
height = item.height height = item.height
} }
x = x + item.width + spacing x = x + math.floor(item.finalWidth) + spacing
end end
end end
@@ -118,8 +168,25 @@ function flow.calculate(instance)
for i, child in ipairs(children) do for i, child in ipairs(children) do
local childWidth = child.get("width") local childWidth = child.get("width")
local childHeight = child.get("height") 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, { table.insert(columns, {
children = currentColumn, children = currentColumn,
x = currentX, x = currentX,
@@ -135,10 +202,14 @@ function flow.calculate(instance)
table.insert(currentColumn, { table.insert(currentColumn, {
child = child, child = child,
width = childWidth, 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) maxWidthInColumn = math.max(maxWidthInColumn, childWidth)
end end
@@ -151,9 +222,39 @@ function flow.calculate(instance)
end end
for _, column in ipairs(columns) do 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 local columnHeight = 0
for j, item in ipairs(column.children) do for j, item in ipairs(column.children) do
columnHeight = columnHeight + item.height columnHeight = columnHeight + item.finalHeight
if j < #column.children then if j < #column.children then
columnHeight = columnHeight + spacing columnHeight = columnHeight + spacing
end end
@@ -179,10 +280,10 @@ function flow.calculate(instance)
x = x, x = x,
y = y, y = y,
width = item.width, 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 end
end end

View File

@@ -1,6 +1,7 @@
local elementManager = require("elementManager") local elementManager = require("elementManager")
local errorManager = require("errorManager") local errorManager = require("errorManager")
local VisualElement = elementManager.getElement("VisualElement") local VisualElement = elementManager.getElement("VisualElement")
local LayoutManager = require("layoutManager")
local expect = require("libraries/expect") local expect = require("libraries/expect")
local split = require("libraries/utils").split 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 ---@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 --- @param options? table Optional layout-specific options
--- @return Container self For method chaining --- @return Container self For method chaining
function Container:applyLayout(layoutPath, options) function Container:applyLayout(layoutPath, options)
local LayoutManager = require("layoutManager")
if self._layoutInstance then if self._layoutInstance then
LayoutManager.destroy(self._layoutInstance) LayoutManager.destroy(self._layoutInstance)
@@ -716,7 +716,6 @@ end
--- @return Container self For method chaining --- @return Container self For method chaining
function Container:updateLayout() function Container:updateLayout()
if self._layoutInstance then if self._layoutInstance then
local LayoutManager = require("layoutManager")
LayoutManager.update(self._layoutInstance) LayoutManager.update(self._layoutInstance)
end end
return self return self

View File

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

View File

@@ -64,6 +64,9 @@ end})
---@property ignoreOffset boolean false Whether to ignore the parent's offset ---@property ignoreOffset boolean false Whether to ignore the parent's offset
VisualElement.defineProperty(VisualElement, "ignoreOffset", {default = false, type = "boolean"}) 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 ---@combinedProperty position {x number, y number} Combined x, y position
VisualElement.combineProperties(VisualElement, "position", "x", "y") VisualElement.combineProperties(VisualElement, "position", "x", "y")
---@combinedProperty size {width number, height number} Combined width, height ---@combinedProperty size {width number, height number} Combined width, height
@@ -178,6 +181,27 @@ function VisualElement:setConstraint(property, targetElement, targetProperty, of
return self return self
end 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 --- Resolves all constraints for the element
--- @shortDescription Resolves all constraints for the element --- @shortDescription Resolves all constraints for the element
--- @return VisualElement self The element instance --- @return VisualElement self The element instance
@@ -191,7 +215,7 @@ function VisualElement:resolveAllConstraints()
for _, property in ipairs(order) do for _, property in ipairs(order) do
if constraints[property] then if constraints[property] then
local value = self:_resolveConstraint(property, constraints[property]) local value = self:_resolveConstraint(property, constraints[property])
self:_applyConstraintValue(property, value) self:_applyConstraintValue(property, value, constraints)
end end
end end
self._constraintsDirty = false self._constraintsDirty = false
@@ -200,17 +224,31 @@ end
--- Applies a resolved constraint value to the appropriate property --- Applies a resolved constraint value to the appropriate property
--- @private --- @private
function VisualElement:_applyConstraintValue(property, value) function VisualElement:_applyConstraintValue(property, value, constraints)
if property == "x" or property == "left" then if property == "x" or property == "left" then
self.set("x", value) self.set("x", value)
elseif property == "y" or property == "top" then elseif property == "y" or property == "top" then
self.set("y", value) self.set("y", value)
elseif property == "right" then elseif property == "right" then
local width = self.get("width") if constraints.left then
self.set("x", value - width + 1) 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 elseif property == "bottom" then
local height = self.get("height") if constraints.top then
self.set("y", value - height + 1) 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 elseif property == "centerX" then
local width = self.get("width") local width = self.get("width")
self.set("x", value - math.floor(width / 2)) 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("y", pos.y)
child.set("width", pos.width) child.set("width", pos.width)
child.set("height", pos.height) child.set("height", pos.height)
child._layoutValues = {
x = pos.x,
y = pos.y,
width = pos.width,
height = pos.height
}
end end
end 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) --- Updates a layout instance (recalculates positions)
--- @param instance table The layout instance --- @param instance table The layout instance
function LayoutManager.update(instance) function LayoutManager.update(instance)
if instance and instance.layout and instance.layout.calculate then 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) instance.layout.calculate(instance)
LayoutManager._applyPositions(instance) LayoutManager._applyPositions(instance)
end end