better controls over themed scrollbars
This commit is contained in:
@@ -133,6 +133,7 @@
|
||||
---@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 scrollbarKnobOffset number|table? -- Scrollbar knob/handle offset (number or {x, y} or {horizontal, vertical})
|
||||
---@field _overflowX boolean? -- Internal: whether content overflows horizontally
|
||||
---@field _overflowY boolean? -- Internal: whether content overflows vertically
|
||||
---@field _contentWidth number? -- Internal: total content width
|
||||
@@ -1444,6 +1445,7 @@ function Element.new(props)
|
||||
scrollSpeed = props.scrollSpeed,
|
||||
smoothScrollEnabled = props.smoothScrollEnabled,
|
||||
scrollBarStyle = props.scrollBarStyle,
|
||||
scrollbarKnobOffset = props.scrollbarKnobOffset,
|
||||
hideScrollbars = props.hideScrollbars,
|
||||
_scrollX = props._scrollX,
|
||||
_scrollY = props._scrollY,
|
||||
@@ -1460,6 +1462,7 @@ function Element.new(props)
|
||||
self.scrollbarPadding = self._scrollManager.scrollbarPadding
|
||||
self.scrollSpeed = self._scrollManager.scrollSpeed
|
||||
self.scrollBarStyle = self._scrollManager.scrollBarStyle
|
||||
self.scrollbarKnobOffset = self._scrollManager.scrollbarKnobOffset
|
||||
self.hideScrollbars = self._scrollManager.hideScrollbars
|
||||
|
||||
-- Initialize state properties (will be synced from ScrollManager)
|
||||
|
||||
@@ -900,6 +900,20 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
local frameComponent = scrollbarComponent.frame or scrollbarComponent
|
||||
local barComponent = scrollbarComponent.bar or scrollbarComponent
|
||||
|
||||
-- Calculate knob offset (element overrides theme)
|
||||
local knobOffsetX = 0
|
||||
local knobOffsetY = 0
|
||||
|
||||
-- Use element offset if provided, otherwise use theme offset
|
||||
if element.scrollbarKnobOffset then
|
||||
knobOffsetX = element.scrollbarKnobOffset.x or 0
|
||||
knobOffsetY = element.scrollbarKnobOffset.vertical or 0
|
||||
elseif barComponent and barComponent.knobOffset then
|
||||
local themeOffset = self._utils.normalizeOffsetTable(barComponent.knobOffset, 0)
|
||||
knobOffsetX = themeOffset.x
|
||||
knobOffsetY = themeOffset.vertical
|
||||
end
|
||||
|
||||
-- Draw track (frame) if component exists
|
||||
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
||||
self._NinePatch.draw(
|
||||
@@ -917,8 +931,8 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
self._NinePatch.draw(
|
||||
barComponent,
|
||||
barComponent._loadedAtlas,
|
||||
trackX,
|
||||
trackY + dims.vertical.thumbY,
|
||||
trackX + knobOffsetX,
|
||||
trackY + dims.vertical.thumbY + knobOffsetY,
|
||||
element.scrollbarWidth,
|
||||
dims.vertical.thumbHeight
|
||||
)
|
||||
@@ -961,6 +975,20 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
local frameComponent = scrollbarComponent.frame or scrollbarComponent
|
||||
local barComponent = scrollbarComponent.bar or scrollbarComponent
|
||||
|
||||
-- Calculate knob offset (element overrides theme)
|
||||
local knobOffsetX = 0
|
||||
local knobOffsetY = 0
|
||||
|
||||
-- Use element offset if provided, otherwise use theme offset
|
||||
if element.scrollbarKnobOffset then
|
||||
knobOffsetX = element.scrollbarKnobOffset.horizontal or 0
|
||||
knobOffsetY = element.scrollbarKnobOffset.y or 0
|
||||
elseif barComponent and barComponent.knobOffset then
|
||||
local themeOffset = self._utils.normalizeOffsetTable(barComponent.knobOffset, 0)
|
||||
knobOffsetX = themeOffset.horizontal
|
||||
knobOffsetY = themeOffset.y
|
||||
end
|
||||
|
||||
-- Draw track (frame) if component exists
|
||||
if frameComponent and frameComponent._loadedAtlas and frameComponent.regions then
|
||||
self._NinePatch.draw(
|
||||
@@ -978,8 +1006,8 @@ function Renderer:drawScrollbars(element, x, y, w, h, dims)
|
||||
self._NinePatch.draw(
|
||||
barComponent,
|
||||
barComponent._loadedAtlas,
|
||||
trackX + dims.horizontal.thumbX,
|
||||
trackY,
|
||||
trackX + dims.horizontal.thumbX + knobOffsetX,
|
||||
trackY + knobOffsetY,
|
||||
dims.horizontal.thumbWidth,
|
||||
element.scrollbarWidth
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
---@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 scrollbarKnobOffset table -- {x: number, y: number, horizontal: number, vertical: number} -- Offset for scrollbar knob/handle position
|
||||
---@field hideScrollbars table -- {vertical: boolean, horizontal: boolean}
|
||||
---@field touchScrollEnabled boolean -- Enable touch scrolling
|
||||
---@field momentumScrollEnabled boolean -- Enable momentum scrolling
|
||||
@@ -84,6 +85,9 @@ function ScrollManager.new(config, deps)
|
||||
self.scrollSpeed = config.scrollSpeed or 20
|
||||
self.scrollBarStyle = config.scrollBarStyle -- Theme scrollbar style name (nil = use default)
|
||||
|
||||
-- scrollbarKnobOffset can be number or table {x, y} or {horizontal, vertical}
|
||||
self.scrollbarKnobOffset = self._utils.normalizeOffsetTable(config.scrollbarKnobOffset, 0)
|
||||
|
||||
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
||||
self.hideScrollbars = self._utils.normalizeBooleanTable(config.hideScrollbars, false)
|
||||
|
||||
@@ -656,6 +660,7 @@ function ScrollManager:getState()
|
||||
_scrollbarHoveredVertical = self._scrollbarHoveredVertical or false,
|
||||
_scrollbarHoveredHorizontal = self._scrollbarHoveredHorizontal or false,
|
||||
scrollBarStyle = self.scrollBarStyle,
|
||||
scrollbarKnobOffset = self.scrollbarKnobOffset,
|
||||
_overflowX = self._overflowX,
|
||||
_overflowY = self._overflowY,
|
||||
_contentWidth = self._contentWidth,
|
||||
@@ -730,6 +735,10 @@ function ScrollManager:setState(state)
|
||||
self.scrollBarStyle = state.scrollBarStyle
|
||||
end
|
||||
|
||||
if state.scrollbarKnobOffset ~= nil then
|
||||
self.scrollbarKnobOffset = self._utils.normalizeOffsetTable(state.scrollbarKnobOffset, 0)
|
||||
end
|
||||
|
||||
if state._overflowX ~= nil then
|
||||
self._overflowX = state._overflowX
|
||||
end
|
||||
|
||||
@@ -322,6 +322,7 @@ end
|
||||
---@field contentAutoSizingMultiplier {width:number?, height:number?}? -- Optional: multiplier for auto-sized content dimensions
|
||||
---@field scaleCorners number? -- Optional: scale multiplier for non-stretched regions (corners/edges). E.g., 2 = 2x size. Default: nil (no scaling)
|
||||
---@field scalingAlgorithm "nearest"|"bilinear"? -- Optional: scaling algorithm for non-stretched regions. Default: "bilinear"
|
||||
---@field knobOffset number|table? -- Optional: offset for scrollbar knob/handle (number or {x, y} or {horizontal, vertical})
|
||||
---@field _loadedAtlas string|love.Image? -- Internal: cached loaded atlas image
|
||||
---@field _loadedAtlasData love.ImageData? -- Internal: cached loaded atlas ImageData for pixel access
|
||||
---@field _ninePatchData {insets:table, contentPadding:table, stretchX:table, stretchY:table}? -- Internal: parsed 9-patch data with stretch regions and content padding
|
||||
@@ -569,6 +570,10 @@ function Theme.new(definition)
|
||||
if type(scrollbarDef.bar) == "string" then
|
||||
-- Convert string path to ThemeComponent structure
|
||||
local barComponent = { atlas = scrollbarDef.bar }
|
||||
-- Copy knobOffset from parent scrollbarDef if it exists
|
||||
if scrollbarDef.knobOffset then
|
||||
barComponent.knobOffset = scrollbarDef.knobOffset
|
||||
end
|
||||
loadAtlasWithNinePatch(barComponent, scrollbarDef.bar, "for scrollbar '" .. scrollbarName .. ".bar'")
|
||||
if barComponent.insets then
|
||||
createRegionsFromInsets(barComponent, barComponent._loadedAtlas or self.atlas)
|
||||
@@ -576,6 +581,10 @@ function Theme.new(definition)
|
||||
scrollbarDef.bar = barComponent
|
||||
elseif type(scrollbarDef.bar) == "table" then
|
||||
-- Already a ThemeComponent structure, process it
|
||||
-- Copy knobOffset from parent if bar component doesn't have one
|
||||
if scrollbarDef.knobOffset and not scrollbarDef.bar.knobOffset then
|
||||
scrollbarDef.bar.knobOffset = scrollbarDef.knobOffset
|
||||
end
|
||||
if scrollbarDef.bar.atlas and type(scrollbarDef.bar.atlas) == "string" then
|
||||
loadAtlasWithNinePatch(scrollbarDef.bar, scrollbarDef.bar.atlas, "for scrollbar '" .. scrollbarName .. ".bar'")
|
||||
end
|
||||
|
||||
@@ -123,6 +123,7 @@ local AnimationProps = {}
|
||||
---@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 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 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
|
||||
|
||||
@@ -546,6 +546,36 @@ local function normalizeBooleanTable(value, defaultValue)
|
||||
return { vertical = defaultValue, horizontal = defaultValue }
|
||||
end
|
||||
|
||||
--- Normalize an offset value to {x, y} or {horizontal, vertical} format
|
||||
---@param value number|table|nil Input value (number applies to both, table for individual control)
|
||||
---@param defaultValue number Default value if nil (default: 0)
|
||||
---@return table Normalized table with x/y or horizontal/vertical fields
|
||||
local function normalizeOffsetTable(value, defaultValue)
|
||||
defaultValue = defaultValue or 0
|
||||
|
||||
if value == nil then
|
||||
return { x = defaultValue, y = defaultValue, horizontal = defaultValue, vertical = defaultValue }
|
||||
end
|
||||
|
||||
if type(value) == "number" then
|
||||
return { x = value, y = value, horizontal = value, vertical = value }
|
||||
end
|
||||
|
||||
if type(value) == "table" then
|
||||
-- Support both {x, y} and {horizontal, vertical} formats
|
||||
local x = value.x or value.horizontal or defaultValue
|
||||
local y = value.y or value.vertical or defaultValue
|
||||
return {
|
||||
x = x,
|
||||
y = y,
|
||||
horizontal = x,
|
||||
vertical = y,
|
||||
}
|
||||
end
|
||||
|
||||
return { x = defaultValue, y = defaultValue, horizontal = defaultValue, vertical = defaultValue }
|
||||
end
|
||||
|
||||
-- Text sanitization utilities
|
||||
|
||||
--- Sanitize text to prevent security vulnerabilities
|
||||
@@ -1187,6 +1217,7 @@ return {
|
||||
brightenColor = brightenColor,
|
||||
resolveImagePath = resolveImagePath,
|
||||
normalizeBooleanTable = normalizeBooleanTable,
|
||||
normalizeOffsetTable = normalizeOffsetTable,
|
||||
resolveFontPath = resolveFontPath,
|
||||
getFont = getFont,
|
||||
applyContentMultiplier = applyContentMultiplier,
|
||||
|
||||
Reference in New Issue
Block a user