react-x11 2.13.0 → 2.15.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 (70) 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/glnodes.js +15 -3
  6. package/src/index.d.ts +5 -0
  7. package/src/nodes/boxpaint.js +9 -0
  8. package/src/nodes/preedit.js +64 -14
  9. package/src/ntk.js +10 -1
  10. package/src/scale.js +52 -22
  11. package/src/screencolor.js +104 -17
  12. package/src/types/system.d.ts +14 -1
  13. package/src/wayland/app.js +574 -0
  14. package/src/wayland/backendwindow.js +1199 -0
  15. package/src/wayland/clipboard.js +326 -0
  16. package/src/wayland/connection.js +485 -0
  17. package/src/wayland/context2d.js +2290 -0
  18. package/src/wayland/decorations.js +476 -0
  19. package/src/wayland/device.js +55 -0
  20. package/src/wayland/dmabuf.js +89 -0
  21. package/src/wayland/dnd.js +581 -0
  22. package/src/wayland/fdutil.js +108 -0
  23. package/src/wayland/framestyle.js +257 -0
  24. package/src/wayland/framewatch.js +190 -0
  25. package/src/wayland/glarea.js +372 -0
  26. package/src/wayland/glcontext.js +415 -0
  27. package/src/wayland/glyphatlas.js +237 -0
  28. package/src/wayland/input.js +417 -0
  29. package/src/wayland/keysymnames.js +35 -0
  30. package/src/wayland/layershell.js +363 -0
  31. package/src/wayland/outputs.js +601 -0
  32. package/src/wayland/protocols/cursor-shape-v1.json +1 -0
  33. package/src/wayland/protocols/ext-idle-notify-v1.json +1 -0
  34. package/src/wayland/protocols/ext-image-capture-source-v1.json +1 -0
  35. package/src/wayland/protocols/ext-image-copy-capture-v1.json +1 -0
  36. package/src/wayland/protocols/fractional-scale-v1.json +1 -0
  37. package/src/wayland/protocols/index.json +131 -0
  38. package/src/wayland/protocols/kde-server-decoration.json +1 -0
  39. package/src/wayland/protocols/keyboard-shortcuts-inhibit-unstable-v1.json +1 -0
  40. package/src/wayland/protocols/linux-dmabuf-v1.json +1 -0
  41. package/src/wayland/protocols/pointer-constraints-unstable-v1.json +1 -0
  42. package/src/wayland/protocols/presentation-time.json +1 -0
  43. package/src/wayland/protocols/primary-selection-unstable-v1.json +1 -0
  44. package/src/wayland/protocols/relative-pointer-unstable-v1.json +1 -0
  45. package/src/wayland/protocols/tablet-v2.json +1 -0
  46. package/src/wayland/protocols/text-input-unstable-v3.json +1 -0
  47. package/src/wayland/protocols/viewporter.json +1 -0
  48. package/src/wayland/protocols/wayland.json +1 -0
  49. package/src/wayland/protocols/wlr-layer-shell-unstable-v1.json +1 -0
  50. package/src/wayland/protocols/wlr-screencopy-unstable-v1.json +1 -0
  51. package/src/wayland/protocols/xdg-activation-v1.json +1 -0
  52. package/src/wayland/protocols/xdg-decoration-unstable-v1.json +1 -0
  53. package/src/wayland/protocols/xdg-output-unstable-v1.json +1 -0
  54. package/src/wayland/protocols/xdg-shell.json +1 -0
  55. package/src/wayland/protocols/xdg-toplevel-icon-v1.json +1 -0
  56. package/src/wayland/readback.js +99 -0
  57. package/src/wayland/screencopy.js +584 -0
  58. package/src/wayland/seat.js +584 -0
  59. package/src/wayland/shm.js +226 -0
  60. package/src/wayland/ssd.js +234 -0
  61. package/src/wayland/surface.js +141 -0
  62. package/src/wayland/swapchain.js +411 -0
  63. package/src/wayland/tablet.js +522 -0
  64. package/src/wayland/target.js +269 -0
  65. package/src/wayland/text.js +113 -0
  66. package/src/wayland/textinput.js +671 -0
  67. package/src/wayland/touch.js +284 -0
  68. package/src/wayland/window.js +854 -0
  69. package/src/wayland/xkb.js +425 -0
  70. package/src/windowstate.js +41 -1
@@ -0,0 +1,584 @@
1
+ // Input: the seat, its pointer and its keyboard.
2
+ //
3
+ // The pointer is the easy half and maps almost exactly onto what the X11
4
+ // backend already synthesises — enter/leave/motion/button/axis, with
5
+ // coordinates in surface-local `wl_fixed` that the codec has already turned
6
+ // back into numbers. Three differences are worth knowing:
7
+ //
8
+ // - Events arrive in *frames*: a `wl_pointer.frame` marks the end of a
9
+ // logically atomic group, so a diagonal motion with a wheel tick is one
10
+ // update rather than three.
11
+ // - There is no `QueryPointer`: a client learns where the pointer is by
12
+ // being told, or not at all.
13
+ // - **The client owns the cursor.** On `enter` the pointer image over the
14
+ // surface is whatever the client sets — set nothing and there is no
15
+ // cursor at all. `wp_cursor_shape_manager_v1` makes that one request with
16
+ // a shape name; without it a client would have to load a cursor theme and
17
+ // attach image buffers itself.
18
+ //
19
+ // The keyboard is where this backend earns its transport. The compositor
20
+ // does not send keysyms — it sends **the keymap itself, once, as a file
21
+ // descriptor**, and every later event is a raw keycode the client interprets
22
+ // against that map (xkb.js). Two more things the compositor leaves to the
23
+ // client: **key repeat** (`repeat_info` gives the rate and delay; the events
24
+ // themselves never repeat) and the modifier state, which arrives as its own
25
+ // event rather than on each key.
26
+
27
+ import { EventEmitter } from 'node:events';
28
+ import fs from 'node:fs';
29
+ import { XkbKeymap } from './xkb.js';
30
+
31
+ /** `wl_seat.capability` bits. */
32
+ export const SEAT_CAP = { POINTER: 1, KEYBOARD: 2, TOUCH: 4 };
33
+
34
+ export const RELEASED = 0;
35
+ export const PRESSED = 1;
36
+
37
+ /** Linux evdev button codes, which is what `wl_pointer.button` carries. */
38
+ export const BTN = {
39
+ LEFT: 0x110,
40
+ RIGHT: 0x111,
41
+ MIDDLE: 0x112,
42
+ SIDE: 0x113,
43
+ EXTRA: 0x114,
44
+ };
45
+
46
+ /** evdev -> the 1-based numbering the rest of react-x11 speaks. */
47
+ export const BUTTON_NUMBER = {
48
+ [BTN.LEFT]: 1,
49
+ [BTN.MIDDLE]: 2,
50
+ [BTN.RIGHT]: 3,
51
+ [BTN.SIDE]: 8,
52
+ [BTN.EXTRA]: 9,
53
+ };
54
+
55
+ /** X state-mask bits for held pointer buttons (Button1Mask..). */
56
+ export const BUTTON_MASK = {
57
+ 1: 1 << 8,
58
+ 2: 1 << 9,
59
+ 3: 1 << 10,
60
+ 8: 1 << 15,
61
+ 9: 1 << 16,
62
+ };
63
+
64
+ /**
65
+ * The keycode offset between evdev and X.
66
+ *
67
+ * X keycodes are evdev codes plus 8, an artefact of the original protocol's
68
+ * minimum keycode. Every XKB keymap — including the one the compositor hands
69
+ * over — is indexed the X way, so this is the conversion the keymap expects.
70
+ */
71
+ export const EVDEV_KEYCODE_OFFSET = 8;
72
+
73
+ /** `wp_cursor_shape_device_v1.shape`, by the CSS names react-x11 uses. */
74
+ export const CURSOR_SHAPES = {
75
+ default: 1,
76
+ context_menu: 2,
77
+ 'context-menu': 2,
78
+ help: 3,
79
+ pointer: 4,
80
+ progress: 5,
81
+ wait: 6,
82
+ cell: 7,
83
+ crosshair: 8,
84
+ text: 9,
85
+ vertical_text: 10,
86
+ 'vertical-text': 10,
87
+ alias: 11,
88
+ copy: 12,
89
+ move: 13,
90
+ no_drop: 14,
91
+ 'no-drop': 14,
92
+ not_allowed: 15,
93
+ 'not-allowed': 15,
94
+ grab: 16,
95
+ grabbing: 17,
96
+ e_resize: 18,
97
+ 'e-resize': 18,
98
+ n_resize: 19,
99
+ 'n-resize': 19,
100
+ ne_resize: 20,
101
+ 'ne-resize': 20,
102
+ nw_resize: 21,
103
+ 'nw-resize': 21,
104
+ s_resize: 22,
105
+ 's-resize': 22,
106
+ se_resize: 23,
107
+ 'se-resize': 23,
108
+ sw_resize: 24,
109
+ 'sw-resize': 24,
110
+ w_resize: 25,
111
+ 'w-resize': 25,
112
+ ew_resize: 26,
113
+ 'ew-resize': 26,
114
+ ns_resize: 27,
115
+ 'ns-resize': 27,
116
+ nesw_resize: 28,
117
+ 'nesw-resize': 28,
118
+ nwse_resize: 29,
119
+ 'nwse-resize': 29,
120
+ col_resize: 30,
121
+ 'col-resize': 30,
122
+ row_resize: 31,
123
+ 'row-resize': 31,
124
+ all_scroll: 32,
125
+ 'all-scroll': 32,
126
+ zoom_in: 33,
127
+ 'zoom-in': 33,
128
+ zoom_out: 34,
129
+ 'zoom-out': 34,
130
+ dnd_ask: 35,
131
+ all_resize: 36,
132
+ 'all-resize': 36,
133
+ // X cursor names the tree may still use
134
+ arrow: 1,
135
+ left_ptr: 1,
136
+ hand: 4,
137
+ hand2: 4,
138
+ xterm: 9,
139
+ watch: 6,
140
+ fleur: 13,
141
+ sb_h_double_arrow: 26,
142
+ sb_v_double_arrow: 27,
143
+ top_left_corner: 21,
144
+ top_right_corner: 20,
145
+ bottom_left_corner: 24,
146
+ bottom_right_corner: 23,
147
+ left_side: 25,
148
+ right_side: 18,
149
+ top_side: 19,
150
+ bottom_side: 22,
151
+ question_arrow: 3,
152
+ tcross: 8,
153
+ plus: 7,
154
+ };
155
+
156
+ /** One wheel notch, in the scroll units `wl_pointer.axis` reports. */
157
+ const NOTCH = 10;
158
+
159
+ export class WaylandSeat extends EventEmitter {
160
+ constructor(seat, { cursorShapes = null } = {}) {
161
+ super();
162
+ this.seat = seat;
163
+ this.name = null;
164
+ this.capabilities = 0;
165
+ this.pointer = null;
166
+ this.keyboard = null;
167
+ /** touch.js and tablet.js attach themselves here; both emulate the pointer */
168
+ this.touch = null;
169
+ this.tablet = null;
170
+ this._cursorShapes = cursorShapes;
171
+ this._cursorDevice = null;
172
+ this._cursor = 'default';
173
+ /** the serial of the most recent input event — what move/resize/popups need */
174
+ this.lastSerial = 0;
175
+ /** serial of the most recent pointer press or key press specifically */
176
+ this.lastPressSerial = 0;
177
+ this.pointerX = 0;
178
+ this.pointerY = 0;
179
+ /** the `wl_surface` id under the pointer, or null */
180
+ this.pointerSurface = null;
181
+ /** the `wl_surface` id with keyboard focus, or null */
182
+ this.focus = null;
183
+ this.keymap = null;
184
+ /** parsed keymap, or null until the fd arrives */
185
+ this.xkb = null;
186
+ /** X-style modifier mask: depressed | latched | locked */
187
+ this.mods = 0;
188
+ this.group = 0;
189
+ /** pointer buttons currently held, as an X state mask */
190
+ this.buttonMask = 0;
191
+ this.repeat = { rate: 25, delay: 600 };
192
+ this._repeatTimer = null;
193
+ this._repeatKey = null;
194
+ this._pending = null;
195
+ this._axis = null;
196
+ }
197
+
198
+ /**
199
+ * @param {import('./connection.js').WaylandConnection} conn
200
+ */
201
+ static async bind(conn) {
202
+ // A seat of our own rather than the connection's cached singleton: the
203
+ // capabilities event that says whether there is a pointer and a keyboard
204
+ // arrives once, right after the bind, and a proxy bound earlier by someone
205
+ // else (a popup grab, the clipboard) has already had it. Binding again is
206
+ // legal — wl_seat is a global — and the compositor answers again.
207
+ if (!conn.has('wl_seat')) {
208
+ throw new Error(
209
+ `this compositor does not advertise wl_seat. Available globals: ${[...conn.globals].sort().join(', ')}`,
210
+ );
211
+ }
212
+ const proxy = await conn.display.bind('wl_seat');
213
+ proxy.setMaxListeners?.(0);
214
+ if (!conn._bound.has('wl_seat')) conn._bound.set('wl_seat', proxy);
215
+ const cursorShapes = await conn.bind('wp_cursor_shape_manager_v1');
216
+ const seat = new WaylandSeat(proxy, { cursorShapes });
217
+ seat._wire();
218
+ await conn.roundtrip();
219
+ return seat;
220
+ }
221
+
222
+ _wire() {
223
+ const seat = this.seat;
224
+ seat.on('name', (name) => {
225
+ this.name = name;
226
+ });
227
+ seat.on('capabilities', (caps) => {
228
+ this.capabilities = caps;
229
+ if (caps & SEAT_CAP.POINTER && !this.pointer) this._addPointer();
230
+ if (caps & SEAT_CAP.KEYBOARD && !this.keyboard) this._addKeyboard();
231
+ this.emit('capabilities', caps);
232
+ });
233
+ }
234
+
235
+ // ---- pointer ----------------------------------------------------------
236
+
237
+ _addPointer() {
238
+ const p = this.seat.$.get_pointer();
239
+ this.pointer = p;
240
+ if (this._cursorShapes) {
241
+ this._cursorDevice = this._cursorShapes.$.get_pointer(p.id);
242
+ }
243
+
244
+ p.on('enter', (serial, surfaceId, x, y) => {
245
+ this.lastSerial = serial;
246
+ this.pointerX = x;
247
+ this.pointerY = y;
248
+ this.pointerSurface = surfaceId;
249
+ // The cursor over this surface is ours to set, and now is when.
250
+ this._applyCursor(serial);
251
+ this._queue({ type: 'enter', serial, surface: surfaceId, x, y });
252
+ });
253
+ p.on('leave', (serial, surfaceId) => {
254
+ this.lastSerial = serial;
255
+ if (this.pointerSurface === surfaceId) this.pointerSurface = null;
256
+ this._queue({ type: 'leave', serial, surface: surfaceId });
257
+ });
258
+ p.on('motion', (time, x, y) => {
259
+ this.pointerX = x;
260
+ this.pointerY = y;
261
+ this._queue({ type: 'motion', time, x, y, surface: this.pointerSurface });
262
+ });
263
+ p.on('button', (serial, time, button, state) => {
264
+ this.lastSerial = serial;
265
+ const num = BUTTON_NUMBER[button] ?? 0;
266
+ const bit = BUTTON_MASK[num] ?? 0;
267
+ // the state mask carries the buttons held *before* this event, as X does
268
+ const buttonsBefore = this.buttonMask;
269
+ if (state === PRESSED) {
270
+ this.lastPressSerial = serial;
271
+ this.buttonMask |= bit;
272
+ } else {
273
+ this.buttonMask &= ~bit;
274
+ }
275
+ this._queue({
276
+ type: state === PRESSED ? 'buttonpress' : 'buttonrelease',
277
+ serial,
278
+ time,
279
+ button: num,
280
+ evdev: button,
281
+ x: this.pointerX,
282
+ y: this.pointerY,
283
+ surface: this.pointerSurface,
284
+ state: buttonsBefore,
285
+ });
286
+ });
287
+ // Scrolling, three ways. `axis` is continuous distance; `axis_discrete`
288
+ // (v5-7) and `axis_value120` (v8) are wheel notches, which is what a
289
+ // list expects to page by. Whichever arrives is folded into one wheel
290
+ // event per frame with both a notch count and a smooth delta.
291
+ p.on('axis', (time, axis, value) => {
292
+ const a = (this._axis ??= {
293
+ time,
294
+ dx: 0,
295
+ dy: 0,
296
+ nx: 0,
297
+ ny: 0,
298
+ discrete: false,
299
+ source: null,
300
+ });
301
+ a.time = time;
302
+ if (axis === 0) a.dy += value;
303
+ else a.dx += value;
304
+ });
305
+ p.on('axis_source', (source) => {
306
+ const a = (this._axis ??= {
307
+ dx: 0,
308
+ dy: 0,
309
+ nx: 0,
310
+ ny: 0,
311
+ discrete: false,
312
+ source: null,
313
+ });
314
+ a.source = source; // 0 wheel, 1 finger, 2 continuous, 3 wheel_tilt
315
+ });
316
+ p.on('axis_discrete', (axis, steps) => {
317
+ const a = (this._axis ??= {
318
+ dx: 0,
319
+ dy: 0,
320
+ nx: 0,
321
+ ny: 0,
322
+ discrete: false,
323
+ source: null,
324
+ });
325
+ a.discrete = true;
326
+ if (axis === 0) a.ny += steps;
327
+ else a.nx += steps;
328
+ });
329
+ p.on('axis_value120', (axis, v120) => {
330
+ const a = (this._axis ??= {
331
+ dx: 0,
332
+ dy: 0,
333
+ nx: 0,
334
+ ny: 0,
335
+ discrete: false,
336
+ source: null,
337
+ });
338
+ a.discrete = true;
339
+ if (axis === 0) a.ny += v120 / 120;
340
+ else a.nx += v120 / 120;
341
+ });
342
+ p.on('axis_stop', () => {});
343
+ p.on('axis_relative_direction', () => {});
344
+ p.on('frame', () => this._flush());
345
+ }
346
+
347
+ _queue(ev) {
348
+ (this._pending ??= []).push(ev);
349
+ // Compositors before v5 send no frame; flush on the microtask so a real
350
+ // frame still coalesces everything dispatched from one socket read.
351
+ queueMicrotask(() => this._flush());
352
+ }
353
+
354
+ _flush() {
355
+ const axis = this._axis;
356
+ if (axis) {
357
+ this._axis = null;
358
+ const smooth = axis.source === 1 || axis.source === 2; // finger/continuous
359
+ const ny = axis.discrete ? axis.ny : axis.dy / NOTCH;
360
+ const nx = axis.discrete ? axis.nx : axis.dx / NOTCH;
361
+ (this._pending ??= []).push({
362
+ type: 'wheel',
363
+ time: axis.time,
364
+ deltaX: nx,
365
+ deltaY: ny,
366
+ pixelsX: axis.dx,
367
+ pixelsY: axis.dy,
368
+ smooth,
369
+ x: this.pointerX,
370
+ y: this.pointerY,
371
+ surface: this.pointerSurface,
372
+ });
373
+ }
374
+ const batch = this._pending;
375
+ if (!batch || batch.length === 0) return;
376
+ this._pending = null;
377
+ for (const ev of batch) this.emit(ev.type, ev);
378
+ this.emit('frame', batch);
379
+ }
380
+
381
+ /**
382
+ * Set the pointer image, by CSS/X cursor name. Takes effect over the
383
+ * surface the pointer is in, now and on every later `enter`.
384
+ */
385
+ setCursor(name) {
386
+ this._cursor = name ?? 'default';
387
+ if (this.pointerSurface != null) this._applyCursor(this.lastSerial);
388
+ // a tablet tool in proximity has a cursor of its own and follows this one
389
+ this.emit('cursor', this._cursor);
390
+ }
391
+
392
+ _applyCursor(serial) {
393
+ if (!this.pointer) return;
394
+ const name = this._cursor;
395
+ if (name === 'none') {
396
+ // a null surface hides the cursor
397
+ this.pointer.$.set_cursor(serial, null, 0, 0);
398
+ return;
399
+ }
400
+ if (this._cursorDevice) {
401
+ const shape = CURSOR_SHAPES[name] ?? CURSOR_SHAPES.default;
402
+ this._cursorDevice.$.set_shape(serial, shape);
403
+ }
404
+ // Without cursor-shape there is no way to name a cursor; a themed image
405
+ // buffer would have to be loaded and attached, which is not done here.
406
+ }
407
+
408
+ // ---- keyboard ---------------------------------------------------------
409
+
410
+ _addKeyboard() {
411
+ const k = this.seat.$.get_keyboard();
412
+ this.keyboard = k;
413
+
414
+ k.on('keymap', (format, fd, size) => {
415
+ // THE fd receive. Everything about this backend's transport exists so
416
+ // that this number is a real descriptor rather than -1.
417
+ if (typeof fd !== 'number' || fd < 0) {
418
+ this.emit(
419
+ 'error',
420
+ new Error(
421
+ 'the compositor sent a keymap but no descriptor arrived with it',
422
+ ),
423
+ );
424
+ return;
425
+ }
426
+ try {
427
+ const buf = Buffer.allocUnsafe(size);
428
+ let read = 0;
429
+ while (read < size) {
430
+ const n = fs.readSync(fd, buf, read, size - read, read);
431
+ if (n <= 0) break;
432
+ read += n;
433
+ }
434
+ const text =
435
+ format === 1
436
+ ? buf.toString('utf8', 0, read).replace(/\0+$/, '')
437
+ : null;
438
+ this.keymap = { format, size, text };
439
+ this.xkb = text ? XkbKeymap.parse(text) : null;
440
+ this.emit('keymap', this.keymap, this.xkb);
441
+ } catch (err) {
442
+ this.emit(
443
+ 'error',
444
+ new Error(`could not read the keymap: ${err.message}`, {
445
+ cause: err,
446
+ }),
447
+ );
448
+ } finally {
449
+ // The descriptor is ours once taken, and nothing else will close it.
450
+ try {
451
+ fs.closeSync(fd);
452
+ } catch {
453
+ /* already gone */
454
+ }
455
+ }
456
+ });
457
+
458
+ k.on('enter', (serial, surfaceId, keys) => {
459
+ this.lastSerial = serial;
460
+ this.focus = surfaceId;
461
+ this.emit('focus', {
462
+ serial,
463
+ surface: surfaceId,
464
+ keys: decodeKeys(keys),
465
+ });
466
+ });
467
+ k.on('leave', (serial, surfaceId) => {
468
+ this.lastSerial = serial;
469
+ this._stopRepeat();
470
+ if (this.focus === surfaceId) this.focus = null;
471
+ this.emit('blur', { serial, surface: surfaceId });
472
+ });
473
+ k.on('modifiers', (serial, depressed, latched, locked, group) => {
474
+ this.lastSerial = serial;
475
+ this.mods = (depressed | latched | locked) & 0xff;
476
+ this.group = group;
477
+ this.emit('modifiers', {
478
+ mods: this.mods,
479
+ depressed,
480
+ latched,
481
+ locked,
482
+ group,
483
+ });
484
+ });
485
+ k.on('key', (serial, time, key, state) => {
486
+ this.lastSerial = serial;
487
+ const keycode = key + EVDEV_KEYCODE_OFFSET;
488
+ if (state === PRESSED) {
489
+ this.lastPressSerial = serial;
490
+ const ev = this._keyEvent('keydown', keycode, time, serial);
491
+ this.emit('keydown', ev);
492
+ this._startRepeat(keycode, time);
493
+ } else {
494
+ if (this._repeatKey === keycode) this._stopRepeat();
495
+ this.emit('keyup', this._keyEvent('keyup', keycode, time, serial));
496
+ }
497
+ });
498
+ k.on('repeat_info', (rate, delay) => {
499
+ this.repeat = { rate, delay };
500
+ this.emit('repeat_info', this.repeat);
501
+ });
502
+ }
503
+
504
+ /** A key event in the shape events.js reads, decoded against the keymap. */
505
+ _keyEvent(type, keycode, time, serial, repeat = false) {
506
+ const state = XkbKeymap.stateOf(this.mods, this.group);
507
+ const decoded = this.xkb?.decode(keycode, this.mods, this.group);
508
+ return {
509
+ type,
510
+ keycode,
511
+ keysym: decoded?.keysym ?? 0,
512
+ baseKeysym: decoded?.baseKeysym ?? 0,
513
+ codepoint: decoded?.codepoint,
514
+ buttons: state | this.buttonMask,
515
+ group: this.group,
516
+ time,
517
+ serial,
518
+ repeat,
519
+ surface: this.focus,
520
+ };
521
+ }
522
+
523
+ /**
524
+ * Key repeat, which the compositor does not do for us. X servers
525
+ * auto-repeat and every text control here is written against that, so
526
+ * repeated presses are synthesised at the seat's advertised rate.
527
+ * Modifier keys do not repeat, and losing focus stops everything.
528
+ */
529
+ _startRepeat(keycode, time) {
530
+ this._stopRepeat();
531
+ if (!(this.repeat.rate > 0)) return;
532
+ const sym = this.xkb?.decode(keycode, this.mods, this.group)?.keysym ?? 0;
533
+ if (isModifierKeysym(sym)) return;
534
+ this._repeatKey = keycode;
535
+ const interval = 1000 / this.repeat.rate;
536
+ let t = time;
537
+ this._repeatTimer = setTimeout(() => {
538
+ const tick = () => {
539
+ if (this._repeatKey !== keycode) return;
540
+ t += interval;
541
+ this.emit(
542
+ 'keydown',
543
+ this._keyEvent(
544
+ 'keydown',
545
+ keycode,
546
+ Math.round(t),
547
+ this.lastSerial,
548
+ true,
549
+ ),
550
+ );
551
+ this._repeatTimer = setTimeout(tick, interval);
552
+ };
553
+ tick();
554
+ }, this.repeat.delay);
555
+ this._repeatTimer.unref?.();
556
+ }
557
+
558
+ _stopRepeat() {
559
+ if (this._repeatTimer) clearTimeout(this._repeatTimer);
560
+ this._repeatTimer = null;
561
+ this._repeatKey = null;
562
+ }
563
+
564
+ destroy() {
565
+ this._stopRepeat();
566
+ this.removeAllListeners();
567
+ }
568
+ }
569
+
570
+ /** Shift, Control, Alt, Super, Caps/Num Lock, ISO_Level3: 0xffe1..0xffee and 0xfe03. */
571
+ function isModifierKeysym(sym) {
572
+ return (sym >= 0xffe1 && sym <= 0xffee) || sym === 0xfe03 || sym === 0xfe11;
573
+ }
574
+
575
+ /** `wl_keyboard.enter`'s pressed-key array is a wl_array of uint32. */
576
+ function decodeKeys(keys) {
577
+ const out = [];
578
+ if (!keys) return out;
579
+ const bytes = keys instanceof Uint8Array ? keys : new Uint8Array(keys);
580
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
581
+ for (let i = 0; i + 4 <= bytes.byteLength; i += 4)
582
+ out.push(view.getUint32(i, true) + EVDEV_KEYCODE_OFFSET);
583
+ return out;
584
+ }