Merge pull request #574 from andrewqsantos/feat/switch-nx

Nintendo Switch / love-nx support (#531)
This commit is contained in:
bryanthaboi
2026-08-03 17:08:54 -04:00
committed by GitHub
87 changed files with 8915 additions and 276 deletions
+22 -15
View File
@@ -153,21 +153,28 @@ function NamingScreen:update(dt)
if self.row ~= caseRow then
self.col = self.col < #GRID[self.row] and self.col + 1 or 1
end
elseif input:wasPressed("b") then
table.remove(self.glyphs)
elseif input:wasPressed("a") then
if self.row == edRow and self.col == edCol then
self:confirm()
return
end
if self.row == caseRow then
self.lower = not self.lower
return
end
if #self.glyphs < self.maxLen then
Sound.play(self.game.data, "Press_AB")
table.insert(self.glyphs, GRID[self.row][self.col])
if #self.glyphs >= self.maxLen then self:jumpToEnd() end
else
-- Prefer A over B when both edges fire in one frame (love-nx dual
-- gamepad+raw path historically set both; erase must not win).
local pressedA = input:wasPressed("a")
local pressedB = input:wasPressed("b")
if pressedA and pressedB then pressedB = false end
if pressedB then
table.remove(self.glyphs)
elseif pressedA then
if self.row == edRow and self.col == edCol then
self:confirm()
return
end
if self.row == caseRow then
self.lower = not self.lower
return
end
if #self.glyphs < self.maxLen then
Sound.play(self.game.data, "Press_AB")
table.insert(self.glyphs, GRID[self.row][self.col])
if #self.glyphs >= self.maxLen then self:jumpToEnd() end
end
end
end
end
+215
View File
@@ -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
+72 -3
View File
@@ -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()