gesture handling
This commit is contained in:
@@ -9,7 +9,10 @@
|
||||
---@field _dragStartY table<number, number>
|
||||
---@field _lastMouseX table<number, number>
|
||||
---@field _lastMouseY table<number, number>
|
||||
---@field _touchPressed table<number, boolean>
|
||||
---@field _touches table<string, table> -- Multi-touch state per touch ID
|
||||
---@field _touchStartPositions table<string, table> -- Touch start positions
|
||||
---@field _lastTouchPositions table<string, table> -- Last touch positions for delta
|
||||
---@field _touchHistory table<string, table> -- Touch position history for gestures (last 5)
|
||||
---@field _hovered boolean
|
||||
---@field _element Element?
|
||||
---@field _scrollbarPressHandled boolean
|
||||
@@ -46,7 +49,11 @@ function EventHandler.new(config, deps)
|
||||
self._lastMouseX = config._lastMouseX or {}
|
||||
self._lastMouseY = config._lastMouseY or {}
|
||||
|
||||
self._touchPressed = config._touchPressed or {}
|
||||
-- Multi-touch tracking
|
||||
self._touches = config._touches or {}
|
||||
self._touchStartPositions = config._touchStartPositions or {}
|
||||
self._lastTouchPositions = config._lastTouchPositions or {}
|
||||
self._touchHistory = config._touchHistory or {}
|
||||
|
||||
self._hovered = config._hovered or false
|
||||
|
||||
@@ -75,6 +82,10 @@ function EventHandler:getState()
|
||||
_dragStartY = self._dragStartY,
|
||||
_lastMouseX = self._lastMouseX,
|
||||
_lastMouseY = self._lastMouseY,
|
||||
_touches = self._touches,
|
||||
_touchStartPositions = self._touchStartPositions,
|
||||
_lastTouchPositions = self._lastTouchPositions,
|
||||
_touchHistory = self._touchHistory,
|
||||
_hovered = self._hovered,
|
||||
}
|
||||
end
|
||||
@@ -94,6 +105,10 @@ function EventHandler:setState(state)
|
||||
self._dragStartY = state._dragStartY or {}
|
||||
self._lastMouseX = state._lastMouseX or {}
|
||||
self._lastMouseY = state._lastMouseY or {}
|
||||
self._touches = state._touches or {}
|
||||
self._touchStartPositions = state._touchStartPositions or {}
|
||||
self._lastTouchPositions = state._lastTouchPositions or {}
|
||||
self._touchHistory = state._touchHistory or {}
|
||||
self._hovered = state._hovered or false
|
||||
end
|
||||
|
||||
@@ -395,36 +410,232 @@ end
|
||||
|
||||
--- Process touch events in the update cycle
|
||||
function EventHandler:processTouchEvents()
|
||||
-- Start performance timing
|
||||
local Performance = package.loaded["modules.Performance"] or package.loaded["libs.modules.Performance"]
|
||||
if Performance and Performance.isEnabled() then
|
||||
Performance.startTimer("event_touch")
|
||||
end
|
||||
|
||||
if not self._element then
|
||||
if Performance and Performance.isEnabled() then
|
||||
Performance.stopTimer("event_touch")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
|
||||
-- Check if element can process events
|
||||
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled
|
||||
|
||||
if not canProcessEvents then
|
||||
if Performance and Performance.isEnabled() then
|
||||
Performance.stopTimer("event_touch")
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
-- Get current active touches from LÖVE
|
||||
local activeTouches = {}
|
||||
local touches = love.touch.getTouches()
|
||||
for _, id in ipairs(touches) do
|
||||
activeTouches[tostring(id)] = true
|
||||
end
|
||||
|
||||
-- Process active touches
|
||||
for _, id in ipairs(touches) do
|
||||
local touchId = tostring(id)
|
||||
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 = self._InputEvent.new({
|
||||
type = "click",
|
||||
button = 1,
|
||||
x = tx,
|
||||
y = ty,
|
||||
modifiers = self._utils.getModifiers(),
|
||||
clickCount = 1,
|
||||
})
|
||||
self:_invokeCallback(element, touchEvent)
|
||||
self._touchPressed[id] = false
|
||||
local pressure = 1.0 -- LÖVE doesn't provide pressure by default
|
||||
|
||||
-- Check if touch is within element bounds
|
||||
local isInside = tx >= bx and tx <= bx + bw and ty >= by and ty <= by + bh
|
||||
|
||||
if isInside then
|
||||
if not self._touches[touchId] then
|
||||
-- New touch began
|
||||
self:_handleTouchBegan(touchId, tx, ty, pressure)
|
||||
else
|
||||
-- Touch moved
|
||||
self:_handleTouchMoved(touchId, tx, ty, pressure)
|
||||
end
|
||||
elseif self._touches[touchId] then
|
||||
-- Touch moved outside or ended
|
||||
if activeTouches[touchId] then
|
||||
-- Still active but outside - fire moved event
|
||||
self:_handleTouchMoved(touchId, tx, ty, pressure)
|
||||
else
|
||||
-- Touch ended
|
||||
self:_handleTouchEnded(touchId, tx, ty, pressure)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Check for ended touches (touches that were tracked but are no longer active)
|
||||
for touchId, _ in pairs(self._touches) do
|
||||
if not activeTouches[touchId] then
|
||||
-- Touch ended or cancelled
|
||||
local lastPos = self._lastTouchPositions[touchId]
|
||||
if lastPos then
|
||||
self:_handleTouchEnded(touchId, lastPos.x, lastPos.y, 1.0)
|
||||
else
|
||||
-- Cleanup orphaned touch
|
||||
self:_cleanupTouch(touchId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Stop performance timing
|
||||
if Performance and Performance.isEnabled() then
|
||||
Performance.stopTimer("event_touch")
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle touch began event
|
||||
---@param touchId string Touch ID
|
||||
---@param x number Touch X position
|
||||
---@param y number Touch Y position
|
||||
---@param pressure number Touch pressure (0-1)
|
||||
function EventHandler:_handleTouchBegan(touchId, x, y, pressure)
|
||||
if not self._element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
|
||||
-- Create touch state
|
||||
self._touches[touchId] = {
|
||||
x = x,
|
||||
y = y,
|
||||
pressure = pressure,
|
||||
timestamp = love.timer.getTime(),
|
||||
phase = "began",
|
||||
}
|
||||
|
||||
-- Record start position
|
||||
self._touchStartPositions[touchId] = { x = x, y = y }
|
||||
self._lastTouchPositions[touchId] = { x = x, y = y }
|
||||
|
||||
-- Initialize touch history
|
||||
self._touchHistory[touchId] = { { x = x, y = y, timestamp = love.timer.getTime() } }
|
||||
|
||||
-- Create and fire touch press event
|
||||
local touchEvent = self._InputEvent.fromTouch(touchId, x, y, "began", pressure)
|
||||
touchEvent.type = "touchpress"
|
||||
touchEvent.dx = 0
|
||||
touchEvent.dy = 0
|
||||
self:_invokeCallback(element, touchEvent)
|
||||
end
|
||||
|
||||
--- Handle touch moved event
|
||||
---@param touchId string Touch ID
|
||||
---@param x number Touch X position
|
||||
---@param y number Touch Y position
|
||||
---@param pressure number Touch pressure (0-1)
|
||||
function EventHandler:_handleTouchMoved(touchId, x, y, pressure)
|
||||
if not self._element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
local touchState = self._touches[touchId]
|
||||
|
||||
if not touchState then
|
||||
-- Touch not tracked, ignore
|
||||
return
|
||||
end
|
||||
|
||||
local lastPos = self._lastTouchPositions[touchId]
|
||||
if not lastPos or lastPos.x ~= x or lastPos.y ~= y then
|
||||
-- Touch position changed
|
||||
local startPos = self._touchStartPositions[touchId]
|
||||
local dx = x - startPos.x
|
||||
local dy = y - startPos.y
|
||||
|
||||
-- Update touch state
|
||||
touchState.x = x
|
||||
touchState.y = y
|
||||
touchState.pressure = pressure
|
||||
touchState.phase = "moved"
|
||||
|
||||
-- Update last position
|
||||
self._lastTouchPositions[touchId] = { x = x, y = y }
|
||||
|
||||
-- Add to touch history (keep last 5 positions)
|
||||
local history = self._touchHistory[touchId] or {}
|
||||
table.insert(history, { x = x, y = y, timestamp = love.timer.getTime() })
|
||||
if #history > 5 then
|
||||
table.remove(history, 1)
|
||||
end
|
||||
self._touchHistory[touchId] = history
|
||||
|
||||
-- Create and fire touch move event
|
||||
local touchEvent = self._InputEvent.fromTouch(touchId, x, y, "moved", pressure)
|
||||
touchEvent.type = "touchmove"
|
||||
touchEvent.dx = dx
|
||||
touchEvent.dy = dy
|
||||
self:_invokeCallback(element, touchEvent)
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle touch ended event
|
||||
---@param touchId string Touch ID
|
||||
---@param x number Touch X position
|
||||
---@param y number Touch Y position
|
||||
---@param pressure number Touch pressure (0-1)
|
||||
function EventHandler:_handleTouchEnded(touchId, x, y, pressure)
|
||||
if not self._element then
|
||||
return
|
||||
end
|
||||
|
||||
local element = self._element
|
||||
local touchState = self._touches[touchId]
|
||||
|
||||
if not touchState then
|
||||
-- Touch not tracked, ignore
|
||||
return
|
||||
end
|
||||
|
||||
local startPos = self._touchStartPositions[touchId]
|
||||
local dx = x - startPos.x
|
||||
local dy = y - startPos.y
|
||||
|
||||
-- Create and fire touch release event
|
||||
local touchEvent = self._InputEvent.fromTouch(touchId, x, y, "ended", pressure)
|
||||
touchEvent.type = "touchrelease"
|
||||
touchEvent.dx = dx
|
||||
touchEvent.dy = dy
|
||||
self:_invokeCallback(element, touchEvent)
|
||||
|
||||
-- Cleanup touch state
|
||||
self:_cleanupTouch(touchId)
|
||||
end
|
||||
|
||||
--- Cleanup touch state
|
||||
---@param touchId string Touch ID
|
||||
function EventHandler:_cleanupTouch(touchId)
|
||||
self._touches[touchId] = nil
|
||||
self._touchStartPositions[touchId] = nil
|
||||
self._lastTouchPositions[touchId] = nil
|
||||
self._touchHistory[touchId] = nil
|
||||
end
|
||||
|
||||
--- Get active touches on this element
|
||||
---@return table<string, table> Active touches
|
||||
function EventHandler:getActiveTouches()
|
||||
return self._touches
|
||||
end
|
||||
|
||||
--- Get touch history for gesture recognition
|
||||
---@param touchId string Touch ID
|
||||
---@return table? Touch history (last 5 positions)
|
||||
function EventHandler:getTouchHistory(touchId)
|
||||
return self._touchHistory[touchId]
|
||||
end
|
||||
|
||||
--- Reset scrollbar press flag (called each frame)
|
||||
|
||||
594
modules/GestureRecognizer.lua
Normal file
594
modules/GestureRecognizer.lua
Normal file
@@ -0,0 +1,594 @@
|
||||
---@class GestureRecognizer
|
||||
---@field _touches table<string, table> -- Current touch states
|
||||
---@field _gestureStates table -- Active gesture states
|
||||
---@field _config table -- Gesture configuration (thresholds, etc.)
|
||||
---@field _InputEvent table
|
||||
---@field _utils table
|
||||
local GestureRecognizer = {}
|
||||
GestureRecognizer.__index = GestureRecognizer
|
||||
|
||||
-- Gesture types enum
|
||||
local GestureType = {
|
||||
TAP = "tap",
|
||||
DOUBLE_TAP = "double_tap",
|
||||
LONG_PRESS = "long_press",
|
||||
SWIPE = "swipe",
|
||||
PAN = "pan",
|
||||
PINCH = "pinch",
|
||||
ROTATE = "rotate",
|
||||
}
|
||||
|
||||
-- Gesture states
|
||||
local GestureState = {
|
||||
POSSIBLE = "possible",
|
||||
BEGAN = "began",
|
||||
CHANGED = "changed",
|
||||
ENDED = "ended",
|
||||
CANCELLED = "cancelled",
|
||||
FAILED = "failed",
|
||||
}
|
||||
|
||||
-- Default configuration
|
||||
local defaultConfig = {
|
||||
-- Tap gesture
|
||||
tapMaxDuration = 0.3, -- seconds
|
||||
tapMaxMovement = 10, -- pixels
|
||||
|
||||
-- Double-tap gesture
|
||||
doubleTapInterval = 0.3, -- seconds between taps
|
||||
|
||||
-- Long-press gesture
|
||||
longPressMinDuration = 0.5, -- seconds
|
||||
longPressMaxMovement = 10, -- pixels
|
||||
|
||||
-- Swipe gesture
|
||||
swipeMinDistance = 50, -- pixels
|
||||
swipeMaxDuration = 0.2, -- seconds
|
||||
swipeMinVelocity = 200, -- pixels per second
|
||||
|
||||
-- Pan gesture
|
||||
panMinMovement = 5, -- pixels to start pan
|
||||
|
||||
-- Pinch gesture
|
||||
pinchMinScaleChange = 0.1, -- 10% scale change
|
||||
|
||||
-- Rotate gesture
|
||||
rotateMinAngleChange = 5, -- degrees
|
||||
}
|
||||
|
||||
--- Create a new GestureRecognizer instance
|
||||
---@param config table? Optional configuration options
|
||||
---@param deps table Dependencies {InputEvent, utils}
|
||||
---@return GestureRecognizer
|
||||
function GestureRecognizer.new(config, deps)
|
||||
config = config or {}
|
||||
|
||||
local self = setmetatable({}, GestureRecognizer)
|
||||
|
||||
self._InputEvent = deps.InputEvent
|
||||
self._utils = deps.utils
|
||||
|
||||
-- Merge configuration with defaults
|
||||
self._config = {}
|
||||
for key, value in pairs(defaultConfig) do
|
||||
self._config[key] = config[key] or value
|
||||
end
|
||||
|
||||
self._touches = {}
|
||||
self._gestureStates = {
|
||||
tap = nil,
|
||||
doubleTap = { lastTapTime = 0, tapCount = 0 },
|
||||
longPress = {},
|
||||
swipe = {},
|
||||
pan = {},
|
||||
pinch = {},
|
||||
rotate = {},
|
||||
}
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Update gesture recognizer with touch event
|
||||
---@param event InputEvent Touch event
|
||||
function GestureRecognizer:processTouchEvent(event)
|
||||
if not event.touchId then
|
||||
return
|
||||
end
|
||||
|
||||
local touchId = event.touchId
|
||||
|
||||
-- Update touch state
|
||||
if event.type == "touchpress" then
|
||||
self._touches[touchId] = {
|
||||
startX = event.x,
|
||||
startY = event.y,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
startTime = event.timestamp,
|
||||
lastTime = event.timestamp,
|
||||
phase = "began",
|
||||
}
|
||||
|
||||
-- Initialize gesture detection
|
||||
self:_detectTapBegan(touchId, event)
|
||||
self:_detectLongPressBegan(touchId, event)
|
||||
|
||||
elseif event.type == "touchmove" then
|
||||
local touch = self._touches[touchId]
|
||||
if touch then
|
||||
touch.x = event.x
|
||||
touch.y = event.y
|
||||
touch.lastTime = event.timestamp
|
||||
touch.phase = "moved"
|
||||
|
||||
-- Update gesture detection
|
||||
self:_detectPan(touchId, event)
|
||||
self:_detectSwipe(touchId, event)
|
||||
|
||||
-- Multi-touch gestures
|
||||
if self:_getTouchCount() >= 2 then
|
||||
self:_detectPinch(event)
|
||||
self:_detectRotate(event)
|
||||
end
|
||||
end
|
||||
|
||||
elseif event.type == "touchrelease" then
|
||||
local touch = self._touches[touchId]
|
||||
if touch then
|
||||
touch.phase = "ended"
|
||||
|
||||
-- Finalize gesture detection
|
||||
self:_detectTapEnded(touchId, event)
|
||||
self:_detectSwipeEnded(touchId, event)
|
||||
self:_detectPanEnded(touchId, event)
|
||||
|
||||
-- Cleanup touch
|
||||
self._touches[touchId] = nil
|
||||
end
|
||||
|
||||
elseif event.type == "touchcancel" then
|
||||
-- Cancel all active gestures for this touch
|
||||
self._touches[touchId] = nil
|
||||
self:_cancelAllGestures()
|
||||
end
|
||||
end
|
||||
|
||||
--- Get number of active touches
|
||||
---@return number
|
||||
function GestureRecognizer:_getTouchCount()
|
||||
local count = 0
|
||||
for _ in pairs(self._touches) do
|
||||
count = count + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--- Detect tap gesture began
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
function GestureRecognizer:_detectTapBegan(touchId, event)
|
||||
-- Tap detection happens on touch end
|
||||
-- Just record the touch for now
|
||||
end
|
||||
|
||||
--- Detect tap gesture ended
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
function GestureRecognizer:_detectTapEnded(touchId, event)
|
||||
local touch = self._touches[touchId]
|
||||
if not touch then
|
||||
return
|
||||
end
|
||||
|
||||
local duration = event.timestamp - touch.startTime
|
||||
local dx = event.x - touch.startX
|
||||
local dy = event.y - touch.startY
|
||||
local distance = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
-- Check if it's a valid tap
|
||||
if duration < self._config.tapMaxDuration and distance < self._config.tapMaxMovement then
|
||||
local currentTime = event.timestamp
|
||||
local doubleTapState = self._gestureStates.doubleTap
|
||||
|
||||
-- Check for double-tap
|
||||
if currentTime - doubleTapState.lastTapTime < self._config.doubleTapInterval then
|
||||
doubleTapState.tapCount = doubleTapState.tapCount + 1
|
||||
|
||||
if doubleTapState.tapCount >= 2 then
|
||||
-- Fire double-tap gesture
|
||||
return {
|
||||
type = GestureType.DOUBLE_TAP,
|
||||
state = GestureState.ENDED,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
else
|
||||
doubleTapState.tapCount = 1
|
||||
end
|
||||
|
||||
doubleTapState.lastTapTime = currentTime
|
||||
|
||||
-- Fire tap gesture
|
||||
return {
|
||||
type = GestureType.TAP,
|
||||
state = GestureState.ENDED,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
--- Detect long-press gesture began
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
function GestureRecognizer:_detectLongPressBegan(touchId, event)
|
||||
-- Long-press detection happens continuously during touch
|
||||
self._gestureStates.longPress[touchId] = {
|
||||
startX = event.x,
|
||||
startY = event.y,
|
||||
startTime = event.timestamp,
|
||||
triggered = false,
|
||||
}
|
||||
end
|
||||
|
||||
--- Update long-press detection
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
---@return table? Gesture event
|
||||
function GestureRecognizer:_updateLongPress(touchId, event)
|
||||
local lpState = self._gestureStates.longPress[touchId]
|
||||
if not lpState or lpState.triggered then
|
||||
return nil
|
||||
end
|
||||
|
||||
local duration = event.timestamp - lpState.startTime
|
||||
local dx = event.x - lpState.startX
|
||||
local dy = event.y - lpState.startY
|
||||
local distance = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
-- Check if long-press duration reached and movement within threshold
|
||||
if duration >= self._config.longPressMinDuration and distance < self._config.longPressMaxMovement then
|
||||
lpState.triggered = true
|
||||
|
||||
return {
|
||||
type = GestureType.LONG_PRESS,
|
||||
state = GestureState.BEGAN,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
timestamp = event.timestamp,
|
||||
duration = duration,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Detect pan gesture
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
---@return table? Gesture event
|
||||
function GestureRecognizer:_detectPan(touchId, event)
|
||||
local touch = self._touches[touchId]
|
||||
if not touch then
|
||||
return nil
|
||||
end
|
||||
|
||||
local dx = event.x - touch.startX
|
||||
local dy = event.y - touch.startY
|
||||
local distance = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
local panState = self._gestureStates.pan[touchId]
|
||||
|
||||
if not panState then
|
||||
-- Check if pan should begin
|
||||
if distance >= self._config.panMinMovement then
|
||||
self._gestureStates.pan[touchId] = {
|
||||
active = true,
|
||||
lastX = touch.startX,
|
||||
lastY = touch.startY,
|
||||
}
|
||||
panState = self._gestureStates.pan[touchId]
|
||||
|
||||
return {
|
||||
type = GestureType.PAN,
|
||||
state = GestureState.BEGAN,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
dx = dx,
|
||||
dy = dy,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
else
|
||||
-- Pan is active, fire changed event
|
||||
local panDx = event.x - panState.lastX
|
||||
local panDy = event.y - panState.lastY
|
||||
|
||||
panState.lastX = event.x
|
||||
panState.lastY = event.y
|
||||
|
||||
return {
|
||||
type = GestureType.PAN,
|
||||
state = GestureState.CHANGED,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
dx = panDx,
|
||||
dy = panDy,
|
||||
totalDx = dx,
|
||||
totalDy = dy,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Detect pan ended
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
---@return table? Gesture event
|
||||
function GestureRecognizer:_detectPanEnded(touchId, event)
|
||||
local panState = self._gestureStates.pan[touchId]
|
||||
if panState and panState.active then
|
||||
self._gestureStates.pan[touchId] = nil
|
||||
|
||||
local touch = self._touches[touchId]
|
||||
local dx = event.x - touch.startX
|
||||
local dy = event.y - touch.startY
|
||||
|
||||
return {
|
||||
type = GestureType.PAN,
|
||||
state = GestureState.ENDED,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
dx = dx,
|
||||
dy = dy,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Detect swipe gesture
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
function GestureRecognizer:_detectSwipe(touchId, event)
|
||||
-- Swipe detection happens on touch end
|
||||
end
|
||||
|
||||
--- Detect swipe ended
|
||||
---@param touchId string
|
||||
---@param event InputEvent
|
||||
---@return table? Gesture event
|
||||
function GestureRecognizer:_detectSwipeEnded(touchId, event)
|
||||
local touch = self._touches[touchId]
|
||||
if not touch then
|
||||
return nil
|
||||
end
|
||||
|
||||
local duration = event.timestamp - touch.startTime
|
||||
local dx = event.x - touch.startX
|
||||
local dy = event.y - touch.startY
|
||||
local distance = math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
-- Check if it's a valid swipe
|
||||
if distance >= self._config.swipeMinDistance and duration <= self._config.swipeMaxDuration then
|
||||
local velocity = distance / duration
|
||||
|
||||
if velocity >= self._config.swipeMinVelocity then
|
||||
-- Determine swipe direction
|
||||
local angle = math.atan2(dy, dx)
|
||||
local direction = "right"
|
||||
|
||||
if angle >= -math.pi / 4 and angle < math.pi / 4 then
|
||||
direction = "right"
|
||||
elseif angle >= math.pi / 4 and angle < 3 * math.pi / 4 then
|
||||
direction = "down"
|
||||
elseif angle >= -3 * math.pi / 4 and angle < -math.pi / 4 then
|
||||
direction = "up"
|
||||
else
|
||||
direction = "left"
|
||||
end
|
||||
|
||||
return {
|
||||
type = GestureType.SWIPE,
|
||||
state = GestureState.ENDED,
|
||||
x = event.x,
|
||||
y = event.y,
|
||||
dx = dx,
|
||||
dy = dy,
|
||||
direction = direction,
|
||||
velocity = velocity,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Detect pinch gesture
|
||||
---@param event InputEvent
|
||||
---@return table? Gesture event
|
||||
function GestureRecognizer:_detectPinch(event)
|
||||
-- Get two touches for pinch
|
||||
local touches = {}
|
||||
for touchId, touch in pairs(self._touches) do
|
||||
table.insert(touches, { id = touchId, touch = touch })
|
||||
if #touches >= 2 then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if #touches < 2 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local t1 = touches[1].touch
|
||||
local t2 = touches[2].touch
|
||||
|
||||
-- Calculate current distance
|
||||
local currentDx = t2.x - t1.x
|
||||
local currentDy = t2.y - t1.y
|
||||
local currentDistance = math.sqrt(currentDx * currentDx + currentDy * currentDy)
|
||||
|
||||
-- Calculate initial distance
|
||||
local initialDx = t2.startX - t1.startX
|
||||
local initialDy = t2.startY - t1.startY
|
||||
local initialDistance = math.sqrt(initialDx * initialDx + initialDy * initialDy)
|
||||
|
||||
if initialDistance == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Calculate scale
|
||||
local scale = currentDistance / initialDistance
|
||||
local pinchState = self._gestureStates.pinch
|
||||
|
||||
if not pinchState.active then
|
||||
-- Check if pinch should begin
|
||||
if math.abs(scale - 1.0) >= self._config.pinchMinScaleChange then
|
||||
pinchState.active = true
|
||||
pinchState.initialScale = scale
|
||||
pinchState.lastScale = scale
|
||||
|
||||
-- Calculate center point
|
||||
local centerX = (t1.x + t2.x) / 2
|
||||
local centerY = (t1.y + t2.y) / 2
|
||||
|
||||
return {
|
||||
type = GestureType.PINCH,
|
||||
state = GestureState.BEGAN,
|
||||
scale = scale,
|
||||
centerX = centerX,
|
||||
centerY = centerY,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
else
|
||||
-- Pinch is active, fire changed event
|
||||
local centerX = (t1.x + t2.x) / 2
|
||||
local centerY = (t1.y + t2.y) / 2
|
||||
|
||||
local scaleChange = scale - pinchState.lastScale
|
||||
pinchState.lastScale = scale
|
||||
|
||||
return {
|
||||
type = GestureType.PINCH,
|
||||
state = GestureState.CHANGED,
|
||||
scale = scale,
|
||||
scaleChange = scaleChange,
|
||||
centerX = centerX,
|
||||
centerY = centerY,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Detect rotate gesture
|
||||
---@param event InputEvent
|
||||
---@return table? Gesture event
|
||||
function GestureRecognizer:_detectRotate(event)
|
||||
-- Get two touches for rotation
|
||||
local touches = {}
|
||||
for touchId, touch in pairs(self._touches) do
|
||||
table.insert(touches, { id = touchId, touch = touch })
|
||||
if #touches >= 2 then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if #touches < 2 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local t1 = touches[1].touch
|
||||
local t2 = touches[2].touch
|
||||
|
||||
-- Calculate current angle
|
||||
local currentAngle = math.atan2(t2.y - t1.y, t2.x - t1.x)
|
||||
|
||||
-- Calculate initial angle
|
||||
local initialAngle = math.atan2(t2.startY - t1.startY, t2.startX - t1.startX)
|
||||
|
||||
-- Calculate rotation (in degrees)
|
||||
local rotation = (currentAngle - initialAngle) * 180 / math.pi
|
||||
|
||||
local rotateState = self._gestureStates.rotate
|
||||
|
||||
if not rotateState.active then
|
||||
-- Check if rotation should begin
|
||||
if math.abs(rotation) >= self._config.rotateMinAngleChange then
|
||||
rotateState.active = true
|
||||
rotateState.initialRotation = rotation
|
||||
rotateState.lastRotation = rotation
|
||||
|
||||
-- Calculate center point
|
||||
local centerX = (t1.x + t2.x) / 2
|
||||
local centerY = (t1.y + t2.y) / 2
|
||||
|
||||
return {
|
||||
type = GestureType.ROTATE,
|
||||
state = GestureState.BEGAN,
|
||||
rotation = rotation,
|
||||
centerX = centerX,
|
||||
centerY = centerY,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
else
|
||||
-- Rotation is active, fire changed event
|
||||
local centerX = (t1.x + t2.x) / 2
|
||||
local centerY = (t1.y + t2.y) / 2
|
||||
|
||||
local rotationChange = rotation - rotateState.lastRotation
|
||||
rotateState.lastRotation = rotation
|
||||
|
||||
return {
|
||||
type = GestureType.ROTATE,
|
||||
state = GestureState.CHANGED,
|
||||
rotation = rotation,
|
||||
rotationChange = rotationChange,
|
||||
centerX = centerX,
|
||||
centerY = centerY,
|
||||
timestamp = event.timestamp,
|
||||
}
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Cancel all active gestures
|
||||
function GestureRecognizer:_cancelAllGestures()
|
||||
for gestureType, state in pairs(self._gestureStates) do
|
||||
if type(state) == "table" and state.active then
|
||||
state.active = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Reset gesture recognizer state
|
||||
function GestureRecognizer:reset()
|
||||
self._touches = {}
|
||||
self._gestureStates = {
|
||||
tap = nil,
|
||||
doubleTap = { lastTapTime = 0, tapCount = 0 },
|
||||
longPress = {},
|
||||
swipe = {},
|
||||
pan = {},
|
||||
pinch = { active = false },
|
||||
rotate = { active = false },
|
||||
}
|
||||
end
|
||||
|
||||
-- Export gesture types and states
|
||||
GestureRecognizer.GestureType = GestureType
|
||||
GestureRecognizer.GestureState = GestureState
|
||||
|
||||
return GestureRecognizer
|
||||
@@ -1,18 +1,21 @@
|
||||
---@class InputEvent
|
||||
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
||||
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel"
|
||||
---@field button number -- Mouse button: 1 (left), 2 (right), 3 (middle)
|
||||
---@field x number -- Mouse X position
|
||||
---@field y number -- Mouse Y position
|
||||
---@field dx number? -- Delta X from drag start (only for drag events)
|
||||
---@field dy number? -- Delta Y from drag start (only for drag events)
|
||||
---@field x number -- Mouse/Touch X position
|
||||
---@field y number -- Mouse/Touch Y position
|
||||
---@field dx number? -- Delta X from drag/touch start (only for drag/touch events)
|
||||
---@field dy number? -- Delta Y from drag/touch start (only for drag/touch events)
|
||||
---@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
||||
---@field clickCount number -- Number of clicks (for double/triple click detection)
|
||||
---@field timestamp number -- Time when event occurred
|
||||
---@field touchId string? -- Touch identifier (for multi-touch)
|
||||
---@field pressure number? -- Touch pressure (0-1, defaults to 1.0)
|
||||
---@field phase string? -- Touch phase: "began", "moved", "ended", "cancelled"
|
||||
local InputEvent = {}
|
||||
InputEvent.__index = InputEvent
|
||||
|
||||
---@class InputEventProps
|
||||
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"
|
||||
---@field type "click"|"press"|"release"|"rightclick"|"middleclick"|"drag"|"touchpress"|"touchmove"|"touchrelease"|"touchcancel"
|
||||
---@field button number
|
||||
---@field x number
|
||||
---@field y number
|
||||
@@ -21,6 +24,9 @@ InputEvent.__index = InputEvent
|
||||
---@field modifiers {shift:boolean, ctrl:boolean, alt:boolean, super:boolean}
|
||||
---@field clickCount number?
|
||||
---@field timestamp number?
|
||||
---@field touchId string?
|
||||
---@field pressure number?
|
||||
---@field phase string?
|
||||
|
||||
--- Create a new input event
|
||||
---@param props InputEventProps
|
||||
@@ -36,7 +42,47 @@ function InputEvent.new(props)
|
||||
self.modifiers = props.modifiers
|
||||
self.clickCount = props.clickCount or 1
|
||||
self.timestamp = props.timestamp or love.timer.getTime()
|
||||
|
||||
-- Touch-specific properties
|
||||
self.touchId = props.touchId
|
||||
self.pressure = props.pressure or 1.0
|
||||
self.phase = props.phase
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Create an InputEvent from LÖVE touch data
|
||||
---@param id userdata Touch ID from LÖVE
|
||||
---@param x number Touch X position
|
||||
---@param y number Touch Y position
|
||||
---@param phase string Touch phase: "began", "moved", "ended", "cancelled"
|
||||
---@param pressure number? Touch pressure (0-1, defaults to 1.0)
|
||||
---@return InputEvent
|
||||
function InputEvent.fromTouch(id, x, y, phase, pressure)
|
||||
local touchIdStr = tostring(id)
|
||||
local eventType = "touchpress"
|
||||
if phase == "moved" then
|
||||
eventType = "touchmove"
|
||||
elseif phase == "ended" then
|
||||
eventType = "touchrelease"
|
||||
elseif phase == "cancelled" then
|
||||
eventType = "touchcancel"
|
||||
end
|
||||
|
||||
return InputEvent.new({
|
||||
type = eventType,
|
||||
button = 1, -- Treat touch as left button
|
||||
x = x,
|
||||
y = y,
|
||||
dx = 0,
|
||||
dy = 0,
|
||||
modifiers = {shift = false, ctrl = false, alt = false, super = false},
|
||||
clickCount = 1,
|
||||
timestamp = love.timer.getTime(),
|
||||
touchId = touchIdStr,
|
||||
pressure = pressure or 1.0,
|
||||
phase = phase,
|
||||
})
|
||||
end
|
||||
|
||||
return InputEvent
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
---@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 touchScrollEnabled boolean -- Enable touch scrolling
|
||||
---@field momentumScrollEnabled boolean -- Enable momentum scrolling
|
||||
---@field bounceEnabled boolean -- Enable bounce effects at boundaries
|
||||
---@field scrollFriction number -- Friction coefficient for momentum (0.95-0.98)
|
||||
---@field bounceStiffness number -- Bounce spring constant (0.1-0.3)
|
||||
---@field maxOverscroll number -- Maximum overscroll distance (pixels)
|
||||
---@field _element Element? -- Reference to parent Element (set via initialize)
|
||||
---@field _overflowX boolean -- True if content overflows horizontally
|
||||
---@field _overflowY boolean -- True if content overflows vertically
|
||||
@@ -24,6 +30,13 @@
|
||||
---@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
|
||||
---@field _touchScrolling boolean -- True if currently touch scrolling
|
||||
---@field _scrollVelocityX number -- Current horizontal scroll velocity (px/s)
|
||||
---@field _scrollVelocityY number -- Current vertical scroll velocity (px/s)
|
||||
---@field _momentumScrolling boolean -- True if momentum scrolling is active
|
||||
---@field _lastTouchTime number -- Timestamp of last touch move
|
||||
---@field _lastTouchX number -- Last touch X position
|
||||
---@field _lastTouchY number -- Last touch Y position
|
||||
---@field _Color table
|
||||
---@field _utils table
|
||||
---@field _ErrorHandler table?
|
||||
@@ -61,6 +74,14 @@ function ScrollManager.new(config, deps)
|
||||
-- hideScrollbars can be boolean or table {vertical: boolean, horizontal: boolean}
|
||||
self.hideScrollbars = self._utils.normalizeBooleanTable(config.hideScrollbars, false)
|
||||
|
||||
-- Touch scrolling configuration
|
||||
self.touchScrollEnabled = config.touchScrollEnabled ~= false -- Default true
|
||||
self.momentumScrollEnabled = config.momentumScrollEnabled ~= false -- Default true
|
||||
self.bounceEnabled = config.bounceEnabled ~= false -- Default true
|
||||
self.scrollFriction = config.scrollFriction or 0.95 -- Exponential decay per frame
|
||||
self.bounceStiffness = config.bounceStiffness or 0.2 -- Spring constant
|
||||
self.maxOverscroll = config.maxOverscroll or 100 -- pixels
|
||||
|
||||
-- Internal overflow state
|
||||
self._overflowX = false
|
||||
self._overflowY = false
|
||||
@@ -81,6 +102,15 @@ function ScrollManager.new(config, deps)
|
||||
self._scrollbarDragOffset = 0
|
||||
self._scrollbarPressHandled = false
|
||||
|
||||
-- Touch scrolling state
|
||||
self._touchScrolling = false
|
||||
self._scrollVelocityX = 0
|
||||
self._scrollVelocityY = 0
|
||||
self._momentumScrolling = false
|
||||
self._lastTouchTime = 0
|
||||
self._lastTouchX = 0
|
||||
self._lastTouchY = 0
|
||||
|
||||
-- Element reference (set via initialize)
|
||||
self._element = nil
|
||||
|
||||
@@ -668,4 +698,224 @@ function ScrollManager:setState(state)
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle touch press for scrolling
|
||||
---@param touchX number
|
||||
---@param touchY number
|
||||
---@return boolean -- True if touch scroll started
|
||||
function ScrollManager:handleTouchPress(touchX, touchY)
|
||||
if not self.touchScrollEnabled then
|
||||
return false
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
-- Stop momentum scrolling if active
|
||||
if self._momentumScrolling then
|
||||
self._momentumScrolling = false
|
||||
self._scrollVelocityX = 0
|
||||
self._scrollVelocityY = 0
|
||||
end
|
||||
|
||||
-- Start touch scrolling
|
||||
self._touchScrolling = true
|
||||
self._lastTouchX = touchX
|
||||
self._lastTouchY = touchY
|
||||
self._lastTouchTime = love.timer.getTime()
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Handle touch move for scrolling
|
||||
---@param touchX number
|
||||
---@param touchY number
|
||||
---@return boolean -- True if touch scroll was handled
|
||||
function ScrollManager:handleTouchMove(touchX, touchY)
|
||||
if not self._touchScrolling then
|
||||
return false
|
||||
end
|
||||
|
||||
local currentTime = love.timer.getTime()
|
||||
local dt = currentTime - self._lastTouchTime
|
||||
|
||||
if dt <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Calculate delta and velocity
|
||||
local dx = touchX - self._lastTouchX
|
||||
local dy = touchY - self._lastTouchY
|
||||
|
||||
-- Invert deltas (touch moves opposite to scroll)
|
||||
dx = -dx
|
||||
dy = -dy
|
||||
|
||||
-- Calculate velocity (pixels per second)
|
||||
self._scrollVelocityX = dx / dt
|
||||
self._scrollVelocityY = dy / dt
|
||||
|
||||
-- Apply scroll with bounce if enabled
|
||||
if self.bounceEnabled then
|
||||
-- Allow overscroll
|
||||
local newScrollX = self._scrollX + dx
|
||||
local newScrollY = self._scrollY + dy
|
||||
|
||||
-- Clamp to max overscroll limits
|
||||
local minScrollX = -self.maxOverscroll
|
||||
local maxScrollX = self._maxScrollX + self.maxOverscroll
|
||||
local minScrollY = -self.maxOverscroll
|
||||
local maxScrollY = self._maxScrollY + self.maxOverscroll
|
||||
|
||||
newScrollX = self._utils.clamp(newScrollX, minScrollX, maxScrollX)
|
||||
newScrollY = self._utils.clamp(newScrollY, minScrollY, maxScrollY)
|
||||
|
||||
self._scrollX = newScrollX
|
||||
self._scrollY = newScrollY
|
||||
else
|
||||
-- Normal clamped scrolling
|
||||
self:scrollBy(dx, dy)
|
||||
end
|
||||
|
||||
-- Update last touch state
|
||||
self._lastTouchX = touchX
|
||||
self._lastTouchY = touchY
|
||||
self._lastTouchTime = currentTime
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Handle touch release for scrolling
|
||||
---@return boolean -- True if touch scroll was active
|
||||
function ScrollManager:handleTouchRelease()
|
||||
if not self._touchScrolling then
|
||||
return false
|
||||
end
|
||||
|
||||
self._touchScrolling = false
|
||||
|
||||
-- Start momentum scrolling if enabled and velocity is significant
|
||||
if self.momentumScrollEnabled then
|
||||
local velocityThreshold = 50 -- pixels per second
|
||||
local totalVelocity = math.sqrt(self._scrollVelocityX^2 + self._scrollVelocityY^2)
|
||||
|
||||
if totalVelocity > velocityThreshold then
|
||||
self._momentumScrolling = true
|
||||
else
|
||||
self._scrollVelocityX = 0
|
||||
self._scrollVelocityY = 0
|
||||
end
|
||||
else
|
||||
self._scrollVelocityX = 0
|
||||
self._scrollVelocityY = 0
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Update momentum scrolling (call every frame with dt)
|
||||
---@param dt number Delta time in seconds
|
||||
function ScrollManager:update(dt)
|
||||
if not self._momentumScrolling then
|
||||
-- Handle bounce back if overscrolled
|
||||
if self.bounceEnabled then
|
||||
self:_updateBounce(dt)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Apply velocity to scroll position
|
||||
local dx = self._scrollVelocityX * dt
|
||||
local dy = self._scrollVelocityY * dt
|
||||
|
||||
if self.bounceEnabled then
|
||||
-- Allow overscroll during momentum
|
||||
self._scrollX = self._scrollX + dx
|
||||
self._scrollY = self._scrollY + dy
|
||||
else
|
||||
self:scrollBy(dx, dy)
|
||||
end
|
||||
|
||||
-- Apply friction (exponential decay)
|
||||
self._scrollVelocityX = self._scrollVelocityX * self.scrollFriction
|
||||
self._scrollVelocityY = self._scrollVelocityY * self.scrollFriction
|
||||
|
||||
-- Stop momentum when velocity is very low
|
||||
local totalVelocity = math.sqrt(self._scrollVelocityX^2 + self._scrollVelocityY^2)
|
||||
if totalVelocity < 1 then
|
||||
self._momentumScrolling = false
|
||||
self._scrollVelocityX = 0
|
||||
self._scrollVelocityY = 0
|
||||
end
|
||||
|
||||
-- Handle bounce back if overscrolled
|
||||
if self.bounceEnabled then
|
||||
self:_updateBounce(dt)
|
||||
end
|
||||
end
|
||||
|
||||
--- Update bounce effect when overscrolled (internal)
|
||||
---@param dt number Delta time in seconds
|
||||
function ScrollManager:_updateBounce(dt)
|
||||
local bounced = false
|
||||
|
||||
-- Bounce back horizontal overscroll
|
||||
if self._scrollX < 0 then
|
||||
local springForce = -self._scrollX * self.bounceStiffness
|
||||
self._scrollX = self._scrollX + springForce
|
||||
if math.abs(self._scrollX) < 0.5 then
|
||||
self._scrollX = 0
|
||||
end
|
||||
bounced = true
|
||||
elseif self._scrollX > self._maxScrollX then
|
||||
local overflow = self._scrollX - self._maxScrollX
|
||||
local springForce = -overflow * self.bounceStiffness
|
||||
self._scrollX = self._scrollX + springForce
|
||||
if math.abs(overflow) < 0.5 then
|
||||
self._scrollX = self._maxScrollX
|
||||
end
|
||||
bounced = true
|
||||
end
|
||||
|
||||
-- Bounce back vertical overscroll
|
||||
if self._scrollY < 0 then
|
||||
local springForce = -self._scrollY * self.bounceStiffness
|
||||
self._scrollY = self._scrollY + springForce
|
||||
if math.abs(self._scrollY) < 0.5 then
|
||||
self._scrollY = 0
|
||||
end
|
||||
bounced = true
|
||||
elseif self._scrollY > self._maxScrollY then
|
||||
local overflow = self._scrollY - self._maxScrollY
|
||||
local springForce = -overflow * self.bounceStiffness
|
||||
self._scrollY = self._scrollY + springForce
|
||||
if math.abs(overflow) < 0.5 then
|
||||
self._scrollY = self._maxScrollY
|
||||
end
|
||||
bounced = true
|
||||
end
|
||||
|
||||
-- Stop momentum if bouncing
|
||||
if bounced and self._momentumScrolling then
|
||||
-- Reduce velocity during bounce
|
||||
self._scrollVelocityX = self._scrollVelocityX * 0.9
|
||||
self._scrollVelocityY = self._scrollVelocityY * 0.9
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if currently touch scrolling
|
||||
---@return boolean
|
||||
function ScrollManager:isTouchScrolling()
|
||||
return self._touchScrolling
|
||||
end
|
||||
|
||||
--- Check if currently momentum scrolling
|
||||
---@return boolean
|
||||
function ScrollManager:isMomentumScrolling()
|
||||
return self._momentumScrolling
|
||||
end
|
||||
|
||||
return ScrollManager
|
||||
|
||||
Reference in New Issue
Block a user