From f02d0d9d229854efe0852b63476f993e7e672a8d Mon Sep 17 00:00:00 2001 From: lippdev <108258235+lippdev@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:31:31 +0000 Subject: [PATCH] fix(controller): click of the right stick reaches Cursor mode The shell's button map had RightStick on 10, which is the left stick's click: pressing R3 toggled nothing and Cursor mode stayed unreachable by the only button that offers it. The map now comes from the shared table in controllerInput.ts, where RightStick is 11, instead of a second copy that could drift from it. The toolbar hints also name the button (`R3 cursor`), so the one control with no visible counterpart in the UI is discoverable. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- app/src/renderer/shell/app.tsx | 2 +- app/src/renderer/shell/gamepad.ts | 6 +----- app/src/shared/controllerGlyphs.ts | 21 +++++++++++++++------ app/tests/e2e/controller.spec.ts | 25 +++++++++++++++++++++++++ app/tests/unit/services.test.ts | 2 +- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/app/src/renderer/shell/app.tsx b/app/src/renderer/shell/app.tsx index 5b9dda1..9f8d160 100644 --- a/app/src/renderer/shell/app.tsx +++ b/app/src/renderer/shell/app.tsx @@ -181,7 +181,7 @@ export function App() { {(() => { const g = buttonGlyphs(state.controllerFamily); - return `${g.back} voltar · ${g.select} abrir · ${g.reload} recarregar · ${g.keyboard} teclado`; + return `${g.back} voltar · ${g.select} abrir · ${g.reload} recarregar · ${g.keyboard} teclado · ${g.stick} cursor`; })()} diff --git a/app/src/renderer/shell/gamepad.ts b/app/src/renderer/shell/gamepad.ts index ff1c669..9cf2f40 100644 --- a/app/src/renderer/shell/gamepad.ts +++ b/app/src/renderer/shell/gamepad.ts @@ -1,16 +1,12 @@ import { useEffect, useRef } from "preact/hooks"; import { controllerFamily, type ControllerFamily } from "../../shared/controllerGlyphs"; -import { directionOf, stickToScroll, stickToVelocity, type Direction } from "../../shared/controllerInput"; +import { Button, directionOf, stickToScroll, stickToVelocity, type Direction } from "../../shared/controllerInput"; import { activePad, padsInUse, startedUsing, type PadSample } from "../../shared/controllerSelection"; import { GAMEPAD_POLL_MS, gamepadPoll, STICK_INTERVAL_MS } from "../../shared/gamepadLoop"; import type { ShellState } from "../../shared/ipc"; import { bridge, cmd } from "./bridge"; import { trackActivePad, vibrate } from "./haptics"; -const Button = { - A: 0, B: 1, X: 2, Y: 3, LB: 4, RB: 5, LT: 6, RT: 7, Back: 8, Start: 9, RightStick: 10, - DpadUp: 12, DpadDown: 13, DpadLeft: 14, DpadRight: 15, -} as const; type Overlay = "none" | "tabs" | "menu"; diff --git a/app/src/shared/controllerGlyphs.ts b/app/src/shared/controllerGlyphs.ts index 342c878..2d3e543 100644 --- a/app/src/shared/controllerGlyphs.ts +++ b/app/src/shared/controllerGlyphs.ts @@ -10,13 +10,22 @@ export function controllerFamily(id: string | undefined): ControllerFamily { return "generic"; } -const GLYPHS: Record = { - xbox: { select: "A", back: "B", reload: "X", keyboard: "Y" }, - playstation: { select: "✕", back: "○", reload: "□", keyboard: "△" }, - switch: { select: "A", back: "B", reload: "Y", keyboard: "X" }, - generic: { select: "A", back: "B", reload: "X", keyboard: "Y" }, +export interface ControllerGlyphs { + select: string; + back: string; + reload: string; + keyboard: string; + /** Clicking the right stick: the only way into Cursor mode, where the same stick moves a pointer. */ + stick: string; +} + +const GLYPHS: Record = { + xbox: { select: "A", back: "B", reload: "X", keyboard: "Y", stick: "R3" }, + playstation: { select: "✕", back: "○", reload: "□", keyboard: "△", stick: "R3" }, + switch: { select: "A", back: "B", reload: "Y", keyboard: "X", stick: "R3" }, + generic: { select: "A", back: "B", reload: "X", keyboard: "Y", stick: "R3" }, }; -export function buttonGlyphs(family: ControllerFamily): { select: string; back: string; reload: string; keyboard: string } { +export function buttonGlyphs(family: ControllerFamily): ControllerGlyphs { return GLYPHS[family]; } diff --git a/app/tests/e2e/controller.spec.ts b/app/tests/e2e/controller.spec.ts index 44a4aba..f384dac 100644 --- a/app/tests/e2e/controller.spec.ts +++ b/app/tests/e2e/controller.spec.ts @@ -75,6 +75,31 @@ async function press(pad: number, button: number): Promise { await expect.poll(polls, { timeout: 10_000, message: "the shell's controller loop never read the release, so the next press would count as a hold" }).toBeGreaterThan(up); } +/** How far the page's own cursor overlay has moved horizontally, and whether it is drawn at all. */ +const cursor = () => app.evaluate(() => globalThis.joy.pageEval( + "(() => { const c = document.getElementById('__joy-cursor'); return c ? { shown: c.style.display === 'block', x: parseFloat((c.style.transform.match(/translate\\(([-\\d.]+)px/) || [0, '0'])[1]) } : null; })()", +)) as Promise<{ shown: boolean; x: number } | null>; + +test("the right stick turns the pointer on and the left stick moves it", async () => { + await installPads([XBOX]); + await expect.poll(hints, { timeout: 15_000 }).toContain("R3 cursor"); + const mode = () => chrome.locator(".status-line .mode").textContent(); + expect(await mode()).toBe("SPATIAL"); + + // The right stick is buttons[11]: pressing buttons[10] is the left stick and must not change the mode. + await press(0, 10); + expect(await mode()).toBe("SPATIAL"); + await press(0, 11); + await expect.poll(mode, { timeout: 10_000 }).toBe("CURSOR"); + await expect.poll(cursor, { timeout: 10_000 }).toMatchObject({ shown: true }); + + // In Cursor mode the stick drives the page's pointer instead of walking the focus. + const before = (await cursor())!.x; + await chrome.evaluate(() => { (window as unknown as { __pads: { axes: number[] }[] }).__pads[0]!.axes[0] = 1; }); + await expect.poll(async () => (await cursor())!.x, { timeout: 10_000 }).toBeGreaterThan(before + 20); + await chrome.evaluate(() => { (window as unknown as { __pads: { axes: number[] }[] }).__pads[0]!.axes[0] = 0; }); +}); + test("the pad last used owns the input and the hints follow it", async () => { await installPads([XBOX, DUALSENSE]); await expect.poll(hints, { timeout: 15_000 }).toContain("B voltar"); diff --git a/app/tests/unit/services.test.ts b/app/tests/unit/services.test.ts index f347085..fc86698 100644 --- a/app/tests/unit/services.test.ts +++ b/app/tests/unit/services.test.ts @@ -219,7 +219,7 @@ describe("controller glyphs", () => { expect(controllerFamily("Pro Controller (STANDARD GAMEPAD Vendor: 057e Product: 2009)")).toBe("switch"); expect(controllerFamily("8BitDo Pro 2")).toBe("generic"); expect(controllerFamily(undefined)).toBe("generic"); - expect(buttonGlyphs("playstation")).toEqual({ select: "✕", back: "○", reload: "□", keyboard: "△" }); + expect(buttonGlyphs("playstation")).toEqual({ select: "✕", back: "○", reload: "□", keyboard: "△", stick: "R3" }); expect(buttonGlyphs("xbox").select).toBe("A"); }); });