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
+3 -215
View File
@@ -1,215 +1,3 @@
-- Virtual pointer for the save editor on Switch / handhelds / any gamepad.
-- Mirrors the launcher's RomImporter pad cursor (speeds, deadzone, dual-path
-- raw gate) without sharing that module -- keeps RomImporter risk-free.
--
-- Stick / D-pad move; real mouse motion yields so desktop stays normal.
-- Callers (App.lua) map A → click, B → close, shoulders → tabs, right stick
-- → wheel notches.
local SafeArea = require("src.core.SafeArea")
local GamepadMap = require("src.core.GamepadMap")
local PAD_DEAD = 0.28
local PAD_SPEED = 560
local PAD_DPAD_SPEED = 420
-- Right stick → Kit wheel notches: ~2 notches/sec at full deflection so lists
-- scroll at a usable pace without flooding one frame.
local PAD_WHEEL_RATE = 2.0
local PadInput = {}
local cursor = { x = 0, y = 0 }
local active = false
local inited = false
local axis = { leftx = 0, lefty = 0, righty = 0 }
local dir = {}
local rawHatDirs = {}
local lastMouseX, lastMouseY
local wheelAcc = 0
local function activate()
if active then return end
local ox, oy, w, h = SafeArea.rect()
if not inited then
cursor.x = ox + w * 0.5
cursor.y = oy + h * 0.45
inited = true
end
active = true
end
function PadInput.reset()
cursor.x, cursor.y = 0, 0
active = false
inited = false
axis.leftx, axis.lefty, axis.righty = 0, 0, 0
for k in pairs(dir) do dir[k] = nil end
for k in pairs(rawHatDirs) do rawHatDirs[k] = nil end
lastMouseX, lastMouseY = nil, nil
wheelAcc = 0
end
-- Touch / mouse press: drop the virtual cursor for this interaction so a tap
-- is not swallowed by the Joy-Con pointer sitting elsewhere on screen.
function PadInput.yieldToPointer()
active = false
end
-- Returns mx, my, isActive. When inactive the caller should use the system
-- mouse; when active these coords feed Kit.beginFrame.
function PadInput.pointer()
return cursor.x, cursor.y, active
end
function PadInput.isActive()
return active
end
-- Consume accumulated right-stick scroll as integer wheel notches (same
-- units App.wheelmoved feeds Kit). Fractional remainder stays for next frame.
function PadInput.takeWheel()
local notches = 0
if wheelAcc >= 1 or wheelAcc <= -1 then
notches = wheelAcc > 0 and math.floor(wheelAcc) or math.ceil(wheelAcc)
wheelAcc = wheelAcc - notches
end
return notches
end
function PadInput.update(dt)
if not (love and love.mouse and love.mouse.getPosition) then return end
local mx, my = love.mouse.getPosition()
if lastMouseX and active then
if math.abs(mx - lastMouseX) > 3 or math.abs(my - lastMouseY) > 3 then
active = false
end
end
lastMouseX, lastMouseY = mx, my
local ax = axis.leftx or 0
local ay = axis.lefty or 0
local dx, dy = 0, 0
if math.abs(ax) > PAD_DEAD then dx = dx + ax end
if math.abs(ay) > PAD_DEAD then dy = dy + ay end
if dir.dpleft then dx = dx - 1 end
if dir.dpright then dx = dx + 1 end
if dir.dpup then dy = dy - 1 end
if dir.dpdown then dy = dy + 1 end
if dx ~= 0 or dy ~= 0 then
activate()
local mag = math.sqrt(dx * dx + dy * dy)
if mag > 1 then dx, dy = dx / mag, dy / mag end
local speed = (math.abs(ax) > PAD_DEAD or math.abs(ay) > PAD_DEAD)
and PAD_SPEED or PAD_DPAD_SPEED
local ox, oy, w, h = SafeArea.rect()
local nx = cursor.x + dx * speed * dt
local ny = cursor.y + dy * speed * dt
cursor.x = math.max(ox, math.min(ox + w, nx))
cursor.y = math.max(oy, math.min(oy + h, ny))
end
local ry = axis.righty or 0
if math.abs(ry) > PAD_DEAD then
activate()
-- Negative righty (stick up) scrolls lists up = positive wheel notches.
wheelAcc = wheelAcc + (-ry) * PAD_WHEEL_RATE * dt
end
end
-- Returns a string action the App layer handles:
-- "a" | "b" | "tab_prev" | "tab_next" | nil
function PadInput.gamepadpressed(_, button)
activate()
local action = GamepadMap.mapGamepadButton(button)
if action == "a" or action == "b" then
return action
elseif button == "leftshoulder" then
return "tab_prev"
elseif button == "rightshoulder" then
return "tab_next"
elseif button == "dpup" or button == "dpdown"
or button == "dpleft" or button == "dpright" then
dir[button] = true
end
return nil
end
function PadInput.gamepadreleased(_, button)
if button == "dpup" or button == "dpdown"
or button == "dpleft" or button == "dpright" then
dir[button] = nil
end
end
function PadInput.gamepadaxis(_, axisName, value)
if axisName == "leftx" or axisName == "lefty" or axisName == "righty" then
axis[axisName] = value
if math.abs(value) > PAD_DEAD then activate() end
end
end
function PadInput.joystickpressed(joystick, button)
if GamepadMap.ignoreRawForJoystick(joystick) then return nil end
local padButton = GamepadMap.mapRawToGamepadButton(button)
if padButton then return PadInput.gamepadpressed(joystick, padButton) end
return nil
end
function PadInput.joystickreleased(joystick, button)
if GamepadMap.ignoreRawForJoystick(joystick) then return end
local padButton = GamepadMap.mapRawToGamepadButton(button)
if padButton then PadInput.gamepadreleased(joystick, padButton) end
end
function PadInput.joystickaxis(joystick, axisIndex, value)
if GamepadMap.ignoreRawForJoystick(joystick) then return end
if axisIndex == 1 then
PadInput.gamepadaxis(joystick, "leftx", value)
elseif axisIndex == 2 then
PadInput.gamepadaxis(joystick, "lefty", value)
end
end
function PadInput.joystickhat(joystick, hat, direction)
if GamepadMap.ignoreRawForJoystick(joystick) then return end
for _, d in ipairs(rawHatDirs[hat] or {}) do
dir[d] = nil
end
local dirs = ({
u = { "dpup" }, d = { "dpdown" }, l = { "dpleft" }, r = { "dpright" },
lu = { "dpleft", "dpup" }, ru = { "dpright", "dpup" },
ld = { "dpleft", "dpdown" }, rd = { "dpright", "dpdown" },
})[direction] or {}
for _, d in ipairs(dirs) do dir[d] = true end
rawHatDirs[hat] = dirs
if #dirs > 0 then activate() end
end
function PadInput.draw()
if not active then return end
if not (love and love.graphics) then return end
local x, y = cursor.x, cursor.y
love.graphics.push("all")
if love.graphics.origin then love.graphics.origin() end
if love.graphics.setLineWidth then love.graphics.setLineWidth(1) end
love.graphics.setColor(0, 0, 0, 0.45)
if love.graphics.polygon then
love.graphics.polygon("fill",
x + 2, y + 2, x + 2, y + 22, x + 8, y + 16, x + 14, y + 26,
x + 18, y + 24, x + 11, y + 14, x + 20, y + 14)
love.graphics.setColor(1, 1, 1, 1)
love.graphics.polygon("fill",
x, y, x, y + 20, x + 6, y + 14, x + 12, y + 24,
x + 16, y + 22, x + 9, y + 12, x + 18, y + 12)
love.graphics.setColor(0.05, 0.07, 0.12, 1)
love.graphics.polygon("line",
x, y, x, y + 20, x + 6, y + 14, x + 12, y + 24,
x + 16, y + 22, x + 9, y + 12, x + 18, y + 12)
else
love.graphics.rectangle("fill", x, y, 12, 18)
end
love.graphics.pop()
end
return PadInput
-- Compat shim: PadInput lived here first; shared implementation is now
-- src/ui/PadCursor.lua so the touch-controls editor can reuse it.
return require("src.ui.PadCursor")