feat: scrollbar balance
This commit is contained in:
@@ -141,6 +141,7 @@
|
||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
|
||||
---@field scrollbarKnobOffset number|table? -- Scrollbar knob/handle offset (number or {x, y} or {horizontal, vertical})
|
||||
---@field scrollbarPlacement string? -- "reserve-space"|"overlay" -- Whether scrollbar reserves space or overlays content (default: "reserve-space")
|
||||
---@field scrollbarBalance boolean? -- When true, reserve space on both sides of content for visual balance (default: false)
|
||||
---@field _overflowX boolean? -- Internal: whether content overflows horizontally
|
||||
---@field _overflowY boolean? -- Internal: whether content overflows vertically
|
||||
---@field _contentWidth number? -- Internal: total content width
|
||||
@@ -1941,6 +1942,7 @@ function Element.new(props)
|
||||
scrollbarKnobOffset = props.scrollbarKnobOffset,
|
||||
hideScrollbars = props.hideScrollbars,
|
||||
scrollbarPlacement = props.scrollbarPlacement,
|
||||
scrollbarBalance = props.scrollbarBalance,
|
||||
_scrollX = props._scrollX,
|
||||
_scrollY = props._scrollY,
|
||||
}, scrollManagerDeps)
|
||||
@@ -1960,6 +1962,7 @@ function Element.new(props)
|
||||
self.scrollbarKnobOffset = self._scrollManager.scrollbarKnobOffset
|
||||
self.hideScrollbars = self._scrollManager.hideScrollbars
|
||||
self.scrollbarPlacement = self._scrollManager.scrollbarPlacement
|
||||
self.scrollbarBalance = self._scrollManager.scrollbarBalance
|
||||
|
||||
-- Initialize state properties (will be synced from ScrollManager)
|
||||
self._overflowX = false
|
||||
|
||||
@@ -466,20 +466,32 @@ function LayoutEngine:layoutChildren()
|
||||
local isHorizontal = self.flexDirection == self._FlexDirection.HORIZONTAL
|
||||
for _, child in ipairs(flexChildren) do
|
||||
if isHorizontal then
|
||||
-- Horizontal flex: cross-axis is height
|
||||
-- Horizontal flex: main-axis is width, cross-axis is height
|
||||
-- Adjust main-axis width if percentage-based
|
||||
if child.units and child.units.width and child.units.width.unit == "%" then
|
||||
local newBorderBoxWidth = (child.units.width.value / 100) * availableMainSize
|
||||
local newWidth = math.max(0, newBorderBoxWidth - child.padding.left - child.padding.right)
|
||||
child.width = newWidth
|
||||
child._borderBoxWidth = newBorderBoxWidth
|
||||
end
|
||||
-- Adjust cross-axis height if percentage-based
|
||||
if child.units and child.units.height and child.units.height.unit == "%" then
|
||||
-- Re-resolve percentage height against reduced cross-axis size
|
||||
-- The percentage applies to border-box, so we need to subtract padding to get content height
|
||||
local newBorderBoxHeight = (child.units.height.value / 100) * availableCrossSize
|
||||
local newHeight = math.max(0, newBorderBoxHeight - child.padding.top - child.padding.bottom)
|
||||
child.height = newHeight
|
||||
child._borderBoxHeight = newBorderBoxHeight
|
||||
end
|
||||
else
|
||||
-- Vertical flex: cross-axis is width
|
||||
-- Vertical flex: main-axis is height, cross-axis is width
|
||||
-- Adjust main-axis height if percentage-based
|
||||
if child.units and child.units.height and child.units.height.unit == "%" then
|
||||
local newBorderBoxHeight = (child.units.height.value / 100) * availableMainSize
|
||||
local newHeight = math.max(0, newBorderBoxHeight - child.padding.top - child.padding.bottom)
|
||||
child.height = newHeight
|
||||
child._borderBoxHeight = newBorderBoxHeight
|
||||
end
|
||||
-- Adjust cross-axis width if percentage-based
|
||||
if child.units and child.units.width and child.units.width.unit == "%" then
|
||||
-- Re-resolve percentage width against reduced cross-axis size
|
||||
-- The percentage applies to border-box, so we need to subtract padding to get content width
|
||||
local newBorderBoxWidth = (child.units.width.value / 100) * availableCrossSize
|
||||
local newWidth = math.max(0, newBorderBoxWidth - child.padding.left - child.padding.right)
|
||||
child.width = newWidth
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
---@field scrollbarKnobOffset table -- {x: number, y: number, horizontal: number, vertical: number} -- Offset for scrollbar knob/handle position
|
||||
---@field hideScrollbars table -- {vertical: boolean, horizontal: boolean}
|
||||
---@field scrollbarPlacement string -- "reserve-space"|"overlay" -- Whether scrollbar reserves space or overlays content (default: "reserve-space")
|
||||
---@field scrollbarBalance boolean -- When true, reserve space on both sides of content for visual balance (default: false)
|
||||
---@field touchScrollEnabled boolean -- Enable touch scrolling
|
||||
---@field momentumScrollEnabled boolean -- Enable momentum scrolling
|
||||
---@field bounceEnabled boolean -- Enable bounce effects at boundaries
|
||||
@@ -101,6 +102,9 @@ function ScrollManager.new(config, deps)
|
||||
|
||||
-- Scrollbar placement: "reserve-space" (default) or "overlay"
|
||||
self.scrollbarPlacement = config.scrollbarPlacement or "reserve-space"
|
||||
|
||||
-- Scrollbar balance: when true, reserve space on both sides for visual balance
|
||||
self.scrollbarBalance = config.scrollbarBalance or false
|
||||
|
||||
-- Touch scrolling configuration
|
||||
self.touchScrollEnabled = config.touchScrollEnabled ~= false -- Default true
|
||||
@@ -167,12 +171,14 @@ function ScrollManager:getReservedSpace(element)
|
||||
|
||||
-- Reserve space for vertical scrollbar if overflow mode requires it
|
||||
if (overflowY == "scroll" or overflowY == "auto") and not self.hideScrollbars.vertical then
|
||||
reservedWidth = self.scrollbarWidth + (self.scrollbarPadding * 2)
|
||||
local scrollbarSpace = self.scrollbarWidth + (self.scrollbarPadding * 2)
|
||||
reservedWidth = self.scrollbarBalance and (scrollbarSpace * 2) or scrollbarSpace
|
||||
end
|
||||
|
||||
-- Reserve space for horizontal scrollbar if overflow mode requires it
|
||||
if (overflowX == "scroll" or overflowX == "auto") and not self.hideScrollbars.horizontal then
|
||||
reservedHeight = self.scrollbarWidth + (self.scrollbarPadding * 2)
|
||||
local scrollbarSpace = self.scrollbarWidth + (self.scrollbarPadding * 2)
|
||||
reservedHeight = self.scrollbarBalance and (scrollbarSpace * 2) or scrollbarSpace
|
||||
end
|
||||
|
||||
return reservedWidth, reservedHeight
|
||||
@@ -715,6 +721,7 @@ function ScrollManager:getState()
|
||||
scrollBarStyle = self.scrollBarStyle,
|
||||
scrollbarKnobOffset = self.scrollbarKnobOffset,
|
||||
scrollbarPlacement = self.scrollbarPlacement,
|
||||
scrollbarBalance = self.scrollbarBalance,
|
||||
_overflowX = self._overflowX,
|
||||
_overflowY = self._overflowY,
|
||||
_contentWidth = self._contentWidth,
|
||||
@@ -797,6 +804,10 @@ function ScrollManager:setState(state)
|
||||
self.scrollbarPlacement = state.scrollbarPlacement
|
||||
end
|
||||
|
||||
if state.scrollbarBalance ~= nil then
|
||||
self.scrollbarBalance = state.scrollbarBalance
|
||||
end
|
||||
|
||||
if state._overflowX ~= nil then
|
||||
self._overflowX = state._overflowX
|
||||
end
|
||||
|
||||
@@ -131,6 +131,7 @@ local AnimationProps = {}
|
||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars, default: uses first scrollbar or fallback rendering)
|
||||
---@field scrollbarKnobOffset number|{x:number, y:number}|{horizontal:number, vertical:number}? -- Offset for scrollbar knob/handle position in pixels (number for both axes, or table for per-axis control, default: 0, adds to theme offset)
|
||||
---@field scrollbarPlacement "reserve-space"|"overlay"? -- Scrollbar rendering mode: "reserve-space" (reduces content area, default) or "overlay" (renders over content)
|
||||
---@field scrollbarBalance boolean? -- When true, reserve scrollbar space on both sides of content for visual balance (default: false)
|
||||
---@field hideScrollbars boolean|{vertical:boolean, horizontal:boolean}? -- Hide scrollbars (boolean for both, or table for individual control, default: false)
|
||||
---@field imagePath string? -- Path to image file (auto-loads via ImageCache)
|
||||
---@field image love.Image? -- Image object to display
|
||||
|
||||
@@ -171,6 +171,94 @@ function TestScrollbarPlacement:test_vertical_overflow_detected_with_reserved_sp
|
||||
luaunit.assertEquals(child1.width, 184, "Child width should be reduced for scrollbar")
|
||||
end
|
||||
|
||||
function TestScrollbarPlacement:test_scrollbar_balance_vertical()
|
||||
-- Test scrollbarBalance with vertical scrollbar
|
||||
local container = FlexLove.new({
|
||||
width = 200,
|
||||
height = 200,
|
||||
positioning = "flex",
|
||||
flexDirection = "column",
|
||||
overflow = "scroll",
|
||||
scrollbarPlacement = "reserve-space",
|
||||
scrollbarBalance = true,
|
||||
})
|
||||
|
||||
local child1 = FlexLove.new({
|
||||
width = "100%",
|
||||
height = 100,
|
||||
parent = container
|
||||
})
|
||||
|
||||
container:layoutChildren()
|
||||
|
||||
local reservedW, reservedH = container._scrollManager:getReservedSpace(container)
|
||||
|
||||
-- Should reserve double the space (16 * 2 = 32)
|
||||
luaunit.assertEquals(reservedW, 32, "Should reserve doubled width for balance")
|
||||
|
||||
-- Child width should account for balanced space
|
||||
luaunit.assertEquals(child1.width, 168, "Child width should be 200 - 32")
|
||||
end
|
||||
|
||||
function TestScrollbarPlacement:test_scrollbar_balance_horizontal()
|
||||
-- Test scrollbarBalance with horizontal scrollbar
|
||||
local container = FlexLove.new({
|
||||
width = 200,
|
||||
height = 200,
|
||||
positioning = "flex",
|
||||
flexDirection = "horizontal",
|
||||
overflow = "scroll",
|
||||
scrollbarPlacement = "reserve-space",
|
||||
scrollbarBalance = true,
|
||||
})
|
||||
|
||||
local child1 = FlexLove.new({
|
||||
width = 100,
|
||||
height = "100%",
|
||||
parent = container
|
||||
})
|
||||
|
||||
container:layoutChildren()
|
||||
|
||||
local reservedW, reservedH = container._scrollManager:getReservedSpace(container)
|
||||
|
||||
-- Should reserve double the space (16 * 2 = 32)
|
||||
luaunit.assertEquals(reservedH, 32, "Should reserve doubled height for balance")
|
||||
|
||||
-- Child height should account for balanced space
|
||||
luaunit.assertEquals(child1.height, 168, "Child height should be 200 - 32")
|
||||
end
|
||||
|
||||
function TestScrollbarPlacement:test_scrollbar_balance_both()
|
||||
-- Test scrollbarBalance with both scrollbars
|
||||
local container = FlexLove.new({
|
||||
width = 200,
|
||||
height = 200,
|
||||
positioning = "flex",
|
||||
overflow = "scroll",
|
||||
scrollbarPlacement = "reserve-space",
|
||||
scrollbarBalance = true,
|
||||
})
|
||||
|
||||
local child1 = FlexLove.new({
|
||||
width = "100%",
|
||||
height = "100%",
|
||||
parent = container
|
||||
})
|
||||
|
||||
container:layoutChildren()
|
||||
|
||||
local reservedW, reservedH = container._scrollManager:getReservedSpace(container)
|
||||
|
||||
-- Both should reserve double the space
|
||||
luaunit.assertEquals(reservedW, 32, "Should reserve doubled width for balance")
|
||||
luaunit.assertEquals(reservedH, 32, "Should reserve doubled height for balance")
|
||||
|
||||
-- Child should be sized to balanced available space
|
||||
luaunit.assertEquals(child1.width, 168, "Child width should be 200 - 32")
|
||||
luaunit.assertEquals(child1.height, 168, "Child height should be 200 - 32")
|
||||
end
|
||||
|
||||
if not _G.RUNNING_ALL_TESTS then
|
||||
os.exit(luaunit.LuaUnit.run())
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user