fix(switch): restore pad cursor in Touch Controls and launcher overlays

Opening Touch Controls dropped the launcher virtual cursor and swallowed
gamepad input while touch still worked. Share PadCursor with the save
editor, forward pad events, and centralize overlay handoff/resume so both
hosts park and re-arm the pointer cleanly.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Andrew Quenehen
2026-08-03 16:46:48 -03:00
parent 0dd6ecc219
commit 105672c132
8 changed files with 513 additions and 237 deletions
+43 -2
View File
@@ -156,6 +156,41 @@ do
check(desk._padCursorActive, "desktop parkNxPointerForHost is a no-op")
end
-- ------- Overlay handoff / resume (Edit Save + Touch Controls)
do
mouseX, mouseY = 9, 10
local imp = freshImporter(true)
imp.launcher = true
stickRight(imp, 3)
check(imp._padCursorActive, "pad active before overlay handoff")
check(imp._nxPointerBridge, "bridge on before overlay handoff")
imp:prepareOverlayHandoff()
check(not imp._padCursorActive, "prepareOverlayHandoff clears pad")
check(not imp._nxPointerBridge, "prepareOverlayHandoff clears NX bridge")
check(imp._flex == nil, "prepareOverlayHandoff clears flex without FlexLove")
local gx, gy = love.mouse.getPosition()
eq(gx, 9, "after overlay handoff getPosition is real mouse X")
eq(gy, 10, "after overlay handoff getPosition is real mouse Y")
local prevCount = love.joystick and love.joystick.getJoystickCount
love.joystick = love.joystick or {}
love.joystick.getJoystickCount = function() return 1 end
imp:resumeAfterOverlay()
check(imp._padCursorActive, "NX resumeAfterOverlay re-arms pad with a stick")
love.joystick.getJoystickCount = function() return 0 end
imp._padCursorActive = false
imp:resumeAfterOverlay()
check(not imp._padCursorActive, "resumeAfterOverlay stays latent with no stick")
love.joystick.getJoystickCount = prevCount
local desk = freshImporter(false)
desk.launcher = true
desk._padCursorActive = true
desk:prepareOverlayHandoff()
check(not desk._padCursorActive, "desktop prepareOverlayHandoff clears pad too")
end
-- ------- Desktop: setPosition still warps (unchanged path)
do
@@ -228,6 +263,10 @@ do
"RomImporter owns NX getPosition bridge")
check(impSrc:find("function RomImporter:parkNxPointerForHost", 1, true) ~= nil,
"RomImporter exports parkNxPointerForHost")
check(impSrc:find("function RomImporter:prepareOverlayHandoff", 1, true) ~= nil,
"RomImporter exports prepareOverlayHandoff")
check(impSrc:find("function RomImporter:resumeAfterOverlay", 1, true) ~= nil,
"RomImporter exports resumeAfterOverlay")
check(impSrc:find("if not self.isNX then", 1, true) ~= nil,
"NX skips desktop mouse-yield path")
check(impSrc:find("if not self.isNX and love.mouse.setPosition", 1, true) ~= nil,
@@ -236,8 +275,10 @@ do
"NX clamps pad cursor dt")
local mainSrc = read("main.lua")
check(mainSrc:find("parkNxPointerForHost", 1, true) ~= nil,
"openEditor parks NX pointer before save editor")
check(mainSrc:find("prepareOverlayHandoff", 1, true) ~= nil,
"openEditor / Touch Controls use prepareOverlayHandoff")
check(mainSrc:find("resumeAfterOverlay", 1, true) ~= nil,
"close paths resume the launcher pad cursor")
check(view:find("math.floor(x + 0.5)", 1, true) ~= nil,
"NX pad cursor draw is pixel-snapped")
@@ -0,0 +1,85 @@
-- Touch-controls editor pad / Joy-Con cursor (same class as save-editor soft-lock).
-- Opening Touch Controls parked the launcher cursor and dropped all gamepad
-- input; touch still worked. PadCursor + main.lua forwarding restore the
-- virtual pointer.
-- luajit tests/engine/touch_controls_pad_cursor_test.lua
package.path = "./?.lua;./?/init.lua;" .. package.path
if not _G.love then _G.love = require("tests.love_stub") end
local T = require("tests.harness")
local check, eq = T.check, T.eq
local PadCursor = require("src.ui.PadCursor")
local GamepadMap = require("src.core.GamepadMap")
PadCursor.reset()
PadCursor.gamepadaxis(nil, "leftx", 1)
PadCursor.update(0.05)
check(PadCursor.isActive(), "left stick activates pad cursor")
local x0 = select(1, PadCursor.pointer())
PadCursor.update(0.05)
check(select(1, PadCursor.pointer()) > x0, "left stick moves cursor right")
eq(PadCursor.gamepadpressed(nil, "a"), "a", "gamepad a → click")
eq(PadCursor.gamepadpressed(nil, "b"), "b", "gamepad b → close")
eq(PadCursor.gamepadpressed(nil, "leftshoulder"), "tab_prev", "L → size down")
eq(PadCursor.gamepadpressed(nil, "rightshoulder"), "tab_next", "R → size up")
GamepadMap._setForceNXForTests(true)
eq(PadCursor.gamepadpressed(nil, "a"), "b", "NX SDL a (south) → close (GB b)")
eq(PadCursor.gamepadpressed(nil, "b"), "a", "NX SDL b (east) → click (GB a)")
GamepadMap._setForceNXForTests(false)
PadCursor.yieldToPointer()
check(not PadCursor.isActive(), "yieldToPointer drops the virtual cursor")
-- Compat: tools/save-editor/PadInput.lua re-exports the shared module.
package.path = package.path .. ";./tools/save-editor/?.lua"
local PadInput = require("PadInput")
eq(PadInput, PadCursor, "PadInput shim is PadCursor")
local function read(path)
local f = assert(io.open(path, "r"))
local src = f:read("*a")
f:close()
return src
end
local mainSrc = read("main.lua")
check(mainSrc:find("prepareOverlayHandoff", 1, true) ~= nil
and mainSrc:find("openTouchControlsEditor", 1, true) ~= nil,
"main.lua mentions prepareOverlayHandoff + touch editor")
-- prepare must run inside openTouchControlsEditor, not only openEditor
local touchOpen = mainSrc:match("local function openTouchControlsEditor%(%)(.-)\nend")
check(touchOpen ~= nil, "openTouchControlsEditor body found")
check(touchOpen:find("prepareOverlayHandoff", 1, true) ~= nil,
"openTouchControlsEditor prepares overlay handoff like the save editor")
local touchClose = mainSrc:match("function closeTouchControlsEditor%(%)(.-)\nend")
check(touchClose ~= nil, "closeTouchControlsEditor body found")
check(touchClose:find("resumeAfterOverlay", 1, true) ~= nil,
"closeTouchControlsEditor resumes the launcher pad cursor")
check(mainSrc:find("TouchEditor.gamepadpressed", 1, true) ~= nil,
"main.lua forwards gamepadpressed to TouchEditor")
check(mainSrc:find("TouchEditor.gamepadaxis", 1, true) ~= nil,
"main.lua forwards gamepadaxis to TouchEditor")
local edSrc = read("src/ui/TouchControlsEditor.lua")
check(edSrc:find('require("src.ui.PadCursor")', 1, true) ~= nil,
"TouchControlsEditor loads PadCursor")
check(edSrc:find("function Editor.gamepadpressed", 1, true) ~= nil,
"TouchControlsEditor exposes gamepadpressed")
check(edSrc:find("PadCursor.draw()", 1, true) ~= nil,
"TouchControlsEditor draws the pad cursor")
check(edSrc:find("PadCursor.yieldToPointer()", 1, true) ~= nil,
"touch/mouse yields the pad so taps use event coords")
check(edSrc:find('beginDrag("pad"', 1, true) ~= nil,
"A begins a pad drag at the virtual cursor")
check(edSrc:find('endDrag("pad")', 1, true) ~= nil,
"A release ends a pad drag")
local packSrc = read("scripts/pack_love.sh")
check(packSrc:find("tools/save-editor/PadInput.lua", 1, true) ~= nil,
"pack still requires PadInput path (compat shim)")
T.finish("touch_controls_pad_cursor")