Skip to content
Open
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
1 change: 0 additions & 1 deletion app/src/main/services/inputRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export class InputRouter {
}
break;
case "cursor-click": if (wc && this.host.inputMode !== "Arrows") this.cursor.click(wc, input.button); break;
case "cursor-hide": if (wc) await this.cursor.show(wc, false); break;
case "scroll":
if (!wc) return;
// Never pointed anywhere yet: the wheel turns over the middle of the page, not over a corner.
Expand Down
16 changes: 14 additions & 2 deletions app/src/main/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,11 +657,23 @@ export class Shell {

applyInputMode(mode: InputMode, announce: boolean): void {
this.inputMode = mode;
const wc = this.activeWebContents;
if (wc) void this.cursor.show(wc, mode === "Cursor");
this.showPointer();
if (announce) this.notify("inputMode", mode.toUpperCase());
}

/**
* The pointer is always on screen, where it was last left: each page draws its own, so a page that loads, a tab that is
* switched to or a mode that changes gets it drawn again (this runs on every change of the tab, its load included). The
* first time it starts in the middle of the page. Arrows mode is the exception: its sites take the stick as arrow keys.
*/
private showPointer(): void {
const wc = this.activeWebContents;
if (!wc) return;
const size = this.pageSize();
if (!this.cursor.placed && size.width > 0) this.cursor.place(size.width / 2, size.height / 2, size.width, size.height);
void this.cursor.show(wc, this.inputMode !== "Arrows" && this.cursor.placed);
}

cycleInputMode(): void {
const next = nextMode(this.inputMode);
const tab = this.tabs.active;
Expand Down
7 changes: 2 additions & 5 deletions app/src/renderer/shell/gamepad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,8 @@ export function useGamepad(state: ShellState, overlay: Overlay, setOverlay: (o:
// Down with nothing under the toolbar goes back into the page.
else if (chrome) { if (!moveFocusAmong(chromeControls(), direction) && direction === "down") leaveChrome(); }
else {
// The D-pad takes over from the pointer (in Cursor mode the D-pad steps the pointer itself, so it stays).
if (pointing.current && s.inputMode !== "Cursor") {
pointing.current = false;
bridge().controller({ type: "cursor-hide" });
}
// The D-pad takes A back from the pointer, which stays on screen (in Cursor mode the D-pad steps the pointer itself).
if (s.inputMode !== "Cursor") pointing.current = false;
bridge().controller({ type: "direction", direction });
}
}
Expand Down
2 changes: 0 additions & 2 deletions app/src/shared/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,6 @@ export type ControllerEvent =
| { type: "cursor-move"; dx: number; dy: number }
/** A at the pointer: a left click, or a right click when A was pressed twice in a row. */
| { type: "cursor-click"; button?: "left" | "right" }
/** The D-pad took over: the pointer goes away until the stick moves it again. */
| { type: "cursor-hide" }
/** The right stick: wheel notches down (positive) and to the right (`sideways`, positive). */
| { type: "scroll"; notches: number; sideways?: number }
| { type: "connected"; connected: boolean; family?: "xbox" | "playstation" | "switch" | "generic" };
28 changes: 25 additions & 3 deletions app/tests/e2e/controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ const tilt = (axis: number, value: number) => chrome.evaluate(([a, v]) => {
const pageLog = () => app.evaluate(() => globalThis.joy.pageEval("window.__log.join(',')")) as Promise<string>;
const pointerShown = () => app.evaluate(() => globalThis.joy.pageEval("(document.getElementById('__joy-cursor') || {}).style?.display === 'block'"));

test("the left stick points and A clicks there, twice for a right click, until the D-pad takes over", async () => {
test("the left stick points and A clicks there, twice for a right click, and the D-pad takes A back", async () => {
await installPads([XBOX]);
await app.evaluate(({ protocol }, page) => {
protocol.handle("https", () => new Response(page, { headers: { "content-type": "text/html" } }));
Expand All @@ -276,10 +276,10 @@ test("the left stick points and A clicks there, twice for a right click, until t
await press(0, Pad.A);
await expect.poll(pageLog, { timeout: 5_000 }).toBe("down0,down2,context");

// The D-pad takes over: the pointer goes away and A presses what the D-pad focused, not the spot under the pointer.
// The D-pad takes A over: A presses what the D-pad focused, not the spot under the pointer, which stays on screen.
await step(Pad.Down);
await expect.poll(pageFocus).toBe("b");
await expect.poll(pointerShown).toBe(false);
expect(await pointerShown()).toBe(true);
await step(Pad.A);
await expect.poll(pageLog).toBe("down0,down2,context,button");

Expand Down Expand Up @@ -351,3 +351,25 @@ test("the pointer is an arrow, and a hand over what can be clicked", async () =>
expect(await shapeAt("document.getElementById('area')")).toBe("arrow");
expect(await shapeAt("document.getElementById('b')")).toBe("hand");
});

test("the pointer is on every page from the start, and stays where it was when the page changes", async () => {
await app.evaluate(({ protocol }, page) => {
protocol.handle("https", () => new Response(page, { headers: { "content-type": "text/html" } }));
}, POINTER_PAGE);
const open = async (url: string) => {
await app.evaluate(({}, u) => globalThis.joy.command({ type: "navigate", url: u } as never), url);
await expect.poll(async () => {
const tab = (await tabs()).find((t) => t.isActive);
return tab?.url === url && !tab.isLoading;
}, { timeout: 30_000 }).toBe(true);
};
const where = () => app.evaluate(() => globalThis.joy.pageEval("document.getElementById('__joy-cursor')?.style.transform ?? ''")) as Promise<string>;
// No stick touched: the pointer is already there, in the middle of the page.
await open("https://one.example/");
await expect.poll(pointerShown, { timeout: 10_000 }).toBe(true);
const first = await where();
// Another page: the pointer is drawn again, at the same spot.
await open("https://two.example/");
await expect.poll(pointerShown, { timeout: 10_000 }).toBe(true);
await expect.poll(where).toBe(first);
});