mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 03:02:39 +02:00
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:
@@ -0,0 +1,215 @@
|
||||
-- Virtual pointer for overlay hosts (save editor, touch-controls editor) on
|
||||
-- Switch / handhelds / any gamepad. Mirrors the launcher's RomImporter pad
|
||||
-- cursor (speeds, deadzone, dual-path raw gate) without sharing that module.
|
||||
--
|
||||
-- Stick / D-pad move; real mouse motion yields so desktop stays normal.
|
||||
-- Callers map A → click, B → close, shoulders → host-specific actions, right
|
||||
-- stick → wheel notches (save editor lists).
|
||||
|
||||
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 PadCursor = {}
|
||||
|
||||
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 PadCursor.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 PadCursor.yieldToPointer()
|
||||
active = false
|
||||
end
|
||||
|
||||
-- Returns mx, my, isActive. When inactive the caller should use the system
|
||||
-- mouse; when active these coords feed hit-tests / draws.
|
||||
function PadCursor.pointer()
|
||||
return cursor.x, cursor.y, active
|
||||
end
|
||||
|
||||
function PadCursor.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 PadCursor.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 PadCursor.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 host handles:
|
||||
-- "a" | "b" | "tab_prev" | "tab_next" | nil
|
||||
function PadCursor.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 PadCursor.gamepadreleased(_, button)
|
||||
if button == "dpup" or button == "dpdown"
|
||||
or button == "dpleft" or button == "dpright" then
|
||||
dir[button] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function PadCursor.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 PadCursor.joystickpressed(joystick, button)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return nil end
|
||||
local padButton = GamepadMap.mapRawToGamepadButton(button)
|
||||
if padButton then return PadCursor.gamepadpressed(joystick, padButton) end
|
||||
return nil
|
||||
end
|
||||
|
||||
function PadCursor.joystickreleased(joystick, button)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return end
|
||||
local padButton = GamepadMap.mapRawToGamepadButton(button)
|
||||
if padButton then PadCursor.gamepadreleased(joystick, padButton) end
|
||||
end
|
||||
|
||||
function PadCursor.joystickaxis(joystick, axisIndex, value)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return end
|
||||
if axisIndex == 1 then
|
||||
PadCursor.gamepadaxis(joystick, "leftx", value)
|
||||
elseif axisIndex == 2 then
|
||||
PadCursor.gamepadaxis(joystick, "lefty", value)
|
||||
end
|
||||
end
|
||||
|
||||
function PadCursor.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 PadCursor.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 PadCursor
|
||||
@@ -11,9 +11,15 @@
|
||||
--
|
||||
-- Draws in full window LOVE units -- the same space TouchControls uses
|
||||
-- after Renderer:endFrame -- so what you drag here is what you get in play.
|
||||
--
|
||||
-- Switch / gamepad: PadCursor draws the virtual pointer the launcher just
|
||||
-- dropped (same soft-lock class as the save editor). A clicks / drags, B
|
||||
-- closes (Done), shoulders nudge button size.
|
||||
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local TouchControls = require("src.core.TouchControls")
|
||||
local PadCursor = require("src.ui.PadCursor")
|
||||
local GamepadMap = require("src.core.GamepadMap")
|
||||
|
||||
local Editor = {}
|
||||
|
||||
@@ -56,11 +62,13 @@ function Editor.load(opts)
|
||||
TouchControls:applyOptions(optsTbl)
|
||||
TouchControls:setPreview(true)
|
||||
Editor.enabled = TouchControls.enabled ~= false
|
||||
PadCursor.reset()
|
||||
end
|
||||
|
||||
function Editor.unload()
|
||||
TouchControls:setPreview(false)
|
||||
TouchControls:reset()
|
||||
PadCursor.reset()
|
||||
Editor.drag = nil
|
||||
Editor.onClose = nil
|
||||
end
|
||||
@@ -94,13 +102,16 @@ local function toggleEnabled()
|
||||
if not Editor.enabled then TouchControls:reset() end
|
||||
end
|
||||
|
||||
function Editor.update(_dt)
|
||||
-- drag follows the live pointer when love.touch / mouse is available;
|
||||
function Editor.update(dt)
|
||||
PadCursor.update(dt or 0)
|
||||
-- drag follows the live pointer when love.touch / mouse / pad is available;
|
||||
-- touchmoved / mousemoved also update, so this is a belt-and-suspenders
|
||||
-- path for Android where move events can be thin
|
||||
if not Editor.drag then return end
|
||||
local x, y
|
||||
if love.touch and love.touch.getPosition and Editor.drag.touchId then
|
||||
if Editor.drag.touchId == "pad" then
|
||||
x, y = PadCursor.pointer()
|
||||
elseif love.touch and love.touch.getPosition and Editor.drag.touchId then
|
||||
local ok, tx, ty = pcall(love.touch.getPosition, Editor.drag.touchId)
|
||||
if ok and tx then x, y = tx, ty end
|
||||
end
|
||||
@@ -244,6 +255,9 @@ function Editor.draw()
|
||||
love.graphics.circle("line", zone.cx, zone.cy, zone.w * 0.62)
|
||||
end
|
||||
end
|
||||
|
||||
-- pad / Joy-Con virtual cursor (after chrome so it sits on top)
|
||||
PadCursor.draw()
|
||||
end
|
||||
|
||||
local function beginDrag(id, x, y)
|
||||
@@ -285,6 +299,9 @@ end
|
||||
|
||||
function Editor.mousepressed(x, y, button)
|
||||
if button ~= 1 then return end
|
||||
-- Finger / mouse tap yields the Joy-Con pointer so the click lands where
|
||||
-- the event said (same NX soft-miss fix as the save editor).
|
||||
PadCursor.yieldToPointer()
|
||||
beginDrag("mouse", x, y)
|
||||
end
|
||||
|
||||
@@ -298,6 +315,7 @@ function Editor.mousereleased(x, y, button)
|
||||
end
|
||||
|
||||
function Editor.touchpressed(id, x, y)
|
||||
PadCursor.yieldToPointer()
|
||||
beginDrag(id, x, y)
|
||||
end
|
||||
|
||||
@@ -309,6 +327,57 @@ function Editor.touchreleased(id, x, y)
|
||||
endDrag(id)
|
||||
end
|
||||
|
||||
local function handlePadAction(action)
|
||||
if not action then return end
|
||||
if action == "a" then
|
||||
local mx, my = PadCursor.pointer()
|
||||
beginDrag("pad", mx, my)
|
||||
elseif action == "b" then
|
||||
close()
|
||||
elseif action == "tab_prev" then
|
||||
TouchControls:nudgeScale(-TouchControls.SCALE_STEP)
|
||||
elseif action == "tab_next" then
|
||||
TouchControls:nudgeScale(TouchControls.SCALE_STEP)
|
||||
end
|
||||
end
|
||||
|
||||
function Editor.gamepadpressed(joystick, button)
|
||||
handlePadAction(PadCursor.gamepadpressed(joystick, button))
|
||||
end
|
||||
|
||||
function Editor.gamepadreleased(joystick, button)
|
||||
PadCursor.gamepadreleased(joystick, button)
|
||||
-- A release ends a pad drag (hold A + stick to reposition a control).
|
||||
local action = GamepadMap.mapGamepadButton(button)
|
||||
if action == "a" then endDrag("pad") end
|
||||
end
|
||||
|
||||
function Editor.gamepadaxis(joystick, axis, value)
|
||||
PadCursor.gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function Editor.joystickpressed(joystick, button)
|
||||
handlePadAction(PadCursor.joystickpressed(joystick, button))
|
||||
end
|
||||
|
||||
function Editor.joystickreleased(joystick, button)
|
||||
PadCursor.joystickreleased(joystick, button)
|
||||
if GamepadMap.ignoreRawForJoystick(joystick) then return end
|
||||
local padButton = GamepadMap.mapRawToGamepadButton(button)
|
||||
if padButton then
|
||||
local action = GamepadMap.mapGamepadButton(padButton)
|
||||
if action == "a" then endDrag("pad") end
|
||||
end
|
||||
end
|
||||
|
||||
function Editor.joystickaxis(joystick, axis, value)
|
||||
PadCursor.joystickaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function Editor.joystickhat(joystick, hat, direction)
|
||||
PadCursor.joystickhat(joystick, hat, direction)
|
||||
end
|
||||
|
||||
function Editor.keypressed(key)
|
||||
if key == "escape" or key == "return" or key == "space" then
|
||||
close()
|
||||
|
||||
Reference in New Issue
Block a user