scrolling improvements
This commit is contained in:
@@ -132,6 +132,7 @@
|
||||
---@field scrollbarRadius number? -- Scrollbar corner radius
|
||||
---@field scrollbarPadding number? -- Scrollbar padding from edges
|
||||
---@field scrollSpeed number? -- Scroll speed multiplier
|
||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
|
||||
---@field _overflowX boolean? -- Internal: whether content overflows horizontally
|
||||
---@field _overflowY boolean? -- Internal: whether content overflows vertically
|
||||
---@field _contentWidth number? -- Internal: total content width
|
||||
@@ -1423,6 +1424,8 @@ function Element.new(props)
|
||||
scrollbarRadius = props.scrollbarRadius,
|
||||
scrollbarPadding = props.scrollbarPadding,
|
||||
scrollSpeed = props.scrollSpeed,
|
||||
smoothScrollEnabled = props.smoothScrollEnabled,
|
||||
scrollBarStyle = props.scrollBarStyle,
|
||||
hideScrollbars = props.hideScrollbars,
|
||||
_scrollX = props._scrollX,
|
||||
_scrollY = props._scrollY,
|
||||
@@ -1438,6 +1441,7 @@ function Element.new(props)
|
||||
self.scrollbarRadius = self._scrollManager.scrollbarRadius
|
||||
self.scrollbarPadding = self._scrollManager.scrollbarPadding
|
||||
self.scrollSpeed = self._scrollManager.scrollSpeed
|
||||
self.scrollBarStyle = self._scrollManager.scrollBarStyle
|
||||
self.hideScrollbars = self._scrollManager.hideScrollbars
|
||||
|
||||
-- Initialize state properties (will be synced from ScrollManager)
|
||||
@@ -2212,6 +2216,12 @@ function Element:update(dt)
|
||||
self._textEditor:update(self, dt)
|
||||
end
|
||||
|
||||
-- Update scroll manager for smooth scrolling and momentum
|
||||
if self._scrollManager then
|
||||
self._scrollManager:update(dt)
|
||||
self:_syncScrollManagerState()
|
||||
end
|
||||
|
||||
-- Update animation if exists
|
||||
if self.animation then
|
||||
-- Ensure animation has Color module reference for color interpolation
|
||||
|
||||
@@ -266,6 +266,14 @@ function EventHandler:_handleMouseDrag(element, mx, my, button, isHovering)
|
||||
local lastY = self._lastMouseY[button] or my
|
||||
|
||||
if lastX ~= mx or lastY ~= my then
|
||||
-- Handle scrollbar drag if scrollbar was pressed
|
||||
if button == 1 and self._scrollbarPressHandled and element._handleScrollbarDrag then
|
||||
element:_handleScrollbarDrag(mx, my)
|
||||
self._lastMouseX[button] = mx
|
||||
self._lastMouseY[button] = my
|
||||
return -- Don't process other drag events while dragging scrollbar
|
||||
end
|
||||
|
||||
-- Mouse has moved - fire drag event only if still hovering
|
||||
if isHovering then
|
||||
local modifiers = EventHandler._utils.getModifiers()
|
||||
@@ -304,6 +312,16 @@ function EventHandler:_handleMouseRelease(element, mx, my, button)
|
||||
local currentTime = love.timer.getTime()
|
||||
local modifiers = EventHandler._utils.getModifiers()
|
||||
|
||||
-- Handle scrollbar release if scrollbar was pressed
|
||||
if button == 1 and self._scrollbarPressHandled and element._handleScrollbarRelease then
|
||||
element:_handleScrollbarRelease(button)
|
||||
self._scrollbarPressHandled = false -- Reset flag
|
||||
self._pressed[button] = false
|
||||
self._dragStartX[button] = nil
|
||||
self._dragStartY[button] = nil
|
||||
return -- Don't process click events for scrollbar release
|
||||
end
|
||||
|
||||
-- Determine click count (double-click detection)
|
||||
local clickCount = 1
|
||||
local doubleClickThreshold = 0.3 -- 300ms for double-click
|
||||
|
||||
@@ -880,6 +880,12 @@ end
|
||||
---@param h number Height
|
||||
---@param dims table Scrollbar dimensions from _calculateScrollbarDimensions
|
||||
function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
-- Try to get themed scrollbar component
|
||||
local scrollbarComponent = nil
|
||||
if element.scrollBarStyle or self._Theme.hasActive() then
|
||||
scrollbarComponent = self._Theme.getScrollbar(element.scrollBarStyle)
|
||||
end
|
||||
|
||||
-- Vertical scrollbar
|
||||
if dims.vertical.visible and not element.hideScrollbars.vertical then
|
||||
-- Position scrollbar within content area (x, y is border-box origin)
|
||||
@@ -888,25 +894,57 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
local trackX = contentX + w - element.scrollbarWidth - element.scrollbarPadding
|
||||
local trackY = contentY + element.scrollbarPadding
|
||||
|
||||
-- Determine thumb color based on state (independent for vertical)
|
||||
local thumbColor = element.scrollbarColor
|
||||
if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then
|
||||
-- Active state: brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
elseif element._scrollbarHoveredVertical then
|
||||
-- Hover state: slightly brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
-- Check if we should use themed rendering
|
||||
if scrollbarComponent then
|
||||
-- Themed scrollbar rendering using NinePatch
|
||||
local frameComponent = scrollbarComponent.frame or scrollbarComponent
|
||||
local barComponent = scrollbarComponent.bar or scrollbarComponent
|
||||
|
||||
-- Draw track (frame) if component exists
|
||||
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
||||
self._NinePatch.draw(
|
||||
frameComponent,
|
||||
frameComponent._loadedAtlas,
|
||||
trackX,
|
||||
trackY,
|
||||
element.scrollbarWidth,
|
||||
dims.vertical.trackHeight
|
||||
)
|
||||
end
|
||||
|
||||
-- Draw thumb (bar) if component exists
|
||||
if barComponent and barComponent._loadedAtlas and barComponent.regions then
|
||||
self._NinePatch.draw(
|
||||
barComponent,
|
||||
barComponent._loadedAtlas,
|
||||
trackX,
|
||||
trackY + dims.vertical.thumbY,
|
||||
element.scrollbarWidth,
|
||||
dims.vertical.thumbHeight
|
||||
)
|
||||
end
|
||||
else
|
||||
-- Fallback to color-based rendering
|
||||
-- Determine thumb color based on state (independent for vertical)
|
||||
local thumbColor = element.scrollbarColor
|
||||
if element._scrollbarDragging and element._hoveredScrollbar == "vertical" then
|
||||
-- Active state: brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
elseif element._scrollbarHoveredVertical then
|
||||
-- Hover state: slightly brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
end
|
||||
|
||||
-- Draw track
|
||||
love.graphics.setColor(element.scrollbarTrackColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX, trackY, element.scrollbarWidth, dims.vertical.trackHeight, element.scrollbarRadius)
|
||||
|
||||
-- Draw thumb with state-based color
|
||||
love.graphics.setColor(thumbColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX, trackY + dims.vertical.thumbY, element.scrollbarWidth, dims.vertical.thumbHeight, element.scrollbarRadius)
|
||||
end
|
||||
|
||||
-- Draw track
|
||||
love.graphics.setColor(element.scrollbarTrackColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX, trackY, element.scrollbarWidth, dims.vertical.trackHeight, element.scrollbarRadius)
|
||||
|
||||
-- Draw thumb with state-based color
|
||||
love.graphics.setColor(thumbColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX, trackY + dims.vertical.thumbY, element.scrollbarWidth, dims.vertical.thumbHeight, element.scrollbarRadius)
|
||||
end
|
||||
|
||||
-- Horizontal scrollbar
|
||||
@@ -917,25 +955,57 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
local trackX = contentX + element.scrollbarPadding
|
||||
local trackY = contentY + h - element.scrollbarWidth - element.scrollbarPadding
|
||||
|
||||
-- Determine thumb color based on state (independent for horizontal)
|
||||
local thumbColor = element.scrollbarColor
|
||||
if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then
|
||||
-- Active state: brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
elseif element._scrollbarHoveredHorizontal then
|
||||
-- Hover state: slightly brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
-- Check if we should use themed rendering
|
||||
if scrollbarComponent then
|
||||
-- Themed scrollbar rendering using NinePatch
|
||||
local frameComponent = scrollbarComponent.frame or scrollbarComponent
|
||||
local barComponent = scrollbarComponent.bar or scrollbarComponent
|
||||
|
||||
-- Draw track (frame) if component exists
|
||||
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
||||
self._NinePatch.draw(
|
||||
frameComponent,
|
||||
frameComponent._loadedAtlas,
|
||||
trackX,
|
||||
trackY,
|
||||
dims.horizontal.trackWidth,
|
||||
element.scrollbarWidth
|
||||
)
|
||||
end
|
||||
|
||||
-- Draw thumb (bar) if component exists
|
||||
if barComponent and barComponent._loadedAtlas and barComponent.regions then
|
||||
self._NinePatch.draw(
|
||||
barComponent,
|
||||
barComponent._loadedAtlas,
|
||||
trackX + dims.horizontal.thumbX,
|
||||
trackY,
|
||||
dims.horizontal.thumbWidth,
|
||||
element.scrollbarWidth
|
||||
)
|
||||
end
|
||||
else
|
||||
-- Fallback to color-based rendering
|
||||
-- Determine thumb color based on state (independent for horizontal)
|
||||
local thumbColor = element.scrollbarColor
|
||||
if element._scrollbarDragging and element._hoveredScrollbar == "horizontal" then
|
||||
-- Active state: brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.4)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
elseif element._scrollbarHoveredHorizontal then
|
||||
-- Hover state: slightly brighter
|
||||
local r, g, b, a = self._utils.brightenColor(thumbColor.r, thumbColor.g, thumbColor.b, thumbColor.a, 1.2)
|
||||
thumbColor = self._Color.new(r, g, b, a)
|
||||
end
|
||||
|
||||
-- Draw track
|
||||
love.graphics.setColor(element.scrollbarTrackColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX, trackY, dims.horizontal.trackWidth, element.scrollbarWidth, element.scrollbarRadius)
|
||||
|
||||
-- Draw thumb with state-based color
|
||||
love.graphics.setColor(thumbColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX + dims.horizontal.thumbX, trackY, dims.horizontal.thumbWidth, element.scrollbarWidth, element.scrollbarRadius)
|
||||
end
|
||||
|
||||
-- Draw track
|
||||
love.graphics.setColor(element.scrollbarTrackColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX, trackY, dims.horizontal.trackWidth, element.scrollbarWidth, element.scrollbarRadius)
|
||||
|
||||
-- Draw thumb with state-based color
|
||||
love.graphics.setColor(thumbColor:toRGBA())
|
||||
love.graphics.rectangle("fill", trackX + dims.horizontal.thumbX, trackY, dims.horizontal.thumbWidth, element.scrollbarWidth, element.scrollbarRadius)
|
||||
end
|
||||
|
||||
-- Reset color
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
---@field scrollbarRadius number -- Border radius for scrollbars
|
||||
---@field scrollbarPadding number -- Padding around scrollbar
|
||||
---@field scrollSpeed number -- Scroll speed for wheel events (pixels per wheel unit)
|
||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars)
|
||||
---@field hideScrollbars table -- {vertical: boolean, horizontal: boolean}
|
||||
---@field touchScrollEnabled boolean -- Enable touch scrolling
|
||||
---@field momentumScrollEnabled boolean -- Enable momentum scrolling
|
||||
@@ -21,6 +22,9 @@
|
||||
---@field _contentHeight number -- Total content height (including overflow)
|
||||
---@field _scrollX number -- Current horizontal scroll position
|
||||
---@field _scrollY number -- Current vertical scroll position
|
||||
---@field _targetScrollX number? -- Target scroll X for smooth scrolling
|
||||
---@field _targetScrollY number? -- Target scroll Y for smooth scrolling
|
||||
---@field _smoothScrollSpeed number -- Speed of smooth scroll interpolation (0-1, higher = faster)
|
||||
---@field _maxScrollX number -- Maximum horizontal scroll (contentWidth - containerWidth)
|
||||
---@field _maxScrollY number -- Maximum vertical scroll (contentHeight - containerHeight)
|
||||
---@field _scrollbarHoveredVertical boolean -- True if mouse is over vertical scrollbar
|
||||
@@ -74,6 +78,7 @@ function ScrollManager.new(config, deps)
|
||||
self.scrollbarRadius = config.scrollbarRadius or 6
|
||||
self.scrollbarPadding = config.scrollbarPadding or 2
|
||||
self.scrollSpeed = config.scrollSpeed or 20
|
||||
self.scrollBarStyle = config.scrollBarStyle -- Theme scrollbar style name (nil = use default)
|
||||
|
||||
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
||||
self.hideScrollbars = self._utils.normalizeBooleanTable(config.hideScrollbars, false)
|
||||
@@ -95,6 +100,10 @@ function ScrollManager.new(config, deps)
|
||||
-- Scroll state (can be restored from config in immediate mode)
|
||||
self._scrollX = config._scrollX or 0
|
||||
self._scrollY = config._scrollY or 0
|
||||
self._targetScrollX = nil
|
||||
self._targetScrollY = nil
|
||||
self._smoothScrollSpeed = 0.25 -- Interpolation speed (0-1, higher = faster)
|
||||
self.smoothScrollEnabled = config.smoothScrollEnabled or false -- Enable smooth wheel scrolling
|
||||
self._maxScrollX = 0
|
||||
self._maxScrollY = 0
|
||||
|
||||
@@ -552,24 +561,38 @@ function ScrollManager:handleWheel(x, y)
|
||||
return false
|
||||
end
|
||||
|
||||
local hasVerticalOverflow = self._overflowY and self._maxScrollY > 0
|
||||
local hasHorizontalOverflow = self._overflowX and self._maxScrollX > 0
|
||||
-- In immediate mode, overflow might not be calculated yet, so allow scrolling based on maxScroll values
|
||||
-- If _overflowY is nil/false but _maxScrollY > 0, we should still allow scrolling (from restored state)
|
||||
local hasVerticalOverflow = (self._overflowY and self._maxScrollY > 0) or (self._maxScrollY and self._maxScrollY > 0)
|
||||
local hasHorizontalOverflow = (self._overflowX and self._maxScrollX > 0) or (self._maxScrollX and self._maxScrollX > 0)
|
||||
|
||||
local scrolled = false
|
||||
|
||||
-- Vertical scrolling
|
||||
if y ~= 0 and hasVerticalOverflow then
|
||||
local delta = -y * self.scrollSpeed -- Negative because wheel up = scroll up
|
||||
local newScrollY = self._scrollY + delta
|
||||
self:setScroll(nil, newScrollY)
|
||||
if self.smoothScrollEnabled then
|
||||
-- Set target for smooth scrolling instead of instant jump
|
||||
self._targetScrollY = self._utils.clamp((self._targetScrollY or self._scrollY) + delta, 0, self._maxScrollY)
|
||||
else
|
||||
-- Instant scrolling (default behavior)
|
||||
local newScrollY = self._scrollY + delta
|
||||
self:setScroll(nil, newScrollY)
|
||||
end
|
||||
scrolled = true
|
||||
end
|
||||
|
||||
-- Horizontal scrolling
|
||||
if x ~= 0 and hasHorizontalOverflow then
|
||||
local delta = -x * self.scrollSpeed
|
||||
local newScrollX = self._scrollX + delta
|
||||
self:setScroll(newScrollX, nil)
|
||||
if self.smoothScrollEnabled then
|
||||
-- Set target for smooth scrolling instead of instant jump
|
||||
self._targetScrollX = self._utils.clamp((self._targetScrollX or self._scrollX) + delta, 0, self._maxScrollX)
|
||||
else
|
||||
-- Instant scrolling (default behavior)
|
||||
local newScrollX = self._scrollX + delta
|
||||
self:setScroll(newScrollX, nil)
|
||||
end
|
||||
scrolled = true
|
||||
end
|
||||
|
||||
@@ -619,11 +642,18 @@ function ScrollManager:getState()
|
||||
return {
|
||||
_scrollX = self._scrollX or 0,
|
||||
_scrollY = self._scrollY or 0,
|
||||
_targetScrollX = self._targetScrollX,
|
||||
_targetScrollY = self._targetScrollY,
|
||||
_scrollbarDragging = self._scrollbarDragging or false,
|
||||
_hoveredScrollbar = self._hoveredScrollbar,
|
||||
_scrollbarDragOffset = self._scrollbarDragOffset or 0,
|
||||
_scrollbarHoveredVertical = self._scrollbarHoveredVertical or false,
|
||||
_scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal or false,
|
||||
scrollBarStyle = self.scrollBarStyle,
|
||||
_overflowX = self._overflowX,
|
||||
_overflowY = self._overflowY,
|
||||
_contentWidth = self._contentWidth,
|
||||
_contentHeight = self._contentHeight,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -672,6 +702,34 @@ function ScrollManager:setState(state)
|
||||
if state._scrollbarHoveredHorizontal ~= nil then
|
||||
self._scrollbarHoveredHorizontal = state._scrollbarHoveredHorizontal
|
||||
end
|
||||
|
||||
if state.scrollBarStyle ~= nil then
|
||||
self.scrollBarStyle = state.scrollBarStyle
|
||||
end
|
||||
|
||||
if state._overflowX ~= nil then
|
||||
self._overflowX = state._overflowX
|
||||
end
|
||||
|
||||
if state._overflowY ~= nil then
|
||||
self._overflowY = state._overflowY
|
||||
end
|
||||
|
||||
if state._contentWidth ~= nil then
|
||||
self._contentWidth = state._contentWidth
|
||||
end
|
||||
|
||||
if state._contentHeight ~= nil then
|
||||
self._contentHeight = state._contentHeight
|
||||
end
|
||||
|
||||
if state._targetScrollX ~= nil then
|
||||
self._targetScrollX = state._targetScrollX
|
||||
end
|
||||
|
||||
if state._targetScrollY ~= nil then
|
||||
self._targetScrollY = state._targetScrollY
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle touch press for scrolling
|
||||
@@ -795,6 +853,29 @@ end
|
||||
--- Update momentum scrolling (call every frame with dt)
|
||||
---@param dt number Delta time in seconds
|
||||
function ScrollManager:update(dt)
|
||||
-- Smooth scroll interpolation
|
||||
if self._targetScrollX or self._targetScrollY then
|
||||
if self._targetScrollY then
|
||||
local diff = self._targetScrollY - self._scrollY
|
||||
if math.abs(diff) > 0.5 then
|
||||
self._scrollY = self._scrollY + diff * self._smoothScrollSpeed
|
||||
else
|
||||
self._scrollY = self._targetScrollY
|
||||
self._targetScrollY = nil
|
||||
end
|
||||
end
|
||||
|
||||
if self._targetScrollX then
|
||||
local diff = self._targetScrollX - self._scrollX
|
||||
if math.abs(diff) > 0.5 then
|
||||
self._scrollX = self._scrollX + diff * self._smoothScrollSpeed
|
||||
else
|
||||
self._scrollX = self._targetScrollX
|
||||
self._targetScrollX = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not self._momentumScrolling then
|
||||
-- Handle bounce back if overscrolled
|
||||
if self.bounceEnabled then
|
||||
|
||||
@@ -64,6 +64,10 @@ local function validateThemeDefinition(definition)
|
||||
return false, "Theme 'fonts' must be a table"
|
||||
end
|
||||
|
||||
if definition.scrollbars and type(definition.scrollbars) ~= "table" then
|
||||
return false, "Theme 'scrollbars' must be a table"
|
||||
end
|
||||
|
||||
return true, nil
|
||||
end
|
||||
|
||||
@@ -331,6 +335,7 @@ end
|
||||
---@field name string
|
||||
---@field atlas string|love.Image? -- Optional: global atlas (can be overridden per component)
|
||||
---@field components table<string, ThemeComponent>
|
||||
---@field scrollbars table<string, ThemeComponent>? -- Optional: scrollbar component definitions (uses ThemeComponent format)
|
||||
---@field colors table<string, Color>?
|
||||
---@field fonts table<string, string>? -- Optional: font family definitions (name -> path)
|
||||
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Optional: default multiplier for auto-sized content dimensions
|
||||
@@ -340,6 +345,7 @@ end
|
||||
---@field atlas love.Image? -- Optional: global atlas
|
||||
---@field atlasData love.ImageData?
|
||||
---@field components table<string, ThemeComponent>
|
||||
---@field scrollbars table<string, ThemeComponent> -- Scrollbar component definitions
|
||||
---@field colors table<string, Color>
|
||||
---@field fonts table<string, string> -- Font family definitions
|
||||
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Optional: default multiplier for auto-sized content dimensions
|
||||
@@ -409,6 +415,7 @@ function Theme.new(definition)
|
||||
end
|
||||
|
||||
self.components = definition.components or {}
|
||||
self.scrollbars = definition.scrollbars or {}
|
||||
self.colors = definition.colors or {}
|
||||
self.fonts = definition.fonts or {}
|
||||
self.contentAutoSizingMultiplier = definition.contentAutoSizingMultiplier or nil
|
||||
@@ -552,6 +559,84 @@ function Theme.new(definition)
|
||||
end
|
||||
end
|
||||
|
||||
-- Load scrollbar-specific atlases and process 9-patch definitions
|
||||
-- Scrollbars can have 'bar' and 'frame' subcomponents
|
||||
for scrollbarName, scrollbarDef in pairs(self.scrollbars) do
|
||||
-- Handle scrollbar definitions with bar/frame subcomponents
|
||||
if scrollbarDef.bar or scrollbarDef.frame then
|
||||
-- Process 'bar' subcomponent
|
||||
if scrollbarDef.bar then
|
||||
if type(scrollbarDef.bar) == "string" then
|
||||
-- Convert string path to ThemeComponent structure
|
||||
local barComponent = { atlas = scrollbarDef.bar }
|
||||
loadAtlasWithNinePatch(barComponent, scrollbarDef.bar, "for scrollbar '" .. scrollbarName .. ".bar'")
|
||||
if barComponent.insets then
|
||||
createRegionsFromInsets(barComponent, barComponent._loadedAtlas or self.atlas)
|
||||
end
|
||||
scrollbarDef.bar = barComponent
|
||||
elseif type(scrollbarDef.bar) == "table" then
|
||||
-- Already a ThemeComponent structure, process it
|
||||
if scrollbarDef.bar.atlas and type(scrollbarDef.bar.atlas) == "string" then
|
||||
loadAtlasWithNinePatch(scrollbarDef.bar, scrollbarDef.bar.atlas, "for scrollbar '" .. scrollbarName .. ".bar'")
|
||||
end
|
||||
if scrollbarDef.bar.insets then
|
||||
createRegionsFromInsets(scrollbarDef.bar, scrollbarDef.bar._loadedAtlas or self.atlas)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Process 'frame' subcomponent
|
||||
if scrollbarDef.frame then
|
||||
if type(scrollbarDef.frame) == "string" then
|
||||
-- Convert string path to ThemeComponent structure
|
||||
local frameComponent = { atlas = scrollbarDef.frame }
|
||||
loadAtlasWithNinePatch(frameComponent, scrollbarDef.frame, "for scrollbar '" .. scrollbarName .. ".frame'")
|
||||
if frameComponent.insets then
|
||||
createRegionsFromInsets(frameComponent, frameComponent._loadedAtlas or self.atlas)
|
||||
end
|
||||
scrollbarDef.frame = frameComponent
|
||||
elseif type(scrollbarDef.frame) == "table" then
|
||||
-- Already a ThemeComponent structure, process it
|
||||
if scrollbarDef.frame.atlas and type(scrollbarDef.frame.atlas) == "string" then
|
||||
loadAtlasWithNinePatch(scrollbarDef.frame, scrollbarDef.frame.atlas, "for scrollbar '" .. scrollbarName .. ".frame'")
|
||||
end
|
||||
if scrollbarDef.frame.insets then
|
||||
createRegionsFromInsets(scrollbarDef.frame, scrollbarDef.frame._loadedAtlas or self.atlas)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Treat as a single ThemeComponent (no bar/frame split)
|
||||
if scrollbarDef.atlas then
|
||||
if type(scrollbarDef.atlas) == "string" then
|
||||
loadAtlasWithNinePatch(scrollbarDef, scrollbarDef.atlas, "for scrollbar '" .. scrollbarName .. "'")
|
||||
else
|
||||
scrollbarDef._loadedAtlas = scrollbarDef.atlas
|
||||
end
|
||||
end
|
||||
|
||||
if scrollbarDef.insets then
|
||||
createRegionsFromInsets(scrollbarDef, self.atlas)
|
||||
end
|
||||
|
||||
if scrollbarDef.states then
|
||||
for stateName, stateComponent in pairs(scrollbarDef.states) do
|
||||
if stateComponent.atlas then
|
||||
if type(stateComponent.atlas) == "string" then
|
||||
loadAtlasWithNinePatch(stateComponent, stateComponent.atlas, "for scrollbar '" .. scrollbarName .. "' state '" .. stateName .. "'")
|
||||
else
|
||||
stateComponent._loadedAtlas = stateComponent.atlas
|
||||
end
|
||||
end
|
||||
|
||||
if stateComponent.insets then
|
||||
createRegionsFromInsets(stateComponent, scrollbarDef._loadedAtlas or self.atlas)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -646,6 +731,50 @@ function Theme.getComponent(componentName, state)
|
||||
return component
|
||||
end
|
||||
|
||||
--- Get the first (default) scrollbar from the active theme
|
||||
--- Returns the first scrollbar component in insertion order
|
||||
---@return ThemeComponent? scrollbar Returns first scrollbar component or nil if no scrollbars defined
|
||||
function Theme.getDefaultScrollbar()
|
||||
if not activeTheme or not activeTheme.scrollbars then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Return first scrollbar in insertion order (Lua 5.3+ preserves order)
|
||||
for _, scrollbar in pairs(activeTheme.scrollbars) do
|
||||
return scrollbar
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Retrieve themed scrollbar components for consistent scrollbar styling
|
||||
--- Use this to apply theme-based scrollbar appearance to scrollable elements
|
||||
---@param scrollbarName string? Name of the scrollbar style (e.g., "v1", "v2"). If nil, returns default (first) scrollbar
|
||||
---@param state string? Optional state name (e.g., "hover", "pressed") - currently unused for scrollbars
|
||||
---@return ThemeComponent? scrollbar Returns scrollbar component or nil if not found
|
||||
function Theme.getScrollbar(scrollbarName, state)
|
||||
if not activeTheme or not activeTheme.scrollbars then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- If no scrollbarName specified, return default (first) scrollbar
|
||||
if not scrollbarName then
|
||||
return Theme.getDefaultScrollbar()
|
||||
end
|
||||
|
||||
local scrollbar = activeTheme.scrollbars[scrollbarName]
|
||||
if not scrollbar then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Check for state-specific override (if scrollbar supports states in the future)
|
||||
if state and scrollbar.states and scrollbar.states[state] then
|
||||
return scrollbar.states[state]
|
||||
end
|
||||
|
||||
return scrollbar
|
||||
end
|
||||
|
||||
--- Access theme-defined fonts for consistent typography across your UI
|
||||
--- Use this to load fonts specified in your theme definition
|
||||
---@param fontName string Name of the font family (e.g., "default", "heading")
|
||||
@@ -889,6 +1018,26 @@ function ThemeManager:getStateComponent()
|
||||
return component
|
||||
end
|
||||
|
||||
---Get a scrollbar component from the theme
|
||||
---@param scrollbarName string? The scrollbar style name (e.g., "v1", "v2"). If nil, returns default (first) scrollbar
|
||||
---@return ThemeComponent? scrollbar The scrollbar component, or nil if not found
|
||||
function ThemeManager:getScrollbarComponent(scrollbarName)
|
||||
local themeToUse = self:getTheme()
|
||||
if not themeToUse or not themeToUse.scrollbars or type(themeToUse.scrollbars) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- If no scrollbarName specified, return default (first) scrollbar
|
||||
if not scrollbarName then
|
||||
for _, scrollbar in pairs(themeToUse.scrollbars) do
|
||||
return scrollbar
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return themeToUse.scrollbars[scrollbarName]
|
||||
end
|
||||
|
||||
---Get a style property from the current state component
|
||||
---@param property string The property name
|
||||
---@return any? value The property value, or nil if not found
|
||||
@@ -1219,6 +1368,69 @@ function Theme.validateTheme(theme, options)
|
||||
end
|
||||
end
|
||||
|
||||
-- Scrollbars validation (optional)
|
||||
if theme.scrollbars ~= nil then
|
||||
if type(theme.scrollbars) ~= "table" then
|
||||
table.insert(errors, "Theme 'scrollbars' must be a table")
|
||||
else
|
||||
for scrollbarName, scrollbarDef in pairs(theme.scrollbars) do
|
||||
if type(scrollbarDef) == "table" then
|
||||
-- Check if it has bar/frame subcomponents
|
||||
if scrollbarDef.bar or scrollbarDef.frame then
|
||||
-- Validate bar subcomponent
|
||||
if scrollbarDef.bar ~= nil then
|
||||
if type(scrollbarDef.bar) ~= "string" and type(scrollbarDef.bar) ~= "table" then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' bar must be a string or table")
|
||||
end
|
||||
end
|
||||
-- Validate frame subcomponent
|
||||
if scrollbarDef.frame ~= nil then
|
||||
if type(scrollbarDef.frame) ~= "string" and type(scrollbarDef.frame) ~= "table" then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' frame must be a string or table")
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Validate as a single ThemeComponent
|
||||
-- Validate atlas if present
|
||||
if scrollbarDef.atlas ~= nil and type(scrollbarDef.atlas) ~= "string" then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' atlas must be a string")
|
||||
end
|
||||
|
||||
-- Validate insets if present
|
||||
if scrollbarDef.insets ~= nil then
|
||||
if type(scrollbarDef.insets) ~= "table" then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' insets must be a table")
|
||||
else
|
||||
for _, side in ipairs({ "left", "top", "right", "bottom" }) do
|
||||
if scrollbarDef.insets[side] == nil then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' insets must have '" .. side .. "' field")
|
||||
elseif type(scrollbarDef.insets[side]) ~= "number" then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' insets." .. side .. " must be a number")
|
||||
elseif scrollbarDef.insets[side] < 0 then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' insets." .. side .. " must be non-negative")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Validate states if present
|
||||
if scrollbarDef.states ~= nil then
|
||||
if type(scrollbarDef.states) ~= "table" then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' states must be a table")
|
||||
else
|
||||
for stateName, stateComponent in pairs(scrollbarDef.states) do
|
||||
if type(stateComponent) ~= "table" then
|
||||
table.insert(errors, "Scrollbar '" .. scrollbarName .. "' state '" .. stateName .. "' must be a table")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- contentAutoSizingMultiplier validation (optional)
|
||||
if theme.contentAutoSizingMultiplier ~= nil then
|
||||
if type(theme.contentAutoSizingMultiplier) ~= "table" then
|
||||
@@ -1254,6 +1466,7 @@ function Theme.validateTheme(theme, options)
|
||||
name = true,
|
||||
atlas = true,
|
||||
components = true,
|
||||
scrollbars = true,
|
||||
colors = true,
|
||||
fonts = true,
|
||||
contentAutoSizingMultiplier = true,
|
||||
@@ -1330,6 +1543,11 @@ function Theme.sanitizeTheme(theme)
|
||||
sanitized.components = theme.components
|
||||
end
|
||||
|
||||
-- Sanitize scrollbars (preserve as-is, they're complex like components)
|
||||
if type(theme.scrollbars) == "table" then
|
||||
sanitized.scrollbars = theme.scrollbars
|
||||
end
|
||||
|
||||
-- Sanitize contentAutoSizingMultiplier
|
||||
if type(theme.contentAutoSizingMultiplier) == "table" then
|
||||
sanitized.contentAutoSizingMultiplier = {}
|
||||
|
||||
@@ -121,6 +121,8 @@ local AnimationProps = {}
|
||||
---@field scrollbarRadius number? -- Corner radius for scrollbar (default: 6)
|
||||
---@field scrollbarPadding number? -- Padding between scrollbar and edge (default: 2)
|
||||
---@field scrollSpeed number? -- Pixels per wheel notch (default: 20)
|
||||
---@field smoothScrollEnabled boolean? -- Enable smooth scrolling animation for wheel events (default: false)
|
||||
---@field scrollBarStyle string? -- Scrollbar style name from theme (selects from theme.scrollbars, default: uses first scrollbar or fallback rendering)
|
||||
---@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
|
||||
|
||||
Reference in New Issue
Block a user