keyboard, and back off the size a quarter step

── keys ──

`rfb.ts` already had a `key()` that nothing called. It now has `keysymFor`,
mapping `KeyboardEvent.key` to an X11 keysym — `key` rather than `code`,
because it is already the character the user's own layout produced and `code`
would need that layout re-implemented here to get back to a symbol. Latin-1
passes straight through, above it uses X's 0x01000000 Unicode offset, and the
named keys come from keysymdef.h.

The canvas is focusable and takes focus on mousedown. Scoping keys to focus is
what stops the panel eating keys meant for the rest of the app — this is one
panel on a dashboard, so a window listener would be a global keyboard trap.
Modified keys and unmapped keys are left alone: cmd-W stays the browser's and
Tab keeps moving focus. preventDefault only for keys actually sent.

Keys reach wine without a window manager — verified by pressing F2 on the
display and watching the board reset. So the README's guess that wine needed a
WM to hand it focus was wrong; Auto Options ignores Escape because that VB6
form has no Cancel button, which is a different thing. Corrected there.

20 keysym cases checked against keysymdef.h, including the off-by-one I put in
my own test first: XK_F2 is 0xffbf, not 0xffc0.

── size ──

Half steps overshot: 2x to 2.5x is too small to too large with nothing in
between. Quarter steps from 2x up, which keeps cells whole for the same reason
halves did — a 16px grid at 2.25x is a 36px cell — and lets the picture track
the panel instead of lurching. Plus a 24px device-pixel margin, because
filling the panel edge to edge is most of what "too large" looks like.

Also: the scale was ignoring the panel's own CSS `zoom`. `devicePixelRatio`
knows nothing about it, so at 75% every cell boundary went back onto fractional
device pixels — the exact defect the snapping exists to prevent, reachable from
a button in the panel header. Effective ratio is now dpr x zoom, read as
getBoundingClientRect().width / offsetWidth.

In a 1300x900 device-pixel panel: 2.5x becomes 2.25x, 1260 to 1134 px wide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
This commit is contained in:
2026-08-23 22:32:10 +00:00
co-authored by Claude Opus 5
parent e4247cd760
commit ef74f0c2dd
3 changed files with 133 additions and 20 deletions
+59
View File
@@ -336,6 +336,12 @@ export class RfbClient {
this.send(message);
}
/**
* KeyEvent. The keysym is X11's, not a browser keycode — see `keysymFor`.
*
* Modifiers are not sent alongside: x11vnc resolves a keysym to a keycode and presses Shift itself if
* the layout needs it, so sending the symbol for `A` produces `A` without us tracking Shift.
*/
key(keysym: number, down: boolean): void {
const message = new Uint8Array(8);
const view = new DataView(message.buffer);
@@ -345,3 +351,56 @@ export class RfbClient {
this.send(message);
}
}
/**
* A browser `KeyboardEvent.key` as an X11 keysym, or null for keys the game has no use for.
*
* `key` rather than `code`, because it is already the CHARACTER the layout produced — the shifted one,
* the accented one, the one from the user's own keyboard map. `code` is a physical position and would
* need the layout re-implemented here to get back to a symbol.
*
* Latin-1 maps straight through: for a single printable character the keysym IS the code point. Above
* that, X uses a 0x01000000 offset for Unicode, which is what the last branch does. Everything else is
* a named key from `keysymdef.h`.
*
* Returning null is how a key stays the BROWSER's: unmapped keys are not swallowed by the panel.
*/
const NAMED_KEYSYMS: Record<string, number> = {
Backspace: 0xff08,
Tab: 0xff09,
Enter: 0xff0d,
Escape: 0xff1b,
Delete: 0xffff,
Home: 0xff50,
End: 0xff57,
PageUp: 0xff55,
PageDown: 0xff56,
ArrowLeft: 0xff51,
ArrowUp: 0xff52,
ArrowRight: 0xff53,
ArrowDown: 0xff54,
Insert: 0xff63,
Shift: 0xffe1,
Control: 0xffe3,
Alt: 0xffe9,
Meta: 0xffeb,
CapsLock: 0xffe5,
};
export function keysymFor(key: string): number | null {
const named = NAMED_KEYSYMS[key];
if (named !== undefined) return named;
// F1–F24 are contiguous from 0xffbe.
const fn = /^F(\d{1,2})$/.exec(key);
if (fn) {
const n = Number(fn[1]);
return n >= 1 && n <= 24 ? 0xffbd + n : null;
}
// Anything longer than one character here is a key we have no mapping for — leave it to the browser.
if ([...key].length !== 1) return null;
const code = key.codePointAt(0);
if (code === undefined) return null;
return code <= 0xff ? code : 0x01000000 + code;
}