event handler and scrollmanager
This commit is contained in:
47
FlexLove.lua
47
FlexLove.lua
@@ -202,15 +202,14 @@ function Gui.endFrame()
|
||||
local state = StateManager.getState(element.id, {})
|
||||
|
||||
-- Save stateful properties back to persistent state
|
||||
state._pressed = element._pressed
|
||||
state._lastClickTime = element._lastClickTime
|
||||
state._lastClickButton = element._lastClickButton
|
||||
state._clickCount = element._clickCount
|
||||
state._dragStartX = element._dragStartX
|
||||
state._dragStartY = element._dragStartY
|
||||
state._lastMouseX = element._lastMouseX
|
||||
state._lastMouseY = element._lastMouseY
|
||||
state._hovered = element._hovered
|
||||
-- Get event handler state
|
||||
if element._eventHandler then
|
||||
local eventState = element._eventHandler:getState()
|
||||
for k, v in pairs(eventState) do
|
||||
state[k] = v
|
||||
end
|
||||
end
|
||||
state._focused = element._focused
|
||||
state._focused = element._focused
|
||||
state._cursorPosition = element._cursorPosition
|
||||
state._selectionStart = element._selectionStart
|
||||
@@ -619,16 +618,11 @@ function Gui.new(props)
|
||||
local element = Element.new(props)
|
||||
|
||||
-- Bind persistent state to element (ImmediateModeState)
|
||||
-- Copy stateful properties from persistent state
|
||||
element._pressed = state._pressed or {}
|
||||
element._lastClickTime = state._lastClickTime
|
||||
element._lastClickButton = state._lastClickButton
|
||||
element._clickCount = state._clickCount or 0
|
||||
element._dragStartX = state._dragStartX or element._dragStartX or {}
|
||||
element._dragStartY = state._dragStartY or element._dragStartY or {}
|
||||
element._lastMouseX = state._lastMouseX or element._lastMouseX or {}
|
||||
element._lastMouseY = state._lastMouseY or element._lastMouseY or {}
|
||||
element._hovered = state._hovered
|
||||
-- Restore event handler state
|
||||
if element._eventHandler then
|
||||
element._eventHandler:setState(state)
|
||||
end
|
||||
element._focused = state._focused
|
||||
element._focused = state._focused
|
||||
element._cursorPosition = state._cursorPosition
|
||||
element._selectionStart = state._selectionStart
|
||||
@@ -639,6 +633,14 @@ function Gui.new(props)
|
||||
element._scrollbarDragging = state._scrollbarDragging ~= nil and state._scrollbarDragging or false
|
||||
element._hoveredScrollbar = state._hoveredScrollbar
|
||||
element._scrollbarDragOffset = state._scrollbarDragOffset ~= nil and state._scrollbarDragOffset or 0
|
||||
|
||||
-- Sync scrollbar drag state to ScrollManager if it exists
|
||||
if element._scrollManager then
|
||||
element._scrollManager._scrollbarDragging = element._scrollbarDragging
|
||||
element._scrollManager._hoveredScrollbar = element._hoveredScrollbar
|
||||
element._scrollManager._scrollbarDragOffset = element._scrollbarDragOffset
|
||||
end
|
||||
|
||||
-- Restore cursor blink state
|
||||
element._cursorBlinkTimer = state._cursorBlinkTimer or element._cursorBlinkTimer or 0
|
||||
if state._cursorVisible ~= nil then
|
||||
@@ -660,6 +662,13 @@ function Gui.new(props)
|
||||
element._hoveredScrollbar = state.hoveredScrollbar
|
||||
element._scrollbarDragOffset = state.scrollbarDragOffset or 0
|
||||
|
||||
-- Sync interactive scroll state to ScrollManager if it exists
|
||||
if element._scrollManager then
|
||||
element._scrollManager._scrollbarHoveredVertical = element._scrollbarHoveredVertical or false
|
||||
element._scrollManager._scrollbarHoveredHorizontal = element._scrollbarHoveredHorizontal or false
|
||||
-- Note: drag state already synced earlier (lines 633-643)
|
||||
end
|
||||
|
||||
-- Set initial theme state based on StateManager state
|
||||
-- This will be updated in Element:update() but we need an initial value
|
||||
if element.themeComponent then
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
403
modules/EventHandler.lua
Normal file
403
modules/EventHandler.lua
Normal file
@@ -0,0 +1,403 @@
|
||||
-- ====================
|
||||
-- Event Handler Module
|
||||
-- ====================
|
||||
-- Handles all user input events (mouse, keyboard, touch) for UI elements
|
||||
-- Manages event state, click detection, drag tracking, hover, and focus
|
||||
|
||||
local modulePath = (...):match("(.-)[^%.]+$")
|
||||
local function req(name)
|
||||
return require(modulePath .. name)
|
||||
end
|
||||
|
||||
local InputEvent = req("InputEvent")
|
||||
local GuiState = req("GuiState")
|
||||
|
||||
-- Get keyboard modifiers helper
|
||||
local function getModifiers()
|
||||
return {
|
||||
shift = love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift"),
|
||||
ctrl = love.keyboard.isDown("lctrl") or love.keyboard.isDown("rctrl"),
|
||||
alt = love.keyboard.isDown("lalt") or love.keyboard.isDown("ralt"),
|
||||
meta = love.keyboard.isDown("lgui") or love.keyboard.isDown("rgui"),
|
||||
}
|
||||
end
|
||||
|
||||
---@class EventHandler
|
||||
local EventHandler = {}
|
||||
EventHandler.__index = EventHandler
|
||||
|
||||
--- Create a new EventHandler instance
|
||||
---@param config table Configuration options
|
||||
---@return EventHandler
|
||||
function EventHandler.new(config)
|
||||
config = config or {}
|
||||
|
||||
local self = setmetatable({}, EventHandler)
|
||||
|
||||
-- Event callback
|
||||
self.onEvent = config.onEvent
|
||||
|
||||
-- Mouse button state tracking {button -> boolean}
|
||||
self._pressed = config._pressed or {}
|
||||
|
||||
-- Click detection state
|
||||
self._lastClickTime = config._lastClickTime
|
||||
self._lastClickButton = config._lastClickButton
|
||||
self._clickCount = config._clickCount or 0
|
||||
|
||||
-- Drag tracking per button {button -> position}
|
||||
self._dragStartX = config._dragStartX or {}
|
||||
self._dragStartY = config._dragStartY or {}
|
||||
self._lastMouseX = config._lastMouseX or {}
|
||||
self._lastMouseY = config._lastMouseY or {}
|
||||
|
||||
-- Touch state tracking {touchId -> boolean}
|
||||
self._touchPressed = config._touchPressed or {}
|
||||
|
||||
-- Hover state
|
||||
self._hovered = config._hovered or false
|
||||
|
||||
-- Reference to parent element (set via initialize)
|
||||
self._element = nil
|
||||
|
||||
-- Scrollbar press tracking flag
|
||||
self._scrollbarPressHandled = false
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Initialize EventHandler with parent element reference
|
||||
---@param element Element The parent element
|
||||
function EventHandler:initialize(element)
|
||||
self._element = element
|
||||
end
|
||||
|
||||
--- Get state for persistence (for immediate mode)
|
||||
---@return table State data
|
||||
function EventHandler:getState()
|
||||
return {
|
||||
_pressed = self._pressed,
|
||||
_lastClickTime = self._lastClickTime,
|
||||
_lastClickButton = self._lastClickButton,
|
||||
_clickCount = self._clickCount,
|
||||
_dragStartX = self._dragStartX,
|
||||
_dragStartY = self._dragStartY,
|
||||
_lastMouseX = self._lastMouseX,
|
||||
_lastMouseY = self._lastMouseY,
|
||||
_hovered = self._hovered,
|
||||
}
|
||||
end
|
||||
|
||||
--- Restore state from persistence (for immediate mode)
|
||||
---@param state table State data
|
||||
function EventHandler:setState(state)
|
||||
if not state then return end
|
||||
|
||||
self._pressed = state._pressed or {}
|
||||
self._lastClickTime = state._lastClickTime
|
||||
self._lastClickButton = state._lastClickButton
|
||||
self._clickCount = state._clickCount or 0
|
||||
self._dragStartX = state._dragStartX or {}
|
||||
self._dragStartY = state._dragStartY or {}
|
||||
self._lastMouseX = state._lastMouseX or {}
|
||||
self._lastMouseY = state._lastMouseY or {}
|
||||
self._hovered = state._hovered or false
|
||||
end
|
||||
|
||||
--- Process mouse button events in the update cycle
|
||||
---@param mx number Mouse X position
|
||||
---@param my number Mouse Y position
|
||||
---@param isHovering boolean Whether mouse is over element
|
||||
---@param isActiveElement boolean Whether this is the top element at mouse position
|
||||
function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
|
||||
if not self._element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
|
||||
-- Check if currently dragging (allows drag continuation even if occluded)
|
||||
local isDragging = false
|
||||
for _, button in ipairs({ 1, 2, 3 }) do
|
||||
if self._pressed[button] and love.mouse.isDown(button) then
|
||||
isDragging = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Can only process events if we have handler, element is enabled, and is active or dragging
|
||||
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled and (isActiveElement or isDragging)
|
||||
|
||||
if not canProcessEvents then
|
||||
return
|
||||
end
|
||||
|
||||
-- Process all three mouse buttons
|
||||
local buttons = { 1, 2, 3 } -- left, right, middle
|
||||
|
||||
for _, button in ipairs(buttons) do
|
||||
if isHovering or isDragging then
|
||||
if love.mouse.isDown(button) then
|
||||
-- Button is pressed down
|
||||
if not self._pressed[button] then
|
||||
-- Just pressed - fire press event
|
||||
self:_handleMousePress(mx, my, button)
|
||||
else
|
||||
-- Button is still pressed - check for drag
|
||||
self:_handleMouseDrag(mx, my, button, isHovering)
|
||||
end
|
||||
elseif self._pressed[button] then
|
||||
-- Button was just released - fire click and release events
|
||||
self:_handleMouseRelease(mx, my, button)
|
||||
end
|
||||
else
|
||||
-- Mouse left the element - reset pressed state and drag tracking
|
||||
if self._pressed[button] then
|
||||
self._pressed[button] = false
|
||||
self._dragStartX[button] = nil
|
||||
self._dragStartY[button] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle mouse button press
|
||||
---@param mx number Mouse X position
|
||||
---@param my number Mouse Y position
|
||||
---@param button number Mouse button (1=left, 2=right, 3=middle)
|
||||
function EventHandler:_handleMousePress(mx, my, button)
|
||||
if not self._element then return end
|
||||
|
||||
local element = self._element
|
||||
|
||||
-- Check if press is on scrollbar first (skip if already handled)
|
||||
if button == 1 and not self._scrollbarPressHandled and element._handleScrollbarPress then
|
||||
if element:_handleScrollbarPress(mx, my, button) then
|
||||
-- Scrollbar consumed the event, mark as pressed to prevent onEvent
|
||||
self._pressed[button] = true
|
||||
self._scrollbarPressHandled = true
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Fire press event
|
||||
if self.onEvent then
|
||||
local modifiers = getModifiers()
|
||||
local pressEvent = InputEvent.new({
|
||||
type = "press",
|
||||
button = button,
|
||||
x = mx,
|
||||
y = my,
|
||||
modifiers = modifiers,
|
||||
clickCount = 1,
|
||||
})
|
||||
self.onEvent(element, pressEvent)
|
||||
end
|
||||
|
||||
self._pressed[button] = true
|
||||
|
||||
-- Set mouse down position for text selection on left click
|
||||
if button == 1 and element._textEditor then
|
||||
element._mouseDownPosition = element._textEditor:mouseToTextPosition(mx, my)
|
||||
element._textDragOccurred = false -- Reset drag flag on press
|
||||
end
|
||||
|
||||
-- Record drag start position per button
|
||||
self._dragStartX[button] = mx
|
||||
self._dragStartY[button] = my
|
||||
self._lastMouseX[button] = mx
|
||||
self._lastMouseY[button] = my
|
||||
end
|
||||
|
||||
--- Handle mouse drag (while button is pressed and mouse moves)
|
||||
---@param mx number Mouse X position
|
||||
---@param my number Mouse Y position
|
||||
---@param button number Mouse button
|
||||
---@param isHovering boolean Whether mouse is over element
|
||||
function EventHandler:_handleMouseDrag(mx, my, button, isHovering)
|
||||
if not self._element then return end
|
||||
|
||||
local element = self._element
|
||||
|
||||
local lastX = self._lastMouseX[button] or mx
|
||||
local lastY = self._lastMouseY[button] or my
|
||||
|
||||
if lastX ~= mx or lastY ~= my then
|
||||
-- Mouse has moved - fire drag event only if still hovering
|
||||
if self.onEvent and isHovering then
|
||||
local modifiers = getModifiers()
|
||||
local dx = mx - self._dragStartX[button]
|
||||
local dy = my - self._dragStartY[button]
|
||||
|
||||
local dragEvent = InputEvent.new({
|
||||
type = "drag",
|
||||
button = button,
|
||||
x = mx,
|
||||
y = my,
|
||||
dx = dx,
|
||||
dy = dy,
|
||||
modifiers = modifiers,
|
||||
clickCount = 1,
|
||||
})
|
||||
self.onEvent(element, dragEvent)
|
||||
end
|
||||
|
||||
-- Handle text selection drag for editable elements
|
||||
if button == 1 and element.editable and element._focused and element._handleTextDrag then
|
||||
element:_handleTextDrag(mx, my)
|
||||
end
|
||||
|
||||
-- Update last known position for this button
|
||||
self._lastMouseX[button] = mx
|
||||
self._lastMouseY[button] = my
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle mouse button release
|
||||
---@param mx number Mouse X position
|
||||
---@param my number Mouse Y position
|
||||
---@param button number Mouse button
|
||||
function EventHandler:_handleMouseRelease(mx, my, button)
|
||||
if not self._element then return end
|
||||
|
||||
local element = self._element
|
||||
|
||||
local currentTime = love.timer.getTime()
|
||||
local modifiers = getModifiers()
|
||||
|
||||
-- Determine click count (double-click detection)
|
||||
local clickCount = 1
|
||||
local doubleClickThreshold = 0.3 -- 300ms for double-click
|
||||
|
||||
if self._lastClickTime and self._lastClickButton == button and (currentTime - self._lastClickTime) < doubleClickThreshold then
|
||||
clickCount = self._clickCount + 1
|
||||
else
|
||||
clickCount = 1
|
||||
end
|
||||
|
||||
self._clickCount = clickCount
|
||||
self._lastClickTime = currentTime
|
||||
self._lastClickButton = button
|
||||
|
||||
-- Determine event type based on button
|
||||
local eventType = "click"
|
||||
if button == 2 then
|
||||
eventType = "rightclick"
|
||||
elseif button == 3 then
|
||||
eventType = "middleclick"
|
||||
end
|
||||
|
||||
-- Fire click event
|
||||
if self.onEvent then
|
||||
local clickEvent = InputEvent.new({
|
||||
type = eventType,
|
||||
button = button,
|
||||
x = mx,
|
||||
y = my,
|
||||
modifiers = modifiers,
|
||||
clickCount = clickCount,
|
||||
})
|
||||
self.onEvent(element, clickEvent)
|
||||
end
|
||||
|
||||
self._pressed[button] = false
|
||||
|
||||
-- Clean up drag tracking
|
||||
self._dragStartX[button] = nil
|
||||
self._dragStartY[button] = nil
|
||||
|
||||
-- Clean up text selection drag tracking
|
||||
if button == 1 then
|
||||
element._mouseDownPosition = nil
|
||||
end
|
||||
|
||||
-- Focus editable elements on left click
|
||||
if button == 1 and element.editable then
|
||||
-- Only focus if not already focused (to avoid moving cursor to end)
|
||||
local wasFocused = element:isFocused()
|
||||
if not wasFocused then
|
||||
element:focus()
|
||||
end
|
||||
|
||||
-- Handle text click for cursor positioning and word selection
|
||||
-- Only process click if no text drag occurred (to preserve drag selection)
|
||||
if element._handleTextClick and not element._textDragOccurred then
|
||||
element:_handleTextClick(mx, my, clickCount)
|
||||
end
|
||||
|
||||
-- Reset drag flag after release
|
||||
element._textDragOccurred = false
|
||||
end
|
||||
|
||||
-- Fire release event
|
||||
if self.onEvent then
|
||||
local releaseEvent = InputEvent.new({
|
||||
type = "release",
|
||||
button = button,
|
||||
x = mx,
|
||||
y = my,
|
||||
modifiers = modifiers,
|
||||
clickCount = clickCount,
|
||||
})
|
||||
self.onEvent(element, releaseEvent)
|
||||
end
|
||||
end
|
||||
|
||||
--- Process touch events in the update cycle
|
||||
function EventHandler:processTouchEvents()
|
||||
if not self._element or not self.onEvent then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
|
||||
local bx = element.x
|
||||
local by = element.y
|
||||
local bw = element._borderBoxWidth or (element.width + element.padding.left + element.padding.right)
|
||||
local bh = element._borderBoxHeight or (element.height + element.padding.top + element.padding.bottom)
|
||||
|
||||
local touches = love.touch.getTouches()
|
||||
for _, id in ipairs(touches) do
|
||||
local tx, ty = love.touch.getPosition(id)
|
||||
if tx >= bx and tx <= bx + bw and ty >= by and ty <= by + bh then
|
||||
self._touchPressed[id] = true
|
||||
elseif self._touchPressed[id] then
|
||||
-- Create touch event (treat as left click)
|
||||
local touchEvent = InputEvent.new({
|
||||
type = "click",
|
||||
button = 1,
|
||||
x = tx,
|
||||
y = ty,
|
||||
modifiers = getModifiers(),
|
||||
clickCount = 1,
|
||||
})
|
||||
self.onEvent(element, touchEvent)
|
||||
self._touchPressed[id] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Reset scrollbar press flag (called each frame)
|
||||
function EventHandler:resetScrollbarPressFlag()
|
||||
self._scrollbarPressHandled = false
|
||||
end
|
||||
|
||||
--- Check if any mouse button is pressed
|
||||
---@return boolean True if any button is pressed
|
||||
function EventHandler:isAnyButtonPressed()
|
||||
for _, pressed in pairs(self._pressed) do
|
||||
if pressed then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
--- Check if a specific button is pressed
|
||||
---@param button number Mouse button (1=left, 2=right, 3=middle)
|
||||
---@return boolean True if button is pressed
|
||||
function EventHandler:isButtonPressed(button)
|
||||
return self._pressed[button] == true
|
||||
end
|
||||
|
||||
return EventHandler
|
||||
661
modules/ScrollManager.lua
Normal file
661
modules/ScrollManager.lua
Normal file
@@ -0,0 +1,661 @@
|
||||
--- ScrollManager.lua
|
||||
--- Handles scrolling, overflow detection, and scrollbar rendering/interaction for Elements
|
||||
--- Extracted from Element.lua as part of element-refactor-modularization task 05
|
||||
|
||||
-- Setup module path for relative requires
|
||||
local modulePath = (...):match("(.-)[^%.]+$")
|
||||
local function req(name)
|
||||
return require(modulePath .. name)
|
||||
end
|
||||
|
||||
local Color = req("Color")
|
||||
|
||||
---@class ScrollManager
|
||||
---@field overflow string -- "visible"|"hidden"|"auto"|"scroll"
|
||||
---@field overflowX string? -- X-axis specific overflow (overrides overflow)
|
||||
---@field overflowY string? -- Y-axis specific overflow (overrides overflow)
|
||||
---@field scrollbarWidth number -- Width/height of scrollbar track
|
||||
---@field scrollbarColor Color -- Scrollbar thumb color
|
||||
---@field scrollbarTrackColor Color -- Scrollbar track background color
|
||||
---@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 hideScrollbars table -- {vertical: boolean, horizontal: boolean}
|
||||
---@field _element table? -- Reference to parent Element (set via initialize)
|
||||
---@field _overflowX boolean -- True if content overflows horizontally
|
||||
---@field _overflowY boolean -- True if content overflows vertically
|
||||
---@field _contentWidth number -- Total content width (including overflow)
|
||||
---@field _contentHeight number -- Total content height (including overflow)
|
||||
---@field _scrollX number -- Current horizontal scroll position
|
||||
---@field _scrollY number -- Current vertical scroll position
|
||||
---@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
|
||||
---@field _scrollbarHoveredHorizontal boolean -- True if mouse is over horizontal scrollbar
|
||||
---@field _scrollbarDragging boolean -- True if currently dragging a scrollbar
|
||||
---@field _hoveredScrollbar string? -- "vertical" or "horizontal" when dragging
|
||||
---@field _scrollbarDragOffset number -- Offset from thumb top when drag started
|
||||
---@field _scrollbarPressHandled boolean -- Track if scrollbar press was handled this frame
|
||||
local ScrollManager = {}
|
||||
ScrollManager.__index = ScrollManager
|
||||
|
||||
--- Create a new ScrollManager instance
|
||||
---@param config table Configuration options
|
||||
---@return ScrollManager
|
||||
function ScrollManager.new(config)
|
||||
local self = setmetatable({}, ScrollManager)
|
||||
|
||||
-- Configuration
|
||||
self.overflow = config.overflow or "hidden"
|
||||
self.overflowX = config.overflowX
|
||||
self.overflowY = config.overflowY
|
||||
|
||||
-- Scrollbar appearance
|
||||
self.scrollbarWidth = config.scrollbarWidth or 12
|
||||
self.scrollbarColor = config.scrollbarColor or Color.new(0.5, 0.5, 0.5, 0.8)
|
||||
self.scrollbarTrackColor = config.scrollbarTrackColor or Color.new(0.2, 0.2, 0.2, 0.5)
|
||||
self.scrollbarRadius = config.scrollbarRadius or 6
|
||||
self.scrollbarPadding = config.scrollbarPadding or 2
|
||||
self.scrollSpeed = config.scrollSpeed or 20
|
||||
|
||||
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
||||
if config.hideScrollbars ~= nil then
|
||||
if type(config.hideScrollbars) == "boolean" then
|
||||
self.hideScrollbars = { vertical = config.hideScrollbars, horizontal = config.hideScrollbars }
|
||||
elseif type(config.hideScrollbars) == "table" then
|
||||
self.hideScrollbars = {
|
||||
vertical = config.hideScrollbars.vertical ~= nil and config.hideScrollbars.vertical or false,
|
||||
horizontal = config.hideScrollbars.horizontal ~= nil and config.hideScrollbars.horizontal or false,
|
||||
}
|
||||
else
|
||||
self.hideScrollbars = { vertical = false, horizontal = false }
|
||||
end
|
||||
else
|
||||
self.hideScrollbars = { vertical = false, horizontal = false }
|
||||
end
|
||||
|
||||
-- Internal overflow state
|
||||
self._overflowX = false
|
||||
self._overflowY = false
|
||||
self._contentWidth = 0
|
||||
self._contentHeight = 0
|
||||
|
||||
-- Scroll state (can be restored from config in immediate mode)
|
||||
self._scrollX = config._scrollX or 0
|
||||
self._scrollY = config._scrollY or 0
|
||||
self._maxScrollX = 0
|
||||
self._maxScrollY = 0
|
||||
|
||||
-- Scrollbar interaction state
|
||||
self._scrollbarHoveredVertical = false
|
||||
self._scrollbarHoveredHorizontal = false
|
||||
self._scrollbarDragging = false
|
||||
self._hoveredScrollbar = nil -- "vertical" or "horizontal"
|
||||
self._scrollbarDragOffset = 0
|
||||
self._scrollbarPressHandled = false
|
||||
|
||||
-- Element reference (set via initialize)
|
||||
self._element = nil
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Initialize with parent element reference
|
||||
---@param element table The parent Element instance
|
||||
function ScrollManager:initialize(element)
|
||||
self._element = element
|
||||
end
|
||||
|
||||
--- Detect if content overflows container bounds
|
||||
function ScrollManager:detectOverflow()
|
||||
if not self._element then
|
||||
error("ScrollManager:detectOverflow() called before initialize()")
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
|
||||
-- Reset overflow state
|
||||
self._overflowX = false
|
||||
self._overflowY = false
|
||||
self._contentWidth = element.width
|
||||
self._contentHeight = element.height
|
||||
|
||||
-- Skip detection if overflow is visible (no clipping needed)
|
||||
local overflowX = self.overflowX or self.overflow
|
||||
local overflowY = self.overflowY or self.overflow
|
||||
if overflowX == "visible" and overflowY == "visible" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Calculate content bounds based on children
|
||||
if #element.children == 0 then
|
||||
return -- No children, no overflow
|
||||
end
|
||||
|
||||
local minX, minY = 0, 0
|
||||
local maxX, maxY = 0, 0
|
||||
|
||||
-- Content area starts after padding
|
||||
local contentX = element.x + element.padding.left
|
||||
local contentY = element.y + element.padding.top
|
||||
|
||||
for _, child in ipairs(element.children) do
|
||||
-- Skip absolutely positioned children (they don't contribute to overflow)
|
||||
if not child._explicitlyAbsolute then
|
||||
-- Calculate child position relative to content area
|
||||
local childLeft = child.x - contentX
|
||||
local childTop = child.y - contentY
|
||||
local childRight = childLeft + child:getBorderBoxWidth() + child.margin.right
|
||||
local childBottom = childTop + child:getBorderBoxHeight() + child.margin.bottom
|
||||
|
||||
maxX = math.max(maxX, childRight)
|
||||
maxY = math.max(maxY, childBottom)
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate content dimensions
|
||||
self._contentWidth = maxX
|
||||
self._contentHeight = maxY
|
||||
|
||||
-- Detect overflow
|
||||
local containerWidth = element.width
|
||||
local containerHeight = element.height
|
||||
|
||||
self._overflowX = self._contentWidth > containerWidth
|
||||
self._overflowY = self._contentHeight > containerHeight
|
||||
|
||||
-- Calculate maximum scroll bounds
|
||||
self._maxScrollX = math.max(0, self._contentWidth - containerWidth)
|
||||
self._maxScrollY = math.max(0, self._contentHeight - containerHeight)
|
||||
|
||||
-- Clamp current scroll position to new bounds
|
||||
self._scrollX = math.max(0, math.min(self._scrollX, self._maxScrollX))
|
||||
self._scrollY = math.max(0, math.min(self._scrollY, self._maxScrollY))
|
||||
end
|
||||
|
||||
--- Set scroll position with bounds clamping
|
||||
---@param x number? -- X scroll position (nil to keep current)
|
||||
---@param y number? -- Y scroll position (nil to keep current)
|
||||
function ScrollManager:setScroll(x, y)
|
||||
if x ~= nil then
|
||||
self._scrollX = math.max(0, math.min(x, self._maxScrollX))
|
||||
end
|
||||
if y ~= nil then
|
||||
self._scrollY = math.max(0, math.min(y, self._maxScrollY))
|
||||
end
|
||||
end
|
||||
|
||||
--- Get current scroll position
|
||||
---@return number scrollX, number scrollY
|
||||
function ScrollManager:getScroll()
|
||||
return self._scrollX, self._scrollY
|
||||
end
|
||||
|
||||
--- Scroll by delta amount
|
||||
---@param dx number? -- X delta (nil for no change)
|
||||
---@param dy number? -- Y delta (nil for no change)
|
||||
function ScrollManager:scrollBy(dx, dy)
|
||||
if dx then
|
||||
self._scrollX = math.max(0, math.min(self._scrollX + dx, self._maxScrollX))
|
||||
end
|
||||
if dy then
|
||||
self._scrollY = math.max(0, math.min(self._scrollY + dy, self._maxScrollY))
|
||||
end
|
||||
end
|
||||
|
||||
--- Get maximum scroll bounds
|
||||
---@return number maxScrollX, number maxScrollY
|
||||
function ScrollManager:getMaxScroll()
|
||||
return self._maxScrollX, self._maxScrollY
|
||||
end
|
||||
|
||||
--- Get scroll percentage (0-1)
|
||||
---@return number percentX, number percentY
|
||||
function ScrollManager:getScrollPercentage()
|
||||
local percentX = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
|
||||
local percentY = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
|
||||
return percentX, percentY
|
||||
end
|
||||
|
||||
--- Check if element has overflow
|
||||
---@return boolean hasOverflowX, boolean hasOverflowY
|
||||
function ScrollManager:hasOverflow()
|
||||
return self._overflowX, self._overflowY
|
||||
end
|
||||
|
||||
--- Get content dimensions (including overflow)
|
||||
---@return number contentWidth, number contentHeight
|
||||
function ScrollManager:getContentSize()
|
||||
return self._contentWidth, self._contentHeight
|
||||
end
|
||||
|
||||
--- Calculate scrollbar dimensions and positions
|
||||
---@return table -- {vertical: {visible, trackHeight, thumbHeight, thumbY}, horizontal: {visible, trackWidth, thumbWidth, thumbX}}
|
||||
function ScrollManager:calculateScrollbarDimensions()
|
||||
if not self._element then
|
||||
error("ScrollManager:calculateScrollbarDimensions() called before initialize()")
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
local result = {
|
||||
vertical = { visible = false, trackHeight = 0, thumbHeight = 0, thumbY = 0 },
|
||||
horizontal = { visible = false, trackWidth = 0, thumbWidth = 0, thumbX = 0 },
|
||||
}
|
||||
|
||||
local overflowX = self.overflowX or self.overflow
|
||||
local overflowY = self.overflowY or self.overflow
|
||||
|
||||
-- Vertical scrollbar
|
||||
-- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
|
||||
if overflowY == "scroll" then
|
||||
-- Always show scrollbar for "scroll" mode
|
||||
result.vertical.visible = true
|
||||
result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
|
||||
|
||||
if self._overflowY then
|
||||
-- Content overflows, calculate proper thumb size
|
||||
local contentRatio = element.height / math.max(self._contentHeight, element.height)
|
||||
result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
|
||||
|
||||
-- Calculate thumb position based on scroll ratio
|
||||
local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
|
||||
local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
|
||||
result.vertical.thumbY = maxThumbY * scrollRatio
|
||||
else
|
||||
-- No overflow, thumb fills entire track
|
||||
result.vertical.thumbHeight = result.vertical.trackHeight
|
||||
result.vertical.thumbY = 0
|
||||
end
|
||||
elseif self._overflowY and overflowY == "auto" then
|
||||
-- Only show scrollbar when content actually overflows
|
||||
result.vertical.visible = true
|
||||
result.vertical.trackHeight = element.height - (self.scrollbarPadding * 2)
|
||||
|
||||
-- Calculate thumb height based on content ratio
|
||||
local contentRatio = element.height / math.max(self._contentHeight, element.height)
|
||||
result.vertical.thumbHeight = math.max(20, result.vertical.trackHeight * contentRatio)
|
||||
|
||||
-- Calculate thumb position based on scroll ratio
|
||||
local scrollRatio = self._maxScrollY > 0 and (self._scrollY / self._maxScrollY) or 0
|
||||
local maxThumbY = result.vertical.trackHeight - result.vertical.thumbHeight
|
||||
result.vertical.thumbY = maxThumbY * scrollRatio
|
||||
end
|
||||
|
||||
-- Horizontal scrollbar
|
||||
-- Note: overflow="scroll" always shows scrollbar; overflow="auto" only when content overflows
|
||||
if overflowX == "scroll" then
|
||||
-- Always show scrollbar for "scroll" mode
|
||||
result.horizontal.visible = true
|
||||
result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
|
||||
|
||||
if self._overflowX then
|
||||
-- Content overflows, calculate proper thumb size
|
||||
local contentRatio = element.width / math.max(self._contentWidth, element.width)
|
||||
result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
|
||||
|
||||
-- Calculate thumb position based on scroll ratio
|
||||
local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
|
||||
local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
|
||||
result.horizontal.thumbX = maxThumbX * scrollRatio
|
||||
else
|
||||
-- No overflow, thumb fills entire track
|
||||
result.horizontal.thumbWidth = result.horizontal.trackWidth
|
||||
result.horizontal.thumbX = 0
|
||||
end
|
||||
elseif self._overflowX and overflowX == "auto" then
|
||||
-- Only show scrollbar when content actually overflows
|
||||
result.horizontal.visible = true
|
||||
result.horizontal.trackWidth = element.width - (self.scrollbarPadding * 2)
|
||||
|
||||
-- Calculate thumb width based on content ratio
|
||||
local contentRatio = element.width / math.max(self._contentWidth, element.width)
|
||||
result.horizontal.thumbWidth = math.max(20, result.horizontal.trackWidth * contentRatio)
|
||||
|
||||
-- Calculate thumb position based on scroll ratio
|
||||
local scrollRatio = self._maxScrollX > 0 and (self._scrollX / self._maxScrollX) or 0
|
||||
local maxThumbX = result.horizontal.trackWidth - result.horizontal.thumbWidth
|
||||
result.horizontal.thumbX = maxThumbX * scrollRatio
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--- Get scrollbar at mouse position
|
||||
---@param mouseX number
|
||||
---@param mouseY number
|
||||
---@return table|nil -- {component: "vertical"|"horizontal", region: "thumb"|"track"}
|
||||
function ScrollManager:getScrollbarAtPosition(mouseX, mouseY)
|
||||
if not self._element then
|
||||
error("ScrollManager:getScrollbarAtPosition() called before initialize()")
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
local overflowX = self.overflowX or self.overflow
|
||||
local overflowY = self.overflowY or self.overflow
|
||||
|
||||
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
|
||||
return nil
|
||||
end
|
||||
|
||||
local dims = self:calculateScrollbarDimensions()
|
||||
local x, y = element.x, element.y
|
||||
local w, h = element.width, element.height
|
||||
|
||||
-- Check vertical scrollbar (only if not hidden)
|
||||
if dims.vertical.visible and not self.hideScrollbars.vertical then
|
||||
-- Position scrollbar within content area (x, y is border-box origin)
|
||||
local contentX = x + element.padding.left
|
||||
local contentY = y + element.padding.top
|
||||
local trackX = contentX + w - self.scrollbarWidth - self.scrollbarPadding
|
||||
local trackY = contentY + self.scrollbarPadding
|
||||
local trackW = self.scrollbarWidth
|
||||
local trackH = dims.vertical.trackHeight
|
||||
|
||||
if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
|
||||
-- Check if over thumb
|
||||
local thumbY = trackY + dims.vertical.thumbY
|
||||
local thumbH = dims.vertical.thumbHeight
|
||||
if mouseY >= thumbY and mouseY <= thumbY + thumbH then
|
||||
return { component = "vertical", region = "thumb" }
|
||||
else
|
||||
return { component = "vertical", region = "track" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check horizontal scrollbar (only if not hidden)
|
||||
if dims.horizontal.visible and not self.hideScrollbars.horizontal then
|
||||
-- Position scrollbar within content area (x, y is border-box origin)
|
||||
local contentX = x + element.padding.left
|
||||
local contentY = y + element.padding.top
|
||||
local trackX = contentX + self.scrollbarPadding
|
||||
local trackY = contentY + h - self.scrollbarWidth - self.scrollbarPadding
|
||||
local trackW = dims.horizontal.trackWidth
|
||||
local trackH = self.scrollbarWidth
|
||||
|
||||
if mouseX >= trackX and mouseX <= trackX + trackW and mouseY >= trackY and mouseY <= trackY + trackH then
|
||||
-- Check if over thumb
|
||||
local thumbX = trackX + dims.horizontal.thumbX
|
||||
local thumbW = dims.horizontal.thumbWidth
|
||||
if mouseX >= thumbX and mouseX <= thumbX + thumbW then
|
||||
return { component = "horizontal", region = "thumb" }
|
||||
else
|
||||
return { component = "horizontal", region = "track" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Handle scrollbar mouse press
|
||||
---@param mouseX number
|
||||
---@param mouseY number
|
||||
---@param button number
|
||||
---@return boolean -- True if event was consumed
|
||||
function ScrollManager:handleMousePress(mouseX, mouseY, button)
|
||||
if not self._element then
|
||||
error("ScrollManager:handleMousePress() called before initialize()")
|
||||
end
|
||||
|
||||
if button ~= 1 then
|
||||
return false
|
||||
end -- Only left click
|
||||
|
||||
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
||||
if not scrollbar then
|
||||
return false
|
||||
end
|
||||
|
||||
if scrollbar.region == "thumb" then
|
||||
-- Start dragging thumb
|
||||
self._scrollbarDragging = true
|
||||
self._hoveredScrollbar = scrollbar.component
|
||||
local dims = self:calculateScrollbarDimensions()
|
||||
local element = self._element
|
||||
|
||||
if scrollbar.component == "vertical" then
|
||||
local contentY = element.y + element.padding.top
|
||||
local trackY = contentY + self.scrollbarPadding
|
||||
local thumbY = trackY + dims.vertical.thumbY
|
||||
self._scrollbarDragOffset = mouseY - thumbY
|
||||
elseif scrollbar.component == "horizontal" then
|
||||
local contentX = element.x + element.padding.left
|
||||
local trackX = contentX + self.scrollbarPadding
|
||||
local thumbX = trackX + dims.horizontal.thumbX
|
||||
self._scrollbarDragOffset = mouseX - thumbX
|
||||
end
|
||||
|
||||
return true -- Event consumed
|
||||
elseif scrollbar.region == "track" then
|
||||
-- Click on track - jump to position
|
||||
self:_scrollToTrackPosition(mouseX, mouseY, scrollbar.component)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Handle scrollbar drag
|
||||
---@param mouseX number
|
||||
---@param mouseY number
|
||||
---@return boolean -- True if event was consumed
|
||||
function ScrollManager:handleMouseMove(mouseX, mouseY)
|
||||
if not self._element then
|
||||
return false
|
||||
end
|
||||
|
||||
if not self._scrollbarDragging then
|
||||
return false
|
||||
end
|
||||
|
||||
local dims = self:calculateScrollbarDimensions()
|
||||
local element = self._element
|
||||
|
||||
if self._hoveredScrollbar == "vertical" then
|
||||
local contentY = element.y + element.padding.top
|
||||
local trackY = contentY + self.scrollbarPadding
|
||||
local trackH = dims.vertical.trackHeight
|
||||
local thumbH = dims.vertical.thumbHeight
|
||||
|
||||
-- Calculate new thumb position
|
||||
local newThumbY = mouseY - self._scrollbarDragOffset - trackY
|
||||
newThumbY = math.max(0, math.min(newThumbY, trackH - thumbH))
|
||||
|
||||
-- Convert thumb position to scroll position
|
||||
local scrollRatio = (trackH - thumbH) > 0 and (newThumbY / (trackH - thumbH)) or 0
|
||||
local newScrollY = scrollRatio * self._maxScrollY
|
||||
|
||||
self:setScroll(nil, newScrollY)
|
||||
return true
|
||||
elseif self._hoveredScrollbar == "horizontal" then
|
||||
local contentX = element.x + element.padding.left
|
||||
local trackX = contentX + self.scrollbarPadding
|
||||
local trackW = dims.horizontal.trackWidth
|
||||
local thumbW = dims.horizontal.thumbWidth
|
||||
|
||||
-- Calculate new thumb position
|
||||
local newThumbX = mouseX - self._scrollbarDragOffset - trackX
|
||||
newThumbX = math.max(0, math.min(newThumbX, trackW - thumbW))
|
||||
|
||||
-- Convert thumb position to scroll position
|
||||
local scrollRatio = (trackW - thumbW) > 0 and (newThumbX / (trackW - thumbW)) or 0
|
||||
local newScrollX = scrollRatio * self._maxScrollX
|
||||
|
||||
self:setScroll(newScrollX, nil)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Handle scrollbar release
|
||||
---@param button number
|
||||
---@return boolean -- True if event was consumed
|
||||
function ScrollManager:handleMouseRelease(button)
|
||||
if button ~= 1 then
|
||||
return false
|
||||
end
|
||||
|
||||
if self._scrollbarDragging then
|
||||
self._scrollbarDragging = false
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Scroll to track click position (internal helper)
|
||||
---@param mouseX number
|
||||
---@param mouseY number
|
||||
---@param component string -- "vertical" or "horizontal"
|
||||
function ScrollManager:_scrollToTrackPosition(mouseX, mouseY, component)
|
||||
if not self._element then
|
||||
return
|
||||
end
|
||||
|
||||
local dims = self:calculateScrollbarDimensions()
|
||||
local element = self._element
|
||||
|
||||
if component == "vertical" then
|
||||
local contentY = element.y + element.padding.top
|
||||
local trackY = contentY + self.scrollbarPadding
|
||||
local trackH = dims.vertical.trackHeight
|
||||
local thumbH = dims.vertical.thumbHeight
|
||||
|
||||
-- Calculate target thumb position (centered on click)
|
||||
local targetThumbY = mouseY - trackY - (thumbH / 2)
|
||||
targetThumbY = math.max(0, math.min(targetThumbY, trackH - thumbH))
|
||||
|
||||
-- Convert to scroll position
|
||||
local scrollRatio = (trackH - thumbH) > 0 and (targetThumbY / (trackH - thumbH)) or 0
|
||||
local newScrollY = scrollRatio * self._maxScrollY
|
||||
|
||||
self:setScroll(nil, newScrollY)
|
||||
elseif component == "horizontal" then
|
||||
local contentX = element.x + element.padding.left
|
||||
local trackX = contentX + self.scrollbarPadding
|
||||
local trackW = dims.horizontal.trackWidth
|
||||
local thumbW = dims.horizontal.thumbWidth
|
||||
|
||||
-- Calculate target thumb position (centered on click)
|
||||
local targetThumbX = mouseX - trackX - (thumbW / 2)
|
||||
targetThumbX = math.max(0, math.min(targetThumbX, trackW - thumbW))
|
||||
|
||||
-- Convert to scroll position
|
||||
local scrollRatio = (trackW - thumbW) > 0 and (targetThumbX / (trackW - thumbW)) or 0
|
||||
local newScrollX = scrollRatio * self._maxScrollX
|
||||
|
||||
self:setScroll(newScrollX, nil)
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle mouse wheel scrolling
|
||||
---@param x number -- Horizontal scroll amount
|
||||
---@param y number -- Vertical scroll amount
|
||||
---@return boolean -- True if scroll was handled
|
||||
function ScrollManager:handleWheel(x, y)
|
||||
local overflowX = self.overflowX or self.overflow
|
||||
local overflowY = self.overflowY or self.overflow
|
||||
|
||||
if not (overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto") then
|
||||
return false
|
||||
end
|
||||
|
||||
local hasVerticalOverflow = self._overflowY and self._maxScrollY > 0
|
||||
local hasHorizontalOverflow = self._overflowX 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)
|
||||
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)
|
||||
scrolled = true
|
||||
end
|
||||
|
||||
return scrolled
|
||||
end
|
||||
|
||||
--- Update scrollbar hover state based on mouse position
|
||||
---@param mouseX number
|
||||
---@param mouseY number
|
||||
function ScrollManager:updateHoverState(mouseX, mouseY)
|
||||
local scrollbar = self:getScrollbarAtPosition(mouseX, mouseY)
|
||||
|
||||
if scrollbar then
|
||||
if scrollbar.component == "vertical" then
|
||||
self._scrollbarHoveredVertical = true
|
||||
self._scrollbarHoveredHorizontal = false
|
||||
elseif scrollbar.component == "horizontal" then
|
||||
self._scrollbarHoveredVertical = false
|
||||
self._scrollbarHoveredHorizontal = true
|
||||
end
|
||||
else
|
||||
self._scrollbarHoveredVertical = false
|
||||
self._scrollbarHoveredHorizontal = false
|
||||
end
|
||||
end
|
||||
|
||||
--- Reset scrollbar press handled flag (call at start of frame)
|
||||
function ScrollManager:resetScrollbarPressFlag()
|
||||
self._scrollbarPressHandled = false
|
||||
end
|
||||
|
||||
--- Check if scrollbar press was handled this frame
|
||||
---@return boolean
|
||||
function ScrollManager:wasScrollbarPressHandled()
|
||||
return self._scrollbarPressHandled
|
||||
end
|
||||
|
||||
--- Set scrollbar press handled flag
|
||||
function ScrollManager:setScrollbarPressHandled()
|
||||
self._scrollbarPressHandled = true
|
||||
end
|
||||
|
||||
--- Get state for immediate mode persistence
|
||||
---@return table State data
|
||||
function ScrollManager:getState()
|
||||
return {
|
||||
scrollX = self._scrollX,
|
||||
scrollY = self._scrollY,
|
||||
scrollbarDragging = self._scrollbarDragging,
|
||||
hoveredScrollbar = self._hoveredScrollbar,
|
||||
scrollbarDragOffset = self._scrollbarDragOffset,
|
||||
}
|
||||
end
|
||||
|
||||
--- Set state from immediate mode persistence
|
||||
---@param state table State data
|
||||
function ScrollManager:setState(state)
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
|
||||
if state.scrollX then
|
||||
self._scrollX = state.scrollX
|
||||
end
|
||||
if state.scrollY then
|
||||
self._scrollY = state.scrollY
|
||||
end
|
||||
if state.scrollbarDragging ~= nil then
|
||||
self._scrollbarDragging = state.scrollbarDragging
|
||||
end
|
||||
if state.hoveredScrollbar then
|
||||
self._hoveredScrollbar = state.hoveredScrollbar
|
||||
end
|
||||
if state.scrollbarDragOffset then
|
||||
self._scrollbarDragOffset = state.scrollbarDragOffset
|
||||
end
|
||||
end
|
||||
|
||||
return ScrollManager
|
||||
Reference in New Issue
Block a user