react-x11 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/package.json +11 -8
  2. package/src/Reconciler.js +34 -21
  3. package/src/components/Select.js +8 -2
  4. package/src/events.js +8 -2
  5. package/src/index.d.ts +5 -0
  6. package/src/nodes/boxpaint.js +9 -0
  7. package/src/nodes/preedit.js +64 -14
  8. package/src/scale.js +52 -22
  9. package/src/screencolor.js +104 -17
  10. package/src/wayland/app.js +560 -0
  11. package/src/wayland/backendwindow.js +1123 -0
  12. package/src/wayland/clipboard.js +326 -0
  13. package/src/wayland/connection.js +482 -0
  14. package/src/wayland/context2d.js +2133 -0
  15. package/src/wayland/decorations.js +476 -0
  16. package/src/wayland/dmabuf.js +89 -0
  17. package/src/wayland/dnd.js +581 -0
  18. package/src/wayland/fdutil.js +108 -0
  19. package/src/wayland/framestyle.js +257 -0
  20. package/src/wayland/glarea.js +371 -0
  21. package/src/wayland/glcontext.js +415 -0
  22. package/src/wayland/glyphatlas.js +237 -0
  23. package/src/wayland/input.js +417 -0
  24. package/src/wayland/keysymnames.js +35 -0
  25. package/src/wayland/layershell.js +363 -0
  26. package/src/wayland/outputs.js +601 -0
  27. package/src/wayland/protocols/cursor-shape-v1.json +1 -0
  28. package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
  29. package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
  30. package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
  31. package/src/wayland/protocols/fractional-scale-v1.json +1 -0
  32. package/src/wayland/protocols/index.json +127 -0
  33. package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
  34. package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
  35. package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
  36. package/src/wayland/protocols/presentation-time.json +1 -0
  37. package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
  38. package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
  39. package/src/wayland/protocols/tablet-v2.json +1 -0
  40. package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
  41. package/src/wayland/protocols/viewporter.json +1 -0
  42. package/src/wayland/protocols/wayland.json +1 -0
  43. package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
  44. package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
  45. package/src/wayland/protocols/xdg-activation-v1.json +1 -0
  46. package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
  47. package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
  48. package/src/wayland/protocols/xdg-shell.json +1 -0
  49. package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
  50. package/src/wayland/readback.js +99 -0
  51. package/src/wayland/screencopy.js +584 -0
  52. package/src/wayland/seat.js +584 -0
  53. package/src/wayland/shm.js +226 -0
  54. package/src/wayland/ssd.js +106 -0
  55. package/src/wayland/surface.js +123 -0
  56. package/src/wayland/swapchain.js +411 -0
  57. package/src/wayland/tablet.js +522 -0
  58. package/src/wayland/target.js +263 -0
  59. package/src/wayland/text.js +113 -0
  60. package/src/wayland/textinput.js +671 -0
  61. package/src/wayland/touch.js +284 -0
  62. package/src/wayland/window.js +827 -0
  63. package/src/wayland/xkb.js +425 -0
@@ -0,0 +1,417 @@
1
+ // Routing seat events into the window tree, in the vocabulary react-x11's
2
+ // `events.js` already speaks.
3
+ //
4
+ // The renderer subscribes to ntk-shaped window events — `mousedown`,
5
+ // `mousemove`, `wheel`, `keydown`, `focus` and so on, with X-style fields:
6
+ // device-pixel `x`/`y`, a `buttons` state mask, a `keycode`, a server
7
+ // timestamp. The Cocoa backend translates NSEvents into that shape; this
8
+ // translates `wl_seat`'s. The tree above never knows which.
9
+ //
10
+ // Three things are this file's own:
11
+ //
12
+ // - **Coordinates.** Seat events are surface-local, logical, and relative to
13
+ // the whole surface including the client-side decorations. The tree wants
14
+ // content-relative device pixels. Every event goes through one function
15
+ // that subtracts the frame and multiplies by the scale.
16
+ // - **The frame.** Presses on the titlebar and edges never reach the tree:
17
+ // they start compositor moves and resizes, toggle maximise, or hit the
18
+ // buttons. Hover over the frame drives the resize cursors; hover over
19
+ // content hands the cursor back to whatever the tree last set.
20
+ // - **Which window.** A seat event names a `wl_surface`; the map from
21
+ // surface to window lives on the app. Keys go to the keyboard-focused
22
+ // surface's window — its toplevel, when that is one of our popups, which
23
+ // mutter gives the keyboard to when it grabs — pointer events to the one
24
+ // under the pointer, and a popup with a grab receives the outside press
25
+ // that should dismiss it as an ordinary `mousedown` outside its bounds —
26
+ // which is what its `<popup grab>` logic on X11 expects from a grab.
27
+
28
+ import { Decorations } from './decorations.js';
29
+ import { XkbKeymap } from './xkb.js';
30
+ import { TOPLEVEL_STATE } from './window.js';
31
+
32
+ export class InputRouter {
33
+ /**
34
+ * @param {import('./app.js').WaylandApp} app
35
+ */
36
+ constructor(app) {
37
+ this.app = app;
38
+ this.seat = app.seat;
39
+ /** the backend window under the pointer */
40
+ this.pointerWindow = null;
41
+ /** the backend window with keyboard focus — a toplevel, never a popup */
42
+ this.focusWindow = null;
43
+ /** a window whose keyboard leave is held to see if an enter follows */
44
+ this._heldBlur = null;
45
+ /** what the frame's hover last asked the cursor to be, or null */
46
+ this._frameCursor = null;
47
+ /** touch points that began on the frame, which the tree never sees */
48
+ this._frameTouches = new Set();
49
+ this._wire();
50
+ }
51
+
52
+ _wire() {
53
+ const s = this.seat;
54
+ s.on('enter', (ev) => this._onEnter(ev));
55
+ s.on('leave', (ev) => this._onLeave(ev));
56
+ s.on('motion', (ev) => this._onMotion(ev));
57
+ s.on('buttonpress', (ev) => this._onButton(ev, true));
58
+ s.on('buttonrelease', (ev) => this._onButton(ev, false));
59
+ s.on('wheel', (ev) => this._onWheel(ev));
60
+ s.on('keydown', (ev) => this._onKey('keydown', ev));
61
+ s.on('keyup', (ev) => this._onKey('keyup', ev));
62
+ s.on('focus', (ev) => this._onFocus(ev, true));
63
+ s.on('blur', (ev) => this._onFocus(ev, false));
64
+ // touch.js and tablet.js emit the pointer events above for the finger or
65
+ // tool that is standing in for the pointer; only the raw points are new
66
+ for (const n of ['touchstart', 'touchmove', 'touchend', 'touchcancel'])
67
+ s.on(n, (ev) => this._onTouch(ev));
68
+ s.on('keymap', (_km, xkb) => {
69
+ if (xkb) this.app.X.keycode2keysyms = xkb.keycode2keysyms;
70
+ this.app.X.emit('mapping');
71
+ });
72
+ }
73
+
74
+ _window(surfaceId) {
75
+ return surfaceId == null ? null : (this.app.windows.get(surfaceId) ?? null);
76
+ }
77
+
78
+ /** The X-style state mask: modifiers and held buttons together. */
79
+ _state() {
80
+ return (
81
+ XkbKeymap.stateOf(this.seat.mods, this.seat.group) | this.seat.buttonMask
82
+ );
83
+ }
84
+
85
+ /** Surface-local logical -> content-relative device pixels. */
86
+ _point(win, x, y) {
87
+ const i = win.insets;
88
+ const s = win.scale;
89
+ return { x: Math.round((x - i.left) * s), y: Math.round((y - i.top) * s) };
90
+ }
91
+
92
+ _emit(win, name, ev) {
93
+ if (!win || win._destroyed) return;
94
+ win.emit(name, ev);
95
+ this.app.afterInput?.();
96
+ }
97
+
98
+ // ---- pointer ----------------------------------------------------------
99
+
100
+ _onEnter(ev) {
101
+ const win = this._window(ev.surface);
102
+ this.pointerWindow = win;
103
+ if (!win) return;
104
+ this._updateFrameHover(win, ev.x, ev.y);
105
+ const p = this._point(win, ev.x, ev.y);
106
+ this._emit(win, 'mouseover', {
107
+ ...p,
108
+ rootx: p.x,
109
+ rooty: p.y,
110
+ buttons: this._state(),
111
+ time: 0,
112
+ // a finger or a pen standing in for the pointer says so (touch.js,
113
+ // tablet.js); a mouse adds nothing, as on the other backends
114
+ ...ev.device,
115
+ });
116
+ }
117
+
118
+ _onLeave(ev) {
119
+ const win = this._window(ev.surface);
120
+ if (this.pointerWindow === win) this.pointerWindow = null;
121
+ if (!win) return;
122
+ if (win.decor && win.decor.hover) {
123
+ win.decor.hover = null;
124
+ win.repaintFrame();
125
+ }
126
+ this._frameCursor = null;
127
+ this._emit(win, 'mouseout', {
128
+ x: 0,
129
+ y: 0,
130
+ rootx: 0,
131
+ rooty: 0,
132
+ buttons: this._state(),
133
+ time: 0,
134
+ ...ev.device,
135
+ });
136
+ }
137
+
138
+ _onMotion(ev) {
139
+ const win = this.pointerWindow ?? this._window(ev.surface);
140
+ if (!win) return;
141
+ const hit = this._updateFrameHover(win, ev.x, ev.y);
142
+ if (hit.kind !== 'content' && !this.seat.buttonMask) return;
143
+ const p = this._point(win, ev.x, ev.y);
144
+ this._emit(win, 'mousemove', {
145
+ ...p,
146
+ rootx: p.x,
147
+ rooty: p.y,
148
+ buttons: this._state(),
149
+ time: ev.time,
150
+ ...ev.device,
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Frame hover: the button under the pointer (for its highlight) and the
156
+ * cursor the edge wants. Returns the hit so callers can skip the tree.
157
+ */
158
+ _updateFrameHover(win, x, y) {
159
+ if (!win.decor) return { kind: 'content' };
160
+ const hit = win.decor.hitTest(x, y, win.wl.width, win.wl.height);
161
+ const hoverId = hit.kind === 'button' ? hit.id : null;
162
+ if (win.decor.hover !== hoverId) {
163
+ win.decor.hover = hoverId;
164
+ win.repaintFrame();
165
+ }
166
+ const want = hit.kind === 'content' ? null : Decorations.cursorFor(hit);
167
+ if (want !== this._frameCursor) {
168
+ this._frameCursor = want;
169
+ this.seat.setCursor(want ?? win.treeCursor ?? 'default');
170
+ }
171
+ return hit;
172
+ }
173
+
174
+ _onButton(ev, pressed) {
175
+ const win = this.pointerWindow ?? this._window(ev.surface);
176
+ if (!win) return;
177
+ if (win.decor && pressed && !this.seat.buttonMask) {
178
+ // buttonMask already includes this press; the frame decides on a
179
+ // fresh press only, never mid-drag
180
+ }
181
+ if (win.decor) {
182
+ const hit = win.decor.hitTest(ev.x, ev.y, win.wl.width, win.wl.height);
183
+ if (hit.kind !== 'content') {
184
+ if (pressed) this._framePress(win, hit, ev);
185
+ else this._frameRelease(win, hit, ev);
186
+ return;
187
+ }
188
+ }
189
+ const p = this._point(win, ev.x, ev.y);
190
+ this._emit(win, pressed ? 'mousedown' : 'mouseup', {
191
+ ...p,
192
+ rootx: p.x,
193
+ rooty: p.y,
194
+ keycode: ev.button,
195
+ buttons: ev.state | XkbKeymap.stateOf(this.seat.mods, this.seat.group),
196
+ time: ev.time,
197
+ serial: ev.serial,
198
+ ...ev.device,
199
+ });
200
+ }
201
+
202
+ _framePress(win, hit, ev) {
203
+ const seat = this.seat.seat;
204
+ if (hit.kind === 'resize' && ev.button === 1) {
205
+ win.wl.startResize(seat, ev.serial, hit.edges);
206
+ return;
207
+ }
208
+ if (hit.kind === 'button') {
209
+ win.decor.pressed = hit.id;
210
+ return;
211
+ }
212
+ // titlebar: what a click means is the desktop's to say (framestyle.js)
213
+ const style = this.app.frameStyle?.value;
214
+ if (ev.button === 1) {
215
+ const now = ev.time;
216
+ if (win._lastTitlePress && now - win._lastTitlePress < 400) {
217
+ win._lastTitlePress = 0;
218
+ this._titlebarAction(win, style?.doubleClick ?? 'toggle-maximize', ev);
219
+ return;
220
+ }
221
+ win._lastTitlePress = now;
222
+ win.wl.startMove(seat, ev.serial);
223
+ } else if (ev.button === 2) {
224
+ this._titlebarAction(win, style?.middleClick ?? 'none', ev);
225
+ } else if (ev.button === 3) {
226
+ this._titlebarAction(win, style?.rightClick ?? 'menu', ev);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * One of `org.gnome.desktop.wm.preferences`' titlebar actions. `lower` and
232
+ * `toggle-shade` have no xdg-shell request, and a maximise in one
233
+ * direction is a maximise here: they do what the protocol allows, which
234
+ * for the first two is nothing.
235
+ */
236
+ _titlebarAction(win, action, ev) {
237
+ switch (action) {
238
+ case 'toggle-maximize':
239
+ case 'toggle-maximize-horizontally':
240
+ case 'toggle-maximize-vertically':
241
+ win.wl.maximize(!win.decor.maximized);
242
+ return;
243
+ case 'minimize':
244
+ win.wl.minimize();
245
+ return;
246
+ case 'menu':
247
+ win.wl.showWindowMenu(this.seat.seat, ev.serial, ev.x, ev.y);
248
+ return;
249
+ default:
250
+ }
251
+ }
252
+
253
+ _frameRelease(win, hit, ev) {
254
+ const pressed = win.decor.pressed;
255
+ win.decor.pressed = null;
256
+ if (
257
+ !pressed ||
258
+ hit.kind !== 'button' ||
259
+ hit.id !== pressed ||
260
+ ev.button !== 1
261
+ )
262
+ return;
263
+ if (pressed === 'close') win.requestClose();
264
+ else if (pressed === 'maximize') win.wl.maximize(!win.decor.maximized);
265
+ else if (pressed === 'minimize') win.wl.minimize();
266
+ }
267
+
268
+ _onWheel(ev) {
269
+ const win = this.pointerWindow ?? this._window(ev.surface);
270
+ if (!win) return;
271
+ if (
272
+ win.decor &&
273
+ win.decor.hitTest(ev.x, ev.y, win.wl.width, win.wl.height).kind !==
274
+ 'content'
275
+ )
276
+ return;
277
+ const p = this._point(win, ev.x, ev.y);
278
+ // X's wheel is "down is positive notches"; the seat already has that sign.
279
+ this._emit(win, 'wheel', {
280
+ name: 'wheel',
281
+ ...p,
282
+ rootx: p.x,
283
+ rooty: p.y,
284
+ buttons: this._state(),
285
+ deltaX: ev.deltaX,
286
+ deltaY: ev.deltaY,
287
+ smooth: ev.smooth,
288
+ source: ev.smooth ? 'valuator' : 'button',
289
+ time: ev.time,
290
+ ...ev.device,
291
+ });
292
+ }
293
+
294
+ // ---- touch ------------------------------------------------------------
295
+
296
+ /**
297
+ * Raw touch points — `touchstart`/`touchmove`/`touchend`/`touchcancel` on
298
+ * the window, each with every active point over it. No emulation here:
299
+ * touch.js makes the first finger the pointer, and those events arrived
300
+ * above as the pointer's own. A point that began on the frame belongs to
301
+ * the frame (its emulated press already started the move or resize) and
302
+ * is kept from the tree for its whole life.
303
+ */
304
+ _onTouch(ev) {
305
+ const win = this._window(ev.surface);
306
+ if (!win) return;
307
+ if (ev.type === 'touchstart' && win.decor) {
308
+ const hit = win.decor.hitTest(ev.x, ev.y, win.wl.width, win.wl.height);
309
+ if (hit.kind !== 'content') this._frameTouches.add(ev.id);
310
+ }
311
+ if (this._frameTouches.has(ev.id)) {
312
+ if (ev.type === 'touchend' || ev.type === 'touchcancel')
313
+ this._frameTouches.delete(ev.id);
314
+ return;
315
+ }
316
+ const p = this._point(win, ev.x, ev.y);
317
+ const s = win.scale;
318
+ this._emit(win, ev.type, {
319
+ ...p,
320
+ rootx: p.x,
321
+ rooty: p.y,
322
+ id: ev.id,
323
+ touchId: ev.id,
324
+ radiusX: ev.radiusX * s,
325
+ radiusY: ev.radiusY * s,
326
+ rotationAngle: ev.rotationAngle,
327
+ touches: ev.touches
328
+ .filter(
329
+ (t) => t.surface === ev.surface && !this._frameTouches.has(t.id),
330
+ )
331
+ .map((t) => ({ id: t.id, ...this._point(win, t.x, t.y) })),
332
+ buttons: ev.state | XkbKeymap.stateOf(this.seat.mods, this.seat.group),
333
+ pointerType: 'touch',
334
+ time: ev.time,
335
+ serial: ev.serial,
336
+ });
337
+ }
338
+
339
+ // ---- keyboard ---------------------------------------------------------
340
+
341
+ _onKey(name, ev) {
342
+ const win = this.focusWindow ?? this._focusRoot(this._window(ev.surface));
343
+ if (!win) return;
344
+ this._emit(win, name, {
345
+ keycode: ev.keycode,
346
+ keysym: ev.keysym,
347
+ baseKeysym: ev.baseKeysym,
348
+ codepoint: ev.codepoint,
349
+ buttons: ev.buttons,
350
+ group: ev.group,
351
+ time: ev.time,
352
+ repeat: ev.repeat,
353
+ });
354
+ }
355
+
356
+ /**
357
+ * The window whose tree has the keyboard when `win`'s surface does: `win`
358
+ * itself, or — for one of our popups — the toplevel its chain hangs from.
359
+ * Mutter gives the keyboard to a popup that grabs (a menu, a Select's
360
+ * list); X never focuses an override-redirect window, and the tree's focus
361
+ * model is X's: keys reach the focused node through its toplevel, wherever
362
+ * in the tree that node is.
363
+ */
364
+ _focusRoot(win) {
365
+ let w = win;
366
+ for (let i = 0; w?.isPopup && w.parentWindow && i < 16; i++) {
367
+ w = w.parentWindow;
368
+ }
369
+ return w;
370
+ }
371
+
372
+ /**
373
+ * `wl_keyboard` enter and leave. A move inside one window's family — the
374
+ * keyboard onto its grabbing popup and back, which mutter sends as a leave
375
+ * and an enter together — is no focus change at all: a leave is held
376
+ * until the events already read have been handled, and an enter for the
377
+ * same window drops it. Before this the toplevel heard a blur the moment a
378
+ * Select's list was configured, the Select closed the list on it, and on
379
+ * GNOME no dropdown ever showed. A leave naming a surface already gone —
380
+ * the popup that had the keyboard — is a leave of the window that had it.
381
+ */
382
+ _onFocus(ev, focused) {
383
+ const win = this._focusRoot(this._window(ev.surface));
384
+ if (!focused) {
385
+ const lost = win ?? this.focusWindow;
386
+ if (!lost) return;
387
+ this._heldBlur = lost;
388
+ setImmediate(() => {
389
+ if (this._heldBlur !== lost) return;
390
+ this._heldBlur = null;
391
+ this._setFocus(lost, false);
392
+ });
393
+ return;
394
+ }
395
+ const held = this._heldBlur;
396
+ this._heldBlur = null;
397
+ if (held && held !== win) this._setFocus(held, false);
398
+ if (held === win && this.focusWindow === win) return;
399
+ this._setFocus(win, true);
400
+ }
401
+
402
+ _setFocus(win, focused) {
403
+ if (focused) this.focusWindow = win;
404
+ else if (this.focusWindow === win) this.focusWindow = null;
405
+ if (!win) return;
406
+ if (win.decor) {
407
+ const wasActive = win.decor.active;
408
+ win.decor.active = focused || win.wl.states.has(TOPLEVEL_STATE.ACTIVATED);
409
+ if (wasActive !== win.decor.active) win.repaintFrame();
410
+ }
411
+ this._emit(win, focused ? 'focus' : 'blur', { buttons: 0, time: 0 });
412
+ }
413
+
414
+ destroy() {
415
+ this.seat.removeAllListeners();
416
+ }
417
+ }
@@ -0,0 +1,35 @@
1
+ // Keysym names -> values, for reading an XKB keymap.
2
+ //
3
+ // An `xkb_symbols` section names its keysyms the way `keysymdef.h` does —
4
+ // `q`, `exclam`, `Return`, `ISO_Level3_Shift`, `dead_acute`, `KP_1` — with
5
+ // two extra spellings for anything the table does not have: `U20AC` for a
6
+ // Unicode code point and a plain hex number. node-x11 already ships the
7
+ // whole of keysymdef.h as `XK_<name>: { code }`, so this is a lookup and the
8
+ // two conventions, not a table of its own.
9
+
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const table = require('x11/lib/keysyms.js');
14
+
15
+ const cache = new Map();
16
+
17
+ /**
18
+ * @param {string} name a keysym as an XKB file spells it
19
+ * @returns {number|undefined} the keysym, 0 for `NoSymbol`, undefined when unknown
20
+ */
21
+ export function keysymFromName(name) {
22
+ if (!name) return undefined;
23
+ const hit = cache.get(name);
24
+ if (hit !== undefined) return hit;
25
+ let sym;
26
+ if (name === 'NoSymbol' || name === 'VoidSymbol') sym = 0;
27
+ else if (/^U[0-9A-Fa-f]{4,6}$/.test(name)) {
28
+ const cp = parseInt(name.slice(1), 16);
29
+ // Latin-1 is its own keysym range; everything else is 0x01000000 + cp.
30
+ sym = cp >= 0x20 && cp <= 0xff ? cp : 0x01000000 + cp;
31
+ } else if (/^0x[0-9A-Fa-f]+$/.test(name)) sym = parseInt(name, 16);
32
+ else sym = table[`XK_${name}`]?.code;
33
+ cache.set(name, sym);
34
+ return sym;
35
+ }