Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/renderer/shell/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function App() {
<span class="pad-hints">
{(() => {
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`;
})()}
</span>
</div>
Expand Down
6 changes: 1 addition & 5 deletions app/src/renderer/shell/gamepad.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
21 changes: 15 additions & 6 deletions app/src/shared/controllerGlyphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,22 @@ export function controllerFamily(id: string | undefined): ControllerFamily {
return "generic";
}

const GLYPHS: Record<ControllerFamily, { select: string; back: string; reload: string; keyboard: string }> = {
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<ControllerFamily, ControllerGlyphs> = {
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];
}
25 changes: 25 additions & 0 deletions app/tests/e2e/controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,31 @@ async function press(pad: number, button: number): Promise<void> {
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");
Expand Down
2 changes: 1 addition & 1 deletion app/tests/unit/services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
Expand Down
Loading