mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
snip for scroll
This commit is contained in:
+121
-21
@@ -189,6 +189,12 @@ flexlove._accumulatedDt = 0
|
||||
---@type table<string, Element>
|
||||
flexlove._touchOwners = {}
|
||||
|
||||
-- Touch-drag scroll tracking: survives immediate-mode element recreation.
|
||||
-- Maps touch ID -> { id, lastX, lastY }. Scroll position is persisted into
|
||||
-- StateManager on every move (same contract as wheelmoved).
|
||||
---@type table<string, {id: string, lastX: number, lastY: number}>
|
||||
flexlove._touchScroll = {}
|
||||
|
||||
---@type table<number, boolean>
|
||||
flexlove._mouseButtonStates = {}
|
||||
|
||||
@@ -1412,6 +1418,82 @@ function flexlove._getTouchElementAtPosition(x, y)
|
||||
return candidates[1]
|
||||
end
|
||||
|
||||
local function elementIsTouchScrollable(element)
|
||||
if not element or not element._scrollManager then
|
||||
return false
|
||||
end
|
||||
local overflowX = element.overflowX or element.overflow
|
||||
local overflowY = element.overflowY or element.overflow
|
||||
return overflowX == "scroll"
|
||||
or overflowX == "auto"
|
||||
or overflowY == "scroll"
|
||||
or overflowY == "auto"
|
||||
end
|
||||
|
||||
-- Walk parents from a hit target so a finger on a button/row still scrolls
|
||||
-- the containing list. Falls back to the wheel path's scrollable lookup when
|
||||
-- the press lands on empty space inside a scroller.
|
||||
local function findTouchScrollTarget(x, y, startElement)
|
||||
local el = startElement
|
||||
while el do
|
||||
if elementIsTouchScrollable(el) then
|
||||
return el
|
||||
end
|
||||
el = el.parent
|
||||
end
|
||||
return Context.findScrollableAtPosition(x, y)
|
||||
end
|
||||
|
||||
local function findElementByStateId(stateId)
|
||||
if not stateId or stateId == "" then
|
||||
return nil
|
||||
end
|
||||
local function walk(element)
|
||||
if element.id == stateId or element._stateId == stateId then
|
||||
return element
|
||||
end
|
||||
for _, child in ipairs(element.children or {}) do
|
||||
local found = walk(child)
|
||||
if found then
|
||||
return found
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
for _, element in ipairs(flexlove.topElements or {}) do
|
||||
local found = walk(element)
|
||||
if found then
|
||||
return found
|
||||
end
|
||||
end
|
||||
for _, element in ipairs(flexlove._currentFrameElements or {}) do
|
||||
local found = walk(element)
|
||||
if found then
|
||||
return found
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function persistTouchScroll(element)
|
||||
if flexlove._immediateMode and element and element._stateId and element._scrollManager then
|
||||
StateManager.updateState(element._stateId, {
|
||||
scrollManager = element._scrollManager:getState(),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function resolveTouchScrollElement(track)
|
||||
if not track then
|
||||
return nil
|
||||
end
|
||||
local element = findElementByStateId(track.id)
|
||||
if elementIsTouchScrollable(element) then
|
||||
return element
|
||||
end
|
||||
return nil
|
||||
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
|
||||
@@ -1452,15 +1534,22 @@ function flexlove.touchpressed(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
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
|
||||
-- Scroll target is the nearest scrollable ancestor (or the scroller under
|
||||
-- empty space). Tracked by stable id so immediate-mode recreation can resume.
|
||||
local scrollEl = findTouchScrollTarget(touchX, touchY, element)
|
||||
if scrollEl and scrollEl._scrollManager then
|
||||
local scrollId = scrollEl._stateId or scrollEl.id
|
||||
if scrollId and scrollId ~= "" then
|
||||
flexlove._touchScroll[touchId] = {
|
||||
id = scrollId,
|
||||
lastX = touchX,
|
||||
lastY = touchY,
|
||||
}
|
||||
end
|
||||
scrollEl._scrollManager:handleTouchPress(touchX, touchY)
|
||||
persistTouchScroll(scrollEl)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1500,15 +1589,21 @@ function flexlove.touchmoved(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
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
|
||||
local track = flexlove._touchScroll[touchId]
|
||||
local scrollEl = resolveTouchScrollElement(track)
|
||||
if track and scrollEl then
|
||||
local sm = scrollEl._scrollManager
|
||||
-- Immediate mode recreates managers each frame; re-arm drag from the
|
||||
-- last persisted touch point so a move after beginFrame still scrolls.
|
||||
if not sm._touchScrolling then
|
||||
sm:handleTouchPress(track.lastX, track.lastY)
|
||||
end
|
||||
sm:handleTouchMove(touchX, touchY)
|
||||
persistTouchScroll(scrollEl)
|
||||
track.lastX = touchX
|
||||
track.lastY = touchY
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1548,19 +1643,23 @@ function flexlove.touchreleased(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
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
|
||||
local track = flexlove._touchScroll[touchId]
|
||||
local scrollEl = resolveTouchScrollElement(track)
|
||||
if track and scrollEl then
|
||||
local sm = scrollEl._scrollManager
|
||||
if not sm._touchScrolling then
|
||||
sm:handleTouchPress(track.lastX, track.lastY)
|
||||
end
|
||||
sm:handleTouchMove(touchX, touchY)
|
||||
sm:handleTouchRelease()
|
||||
persistTouchScroll(scrollEl)
|
||||
end
|
||||
|
||||
-- Clean up touch ownership (touch is complete)
|
||||
flexlove._touchOwners[touchId] = nil
|
||||
flexlove._touchScroll[touchId] = nil
|
||||
end
|
||||
|
||||
--- Get the number of currently active touches being tracked
|
||||
@@ -1659,6 +1758,7 @@ function flexlove.destroy()
|
||||
|
||||
-- Clean up touch state
|
||||
flexlove._touchOwners = {}
|
||||
flexlove._touchScroll = {}
|
||||
flexlove._mouseButtonStates = {}
|
||||
if flexlove._gestureRecognizer then
|
||||
flexlove._gestureRecognizer:reset()
|
||||
|
||||
@@ -422,10 +422,10 @@ function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||
return TouchEditor.touchpressed(id, x, y)
|
||||
end
|
||||
if Importer then
|
||||
if love.system.getOS() == "iOS" then
|
||||
return Importer:touchpressed(id, x, y)
|
||||
end
|
||||
return Importer:mousepressed(x, y, 1)
|
||||
-- Both mobiles: FlexLove scroll needs the real touch stream. Clicks are
|
||||
-- polled inside the view; the istouch filter on mousepressed still drops
|
||||
-- Android's synthesized mouse twin so Import cannot double-fire (#553).
|
||||
return Importer:touchpressed(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
Game:touchpressed(id, x, y)
|
||||
end
|
||||
@@ -437,10 +437,7 @@ function love.touchmoved(id, x, y, dx, dy, pressure)
|
||||
return TouchEditor.touchmoved(id, x, y)
|
||||
end
|
||||
if Importer then
|
||||
if love.system.getOS() == "iOS" then
|
||||
return Importer:touchmoved(id, x, y)
|
||||
end
|
||||
return
|
||||
return Importer:touchmoved(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
Game:touchmoved(id, x, y)
|
||||
end
|
||||
@@ -452,10 +449,7 @@ function love.touchreleased(id, x, y, dx, dy, pressure)
|
||||
return TouchEditor.touchreleased(id, x, y)
|
||||
end
|
||||
if Importer then
|
||||
if love.system.getOS() == "iOS" then
|
||||
return Importer:touchreleased(id, x, y)
|
||||
end
|
||||
return
|
||||
return Importer:touchreleased(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
Game:touchreleased(id, x, y)
|
||||
end
|
||||
@@ -478,20 +472,12 @@ function love.mousepressed(x, y, button, istouch)
|
||||
return TouchEditor.mousepressed(x, y, button)
|
||||
end
|
||||
if Importer then
|
||||
-- The same double-fire TouchEditor guards against, which the launcher was
|
||||
-- missing: love.touchpressed above forwards the primary touch to the
|
||||
-- Importer on Android, and LÖVE ALSO synthesizes a mouse press for that
|
||||
-- same touch, so one tap ran every launcher button twice. On Import that
|
||||
-- meant two choose() calls and two stacked SAF picker activities: the
|
||||
-- player picked their ROM, the top picker closed, and the second was still
|
||||
-- underneath asking for it again, which is the "import the file twice"
|
||||
-- in #553. Filtering on istouch keeps a real mouse (DeX, a Chromebook, a
|
||||
-- USB mouse) working, which an Android-wide return would have broken.
|
||||
--
|
||||
-- ANDROID ONLY, and the OS test is load bearing: love.touchpressed above
|
||||
-- returns early on iOS and never forwards, so there the synthesized mouse
|
||||
-- press is the ONLY event the launcher gets. Filtering istouch on both
|
||||
-- killed every tap on iOS outright.
|
||||
-- love.touchpressed already forwards the primary touch into FlexLove for
|
||||
-- scroll. LÖVE ALSO synthesizes a mouse press for that same touch; if both
|
||||
-- reached a press handler, one tap ran every launcher button twice and
|
||||
-- stacked two SAF pickers (#553). Clicks are polled inside FlexLove from
|
||||
-- love.touch / mouse.isDown, so dropping the synthesized istouch press is
|
||||
-- safe. A real mouse (DeX, Chromebook, USB) still reaches mousepressed.
|
||||
if istouch and (love.system.getOS() == "Android"
|
||||
or love.system.getOS() == "iOS") then return end
|
||||
return Importer:mousepressed(x, y, button)
|
||||
|
||||
@@ -146,11 +146,48 @@ function LauncherView.update(imp, dt)
|
||||
end
|
||||
end
|
||||
|
||||
-- One dedup window covers a touch release plus the mouse click SDL
|
||||
-- synthesizes for the same tap.
|
||||
local ACT_DEDUP = 0.35
|
||||
-- Finger travel past this (px) is a scroll drag, not a tap — so dragging a
|
||||
-- list row does not also fire that row's button.
|
||||
local TAP_SLOP2 = 16 * 16
|
||||
|
||||
function LauncherView.wheelmoved(imp, dx, dy)
|
||||
if not imp._flex then return end
|
||||
pcall(FlexLove.wheelmoved, dx, dy)
|
||||
end
|
||||
|
||||
-- Touch drag scroll: FlexLove's ScrollManager only moves when these are
|
||||
-- hooked. Clicks still come from EventHandler's love.touch / mouse polling;
|
||||
-- the view's action dedupe covers a tap that also synthesizes a mouse click,
|
||||
-- and a drag past TAP_SLOP suppresses the click that would otherwise fire
|
||||
-- on the row under the finger.
|
||||
function LauncherView.touchpressed(imp, id, x, y, dx, dy, pressure)
|
||||
if not imp._flex then return end
|
||||
imp._touchAt = imp._touchAt or {}
|
||||
imp._touchAt[tostring(id)] = { x = x, y = y }
|
||||
pcall(FlexLove.touchpressed, id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function LauncherView.touchmoved(imp, id, x, y, dx, dy, pressure)
|
||||
if not imp._flex then return end
|
||||
local start = imp._touchAt and imp._touchAt[tostring(id)]
|
||||
if start then
|
||||
local ddx, ddy = x - start.x, y - start.y
|
||||
if ddx * ddx + ddy * ddy > TAP_SLOP2 then
|
||||
imp._suppressClickUntil = love.timer.getTime() + ACT_DEDUP
|
||||
end
|
||||
end
|
||||
pcall(FlexLove.touchmoved, id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function LauncherView.touchreleased(imp, id, x, y, dx, dy, pressure)
|
||||
if not imp._flex then return end
|
||||
if imp._touchAt then imp._touchAt[tostring(id)] = nil end
|
||||
pcall(FlexLove.touchreleased, id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
-- Synthetic click for the gamepad virtual cursor: find the element under the
|
||||
-- pad pointer and run its handler with a click-shaped event.
|
||||
function LauncherView.clickAt(imp, x, y)
|
||||
@@ -166,10 +203,6 @@ end
|
||||
|
||||
-- ------- shared widget helpers
|
||||
|
||||
-- One dedup window covers a touch release plus the mouse click SDL
|
||||
-- synthesizes for the same tap.
|
||||
local ACT_DEDUP = 0.35
|
||||
|
||||
local function queueAction(imp, key, fn, keepArm)
|
||||
local now = love.timer.getTime()
|
||||
local last = imp._actAt[key]
|
||||
@@ -188,6 +221,15 @@ local function handler(imp, key, action, keepArm)
|
||||
elseif ev.type == "unhover" then
|
||||
imp._hot[key] = nil
|
||||
elseif action and (ev.type == "click" or ev.type == "touchrelease") then
|
||||
if ev.type == "touchrelease" then
|
||||
local dx, dy = ev.dx or 0, ev.dy or 0
|
||||
if dx * dx + dy * dy > TAP_SLOP2 then
|
||||
imp._suppressClickUntil = love.timer.getTime() + ACT_DEDUP
|
||||
return
|
||||
end
|
||||
end
|
||||
local untilT = imp._suppressClickUntil
|
||||
if untilT and love.timer.getTime() < untilT then return end
|
||||
queueAction(imp, key, action, keepArm)
|
||||
end
|
||||
end
|
||||
|
||||
+25
-12
@@ -617,12 +617,9 @@ function RomImporter.new(onComplete, opts)
|
||||
-- boot armed and let the first poll tick consume it, rather than making the
|
||||
-- player tap Import a second time to trigger the scan by hand (#553).
|
||||
pickPending = android or nil,
|
||||
-- Android drag: the launcher is handed no move events at all (main.lua
|
||||
-- forwards neither touchmoved nor mousemoved while it is up), and its mouse
|
||||
-- emulation is what "no reliable pointer polling" below refers to.
|
||||
-- love.touch IS pollable, so where it exists a touch drag can be resolved
|
||||
-- inside draw the same way the desktop mouse is. Where it does not, every
|
||||
-- Android path stays exactly as it was: act on press, never arm.
|
||||
-- Mobile drag-scroll goes through FlexLove.touch* (main.lua forwards the
|
||||
-- full touch stream while the launcher is up). love.touch remains pollable
|
||||
-- for click hit-testing inside EventHandler.
|
||||
touchPollable = android and love.touch ~= nil
|
||||
and love.touch.getTouches ~= nil and love.touch.getPosition ~= nil,
|
||||
tab = "red", -- active launcher tab: "red"/"blue"/"yellow"/"mods"
|
||||
@@ -1703,13 +1700,29 @@ function RomImporter:pressDelete(kind, id, version, commit)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Pointer input is polled by the FlexLove view (mouse and touch alike), so
|
||||
-- the host-forwarded press events are inert. The methods stay because
|
||||
-- main.lua forwards to them unconditionally while the launcher is up.
|
||||
-- Clicks are polled inside FlexLove (mouse + love.touch); host-forwarded
|
||||
-- mousepressed stays inert so Android's synthesized mouse path cannot
|
||||
-- double-fire a tap (#553). Touch move/press/release must still reach
|
||||
-- FlexLove.touch* or scroll containers never drag on phones.
|
||||
function RomImporter:mousepressed() end
|
||||
function RomImporter:touchpressed() end
|
||||
function RomImporter:touchmoved() end
|
||||
function RomImporter:touchreleased() end
|
||||
|
||||
function RomImporter:touchpressed(id, x, y, dx, dy, pressure)
|
||||
if not self._flex then return end
|
||||
require("src.import.LauncherView").touchpressed(
|
||||
self, id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function RomImporter:touchmoved(id, x, y, dx, dy, pressure)
|
||||
if not self._flex then return end
|
||||
require("src.import.LauncherView").touchmoved(
|
||||
self, id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function RomImporter:touchreleased(id, x, y, dx, dy, pressure)
|
||||
if not self._flex then return end
|
||||
require("src.import.LauncherView").touchreleased(
|
||||
self, id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
-- Switch the active tab (chips, shoulder buttons). The find search caret and
|
||||
-- the soft keyboard drop with the panel they belonged to; each tab's scroll
|
||||
|
||||
@@ -145,10 +145,9 @@ for os, forwards in pairs(touchForwardsToImporter) do
|
||||
os .. ": the synthesized mouse press is dropped only where touch already forwarded")
|
||||
end
|
||||
|
||||
-- The FlexLove view polls love.touch itself and dedupes a tap's synthesized
|
||||
-- mouse click in its action layer (LauncherView queueAction), so the
|
||||
-- host-forwarded touch events are inert stubs: they must accept any id
|
||||
-- without capturing state or throwing.
|
||||
-- Before the FlexLove view attaches (_flex), touch handlers are no-ops: they
|
||||
-- must accept any id without capturing importer state or throwing. Once the
|
||||
-- view is up they forward into FlexLove.touch* for list drag-scroll.
|
||||
local touch = importer("iOS")
|
||||
touch:touchpressed(101, 20, 20)
|
||||
touch:touchmoved(101, 22, 22)
|
||||
@@ -156,7 +155,9 @@ touch:touchreleased(202, 20, 20)
|
||||
touch:touchpressed(303, 20, 20)
|
||||
touch:touchreleased(303, 20, 20)
|
||||
check(touch._activeTouch == nil,
|
||||
"touch events stay inert: the view's own polling owns touch input")
|
||||
"touch events before the view attaches leave importer touch state alone")
|
||||
check(not touch._flex,
|
||||
"and do not attach the FlexLove view on their own")
|
||||
|
||||
love.system.getOS = saved.getOS
|
||||
love.system.pickFile = saved.pickFile
|
||||
|
||||
Reference in New Issue
Block a user