feat: gesture/multi-touch progress
This commit is contained in:
@@ -160,6 +160,12 @@
|
||||
---@field _mouseDownPosition number? -- Internal: mouse down position for drag tracking
|
||||
---@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 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
|
||||
local Element = {}
|
||||
Element.__index = Element
|
||||
@@ -366,6 +372,14 @@ function Element.new(props)
|
||||
|
||||
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)
|
||||
self._stateId = self.id
|
||||
|
||||
@@ -373,6 +387,12 @@ function Element.new(props)
|
||||
local eventHandlerConfig = {
|
||||
onEvent = self.onEvent,
|
||||
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
|
||||
local state = Element._StateManager.getState(self._stateId)
|
||||
@@ -2603,6 +2623,10 @@ function Element:destroy()
|
||||
|
||||
-- Clear onEvent to prevent closure leaks
|
||||
self.onEvent = nil
|
||||
|
||||
-- Clear touch callbacks to prevent closure leaks
|
||||
self.onTouchEvent = nil
|
||||
self.onGesture = nil
|
||||
end
|
||||
|
||||
--- Draw element and its children
|
||||
@@ -3082,6 +3106,39 @@ function Element:update(dt)
|
||||
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 newViewportHeight number
|
||||
function Element:recalculateUnits(newViewportWidth, newViewportHeight)
|
||||
@@ -4066,6 +4123,8 @@ function Element:_cleanup()
|
||||
self.onEnter = nil
|
||||
self.onImageLoad = nil
|
||||
self.onImageError = nil
|
||||
self.onTouchEvent = nil
|
||||
self.onGesture = nil
|
||||
end
|
||||
|
||||
return Element
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
---@class EventHandler
|
||||
---@field onEvent fun(element:Element, event:InputEvent)?
|
||||
---@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 _lastClickTime number?
|
||||
---@field _lastClickButton number?
|
||||
@@ -39,6 +45,12 @@ function EventHandler.new(config)
|
||||
|
||||
self.onEvent = config.onEvent
|
||||
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 {}
|
||||
|
||||
@@ -462,7 +474,7 @@ function EventHandler:processTouchEvents(element)
|
||||
local activeTouchIds = {}
|
||||
|
||||
-- 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 EventHandler._Performance and EventHandler._Performance.enabled then
|
||||
@@ -483,6 +495,12 @@ function EventHandler:processTouchEvents(element)
|
||||
activeTouches[tostring(id)] = true
|
||||
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
|
||||
for _, id in ipairs(touches) do
|
||||
local touchId = tostring(id)
|
||||
@@ -494,8 +512,15 @@ function EventHandler:processTouchEvents(element)
|
||||
|
||||
if isInside then
|
||||
if not self._touches[touchId] then
|
||||
-- New touch began
|
||||
self:_handleTouchBegan(element, touchId, tx, ty, pressure)
|
||||
-- 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
|
||||
self:_handleTouchBegan(element, touchId, tx, ty, pressure)
|
||||
trackedTouchCount = trackedTouchCount + 1
|
||||
end
|
||||
else
|
||||
-- Touch moved
|
||||
self:_handleTouchMoved(element, touchId, tx, ty, pressure)
|
||||
@@ -561,6 +586,7 @@ function EventHandler:_handleTouchBegan(element, touchId, x, y, pressure)
|
||||
touchEvent.dx = 0
|
||||
touchEvent.dy = 0
|
||||
self:_invokeCallback(element, touchEvent)
|
||||
self:_invokeTouchCallback(element, touchEvent)
|
||||
end
|
||||
|
||||
--- Handle touch moved event
|
||||
@@ -607,6 +633,7 @@ function EventHandler:_handleTouchMoved(element, touchId, x, y, pressure)
|
||||
touchEvent.dx = dx
|
||||
touchEvent.dy = dy
|
||||
self:_invokeCallback(element, touchEvent)
|
||||
self:_invokeTouchCallback(element, touchEvent)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -634,6 +661,7 @@ function EventHandler:_handleTouchEnded(element, touchId, x, y, pressure)
|
||||
touchEvent.dx = dx
|
||||
touchEvent.dy = dy
|
||||
self:_invokeCallback(element, touchEvent)
|
||||
self:_invokeTouchCallback(element, touchEvent)
|
||||
|
||||
-- Cleanup touch state
|
||||
self:_cleanupTouch(touchId)
|
||||
@@ -709,4 +737,52 @@ function EventHandler:_invokeCallback(element, event)
|
||||
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
|
||||
|
||||
@@ -92,10 +92,11 @@ end
|
||||
---@param event InputEvent Touch event
|
||||
function GestureRecognizer:processTouchEvent(event)
|
||||
if not event.touchId then
|
||||
return
|
||||
return nil
|
||||
end
|
||||
|
||||
local touchId = event.touchId
|
||||
local gestures = {}
|
||||
|
||||
-- Update touch state
|
||||
if event.type == "touchpress" then
|
||||
@@ -122,13 +123,17 @@ function GestureRecognizer:processTouchEvent(event)
|
||||
touch.phase = "moved"
|
||||
|
||||
-- Update gesture detection
|
||||
self:_detectPan(touchId, event)
|
||||
self:_detectSwipe(touchId, event)
|
||||
local panGesture = self:_detectPan(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
|
||||
if self:_getTouchCount() >= 2 then
|
||||
self:_detectPinch(event)
|
||||
self:_detectRotate(event)
|
||||
local pinchGesture = self:_detectPinch(event)
|
||||
if pinchGesture then table.insert(gestures, pinchGesture) end
|
||||
local rotateGesture = self:_detectRotate(event)
|
||||
if rotateGesture then table.insert(gestures, rotateGesture) end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -138,9 +143,12 @@ function GestureRecognizer:processTouchEvent(event)
|
||||
touch.phase = "ended"
|
||||
|
||||
-- Finalize gesture detection
|
||||
self:_detectTapEnded(touchId, event)
|
||||
self:_detectSwipeEnded(touchId, event)
|
||||
self:_detectPanEnded(touchId, event)
|
||||
local tapGesture = self:_detectTapEnded(touchId, event)
|
||||
if tapGesture then table.insert(gestures, tapGesture) end
|
||||
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
|
||||
self._touches[touchId] = nil
|
||||
@@ -151,6 +159,8 @@ function GestureRecognizer:processTouchEvent(event)
|
||||
self._touches[touchId] = nil
|
||||
self:_cancelAllGestures()
|
||||
end
|
||||
|
||||
return #gestures > 0 and gestures or nil
|
||||
end
|
||||
|
||||
--- Get number of active touches
|
||||
|
||||
@@ -85,6 +85,12 @@ local AnimationProps = {}
|
||||
---@field onTextChangeDeferred boolean? -- Whether onTextChange callback should be deferred (default: false)
|
||||
---@field onEnter fun(element:Element)? -- Callback when Enter key is pressed
|
||||
---@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 transition TransitionProps? -- Transition settings for animations
|
||||
---@field customDraw fun(element:Element)? -- Custom rendering callback called after standard rendering but before visual feedback (default: nil)
|
||||
|
||||
Reference in New Issue
Block a user