feat: gesture/multi-touch progress
This commit is contained in:
252
FlexLove.lua
252
FlexLove.lua
@@ -112,6 +112,14 @@ flexlove._deferredCallbacks = {}
|
|||||||
-- Track accumulated delta time for immediate mode updates
|
-- Track accumulated delta time for immediate mode updates
|
||||||
flexlove._accumulatedDt = 0
|
flexlove._accumulatedDt = 0
|
||||||
|
|
||||||
|
-- Touch ownership tracking: maps touch ID (string) to the element that owns it
|
||||||
|
---@type table<string, Element>
|
||||||
|
flexlove._touchOwners = {}
|
||||||
|
|
||||||
|
-- Shared GestureRecognizer instance for touch routing (initialized in init())
|
||||||
|
---@type GestureRecognizer|nil
|
||||||
|
flexlove._gestureRecognizer = nil
|
||||||
|
|
||||||
--- Check if FlexLove initialization is complete and ready to create elements
|
--- Check if FlexLove initialization is complete and ready to create elements
|
||||||
--- Use this before creating elements to avoid automatic queueing
|
--- Use this before creating elements to avoid automatic queueing
|
||||||
---@return boolean ready True if FlexLove is initialized and ready to use
|
---@return boolean ready True if FlexLove is initialized and ready to use
|
||||||
@@ -207,6 +215,11 @@ function flexlove.init(config)
|
|||||||
LayoutEngine.init({ ErrorHandler = flexlove._ErrorHandler, Performance = flexlove._Performance, FFI = flexlove._FFI })
|
LayoutEngine.init({ ErrorHandler = flexlove._ErrorHandler, Performance = flexlove._Performance, FFI = flexlove._FFI })
|
||||||
EventHandler.init({ ErrorHandler = flexlove._ErrorHandler, Performance = flexlove._Performance, InputEvent = InputEvent, utils = utils })
|
EventHandler.init({ ErrorHandler = flexlove._ErrorHandler, Performance = flexlove._Performance, InputEvent = InputEvent, utils = utils })
|
||||||
|
|
||||||
|
-- Initialize shared GestureRecognizer for touch routing
|
||||||
|
if GestureRecognizer then
|
||||||
|
flexlove._gestureRecognizer = GestureRecognizer.new({}, { InputEvent = InputEvent, utils = utils })
|
||||||
|
end
|
||||||
|
|
||||||
flexlove._defaultDependencies = {
|
flexlove._defaultDependencies = {
|
||||||
Context = Context,
|
Context = Context,
|
||||||
Theme = Theme,
|
Theme = Theme,
|
||||||
@@ -1107,6 +1120,239 @@ function flexlove.wheelmoved(dx, dy)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Find the touch-interactive element at a given position using z-index ordering
|
||||||
|
--- Similar to getElementAtPosition but checks for touch-enabled elements
|
||||||
|
---@param x number Touch X position
|
||||||
|
---@param y number Touch Y position
|
||||||
|
---@return Element|nil element The topmost touch-enabled element at position
|
||||||
|
function flexlove._getTouchElementAtPosition(x, y)
|
||||||
|
local candidates = {}
|
||||||
|
|
||||||
|
local function collectTouchHits(element, scrollOffsetX, scrollOffsetY)
|
||||||
|
scrollOffsetX = scrollOffsetX or 0
|
||||||
|
scrollOffsetY = scrollOffsetY or 0
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
-- Adjust touch position by accumulated scroll offset for hit testing
|
||||||
|
local adjustedX = x + scrollOffsetX
|
||||||
|
local adjustedY = y + scrollOffsetY
|
||||||
|
|
||||||
|
if adjustedX >= bx and adjustedX <= bx + bw and adjustedY >= by and adjustedY <= by + bh then
|
||||||
|
-- Check if element is touch-enabled and interactive
|
||||||
|
if element.touchEnabled and not element.disabled and (element.onEvent or element.onTouchEvent or element.onGesture) then
|
||||||
|
table.insert(candidates, element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Check if this element has scrollable overflow (for touch scrolling)
|
||||||
|
local overflowX = element.overflowX or element.overflow
|
||||||
|
local overflowY = element.overflowY or element.overflow
|
||||||
|
local hasScrollableOverflow = (
|
||||||
|
overflowX == "scroll"
|
||||||
|
or overflowX == "auto"
|
||||||
|
or overflowY == "scroll"
|
||||||
|
or overflowY == "auto"
|
||||||
|
or overflowX == "hidden"
|
||||||
|
or overflowY == "hidden"
|
||||||
|
)
|
||||||
|
|
||||||
|
-- Accumulate scroll offset for children
|
||||||
|
local childScrollOffsetX = scrollOffsetX
|
||||||
|
local childScrollOffsetY = scrollOffsetY
|
||||||
|
if hasScrollableOverflow then
|
||||||
|
childScrollOffsetX = childScrollOffsetX + (element._scrollX or 0)
|
||||||
|
childScrollOffsetY = childScrollOffsetY + (element._scrollY or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, child in ipairs(element.children) do
|
||||||
|
collectTouchHits(child, childScrollOffsetX, childScrollOffsetY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, element in ipairs(flexlove.topElements) do
|
||||||
|
collectTouchHits(element)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Sort by z-index (highest first) — topmost element wins
|
||||||
|
table.sort(candidates, function(a, b)
|
||||||
|
return a.z > b.z
|
||||||
|
end)
|
||||||
|
|
||||||
|
return candidates[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handle touch press events from LÖVE's touch input system
|
||||||
|
--- Routes touch to the topmost element at the touch position and assigns touch ownership
|
||||||
|
--- Hook this to love.touchpressed() to enable touch interaction
|
||||||
|
---@param id lightuserdata Touch identifier from LÖVE
|
||||||
|
---@param x number Touch X position in screen coordinates
|
||||||
|
---@param y number Touch Y position in screen coordinates
|
||||||
|
---@param dx number X distance moved (usually 0 on press)
|
||||||
|
---@param dy number Y distance moved (usually 0 on press)
|
||||||
|
---@param pressure number Touch pressure (0-1, if supported by device)
|
||||||
|
function flexlove.touchpressed(id, x, y, dx, dy, pressure)
|
||||||
|
local touchId = tostring(id)
|
||||||
|
pressure = pressure or 1.0
|
||||||
|
|
||||||
|
-- Apply base scaling if configured
|
||||||
|
local touchX, touchY = x, y
|
||||||
|
if flexlove.baseScale then
|
||||||
|
touchX = x / flexlove.scaleFactors.x
|
||||||
|
touchY = y / flexlove.scaleFactors.y
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Find the topmost touch-enabled element at this position
|
||||||
|
local element = flexlove._getTouchElementAtPosition(touchX, touchY)
|
||||||
|
|
||||||
|
if element then
|
||||||
|
-- Assign touch ownership: this element receives all subsequent events for this touch
|
||||||
|
flexlove._touchOwners[touchId] = element
|
||||||
|
|
||||||
|
-- Create and route touch event
|
||||||
|
local touchEvent = InputEvent.fromTouch(id, touchX, touchY, "began", pressure)
|
||||||
|
element:handleTouchEvent(touchEvent)
|
||||||
|
|
||||||
|
-- Feed to shared gesture recognizer
|
||||||
|
if flexlove._gestureRecognizer then
|
||||||
|
local gestures = flexlove._gestureRecognizer:processTouchEvent(touchEvent)
|
||||||
|
if gestures then
|
||||||
|
for _, gesture in ipairs(gestures) do
|
||||||
|
element:handleGesture(gesture)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Route to scroll manager for scrollable elements
|
||||||
|
if element._scrollManager then
|
||||||
|
local overflowX = element.overflowX or element.overflow
|
||||||
|
local overflowY = element.overflowY or element.overflow
|
||||||
|
if overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto" then
|
||||||
|
element._scrollManager:handleTouchPress(touchX, touchY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handle touch move events from LÖVE's touch input system
|
||||||
|
--- Routes touch to the element that owns this touch ID (from the original press), regardless of current position
|
||||||
|
--- Hook this to love.touchmoved() to enable touch drag and gesture tracking
|
||||||
|
---@param id lightuserdata Touch identifier from LÖVE
|
||||||
|
---@param x number Touch X position in screen coordinates
|
||||||
|
---@param y number Touch Y position in screen coordinates
|
||||||
|
---@param dx number X distance moved since last event
|
||||||
|
---@param dy number Y distance moved since last event
|
||||||
|
---@param pressure number Touch pressure (0-1, if supported by device)
|
||||||
|
function flexlove.touchmoved(id, x, y, dx, dy, pressure)
|
||||||
|
local touchId = tostring(id)
|
||||||
|
pressure = pressure or 1.0
|
||||||
|
|
||||||
|
-- Apply base scaling if configured
|
||||||
|
local touchX, touchY = x, y
|
||||||
|
if flexlove.baseScale then
|
||||||
|
touchX = x / flexlove.scaleFactors.x
|
||||||
|
touchY = y / flexlove.scaleFactors.y
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Route to owning element (touch ownership persists from press to release)
|
||||||
|
local element = flexlove._touchOwners[touchId]
|
||||||
|
if element then
|
||||||
|
-- Create and route touch event
|
||||||
|
local touchEvent = InputEvent.fromTouch(id, touchX, touchY, "moved", pressure)
|
||||||
|
element:handleTouchEvent(touchEvent)
|
||||||
|
|
||||||
|
-- Feed to shared gesture recognizer
|
||||||
|
if flexlove._gestureRecognizer then
|
||||||
|
local gestures = flexlove._gestureRecognizer:processTouchEvent(touchEvent)
|
||||||
|
if gestures then
|
||||||
|
for _, gesture in ipairs(gestures) do
|
||||||
|
element:handleGesture(gesture)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Route to scroll manager for scrollable elements
|
||||||
|
if element._scrollManager then
|
||||||
|
local overflowX = element.overflowX or element.overflow
|
||||||
|
local overflowY = element.overflowY or element.overflow
|
||||||
|
if overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto" then
|
||||||
|
element._scrollManager:handleTouchMove(touchX, touchY)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handle touch release events from LÖVE's touch input system
|
||||||
|
--- Routes touch to the owning element and cleans up touch ownership tracking
|
||||||
|
--- Hook this to love.touchreleased() to properly end touch interactions
|
||||||
|
---@param id lightuserdata Touch identifier from LÖVE
|
||||||
|
---@param x number Touch X position in screen coordinates
|
||||||
|
---@param y number Touch Y position in screen coordinates
|
||||||
|
---@param dx number X distance moved since last event
|
||||||
|
---@param dy number Y distance moved since last event
|
||||||
|
---@param pressure number Touch pressure (0-1, if supported by device)
|
||||||
|
function flexlove.touchreleased(id, x, y, dx, dy, pressure)
|
||||||
|
local touchId = tostring(id)
|
||||||
|
pressure = pressure or 1.0
|
||||||
|
|
||||||
|
-- Apply base scaling if configured
|
||||||
|
local touchX, touchY = x, y
|
||||||
|
if flexlove.baseScale then
|
||||||
|
touchX = x / flexlove.scaleFactors.x
|
||||||
|
touchY = y / flexlove.scaleFactors.y
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Route to owning element
|
||||||
|
local element = flexlove._touchOwners[touchId]
|
||||||
|
if element then
|
||||||
|
-- Create and route touch event
|
||||||
|
local touchEvent = InputEvent.fromTouch(id, touchX, touchY, "ended", pressure)
|
||||||
|
element:handleTouchEvent(touchEvent)
|
||||||
|
|
||||||
|
-- Feed to shared gesture recognizer
|
||||||
|
if flexlove._gestureRecognizer then
|
||||||
|
local gestures = flexlove._gestureRecognizer:processTouchEvent(touchEvent)
|
||||||
|
if gestures then
|
||||||
|
for _, gesture in ipairs(gestures) do
|
||||||
|
element:handleGesture(gesture)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Route to scroll manager for scrollable elements
|
||||||
|
if element._scrollManager then
|
||||||
|
local overflowX = element.overflowX or element.overflow
|
||||||
|
local overflowY = element.overflowY or element.overflow
|
||||||
|
if overflowX == "scroll" or overflowX == "auto" or overflowY == "scroll" or overflowY == "auto" then
|
||||||
|
element._scrollManager:handleTouchRelease()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clean up touch ownership (touch is complete)
|
||||||
|
flexlove._touchOwners[touchId] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the number of currently active touches being tracked
|
||||||
|
---@return number count Number of active touch points
|
||||||
|
function flexlove.getActiveTouchCount()
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(flexlove._touchOwners) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get the element that currently owns a specific touch
|
||||||
|
---@param touchId string|lightuserdata Touch identifier
|
||||||
|
---@return Element|nil element The element owning this touch, or nil
|
||||||
|
function flexlove.getTouchOwner(touchId)
|
||||||
|
return flexlove._touchOwners[tostring(touchId)]
|
||||||
|
end
|
||||||
|
|
||||||
--- Clean up all UI elements and reset FlexLove to initial state when changing scenes or shutting down
|
--- Clean up all UI elements and reset FlexLove to initial state when changing scenes or shutting down
|
||||||
--- Use this to prevent memory leaks when transitioning between game states or menus
|
--- Use this to prevent memory leaks when transitioning between game states or menus
|
||||||
function flexlove.destroy()
|
function flexlove.destroy()
|
||||||
@@ -1131,6 +1377,12 @@ function flexlove.destroy()
|
|||||||
flexlove._canvasDimensions = { width = 0, height = 0 }
|
flexlove._canvasDimensions = { width = 0, height = 0 }
|
||||||
Context.clearFocus()
|
Context.clearFocus()
|
||||||
StateManager:reset()
|
StateManager:reset()
|
||||||
|
|
||||||
|
-- Clean up touch state
|
||||||
|
flexlove._touchOwners = {}
|
||||||
|
if flexlove._gestureRecognizer then
|
||||||
|
flexlove._gestureRecognizer:reset()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Create a new UI element with flexbox layout, styling, and interaction capabilities
|
--- Create a new UI element with flexbox layout, styling, and interaction capabilities
|
||||||
|
|||||||
@@ -160,6 +160,12 @@
|
|||||||
---@field _mouseDownPosition number? -- Internal: mouse down position for drag tracking
|
---@field _mouseDownPosition number? -- Internal: mouse down position for drag tracking
|
||||||
---@field _textDragOccurred boolean? -- Internal: whether text drag occurred
|
---@field _textDragOccurred boolean? -- Internal: whether text drag occurred
|
||||||
---@field customDraw fun(element:Element)? -- Custom rendering callback called after standard rendering but before visual feedback (default: nil)
|
---@field customDraw fun(element:Element)? -- Custom rendering callback called after standard rendering but before visual feedback (default: nil)
|
||||||
|
---@field onTouchEvent fun(element:Element, touchEvent:InputEvent)? -- Callback for touch-specific events
|
||||||
|
---@field onTouchEventDeferred boolean? -- Whether onTouchEvent callback should be deferred (default: false)
|
||||||
|
---@field onGesture fun(element:Element, gesture:table)? -- Callback for recognized gestures
|
||||||
|
---@field onGestureDeferred boolean? -- Whether onGesture callback should be deferred (default: false)
|
||||||
|
---@field touchEnabled boolean -- Whether the element responds to touch events (default: true)
|
||||||
|
---@field multiTouchEnabled boolean -- Whether the element supports multiple simultaneous touches (default: false)
|
||||||
---@field animation table? -- Animation instance for this element
|
---@field animation table? -- Animation instance for this element
|
||||||
local Element = {}
|
local Element = {}
|
||||||
Element.__index = Element
|
Element.__index = Element
|
||||||
@@ -366,6 +372,14 @@ function Element.new(props)
|
|||||||
|
|
||||||
self.customDraw = props.customDraw -- Custom rendering callback
|
self.customDraw = props.customDraw -- Custom rendering callback
|
||||||
|
|
||||||
|
-- Touch event properties
|
||||||
|
self.onTouchEvent = props.onTouchEvent
|
||||||
|
self.onTouchEventDeferred = props.onTouchEventDeferred or false
|
||||||
|
self.onGesture = props.onGesture
|
||||||
|
self.onGestureDeferred = props.onGestureDeferred or false
|
||||||
|
self.touchEnabled = props.touchEnabled ~= false -- Default true
|
||||||
|
self.multiTouchEnabled = props.multiTouchEnabled or false -- Default false
|
||||||
|
|
||||||
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
|
-- Initialize state manager ID for immediate mode (use self.id which may be auto-generated)
|
||||||
self._stateId = self.id
|
self._stateId = self.id
|
||||||
|
|
||||||
@@ -373,6 +387,12 @@ function Element.new(props)
|
|||||||
local eventHandlerConfig = {
|
local eventHandlerConfig = {
|
||||||
onEvent = self.onEvent,
|
onEvent = self.onEvent,
|
||||||
onEventDeferred = props.onEventDeferred,
|
onEventDeferred = props.onEventDeferred,
|
||||||
|
onTouchEvent = self.onTouchEvent,
|
||||||
|
onTouchEventDeferred = self.onTouchEventDeferred,
|
||||||
|
onGesture = self.onGesture,
|
||||||
|
onGestureDeferred = self.onGestureDeferred,
|
||||||
|
touchEnabled = self.touchEnabled,
|
||||||
|
multiTouchEnabled = self.multiTouchEnabled,
|
||||||
}
|
}
|
||||||
if self._elementMode == "immediate" and self._stateId and self._stateId ~= "" then
|
if self._elementMode == "immediate" and self._stateId and self._stateId ~= "" then
|
||||||
local state = Element._StateManager.getState(self._stateId)
|
local state = Element._StateManager.getState(self._stateId)
|
||||||
@@ -2603,6 +2623,10 @@ function Element:destroy()
|
|||||||
|
|
||||||
-- Clear onEvent to prevent closure leaks
|
-- Clear onEvent to prevent closure leaks
|
||||||
self.onEvent = nil
|
self.onEvent = nil
|
||||||
|
|
||||||
|
-- Clear touch callbacks to prevent closure leaks
|
||||||
|
self.onTouchEvent = nil
|
||||||
|
self.onGesture = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Draw element and its children
|
--- Draw element and its children
|
||||||
@@ -3082,6 +3106,39 @@ function Element:update(dt)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Handle a touch event directly (for external touch routing)
|
||||||
|
--- Invokes both onEvent and onTouchEvent callbacks if set
|
||||||
|
---@param touchEvent InputEvent The touch event to handle
|
||||||
|
function Element:handleTouchEvent(touchEvent)
|
||||||
|
if not self.touchEnabled or self.disabled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self._eventHandler then
|
||||||
|
self._eventHandler:_invokeCallback(self, touchEvent)
|
||||||
|
self._eventHandler:_invokeTouchCallback(self, touchEvent)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Handle a gesture event (from GestureRecognizer or external routing)
|
||||||
|
---@param gesture table The gesture data (type, position, velocity, etc.)
|
||||||
|
function Element:handleGesture(gesture)
|
||||||
|
if not self.touchEnabled or self.disabled then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if self._eventHandler then
|
||||||
|
self._eventHandler:_invokeGestureCallback(self, gesture)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Get active touches currently tracked on this element
|
||||||
|
---@return table<string, table> Active touches keyed by touch ID
|
||||||
|
function Element:getTouches()
|
||||||
|
if self._eventHandler then
|
||||||
|
return self._eventHandler:getActiveTouches()
|
||||||
|
end
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
|
||||||
---@param newViewportWidth number
|
---@param newViewportWidth number
|
||||||
---@param newViewportHeight number
|
---@param newViewportHeight number
|
||||||
function Element:recalculateUnits(newViewportWidth, newViewportHeight)
|
function Element:recalculateUnits(newViewportWidth, newViewportHeight)
|
||||||
@@ -4066,6 +4123,8 @@ function Element:_cleanup()
|
|||||||
self.onEnter = nil
|
self.onEnter = nil
|
||||||
self.onImageLoad = nil
|
self.onImageLoad = nil
|
||||||
self.onImageError = nil
|
self.onImageError = nil
|
||||||
|
self.onTouchEvent = nil
|
||||||
|
self.onGesture = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
return Element
|
return Element
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
---@class EventHandler
|
---@class EventHandler
|
||||||
---@field onEvent fun(element:Element, event:InputEvent)?
|
---@field onEvent fun(element:Element, event:InputEvent)?
|
||||||
---@field onEventDeferred boolean?
|
---@field onEventDeferred boolean?
|
||||||
|
---@field onTouchEvent fun(element:Element, touchEvent:InputEvent)? -- Touch-specific callback
|
||||||
|
---@field onTouchEventDeferred boolean? -- Whether onTouchEvent is deferred
|
||||||
|
---@field onGesture fun(element:Element, gesture:table)? -- Gesture callback
|
||||||
|
---@field onGestureDeferred boolean? -- Whether onGesture is deferred
|
||||||
|
---@field touchEnabled boolean -- Whether touch events are processed (default: true)
|
||||||
|
---@field multiTouchEnabled boolean -- Whether multi-touch is supported (default: false)
|
||||||
---@field _pressed table<number, boolean>
|
---@field _pressed table<number, boolean>
|
||||||
---@field _lastClickTime number?
|
---@field _lastClickTime number?
|
||||||
---@field _lastClickButton number?
|
---@field _lastClickButton number?
|
||||||
@@ -39,6 +45,12 @@ function EventHandler.new(config)
|
|||||||
|
|
||||||
self.onEvent = config.onEvent
|
self.onEvent = config.onEvent
|
||||||
self.onEventDeferred = config.onEventDeferred
|
self.onEventDeferred = config.onEventDeferred
|
||||||
|
self.onTouchEvent = config.onTouchEvent
|
||||||
|
self.onTouchEventDeferred = config.onTouchEventDeferred or false
|
||||||
|
self.onGesture = config.onGesture
|
||||||
|
self.onGestureDeferred = config.onGestureDeferred or false
|
||||||
|
self.touchEnabled = config.touchEnabled ~= false -- Default true
|
||||||
|
self.multiTouchEnabled = config.multiTouchEnabled or false -- Default false
|
||||||
|
|
||||||
self._pressed = config._pressed or {}
|
self._pressed = config._pressed or {}
|
||||||
|
|
||||||
@@ -462,7 +474,7 @@ function EventHandler:processTouchEvents(element)
|
|||||||
local activeTouchIds = {}
|
local activeTouchIds = {}
|
||||||
|
|
||||||
-- Check if element can process events
|
-- Check if element can process events
|
||||||
local canProcessEvents = (self.onEvent or element.editable) and not element.disabled
|
local canProcessEvents = (self.onEvent or self.onTouchEvent or element.editable) and not element.disabled and self.touchEnabled
|
||||||
|
|
||||||
if not canProcessEvents then
|
if not canProcessEvents then
|
||||||
if EventHandler._Performance and EventHandler._Performance.enabled then
|
if EventHandler._Performance and EventHandler._Performance.enabled then
|
||||||
@@ -483,6 +495,12 @@ function EventHandler:processTouchEvents(element)
|
|||||||
activeTouches[tostring(id)] = true
|
activeTouches[tostring(id)] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Count active tracked touches for multi-touch filtering
|
||||||
|
local trackedTouchCount = 0
|
||||||
|
for _ in pairs(self._touches) do
|
||||||
|
trackedTouchCount = trackedTouchCount + 1
|
||||||
|
end
|
||||||
|
|
||||||
-- Process active touches
|
-- Process active touches
|
||||||
for _, id in ipairs(touches) do
|
for _, id in ipairs(touches) do
|
||||||
local touchId = tostring(id)
|
local touchId = tostring(id)
|
||||||
@@ -494,8 +512,15 @@ function EventHandler:processTouchEvents(element)
|
|||||||
|
|
||||||
if isInside then
|
if isInside then
|
||||||
if not self._touches[touchId] then
|
if not self._touches[touchId] then
|
||||||
|
-- Multi-touch filtering: reject new touches when multiTouchEnabled=false
|
||||||
|
-- and we already have an active touch
|
||||||
|
if not self.multiTouchEnabled and trackedTouchCount > 0 then
|
||||||
|
-- Skip this new touch (single-touch mode, already tracking one)
|
||||||
|
else
|
||||||
-- New touch began
|
-- New touch began
|
||||||
self:_handleTouchBegan(element, touchId, tx, ty, pressure)
|
self:_handleTouchBegan(element, touchId, tx, ty, pressure)
|
||||||
|
trackedTouchCount = trackedTouchCount + 1
|
||||||
|
end
|
||||||
else
|
else
|
||||||
-- Touch moved
|
-- Touch moved
|
||||||
self:_handleTouchMoved(element, touchId, tx, ty, pressure)
|
self:_handleTouchMoved(element, touchId, tx, ty, pressure)
|
||||||
@@ -561,6 +586,7 @@ function EventHandler:_handleTouchBegan(element, touchId, x, y, pressure)
|
|||||||
touchEvent.dx = 0
|
touchEvent.dx = 0
|
||||||
touchEvent.dy = 0
|
touchEvent.dy = 0
|
||||||
self:_invokeCallback(element, touchEvent)
|
self:_invokeCallback(element, touchEvent)
|
||||||
|
self:_invokeTouchCallback(element, touchEvent)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Handle touch moved event
|
--- Handle touch moved event
|
||||||
@@ -607,6 +633,7 @@ function EventHandler:_handleTouchMoved(element, touchId, x, y, pressure)
|
|||||||
touchEvent.dx = dx
|
touchEvent.dx = dx
|
||||||
touchEvent.dy = dy
|
touchEvent.dy = dy
|
||||||
self:_invokeCallback(element, touchEvent)
|
self:_invokeCallback(element, touchEvent)
|
||||||
|
self:_invokeTouchCallback(element, touchEvent)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -634,6 +661,7 @@ function EventHandler:_handleTouchEnded(element, touchId, x, y, pressure)
|
|||||||
touchEvent.dx = dx
|
touchEvent.dx = dx
|
||||||
touchEvent.dy = dy
|
touchEvent.dy = dy
|
||||||
self:_invokeCallback(element, touchEvent)
|
self:_invokeCallback(element, touchEvent)
|
||||||
|
self:_invokeTouchCallback(element, touchEvent)
|
||||||
|
|
||||||
-- Cleanup touch state
|
-- Cleanup touch state
|
||||||
self:_cleanupTouch(touchId)
|
self:_cleanupTouch(touchId)
|
||||||
@@ -709,4 +737,52 @@ function EventHandler:_invokeCallback(element, event)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Invoke the onTouchEvent callback, optionally deferring it
|
||||||
|
---@param element Element The element that triggered the event
|
||||||
|
---@param event InputEvent The touch event data
|
||||||
|
function EventHandler:_invokeTouchCallback(element, event)
|
||||||
|
if not self.onTouchEvent then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.onTouchEventDeferred then
|
||||||
|
local FlexLove = package.loaded["FlexLove"] or package.loaded["libs.FlexLove"]
|
||||||
|
if FlexLove and FlexLove.deferCallback then
|
||||||
|
FlexLove.deferCallback(function()
|
||||||
|
self.onTouchEvent(element, event)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
EventHandler._ErrorHandler:error("EventHandler", "SYS_003", {
|
||||||
|
eventType = event.type,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.onTouchEvent(element, event)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Invoke the onGesture callback, optionally deferring it
|
||||||
|
---@param element Element The element that triggered the event
|
||||||
|
---@param gesture table The gesture data from GestureRecognizer
|
||||||
|
function EventHandler:_invokeGestureCallback(element, gesture)
|
||||||
|
if not self.onGesture then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.onGestureDeferred then
|
||||||
|
local FlexLove = package.loaded["FlexLove"] or package.loaded["libs.FlexLove"]
|
||||||
|
if FlexLove and FlexLove.deferCallback then
|
||||||
|
FlexLove.deferCallback(function()
|
||||||
|
self.onGesture(element, gesture)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
EventHandler._ErrorHandler:error("EventHandler", "SYS_003", {
|
||||||
|
gestureType = gesture.type,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
self.onGesture(element, gesture)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return EventHandler
|
return EventHandler
|
||||||
|
|||||||
@@ -92,10 +92,11 @@ end
|
|||||||
---@param event InputEvent Touch event
|
---@param event InputEvent Touch event
|
||||||
function GestureRecognizer:processTouchEvent(event)
|
function GestureRecognizer:processTouchEvent(event)
|
||||||
if not event.touchId then
|
if not event.touchId then
|
||||||
return
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local touchId = event.touchId
|
local touchId = event.touchId
|
||||||
|
local gestures = {}
|
||||||
|
|
||||||
-- Update touch state
|
-- Update touch state
|
||||||
if event.type == "touchpress" then
|
if event.type == "touchpress" then
|
||||||
@@ -122,13 +123,17 @@ function GestureRecognizer:processTouchEvent(event)
|
|||||||
touch.phase = "moved"
|
touch.phase = "moved"
|
||||||
|
|
||||||
-- Update gesture detection
|
-- Update gesture detection
|
||||||
self:_detectPan(touchId, event)
|
local panGesture = self:_detectPan(touchId, event)
|
||||||
self:_detectSwipe(touchId, event)
|
if panGesture then table.insert(gestures, panGesture) end
|
||||||
|
local swipeGesture = self:_detectSwipe(touchId, event)
|
||||||
|
if swipeGesture then table.insert(gestures, swipeGesture) end
|
||||||
|
|
||||||
-- Multi-touch gestures
|
-- Multi-touch gestures
|
||||||
if self:_getTouchCount() >= 2 then
|
if self:_getTouchCount() >= 2 then
|
||||||
self:_detectPinch(event)
|
local pinchGesture = self:_detectPinch(event)
|
||||||
self:_detectRotate(event)
|
if pinchGesture then table.insert(gestures, pinchGesture) end
|
||||||
|
local rotateGesture = self:_detectRotate(event)
|
||||||
|
if rotateGesture then table.insert(gestures, rotateGesture) end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -138,9 +143,12 @@ function GestureRecognizer:processTouchEvent(event)
|
|||||||
touch.phase = "ended"
|
touch.phase = "ended"
|
||||||
|
|
||||||
-- Finalize gesture detection
|
-- Finalize gesture detection
|
||||||
self:_detectTapEnded(touchId, event)
|
local tapGesture = self:_detectTapEnded(touchId, event)
|
||||||
self:_detectSwipeEnded(touchId, event)
|
if tapGesture then table.insert(gestures, tapGesture) end
|
||||||
self:_detectPanEnded(touchId, event)
|
local swipeGesture = self:_detectSwipeEnded(touchId, event)
|
||||||
|
if swipeGesture then table.insert(gestures, swipeGesture) end
|
||||||
|
local panGesture = self:_detectPanEnded(touchId, event)
|
||||||
|
if panGesture then table.insert(gestures, panGesture) end
|
||||||
|
|
||||||
-- Cleanup touch
|
-- Cleanup touch
|
||||||
self._touches[touchId] = nil
|
self._touches[touchId] = nil
|
||||||
@@ -151,6 +159,8 @@ function GestureRecognizer:processTouchEvent(event)
|
|||||||
self._touches[touchId] = nil
|
self._touches[touchId] = nil
|
||||||
self:_cancelAllGestures()
|
self:_cancelAllGestures()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return #gestures > 0 and gestures or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get number of active touches
|
--- Get number of active touches
|
||||||
|
|||||||
@@ -85,6 +85,12 @@ local AnimationProps = {}
|
|||||||
---@field onTextChangeDeferred boolean? -- Whether onTextChange callback should be deferred (default: false)
|
---@field onTextChangeDeferred boolean? -- Whether onTextChange callback should be deferred (default: false)
|
||||||
---@field onEnter fun(element:Element)? -- Callback when Enter key is pressed
|
---@field onEnter fun(element:Element)? -- Callback when Enter key is pressed
|
||||||
---@field onEnterDeferred boolean? -- Whether onEnter callback should be deferred (default: false)
|
---@field onEnterDeferred boolean? -- Whether onEnter callback should be deferred (default: false)
|
||||||
|
---@field onTouchEvent fun(element:Element, touchEvent:InputEvent)? -- Callback for touch-specific events (touchpress, touchmove, touchrelease)
|
||||||
|
---@field onTouchEventDeferred boolean? -- Whether onTouchEvent callback should be deferred (default: false)
|
||||||
|
---@field onGesture fun(element:Element, gesture:table)? -- Callback for recognized gestures (tap, swipe, pinch, etc.)
|
||||||
|
---@field onGestureDeferred boolean? -- Whether onGesture callback should be deferred (default: false)
|
||||||
|
---@field touchEnabled boolean? -- Whether the element responds to touch events (default: true)
|
||||||
|
---@field multiTouchEnabled boolean? -- Whether the element supports multiple simultaneous touches (default: false)
|
||||||
---@field transform TransformProps? -- Transform properties for animations and styling
|
---@field transform TransformProps? -- Transform properties for animations and styling
|
||||||
---@field transition TransitionProps? -- Transition settings for animations
|
---@field transition TransitionProps? -- Transition settings for animations
|
||||||
---@field customDraw fun(element:Element)? -- Custom rendering callback called after standard rendering but before visual feedback (default: nil)
|
---@field customDraw fun(element:Element)? -- Custom rendering callback called after standard rendering but before visual feedback (default: nil)
|
||||||
|
|||||||
328
testing/__tests__/element_touch_test.lua
Normal file
328
testing/__tests__/element_touch_test.lua
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
local originalSearchers = package.searchers or package.loaders
|
||||||
|
table.insert(originalSearchers, 2, function(modname)
|
||||||
|
if modname:match("^FlexLove%.modules%.") then
|
||||||
|
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
|
||||||
|
return function()
|
||||||
|
return require("modules." .. moduleName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
require("testing.loveStub")
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
|
FlexLove.init()
|
||||||
|
|
||||||
|
TestElementTouch = {}
|
||||||
|
|
||||||
|
function TestElementTouch:setUp()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
love.window.setMode(800, 600)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:tearDown()
|
||||||
|
FlexLove.destroy()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Touch Property Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestElementTouch:test_touchEnabled_defaults_true()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertTrue(element.touchEnabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_touchEnabled_can_be_set_false()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100, touchEnabled = false })
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertFalse(element.touchEnabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_multiTouchEnabled_defaults_false()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100 })
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertFalse(element.multiTouchEnabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_multiTouchEnabled_can_be_set_true()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({ width = 100, height = 100, multiTouchEnabled = true })
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertTrue(element.multiTouchEnabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Touch Callback Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestElementTouch:test_onTouchEvent_callback()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(receivedEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertTrue(#receivedEvents >= 1)
|
||||||
|
luaunit.assertEquals(receivedEvents[1].type, "touchpress")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_onGesture_callback()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedGestures = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function() end,
|
||||||
|
onGesture = function(el, gesture)
|
||||||
|
table.insert(receivedGestures, gesture)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Quick tap
|
||||||
|
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
|
||||||
|
love.timer.step(0.05)
|
||||||
|
FlexLove.touchreleased("t1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
local tapGestures = {}
|
||||||
|
for _, g in ipairs(receivedGestures) do
|
||||||
|
if g.type == "tap" then
|
||||||
|
table.insert(tapGestures, g)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertTrue(#tapGestures >= 1, "Should receive tap gesture callback")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_onEvent_also_receives_touch()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onEvent = function(el, event)
|
||||||
|
table.insert(receivedEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
local touchEvents = {}
|
||||||
|
for _, e in ipairs(receivedEvents) do
|
||||||
|
if e.type == "touchpress" then
|
||||||
|
table.insert(touchEvents, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertTrue(#touchEvents >= 1, "onEvent should receive touch events")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- handleTouchEvent direct tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestElementTouch:test_handleTouchEvent_disabled_element()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
disabled = true,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(receivedEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
local InputEvent = package.loaded["modules.InputEvent"]
|
||||||
|
local touchEvent = InputEvent.fromTouch("t1", 100, 100, "began", 1.0)
|
||||||
|
element:handleTouchEvent(touchEvent)
|
||||||
|
|
||||||
|
luaunit.assertEquals(#receivedEvents, 0, "Disabled element should not receive touch events")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_handleTouchEvent_touchEnabled_false()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
touchEnabled = false,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(receivedEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
local InputEvent = package.loaded["modules.InputEvent"]
|
||||||
|
local touchEvent = InputEvent.fromTouch("t1", 100, 100, "began", 1.0)
|
||||||
|
element:handleTouchEvent(touchEvent)
|
||||||
|
|
||||||
|
luaunit.assertEquals(#receivedEvents, 0, "touchEnabled=false should prevent events")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- handleGesture direct tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestElementTouch:test_handleGesture_fires_callback()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedGestures = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onGesture = function(el, gesture)
|
||||||
|
table.insert(receivedGestures, gesture)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
element:handleGesture({ type = "tap", state = "ended", x = 100, y = 100 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(#receivedGestures, 1)
|
||||||
|
luaunit.assertEquals(receivedGestures[1].type, "tap")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_handleGesture_disabled_element()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedGestures = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
disabled = true,
|
||||||
|
onGesture = function(el, gesture)
|
||||||
|
table.insert(receivedGestures, gesture)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
element:handleGesture({ type = "tap", state = "ended", x = 100, y = 100 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(#receivedGestures, 0, "Disabled element should not receive gestures")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_handleGesture_touchEnabled_false()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedGestures = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
touchEnabled = false,
|
||||||
|
onGesture = function(el, gesture)
|
||||||
|
table.insert(receivedGestures, gesture)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
element:handleGesture({ type = "tap", state = "ended", x = 100, y = 100 })
|
||||||
|
|
||||||
|
luaunit.assertEquals(#receivedGestures, 0, "touchEnabled=false should prevent gestures")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- getTouches tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestElementTouch:test_getTouches_returns_table()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function() end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
local touches = element:getTouches()
|
||||||
|
luaunit.assertEquals(type(touches), "table")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Touch + Gesture combined lifecycle
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestElementTouch:test_touch_pan_lifecycle()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local gestureEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 400,
|
||||||
|
height = 400,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
onGesture = function(el, gesture)
|
||||||
|
table.insert(gestureEvents, gesture)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Simulate a pan gesture: press, move significantly, release
|
||||||
|
FlexLove.touchpressed("t1", 100, 100, 0, 0, 1.0)
|
||||||
|
love.timer.step(0.05)
|
||||||
|
FlexLove.touchmoved("t1", 150, 150, 50, 50, 1.0)
|
||||||
|
love.timer.step(0.05)
|
||||||
|
FlexLove.touchmoved("t1", 200, 200, 50, 50, 1.0)
|
||||||
|
love.timer.step(0.05)
|
||||||
|
FlexLove.touchreleased("t1", 200, 200, 0, 0, 1.0)
|
||||||
|
|
||||||
|
-- Should have received touch events
|
||||||
|
luaunit.assertTrue(#touchEvents >= 3, "Should receive press + move + release touch events")
|
||||||
|
|
||||||
|
-- Should have received pan gestures from GestureRecognizer
|
||||||
|
local panGestures = {}
|
||||||
|
for _, g in ipairs(gestureEvents) do
|
||||||
|
if g.type == "pan" then
|
||||||
|
table.insert(panGestures, g)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertTrue(#panGestures >= 1, "Should receive pan gesture events")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Deferred callbacks
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestElementTouch:test_onTouchEventDeferred_prop_accepted()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
-- Just test that the prop is accepted without error
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEventDeferred = function() end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertNotNil(element, "Element with onTouchEventDeferred should be created")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestElementTouch:test_onGestureDeferred_prop_accepted()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onGestureDeferred = function() end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertNotNil(element, "Element with onGestureDeferred should be created")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
533
testing/__tests__/gesture_recognizer_test.lua
Normal file
533
testing/__tests__/gesture_recognizer_test.lua
Normal file
@@ -0,0 +1,533 @@
|
|||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
local originalSearchers = package.searchers or package.loaders
|
||||||
|
table.insert(originalSearchers, 2, function(modname)
|
||||||
|
if modname:match("^FlexLove%.modules%.") then
|
||||||
|
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
|
||||||
|
return function()
|
||||||
|
return require("modules." .. moduleName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
require("testing.loveStub")
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
|
FlexLove.init()
|
||||||
|
|
||||||
|
local InputEvent = package.loaded["modules.InputEvent"]
|
||||||
|
local GestureRecognizer = package.loaded["modules.GestureRecognizer"]
|
||||||
|
|
||||||
|
TestGestureRecognizer = {}
|
||||||
|
|
||||||
|
function TestGestureRecognizer:setUp()
|
||||||
|
self.recognizer = GestureRecognizer.new({}, { InputEvent = InputEvent, utils = {} })
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:tearDown()
|
||||||
|
self.recognizer:reset()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper: create touch event
|
||||||
|
local function touchEvent(id, x, y, phase, time)
|
||||||
|
if time then love.timer.setTime(time) end
|
||||||
|
local event = InputEvent.fromTouch(id, x, y, phase, 1.0)
|
||||||
|
return event
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Tap Gesture Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_tap_detected()
|
||||||
|
local event1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(event1)
|
||||||
|
|
||||||
|
local event2 = touchEvent("t1", 100, 100, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(event2)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures, "Tap gesture should be detected")
|
||||||
|
luaunit.assertEquals(gestures[1].type, "tap")
|
||||||
|
luaunit.assertEquals(gestures[1].state, "ended")
|
||||||
|
luaunit.assertEquals(gestures[1].x, 100)
|
||||||
|
luaunit.assertEquals(gestures[1].y, 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_tap_not_detected_when_too_slow()
|
||||||
|
local event1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(event1)
|
||||||
|
|
||||||
|
-- Release after tapMaxDuration (0.3s)
|
||||||
|
local event2 = touchEvent("t1", 100, 100, "ended", 0.5)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(event2)
|
||||||
|
|
||||||
|
-- Should not be a tap (too slow)
|
||||||
|
if gestures then
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
luaunit.assertNotEquals(g.type, "tap", "Slow touch should not be tap")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_tap_not_detected_when_moved_too_far()
|
||||||
|
local event1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(event1)
|
||||||
|
|
||||||
|
-- Move more than tapMaxMovement (10px)
|
||||||
|
local event2 = touchEvent("t1", 120, 120, "moved", 0.05)
|
||||||
|
self.recognizer:processTouchEvent(event2)
|
||||||
|
|
||||||
|
local event3 = touchEvent("t1", 120, 120, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(event3)
|
||||||
|
|
||||||
|
-- Should not be a tap (moved too far)
|
||||||
|
if gestures then
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "tap" then
|
||||||
|
-- Tap detection checks distance from START to END position
|
||||||
|
local dx = 120 - 100
|
||||||
|
local dy = 120 - 100
|
||||||
|
local dist = math.sqrt(dx*dx + dy*dy)
|
||||||
|
luaunit.assertTrue(dist >= 10, "Movement should exceed tap threshold")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_double_tap_detected()
|
||||||
|
-- First tap
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
local e2 = touchEvent("t1", 100, 100, "ended", 0.05)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
-- Second tap quickly
|
||||||
|
local e3 = touchEvent("t2", 100, 100, "began", 0.15)
|
||||||
|
self.recognizer:processTouchEvent(e3)
|
||||||
|
local e4 = touchEvent("t2", 100, 100, "ended", 0.2)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e4)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures, "Should detect gesture on second tap")
|
||||||
|
-- Should have double_tap
|
||||||
|
local foundDoubleTap = false
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "double_tap" then
|
||||||
|
foundDoubleTap = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertTrue(foundDoubleTap, "Should detect double-tap gesture")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_double_tap_not_detected_when_too_slow()
|
||||||
|
-- First tap
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
local e2 = touchEvent("t1", 100, 100, "ended", 0.05)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
-- Second tap too late (>0.3s interval)
|
||||||
|
local e3 = touchEvent("t2", 100, 100, "began", 0.5)
|
||||||
|
self.recognizer:processTouchEvent(e3)
|
||||||
|
local e4 = touchEvent("t2", 100, 100, "ended", 0.55)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e4)
|
||||||
|
|
||||||
|
-- Should detect tap but NOT double_tap
|
||||||
|
if gestures then
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
luaunit.assertNotEquals(g.type, "double_tap", "Too-slow second tap should not be double-tap")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Pan Gesture Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_pan_began()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
-- Move beyond panMinMovement (5px)
|
||||||
|
local e2 = touchEvent("t1", 110, 110, "moved", 0.05)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures, "Pan should be detected")
|
||||||
|
luaunit.assertEquals(gestures[1].type, "pan")
|
||||||
|
luaunit.assertEquals(gestures[1].state, "began")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_pan_changed()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
-- First move to start pan
|
||||||
|
local e2 = touchEvent("t1", 110, 110, "moved", 0.05)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
-- Continue moving
|
||||||
|
local e3 = touchEvent("t1", 120, 120, "moved", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures)
|
||||||
|
local panChanged = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "pan" and g.state == "changed" then
|
||||||
|
panChanged = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertNotNil(panChanged, "Should detect pan changed")
|
||||||
|
luaunit.assertEquals(panChanged.dx, 20) -- delta from startX=100 (lastX set to startX on began)
|
||||||
|
luaunit.assertEquals(panChanged.dy, 20)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_pan_ended()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
local e2 = touchEvent("t1", 110, 110, "moved", 0.05)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
local e3 = touchEvent("t1", 120, 120, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures)
|
||||||
|
local panEnded = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "pan" and g.state == "ended" then
|
||||||
|
panEnded = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertNotNil(panEnded, "Should detect pan ended")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_pan_not_detected_with_small_movement()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
-- Move less than panMinMovement (5px)
|
||||||
|
local e2 = touchEvent("t1", 102, 102, "moved", 0.05)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
luaunit.assertNil(gestures, "Small movement should not trigger pan")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Swipe Gesture Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_swipe_right()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
-- Fast swipe right (>50px in <0.2s with >200px/s velocity)
|
||||||
|
local e2 = touchEvent("t1", 200, 100, "moved", 0.1)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
local e3 = touchEvent("t1", 200, 100, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures)
|
||||||
|
local swipe = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "swipe" then
|
||||||
|
swipe = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertNotNil(swipe, "Should detect swipe")
|
||||||
|
luaunit.assertEquals(swipe.direction, "right")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_swipe_left()
|
||||||
|
local e1 = touchEvent("t1", 200, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
local e2 = touchEvent("t1", 100, 100, "moved", 0.1)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
local e3 = touchEvent("t1", 100, 100, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures)
|
||||||
|
local swipe = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "swipe" then
|
||||||
|
swipe = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertNotNil(swipe, "Should detect left swipe")
|
||||||
|
luaunit.assertEquals(swipe.direction, "left")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_swipe_down()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
local e2 = touchEvent("t1", 100, 200, "moved", 0.1)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
local e3 = touchEvent("t1", 100, 200, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures)
|
||||||
|
local swipe = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "swipe" then
|
||||||
|
swipe = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertNotNil(swipe, "Should detect down swipe")
|
||||||
|
luaunit.assertEquals(swipe.direction, "down")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_swipe_up()
|
||||||
|
local e1 = touchEvent("t1", 100, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
local e2 = touchEvent("t1", 100, 100, "moved", 0.1)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
local e3 = touchEvent("t1", 100, 100, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures)
|
||||||
|
local swipe = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "swipe" then
|
||||||
|
swipe = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertNotNil(swipe, "Should detect up swipe")
|
||||||
|
luaunit.assertEquals(swipe.direction, "up")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_swipe_not_detected_when_too_slow()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
-- Too slow (>0.2s)
|
||||||
|
local e2 = touchEvent("t1", 200, 100, "moved", 0.3)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
local e3 = touchEvent("t1", 200, 100, "ended", 0.3)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
if gestures then
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
luaunit.assertNotEquals(g.type, "swipe", "Slow movement should not be a swipe")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_swipe_not_detected_when_too_short()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
-- Too short distance (<50px)
|
||||||
|
local e2 = touchEvent("t1", 130, 100, "moved", 0.05)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
local e3 = touchEvent("t1", 130, 100, "ended", 0.05)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
if gestures then
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
luaunit.assertNotEquals(g.type, "swipe", "Short movement should not be a swipe")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Pinch Gesture Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_pinch_detected()
|
||||||
|
-- Two fingers start 100px apart
|
||||||
|
local e1 = touchEvent("t1", 100, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
local e2 = touchEvent("t2", 200, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
-- Move fingers apart to 200px (scale = 2.0)
|
||||||
|
local e3 = touchEvent("t1", 50, 200, "moved", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
local e4 = touchEvent("t2", 250, 200, "moved", 0.1)
|
||||||
|
gestures = self.recognizer:processTouchEvent(e4)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures, "Pinch should be detected")
|
||||||
|
local pinch = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "pinch" then
|
||||||
|
pinch = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
luaunit.assertNotNil(pinch, "Should detect pinch gesture")
|
||||||
|
luaunit.assertTrue(pinch.scale > 1.0, "Scale should be greater than 1.0 for spread")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_pinch_scale_decreases()
|
||||||
|
-- Two fingers start 200px apart
|
||||||
|
local e1 = touchEvent("t1", 50, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
local e2 = touchEvent("t2", 250, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
-- Move fingers closer to 100px (scale = 0.5)
|
||||||
|
local e3 = touchEvent("t1", 100, 200, "moved", 0.1)
|
||||||
|
self.recognizer:processTouchEvent(e3)
|
||||||
|
local e4 = touchEvent("t2", 200, 200, "moved", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e4)
|
||||||
|
|
||||||
|
if gestures then
|
||||||
|
local pinch = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "pinch" then
|
||||||
|
pinch = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if pinch then
|
||||||
|
luaunit.assertTrue(pinch.scale < 1.0, "Scale should be less than 1.0 for pinch")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_pinch_not_detected_with_one_touch()
|
||||||
|
local e1 = touchEvent("t1", 100, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
local e2 = touchEvent("t1", 150, 200, "moved", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
if gestures then
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
luaunit.assertNotEquals(g.type, "pinch", "Single touch should not trigger pinch")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Rotate Gesture Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_rotate_detected()
|
||||||
|
-- Two fingers horizontally
|
||||||
|
local e1 = touchEvent("t1", 100, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
local e2 = touchEvent("t2", 200, 200, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
-- Rotate: move t2 above t1 (significant angle change > 5 degrees)
|
||||||
|
local e3 = touchEvent("t2", 200, 150, "moved", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e3)
|
||||||
|
|
||||||
|
if gestures then
|
||||||
|
local rotate = nil
|
||||||
|
for _, g in ipairs(gestures) do
|
||||||
|
if g.type == "rotate" then
|
||||||
|
rotate = g
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if rotate then
|
||||||
|
luaunit.assertNotNil(rotate.rotation, "Rotate gesture should have rotation angle")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- processTouchEvent return value tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_processTouchEvent_returns_nil_for_no_gesture()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
-- Press alone should not produce gesture
|
||||||
|
luaunit.assertNil(gestures, "Press alone should not produce gesture")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_processTouchEvent_returns_gesture_array()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
local e2 = touchEvent("t1", 100, 100, "ended", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
luaunit.assertNotNil(gestures)
|
||||||
|
luaunit.assertTrue(#gestures >= 1, "Should return array with at least 1 gesture")
|
||||||
|
luaunit.assertEquals(type(gestures[1]), "table")
|
||||||
|
luaunit.assertNotNil(gestures[1].type)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_processTouchEvent_ignores_no_touchId()
|
||||||
|
local event = { type = "touchpress", x = 100, y = 100 } -- No touchId
|
||||||
|
local gestures = self.recognizer:processTouchEvent(event)
|
||||||
|
luaunit.assertNil(gestures)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_touchcancel_cleans_up()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
local e2 = touchEvent("t1", 100, 100, "cancelled", 0.1)
|
||||||
|
local gestures = self.recognizer:processTouchEvent(e2)
|
||||||
|
|
||||||
|
-- After cancel, no touches should remain
|
||||||
|
luaunit.assertEquals(self.recognizer:_getTouchCount(), 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Reset Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_reset_clears_state()
|
||||||
|
local e1 = touchEvent("t1", 100, 100, "began", 0)
|
||||||
|
self.recognizer:processTouchEvent(e1)
|
||||||
|
|
||||||
|
luaunit.assertTrue(self.recognizer:_getTouchCount() > 0)
|
||||||
|
|
||||||
|
self.recognizer:reset()
|
||||||
|
|
||||||
|
luaunit.assertEquals(self.recognizer:_getTouchCount(), 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- Custom Configuration Tests
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_custom_config_overrides_defaults()
|
||||||
|
local custom = GestureRecognizer.new({
|
||||||
|
tapMaxDuration = 1.0,
|
||||||
|
panMinMovement = 20,
|
||||||
|
}, { InputEvent = InputEvent, utils = {} })
|
||||||
|
|
||||||
|
luaunit.assertEquals(custom._config.tapMaxDuration, 1.0)
|
||||||
|
luaunit.assertEquals(custom._config.panMinMovement, 20)
|
||||||
|
-- Defaults for non-overridden values
|
||||||
|
luaunit.assertEquals(custom._config.swipeMinDistance, 50)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- GestureType and GestureState exports
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_gesture_types_exported()
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureType.TAP, "tap")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureType.DOUBLE_TAP, "double_tap")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureType.LONG_PRESS, "long_press")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureType.SWIPE, "swipe")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureType.PAN, "pan")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureType.PINCH, "pinch")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureType.ROTATE, "rotate")
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestGestureRecognizer:test_gesture_states_exported()
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureState.POSSIBLE, "possible")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureState.BEGAN, "began")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureState.CHANGED, "changed")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureState.ENDED, "ended")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureState.CANCELLED, "cancelled")
|
||||||
|
luaunit.assertEquals(GestureRecognizer.GestureState.FAILED, "failed")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
@@ -230,6 +230,7 @@ function TestTouchEvents:testEventHandler_MultiTouch()
|
|||||||
local element = FlexLove.new({
|
local element = FlexLove.new({
|
||||||
width = 200,
|
width = 200,
|
||||||
height = 200,
|
height = 200,
|
||||||
|
multiTouchEnabled = true,
|
||||||
onEvent = function(el, event)
|
onEvent = function(el, event)
|
||||||
table.insert(touchEvents, event)
|
table.insert(touchEvents, event)
|
||||||
end,
|
end,
|
||||||
|
|||||||
528
testing/__tests__/touch_routing_test.lua
Normal file
528
testing/__tests__/touch_routing_test.lua
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
local originalSearchers = package.searchers or package.loaders
|
||||||
|
table.insert(originalSearchers, 2, function(modname)
|
||||||
|
if modname:match("^FlexLove%.modules%.") then
|
||||||
|
local moduleName = modname:gsub("^FlexLove%.modules%.", "")
|
||||||
|
return function()
|
||||||
|
return require("modules." .. moduleName)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
require("testing.loveStub")
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local FlexLove = require("FlexLove")
|
||||||
|
|
||||||
|
FlexLove.init()
|
||||||
|
|
||||||
|
TestTouchRouting = {}
|
||||||
|
|
||||||
|
function TestTouchRouting:setUp()
|
||||||
|
FlexLove.setMode("immediate")
|
||||||
|
love.window.setMode(800, 600)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchRouting:tearDown()
|
||||||
|
FlexLove.destroy()
|
||||||
|
love.touch.getTouches = function() return {} end
|
||||||
|
love.touch.getPosition = function() return 0, 0 end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: touchpressed routes to element at position
|
||||||
|
function TestTouchRouting:test_touchpressed_routes_to_element()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertTrue(#touchEvents >= 1, "Should receive touchpress event")
|
||||||
|
luaunit.assertEquals(touchEvents[1].type, "touchpress")
|
||||||
|
luaunit.assertEquals(touchEvents[1].touchId, "touch1")
|
||||||
|
luaunit.assertEquals(touchEvents[1].x, 100)
|
||||||
|
luaunit.assertEquals(touchEvents[1].y, 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: touchmoved routes to owning element
|
||||||
|
function TestTouchRouting:test_touchmoved_routes_to_owner()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
FlexLove.touchmoved("touch1", 150, 150, 50, 50, 1.0)
|
||||||
|
|
||||||
|
-- Filter for move events
|
||||||
|
local moveEvents = {}
|
||||||
|
for _, e in ipairs(touchEvents) do
|
||||||
|
if e.type == "touchmove" then
|
||||||
|
table.insert(moveEvents, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(#moveEvents >= 1, "Should receive touchmove event")
|
||||||
|
luaunit.assertEquals(moveEvents[1].x, 150)
|
||||||
|
luaunit.assertEquals(moveEvents[1].y, 150)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: touchreleased routes to owning element and cleans up
|
||||||
|
function TestTouchRouting:test_touchreleased_routes_and_cleans_up()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
luaunit.assertNotNil(FlexLove.getTouchOwner("touch1"), "Touch should be owned")
|
||||||
|
|
||||||
|
FlexLove.touchreleased("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "Touch ownership should be cleaned up")
|
||||||
|
|
||||||
|
-- Filter for release events
|
||||||
|
local releaseEvents = {}
|
||||||
|
for _, e in ipairs(touchEvents) do
|
||||||
|
if e.type == "touchrelease" then
|
||||||
|
table.insert(releaseEvents, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(#releaseEvents >= 1, "Should receive touchrelease event")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Touch ownership persists — move events route even outside element bounds
|
||||||
|
function TestTouchRouting:test_touch_ownership_persists_outside_bounds()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Press inside element
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
-- Move far outside element bounds
|
||||||
|
FlexLove.touchmoved("touch1", 500, 500, 400, 400, 1.0)
|
||||||
|
|
||||||
|
-- Should still receive the move event due to ownership
|
||||||
|
local moveEvents = {}
|
||||||
|
for _, e in ipairs(touchEvents) do
|
||||||
|
if e.type == "touchmove" then
|
||||||
|
table.insert(moveEvents, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(#moveEvents >= 1, "Move event should route to owner even outside bounds")
|
||||||
|
luaunit.assertEquals(moveEvents[1].x, 500)
|
||||||
|
luaunit.assertEquals(moveEvents[1].y, 500)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Touch outside all elements creates no ownership
|
||||||
|
function TestTouchRouting:test_touch_outside_elements_no_ownership()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 100,
|
||||||
|
height = 100,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Press outside element bounds
|
||||||
|
FlexLove.touchpressed("touch1", 500, 500, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "No element should own touch outside bounds")
|
||||||
|
luaunit.assertEquals(#touchEvents, 0, "No events should fire for touch outside bounds")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Multiple touches route to different elements
|
||||||
|
function TestTouchRouting:test_multi_touch_different_elements()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local events1 = {}
|
||||||
|
local events2 = {}
|
||||||
|
-- Two elements side by side (default row layout)
|
||||||
|
local container = FlexLove.new({
|
||||||
|
width = 400,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
local element1 = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(events1, event)
|
||||||
|
end,
|
||||||
|
parent = container,
|
||||||
|
})
|
||||||
|
local element2 = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(events2, event)
|
||||||
|
end,
|
||||||
|
parent = container,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Touch element1 (at x=0..200, y=0..200)
|
||||||
|
FlexLove.touchpressed("touch1", 50, 100, 0, 0, 1.0)
|
||||||
|
-- Touch element2 (at x=200..400, y=0..200)
|
||||||
|
FlexLove.touchpressed("touch2", 300, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertTrue(#events1 >= 1, "Element1 should receive touch event")
|
||||||
|
luaunit.assertTrue(#events2 >= 1, "Element2 should receive touch event")
|
||||||
|
luaunit.assertEquals(events1[1].touchId, "touch1")
|
||||||
|
luaunit.assertEquals(events2[1].touchId, "touch2")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Z-index ordering — higher z element receives touch
|
||||||
|
function TestTouchRouting:test_z_index_ordering()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local eventsLow = {}
|
||||||
|
local eventsHigh = {}
|
||||||
|
-- Lower z element
|
||||||
|
local low = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
z = 1,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(eventsLow, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
-- Higher z element overlapping
|
||||||
|
local high = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
z = 10,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(eventsHigh, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Touch overlapping area
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertTrue(#eventsHigh >= 1, "Higher z element should receive touch")
|
||||||
|
luaunit.assertEquals(#eventsLow, 0, "Lower z element should NOT receive touch")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Disabled element does not receive touch
|
||||||
|
function TestTouchRouting:test_disabled_element_no_touch()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
disabled = true,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertEquals(#touchEvents, 0, "Disabled element should not receive touch events")
|
||||||
|
luaunit.assertNil(FlexLove.getTouchOwner("touch1"))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: getActiveTouchCount tracks active touches
|
||||||
|
function TestTouchRouting:test_getActiveTouchCount()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 800,
|
||||||
|
height = 600,
|
||||||
|
onTouchEvent = function() end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 1)
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch2", 200, 200, 0, 0, 1.0)
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 2)
|
||||||
|
|
||||||
|
FlexLove.touchreleased("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 1)
|
||||||
|
|
||||||
|
FlexLove.touchreleased("touch2", 200, 200, 0, 0, 1.0)
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: getTouchOwner returns correct element
|
||||||
|
function TestTouchRouting:test_getTouchOwner()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
id = "owner-test",
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function() end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
luaunit.assertNil(FlexLove.getTouchOwner("touch1"))
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
local owner = FlexLove.getTouchOwner("touch1")
|
||||||
|
luaunit.assertNotNil(owner)
|
||||||
|
luaunit.assertEquals(owner.id, "owner-test")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: destroy() cleans up touch state
|
||||||
|
function TestTouchRouting:test_destroy_cleans_touch_state()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function() end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 1)
|
||||||
|
|
||||||
|
FlexLove.destroy()
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Touch routing with onEvent (not just onTouchEvent)
|
||||||
|
function TestTouchRouting:test_onEvent_receives_touch_events()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local allEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onEvent = function(el, event)
|
||||||
|
table.insert(allEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
-- onEvent should receive touch events via handleTouchEvent -> _invokeCallback
|
||||||
|
local touchPressEvents = {}
|
||||||
|
for _, e in ipairs(allEvents) do
|
||||||
|
if e.type == "touchpress" then
|
||||||
|
table.insert(touchPressEvents, e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(#touchPressEvents >= 1, "onEvent should receive touchpress events")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Touch routing with onGesture callback
|
||||||
|
function TestTouchRouting:test_gesture_routing()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local gestureEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function() end,
|
||||||
|
onGesture = function(el, gesture)
|
||||||
|
table.insert(gestureEvents, gesture)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Simulate a quick tap (press and release at same position within threshold)
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
-- Small time step to avoid zero dt
|
||||||
|
love.timer.step(0.05)
|
||||||
|
FlexLove.touchreleased("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
-- GestureRecognizer should detect a tap gesture
|
||||||
|
local tapGestures = {}
|
||||||
|
for _, g in ipairs(gestureEvents) do
|
||||||
|
if g.type == "tap" then
|
||||||
|
table.insert(tapGestures, g)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertTrue(#tapGestures >= 1, "Should detect tap gesture from press+release")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: touchpressed with no onTouchEvent but onGesture — should still find element
|
||||||
|
function TestTouchRouting:test_element_with_only_onGesture()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local gestureEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onGesture = function(el, gesture)
|
||||||
|
table.insert(gestureEvents, gesture)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
luaunit.assertNotNil(FlexLove.getTouchOwner("touch1"), "Element with onGesture should be found")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: touchEnabled=false prevents touch routing
|
||||||
|
function TestTouchRouting:test_touchEnabled_false_prevents_routing()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
touchEnabled = false,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "touchEnabled=false should prevent ownership")
|
||||||
|
luaunit.assertEquals(#touchEvents, 0, "touchEnabled=false should prevent events")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Complete touch lifecycle (press, move, release)
|
||||||
|
function TestTouchRouting:test_full_lifecycle()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local phases = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(phases, event.type)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
FlexLove.touchmoved("touch1", 110, 110, 10, 10, 1.0)
|
||||||
|
FlexLove.touchmoved("touch1", 120, 120, 10, 10, 1.0)
|
||||||
|
FlexLove.touchreleased("touch1", 120, 120, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertEquals(phases[1], "touchpress")
|
||||||
|
luaunit.assertEquals(phases[2], "touchmove")
|
||||||
|
luaunit.assertEquals(phases[3], "touchmove")
|
||||||
|
luaunit.assertEquals(phases[4], "touchrelease")
|
||||||
|
luaunit.assertEquals(#phases, 4)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Orphaned move/release with no owner (no crash)
|
||||||
|
function TestTouchRouting:test_orphaned_move_release_no_crash()
|
||||||
|
-- Move and release events with no prior press should not crash
|
||||||
|
FlexLove.touchmoved("ghost_touch", 100, 100, 0, 0, 1.0)
|
||||||
|
FlexLove.touchreleased("ghost_touch", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertEquals(FlexLove.getActiveTouchCount(), 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Pressure value is passed through
|
||||||
|
function TestTouchRouting:test_pressure_passthrough()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local receivedPressure = nil
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
if event.type == "touchpress" then
|
||||||
|
receivedPressure = event.pressure
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 0.75)
|
||||||
|
luaunit.assertAlmostEquals(receivedPressure, 0.75, 0.01)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Retained mode touch routing
|
||||||
|
function TestTouchRouting:test_retained_mode_routing()
|
||||||
|
FlexLove.setMode("retained")
|
||||||
|
|
||||||
|
local touchEvents = {}
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(touchEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
luaunit.assertTrue(#touchEvents >= 1, "Touch routing should work in retained mode")
|
||||||
|
luaunit.assertEquals(touchEvents[1].type, "touchpress")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Child element receives touch over parent
|
||||||
|
function TestTouchRouting:test_child_receives_touch_over_parent()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
local parentEvents = {}
|
||||||
|
local childEvents = {}
|
||||||
|
local parent = FlexLove.new({
|
||||||
|
width = 400,
|
||||||
|
height = 400,
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(parentEvents, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
local child = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
z = 1, -- Ensure child has higher z than parent
|
||||||
|
onTouchEvent = function(el, event)
|
||||||
|
table.insert(childEvents, event)
|
||||||
|
end,
|
||||||
|
parent = parent,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
-- Touch within child area (which is also within parent)
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
|
||||||
|
-- Child has explicit higher z, should receive touch
|
||||||
|
luaunit.assertTrue(#childEvents >= 1,
|
||||||
|
string.format("Child should receive touch (child=%d, parent=%d, topElements=%d)",
|
||||||
|
#childEvents, #parentEvents, #FlexLove.topElements))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Test: Element with no callbacks not found by touch routing
|
||||||
|
function TestTouchRouting:test_non_interactive_element_ignored()
|
||||||
|
FlexLove.beginFrame()
|
||||||
|
-- Element with no onEvent, onTouchEvent, or onGesture
|
||||||
|
local element = FlexLove.new({
|
||||||
|
width = 200,
|
||||||
|
height = 200,
|
||||||
|
})
|
||||||
|
FlexLove.endFrame()
|
||||||
|
|
||||||
|
FlexLove.touchpressed("touch1", 100, 100, 0, 0, 1.0)
|
||||||
|
luaunit.assertNil(FlexLove.getTouchOwner("touch1"), "Non-interactive element should not capture touch")
|
||||||
|
end
|
||||||
|
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
475
testing/__tests__/touch_scroll_test.lua
Normal file
475
testing/__tests__/touch_scroll_test.lua
Normal file
@@ -0,0 +1,475 @@
|
|||||||
|
package.path = package.path .. ";./?.lua;./modules/?.lua"
|
||||||
|
|
||||||
|
require("testing.loveStub")
|
||||||
|
local luaunit = require("testing.luaunit")
|
||||||
|
local ErrorHandler = require("modules.ErrorHandler")
|
||||||
|
|
||||||
|
-- Initialize ErrorHandler
|
||||||
|
ErrorHandler.init({})
|
||||||
|
local ScrollManager = require("modules.ScrollManager")
|
||||||
|
local Color = require("modules.Color")
|
||||||
|
local utils = require("modules.utils")
|
||||||
|
|
||||||
|
-- Initialize ScrollManager with ErrorHandler
|
||||||
|
ScrollManager.init({ ErrorHandler = ErrorHandler })
|
||||||
|
|
||||||
|
-- Helper to create ScrollManager with touch config
|
||||||
|
local function createTouchScrollManager(config)
|
||||||
|
config = config or {}
|
||||||
|
config.overflow = config.overflow or "scroll"
|
||||||
|
return ScrollManager.new(config, {
|
||||||
|
Color = Color,
|
||||||
|
utils = utils,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Helper to create mock element with content taller than container
|
||||||
|
local function createMockElement(width, height, contentWidth, contentHeight)
|
||||||
|
local children = {}
|
||||||
|
-- Create a single child that represents all content
|
||||||
|
table.insert(children, {
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = contentWidth or 200,
|
||||||
|
height = contentHeight or 600,
|
||||||
|
margin = { top = 0, right = 0, bottom = 0, left = 0 },
|
||||||
|
_explicitlyAbsolute = false,
|
||||||
|
getBorderBoxWidth = function(self) return self.width end,
|
||||||
|
getBorderBoxHeight = function(self) return self.height end,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
x = 0,
|
||||||
|
y = 0,
|
||||||
|
width = width or 200,
|
||||||
|
height = height or 300,
|
||||||
|
padding = { top = 0, right = 0, bottom = 0, left = 0 },
|
||||||
|
children = children,
|
||||||
|
getBorderBoxWidth = function(self) return self.width end,
|
||||||
|
getBorderBoxHeight = function(self) return self.height end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Touch Press
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestTouchScrollPress = {}
|
||||||
|
|
||||||
|
function TestTouchScrollPress:setUp()
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollPress:test_handleTouchPress_starts_scrolling()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
local started = sm:handleTouchPress(100, 150)
|
||||||
|
|
||||||
|
luaunit.assertTrue(started)
|
||||||
|
luaunit.assertTrue(sm:isTouchScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollPress:test_handleTouchPress_disabled_returns_false()
|
||||||
|
local sm = createTouchScrollManager({ touchScrollEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
local started = sm:handleTouchPress(100, 150)
|
||||||
|
|
||||||
|
luaunit.assertFalse(started)
|
||||||
|
luaunit.assertFalse(sm:isTouchScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollPress:test_handleTouchPress_no_overflow_returns_false()
|
||||||
|
local sm = createTouchScrollManager({ overflow = "hidden" })
|
||||||
|
|
||||||
|
local started = sm:handleTouchPress(100, 150)
|
||||||
|
|
||||||
|
luaunit.assertFalse(started)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollPress:test_handleTouchPress_stops_momentum_scrolling()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
-- Simulate momentum by starting touch, moving fast, releasing
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
-- Manually set momentum state
|
||||||
|
sm._momentumScrolling = true
|
||||||
|
sm._scrollVelocityY = 500
|
||||||
|
|
||||||
|
-- New press should stop momentum
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
|
||||||
|
luaunit.assertFalse(sm:isMomentumScrolling())
|
||||||
|
luaunit.assertEquals(sm._scrollVelocityX, 0)
|
||||||
|
luaunit.assertEquals(sm._scrollVelocityY, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Touch Move
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestTouchScrollMove = {}
|
||||||
|
|
||||||
|
function TestTouchScrollMove:setUp()
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollMove:test_handleTouchMove_scrolls_content()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
|
||||||
|
-- Advance time so dt > 0 in handleTouchMove
|
||||||
|
love.timer.step(1 / 60)
|
||||||
|
local handled = sm:handleTouchMove(100, 150)
|
||||||
|
|
||||||
|
luaunit.assertTrue(handled)
|
||||||
|
-- Touch moved UP by 50px, so scroll should increase (content moves down relative to finger)
|
||||||
|
luaunit.assertTrue(sm._scrollY > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollMove:test_handleTouchMove_without_press_returns_false()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
|
||||||
|
local handled = sm:handleTouchMove(100, 150)
|
||||||
|
|
||||||
|
luaunit.assertFalse(handled)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollMove:test_handleTouchMove_calculates_velocity()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
love.timer.step(1 / 60)
|
||||||
|
sm:handleTouchMove(100, 100) -- Move 100px up
|
||||||
|
|
||||||
|
-- Velocity should be set (non-zero since time elapsed)
|
||||||
|
-- Note: velocity direction is inverted (touch up = scroll down = positive velocity)
|
||||||
|
luaunit.assertTrue(sm._scrollVelocityY > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollMove:test_handleTouchMove_horizontal()
|
||||||
|
local sm = createTouchScrollManager({
|
||||||
|
bounceEnabled = false,
|
||||||
|
overflowX = "scroll",
|
||||||
|
overflowY = "hidden",
|
||||||
|
})
|
||||||
|
local el = createMockElement(200, 300, 600, 300) -- Wide content
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(200, 150)
|
||||||
|
love.timer.step(1 / 60)
|
||||||
|
sm:handleTouchMove(100, 150) -- Move 100px left
|
||||||
|
|
||||||
|
luaunit.assertTrue(sm._scrollX > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollMove:test_handleTouchMove_with_bounce_allows_overscroll()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = true, maxOverscroll = 100 })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
-- Scroll is at 0 (top), try to scroll further up (negative)
|
||||||
|
sm:handleTouchPress(100, 100)
|
||||||
|
love.timer.step(1 / 60)
|
||||||
|
sm:handleTouchMove(100, 200) -- Move down = scroll up (negative)
|
||||||
|
|
||||||
|
-- With bounce, overscroll should be allowed (scroll < 0)
|
||||||
|
luaunit.assertTrue(sm._scrollY < 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Touch Release and Momentum
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestTouchScrollRelease = {}
|
||||||
|
|
||||||
|
function TestTouchScrollRelease:setUp()
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollRelease:test_handleTouchRelease_ends_touch_scrolling()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
luaunit.assertTrue(sm:isTouchScrolling())
|
||||||
|
|
||||||
|
sm:handleTouchRelease()
|
||||||
|
luaunit.assertFalse(sm:isTouchScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollRelease:test_handleTouchRelease_without_press_returns_false()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
|
||||||
|
local released = sm:handleTouchRelease()
|
||||||
|
|
||||||
|
luaunit.assertFalse(released)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollRelease:test_handleTouchRelease_starts_momentum_with_velocity()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
love.timer.step(1 / 60)
|
||||||
|
sm:handleTouchMove(100, 50) -- Fast swipe up
|
||||||
|
|
||||||
|
sm:handleTouchRelease()
|
||||||
|
|
||||||
|
-- Should start momentum scrolling due to high velocity
|
||||||
|
luaunit.assertTrue(sm:isMomentumScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollRelease:test_handleTouchRelease_no_momentum_with_low_velocity()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
-- Simulate a very slow move by setting low velocity manually
|
||||||
|
sm._scrollVelocityX = 0
|
||||||
|
sm._scrollVelocityY = 10 -- Below threshold of 50
|
||||||
|
|
||||||
|
sm:handleTouchRelease()
|
||||||
|
|
||||||
|
luaunit.assertFalse(sm:isMomentumScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollRelease:test_handleTouchRelease_no_momentum_when_disabled()
|
||||||
|
local sm = createTouchScrollManager({ momentumScrollEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
sm._scrollVelocityY = 500
|
||||||
|
|
||||||
|
sm:handleTouchRelease()
|
||||||
|
|
||||||
|
luaunit.assertFalse(sm:isMomentumScrolling())
|
||||||
|
luaunit.assertEquals(sm._scrollVelocityX, 0)
|
||||||
|
luaunit.assertEquals(sm._scrollVelocityY, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Momentum Scrolling
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestMomentumScrolling = {}
|
||||||
|
|
||||||
|
function TestMomentumScrolling:setUp()
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestMomentumScrolling:test_momentum_decelerates_over_time()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
-- Set up momentum manually
|
||||||
|
sm._momentumScrolling = true
|
||||||
|
sm._scrollVelocityY = 200
|
||||||
|
|
||||||
|
local initialVelocity = sm._scrollVelocityY
|
||||||
|
|
||||||
|
sm:update(1 / 60)
|
||||||
|
|
||||||
|
luaunit.assertTrue(sm._scrollVelocityY < initialVelocity)
|
||||||
|
luaunit.assertTrue(sm._scrollVelocityY > 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestMomentumScrolling:test_momentum_stops_at_low_velocity()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm._momentumScrolling = true
|
||||||
|
sm._scrollVelocityY = 200
|
||||||
|
|
||||||
|
-- Run many frames until momentum stops
|
||||||
|
for i = 1, 500 do
|
||||||
|
sm:update(1 / 60)
|
||||||
|
if not sm:isMomentumScrolling() then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertFalse(sm:isMomentumScrolling())
|
||||||
|
luaunit.assertEquals(sm._scrollVelocityY, 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestMomentumScrolling:test_momentum_moves_scroll_position()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm._momentumScrolling = true
|
||||||
|
sm._scrollVelocityY = 500
|
||||||
|
|
||||||
|
local initialScrollY = sm._scrollY
|
||||||
|
sm:update(1 / 60)
|
||||||
|
|
||||||
|
luaunit.assertTrue(sm._scrollY > initialScrollY)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestMomentumScrolling:test_friction_coefficient_affects_deceleration()
|
||||||
|
local smFast = createTouchScrollManager({ scrollFriction = 0.99, bounceEnabled = false })
|
||||||
|
local smSlow = createTouchScrollManager({ scrollFriction = 0.90, bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
smFast:detectOverflow(el)
|
||||||
|
smSlow:detectOverflow(el)
|
||||||
|
|
||||||
|
smFast._momentumScrolling = true
|
||||||
|
smFast._scrollVelocityY = 200
|
||||||
|
smSlow._momentumScrolling = true
|
||||||
|
smSlow._scrollVelocityY = 200
|
||||||
|
|
||||||
|
smFast:update(1 / 60)
|
||||||
|
smSlow:update(1 / 60)
|
||||||
|
|
||||||
|
-- Higher friction (0.99) preserves more velocity than lower friction (0.90)
|
||||||
|
luaunit.assertTrue(smFast._scrollVelocityY > smSlow._scrollVelocityY)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Bounce Effects
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestTouchScrollBounce = {}
|
||||||
|
|
||||||
|
function TestTouchScrollBounce:setUp()
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollBounce:test_bounce_returns_to_boundary()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = true })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
-- Force overscroll position
|
||||||
|
sm._scrollY = -50
|
||||||
|
|
||||||
|
-- Run bounce updates
|
||||||
|
for i = 1, 100 do
|
||||||
|
sm:update(1 / 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Should have bounced back to 0
|
||||||
|
luaunit.assertAlmostEquals(sm._scrollY, 0, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollBounce:test_bounce_at_bottom_boundary()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = true })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
-- Force overscroll past max
|
||||||
|
sm._scrollY = sm._maxScrollY + 50
|
||||||
|
|
||||||
|
for i = 1, 100 do
|
||||||
|
sm:update(1 / 60)
|
||||||
|
end
|
||||||
|
|
||||||
|
luaunit.assertAlmostEquals(sm._scrollY, sm._maxScrollY, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollBounce:test_no_bounce_when_disabled()
|
||||||
|
local sm = createTouchScrollManager({ bounceEnabled = false })
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm._scrollY = -50
|
||||||
|
|
||||||
|
sm:update(1 / 60)
|
||||||
|
|
||||||
|
-- Without bounce, scroll should stay where it is (clamped by scrollBy)
|
||||||
|
-- But here we set it directly, so it stays
|
||||||
|
luaunit.assertEquals(sm._scrollY, -50)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: State Query Methods
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestTouchScrollState = {}
|
||||||
|
|
||||||
|
function TestTouchScrollState:setUp()
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollState:test_isTouchScrolling_initially_false()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
luaunit.assertFalse(sm:isTouchScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollState:test_isMomentumScrolling_initially_false()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
luaunit.assertFalse(sm:isMomentumScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollState:test_isTouchScrolling_true_during_touch()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
local el = createMockElement()
|
||||||
|
sm:detectOverflow(el)
|
||||||
|
|
||||||
|
sm:handleTouchPress(100, 200)
|
||||||
|
luaunit.assertTrue(sm:isTouchScrolling())
|
||||||
|
|
||||||
|
sm:handleTouchRelease()
|
||||||
|
luaunit.assertFalse(sm:isTouchScrolling())
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- Test Suite: Configuration
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
TestTouchScrollConfig = {}
|
||||||
|
|
||||||
|
function TestTouchScrollConfig:setUp()
|
||||||
|
love.timer.setTime(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollConfig:test_default_config_values()
|
||||||
|
local sm = createTouchScrollManager()
|
||||||
|
|
||||||
|
luaunit.assertTrue(sm.touchScrollEnabled)
|
||||||
|
luaunit.assertTrue(sm.momentumScrollEnabled)
|
||||||
|
luaunit.assertTrue(sm.bounceEnabled)
|
||||||
|
luaunit.assertEquals(sm.scrollFriction, 0.95)
|
||||||
|
luaunit.assertEquals(sm.bounceStiffness, 0.2)
|
||||||
|
luaunit.assertEquals(sm.maxOverscroll, 100)
|
||||||
|
end
|
||||||
|
|
||||||
|
function TestTouchScrollConfig:test_custom_config_values()
|
||||||
|
local sm = createTouchScrollManager({
|
||||||
|
touchScrollEnabled = false,
|
||||||
|
momentumScrollEnabled = false,
|
||||||
|
bounceEnabled = false,
|
||||||
|
scrollFriction = 0.98,
|
||||||
|
bounceStiffness = 0.1,
|
||||||
|
maxOverscroll = 50,
|
||||||
|
})
|
||||||
|
|
||||||
|
luaunit.assertFalse(sm.touchScrollEnabled)
|
||||||
|
luaunit.assertFalse(sm.momentumScrollEnabled)
|
||||||
|
luaunit.assertFalse(sm.bounceEnabled)
|
||||||
|
luaunit.assertEquals(sm.scrollFriction, 0.98)
|
||||||
|
luaunit.assertEquals(sm.bounceStiffness, 0.1)
|
||||||
|
luaunit.assertEquals(sm.maxOverscroll, 50)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run all tests
|
||||||
|
if not _G.RUNNING_ALL_TESTS then
|
||||||
|
os.exit(luaunit.LuaUnit.run())
|
||||||
|
end
|
||||||
@@ -44,10 +44,12 @@ local testFiles = {
|
|||||||
"testing/__tests__/calc_test.lua",
|
"testing/__tests__/calc_test.lua",
|
||||||
"testing/__tests__/critical_failures_test.lua",
|
"testing/__tests__/critical_failures_test.lua",
|
||||||
"testing/__tests__/element_test.lua",
|
"testing/__tests__/element_test.lua",
|
||||||
|
"testing/__tests__/element_touch_test.lua",
|
||||||
"testing/__tests__/element_mode_override_test.lua",
|
"testing/__tests__/element_mode_override_test.lua",
|
||||||
"testing/__tests__/event_handler_test.lua",
|
"testing/__tests__/event_handler_test.lua",
|
||||||
"testing/__tests__/flex_grow_shrink_test.lua",
|
"testing/__tests__/flex_grow_shrink_test.lua",
|
||||||
"testing/__tests__/flexlove_test.lua",
|
"testing/__tests__/flexlove_test.lua",
|
||||||
|
"testing/__tests__/gesture_recognizer_test.lua",
|
||||||
"testing/__tests__/grid_test.lua",
|
"testing/__tests__/grid_test.lua",
|
||||||
"testing/__tests__/image_cache_test.lua",
|
"testing/__tests__/image_cache_test.lua",
|
||||||
"testing/__tests__/image_renderer_test.lua",
|
"testing/__tests__/image_renderer_test.lua",
|
||||||
@@ -69,6 +71,8 @@ local testFiles = {
|
|||||||
"testing/__tests__/text_editor_test.lua",
|
"testing/__tests__/text_editor_test.lua",
|
||||||
"testing/__tests__/theme_test.lua",
|
"testing/__tests__/theme_test.lua",
|
||||||
"testing/__tests__/touch_events_test.lua",
|
"testing/__tests__/touch_events_test.lua",
|
||||||
|
"testing/__tests__/touch_routing_test.lua",
|
||||||
|
"testing/__tests__/touch_scroll_test.lua",
|
||||||
"testing/__tests__/transition_test.lua",
|
"testing/__tests__/transition_test.lua",
|
||||||
"testing/__tests__/units_test.lua",
|
"testing/__tests__/units_test.lua",
|
||||||
"testing/__tests__/utils_test.lua",
|
"testing/__tests__/utils_test.lua",
|
||||||
|
|||||||
Reference in New Issue
Block a user