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,560 @@
1
+ // The backend object `createRoot({ backend: 'wayland' })` renders through.
2
+ //
3
+ // react-x11 has three of these — an ntk X connection, `src/cocoa/app.js`,
4
+ // and this — and the shape they share is the contract: an app makes windows
5
+ // and offscreen surfaces, owns the fonts and the clipboard, and routes input;
6
+ // a window hands out a 2d context and tells the renderer when it may paint;
7
+ // everything above `src/nodes/` is supposed not to know which one it got.
8
+ //
9
+ // All the asynchrony in a Wayland client lives in `open()`, on purpose.
10
+ // Binding a global needs the registry and the registry needs a round trip —
11
+ // but that is the only part of making a window that does, so it is done
12
+ // once here and `createWindow` is synchronous, which React's commit phase
13
+ // requires (windows are realised inside it).
14
+ //
15
+ // `X` is a stand-in, not an X connection. The renderer reaches
16
+ // `app.X.keycode2keysyms` for accelerators, `app.X.on('end')` to notice the
17
+ // connection going, and a few guarded properties; the stand-in answers
18
+ // those and nothing else, and the keysym table on it comes from the
19
+ // compositor's keymap (xkb.js).
20
+ //
21
+ // The long-term home for all of this is ntk, beside its `Window` and behind
22
+ // the same `App`/`Drawable` contracts — the RFC's open question 2. It lives
23
+ // here because that is where it can be read and run without vendoring a
24
+ // second copy of the toolkit.
25
+
26
+ import { EventEmitter } from 'node:events';
27
+ import { createRequire } from 'node:module';
28
+ import { WaylandConnection } from './connection.js';
29
+ import { WaylandSeat } from './seat.js';
30
+ import { WaylandBackendWindow } from './backendwindow.js';
31
+ import { WaylandSurface } from './surface.js';
32
+ import { InputRouter } from './input.js';
33
+ import { WaylandTouch } from './touch.js';
34
+ import { WaylandTablet } from './tablet.js';
35
+ import { createWaylandClipboard } from './clipboard.js';
36
+ import { WaylandOutputs } from './outputs.js';
37
+ import { WaylandTextInput } from './textinput.js';
38
+ import { WaylandDnd } from './dnd.js';
39
+ import { createScreenCapture } from './screencopy.js';
40
+ import { sharedGpu } from './glcontext.js';
41
+ import { TextShaper } from './text.js';
42
+ import { FrameStyle } from './framestyle.js';
43
+ import { watchAppearance } from '../appearance.js';
44
+ import { WaylandGLArea, WaylandOverlayPane, GLAREA_VISUAL } from './glarea.js';
45
+ import { pinnedScale, setScaleForTests, traceScale } from '../scale.js';
46
+ import { setCompositingForTests } from '../compositing.js';
47
+
48
+ const require = createRequire(import.meta.url);
49
+
50
+ /** The X connection stand-in. */
51
+ class XStandIn extends EventEmitter {
52
+ constructor(app) {
53
+ super();
54
+ this.setMaxListeners(0);
55
+ this.app = app;
56
+ this.keycode2keysyms = [];
57
+ this._closing = false;
58
+ // `_refreshScreenOrigin` and friends bail on a null root, which is right:
59
+ // no Wayland client knows where it is on the screen.
60
+ this.display = { screen: [{ root: null }], Render: null };
61
+ }
62
+
63
+ require(name) {
64
+ throw new Error(
65
+ `react-x11 (wayland): X.require('${name}') — there is no X server behind this app`,
66
+ );
67
+ }
68
+
69
+ // Selections are the clipboard's business (src/wayland/clipboard.js); the
70
+ // text controls' direct calls are accepted and routed there.
71
+ SetSelectionOwner(_wid, atom) {
72
+ void atom;
73
+ }
74
+
75
+ ChangeWindowAttributes() {}
76
+ flush() {}
77
+ }
78
+
79
+ export class WaylandApp extends EventEmitter {
80
+ constructor(conn, options) {
81
+ super();
82
+ this.setMaxListeners(0);
83
+ this.conn = conn;
84
+ this.options = options;
85
+ this.backend = 'wayland';
86
+ /** wl_surface id -> WaylandBackendWindow */
87
+ this.windows = new Map();
88
+ this.toplevels = [];
89
+ this.seat = null;
90
+ this.input = null;
91
+ /** the seat's touch and tablet devices; the tablet is null without the protocol */
92
+ this.touch = null;
93
+ this.tablet = null;
94
+ this.fonts = null;
95
+ this.fontManager = null;
96
+ this.clipboard = null;
97
+ /** the seat's `zwp_text_input_v3`, or null where the compositor has none */
98
+ this.textInput = null;
99
+ this.dnd = null;
100
+ this.compositor = null;
101
+ this.wmBase = null;
102
+ this.dmabuf = null;
103
+ this.viewporter = null;
104
+ this.fractionalScale = null;
105
+ this.activation = null;
106
+ /** the monitors, published into src/screens.js (outputs.js) */
107
+ this.outputs = null;
108
+ /** xdg-decoration, layer-shell, shm: null where the compositor has none */
109
+ this.decorationManager = null;
110
+ this.layerShell = null;
111
+ this.shm = null;
112
+ /** screen capture and the eyedropper (screencopy.js); null on GNOME */
113
+ this.screenCapture = null;
114
+ this.X = new XStandIn(this);
115
+ this.display = this.X.display;
116
+ this.gpu = null;
117
+ this.gl = null;
118
+ this._holder = null;
119
+ this._currentSurface = null;
120
+ /** the app-wide output scale: buffer pixels per surface pixel, which
121
+ * windows size their buffers and convert input by (`layoutScale` is
122
+ * what the renderer lays out with) */
123
+ this.scale = 1;
124
+ /** REACT_X11_SCALE, else a numeric createRoot({ scale }): pinned */
125
+ this._pinnedScale = null;
126
+ /** the resolution class's factor over a compositor that says 1 */
127
+ this._zoom = 1;
128
+ /** `layoutScale` as last published to src/scale.js */
129
+ this._publishedScale = null;
130
+ this._shaper = null;
131
+ this.frameText = null;
132
+ }
133
+
134
+ static async open(options = {}) {
135
+ const conn = await WaylandConnection.open({
136
+ display: options.waylandDisplay ?? options.display,
137
+ transport: options.transport,
138
+ });
139
+ const app = new WaylandApp(conn, options);
140
+ conn.on('error', (err) => app.emit('error', err));
141
+ conn.on('close', () => {
142
+ app.X._closing = true;
143
+ app.X.emit('end');
144
+ app.emit('close');
145
+ });
146
+
147
+ app.compositor = await conn.require('wl_compositor');
148
+ app.wmBase = await conn.require('xdg_wm_base');
149
+ app.dmabuf = await conn.bind('zwp_linux_dmabuf_v1');
150
+ app.viewporter = await conn.bind('wp_viewporter');
151
+ app.fractionalScale = await conn.bind('wp_fractional_scale_manager_v1');
152
+ app.activation = await conn.bind('xdg_activation_v1');
153
+ // The scale session first, so the outputs have a per-monitor map to
154
+ // fill; then the outputs, awaited like the X11 backend's Xinerama tier:
155
+ // an auto-sized window clamps to a monitor synchronously inside
156
+ // realize(), and the first window is laid out at whatever `app.scale`
157
+ // says by then — which the densest output seeds (`noteScale`). The
158
+ // session exists before createRoot's `beginScale`, which therefore
159
+ // defers to it: the pin is read here instead, from the same options.
160
+ await app._beginOutputs(conn, options);
161
+ app.decorationManager = await conn.bind('zxdg_decoration_manager_v1');
162
+ app.layerShell = await conn.bind('zwlr_layer_shell_v1');
163
+ app.shm = await conn.bind('wl_shm');
164
+ app.seat = await WaylandSeat.bind(conn);
165
+ app.input = new InputRouter(app);
166
+ // both drive the router through the seat's own pointer events
167
+ app.touch = WaylandTouch.attach(app.seat);
168
+ app.tablet = await WaylandTablet.bind(conn, app.seat);
169
+ app.clipboard = await createWaylandClipboard({
170
+ conn,
171
+ seat: app.seat.seat,
172
+ serial: () => app.seat.lastSerial,
173
+ });
174
+ // The compositor's input method (textinput.js): enter/leave follow the
175
+ // keyboard, and the windows keep it told about their focused field.
176
+ app.textInput = await WaylandTextInput.create(app);
177
+ // Drag and drop shares the clipboard's `wl_data_device` — the DnD events
178
+ // arrive on the same object its selection events do (src/wayland/dnd.js).
179
+ if (app.clipboard?.dataDevice && app.clipboard?.manager) {
180
+ app.dnd = new WaylandDnd({
181
+ app,
182
+ device: app.clipboard.dataDevice,
183
+ manager: app.clipboard.manager,
184
+ seat: app.seat,
185
+ });
186
+ }
187
+ app.screenCapture = await createScreenCapture(app);
188
+
189
+ const ntk = require('ntk');
190
+ // `app.fonts` is the name the renderer reaches for (src/fonts.js): one
191
+ // FontManager per connection, so a face registered by `loadFont` is
192
+ // visible to every window and there is one glyph cache rather than two.
193
+ app.fonts = new ntk.FontManager({ source: options.fontSource ?? 'system' });
194
+ app.fontManager = app.fonts;
195
+ app._shaper = new TextShaper(app.fonts);
196
+ app.frameText = (text, size, weight, family = 'sans-serif') =>
197
+ app._shaper.measureSync(text, { family, size, weight });
198
+ // What the desktop says a frame looks like and does (framestyle.js):
199
+ // read before the first window is sized — the title's font sets the
200
+ // titlebar's height — and followed after, with the light or dark the
201
+ // frame takes from the appearance.
202
+ app.frameStyle = await new FrameStyle().start();
203
+ app.frameStyle.on('change', () => app._frameStyleChanged());
204
+ app._unwatchAppearance = watchAppearance(() => app._frameStyleChanged());
205
+
206
+ // The GPU context is shared by every window and surface; make it now so
207
+ // an offscreen surface created before the first window has one to use.
208
+ const dri = require('x11-dri');
209
+ app.dri = dri;
210
+ app.gpu = sharedGpu(dri, {
211
+ format: dri.FORMAT.ARGB8888,
212
+ depthSize: options.glPolicy?.depthSize ?? 16,
213
+ stencilSize: options.glPolicy?.stencilSize ?? 8,
214
+ devicePath: options.glPolicy?.devicePath,
215
+ });
216
+ app.gl = app.gpu.gl;
217
+ app._glCapsResolved = { direct: true };
218
+ // Every Wayland surface is composited: the compositor blends each
219
+ // buffer by its alpha, so `useSupports('transparency')` and the
220
+ // `'@supports transparency'` style block are true — the answer the cocoa
221
+ // backend gives too. Without it, tooltips and menus took the square,
222
+ // opaque look meant for an X server with no compositor running.
223
+ setCompositingForTests(app, true);
224
+ return app;
225
+ }
226
+
227
+ // ---- GL context plumbing ----------------------------------------------
228
+
229
+ /**
230
+ * Make the shared GL context current. Drawing into a render target needs
231
+ * *a* current surface; any window's will do, and when there is none yet a
232
+ * 1×1 holder surface stands in.
233
+ */
234
+ makeCurrent() {
235
+ if (this._currentSurface) {
236
+ try {
237
+ this.gpu.makeCurrent(this._currentSurface);
238
+ return;
239
+ } catch {
240
+ this._currentSurface = null;
241
+ }
242
+ }
243
+ for (const w of this.windows.values()) {
244
+ const s = w.glctx?.chain?.gbm;
245
+ if (s) {
246
+ this._currentSurface = s;
247
+ this.gpu.makeCurrent(s);
248
+ return;
249
+ }
250
+ }
251
+ if (!this._holder) this._holder = this.gpu.createSurface(1, 1);
252
+ this._currentSurface = this._holder;
253
+ this.gpu.makeCurrent(this._holder);
254
+ }
255
+
256
+ /** After an offscreen render, put the window's backing target back. */
257
+ rebindWindowTarget() {
258
+ for (const w of this.windows.values()) {
259
+ if (w._frameSize) {
260
+ w.glctx.bindBacking();
261
+ return;
262
+ }
263
+ }
264
+ }
265
+
266
+ // ---- windows ----------------------------------------------------------
267
+
268
+ /**
269
+ * Make a window. **Synchronous**, because `WindowNode.realize()` runs in
270
+ * React's commit phase and calls `setTitle`/`getContext` on the result the
271
+ * moment it has it. `overrideRedirect` makes it a popup, positioned by the
272
+ * `x`/`y` the tree's anchoring computed, relative to its parent.
273
+ */
274
+ createWindow(attributes = {}) {
275
+ // A `<glarea>`'s child "window" is a rect of its parent's backing target
276
+ // (glarea.js); `chooseGLConfig` marked the visual so it can be told apart.
277
+ if (attributes.visual === GLAREA_VISUAL) {
278
+ if (!attributes.parent?.glctx) {
279
+ throw new Error(
280
+ 'react-x11 (wayland): a <glarea> needs a window of this backend to draw into',
281
+ );
282
+ }
283
+ return new WaylandGLArea(attributes.parent, attributes);
284
+ }
285
+ const win = new WaylandBackendWindow(this, attributes);
286
+ this.windows.set(win.wl.surface.id, win);
287
+ if (!win.isPopup) this.toplevels.push(win);
288
+ win.on('_destroyed', () => this._forgetWindow(win));
289
+ return win;
290
+ }
291
+
292
+ _forgetWindow(win) {
293
+ if (this.windows.get(win.wl.surface.id) === win)
294
+ this.windows.delete(win.wl.surface.id);
295
+ const i = this.toplevels.indexOf(win);
296
+ if (i >= 0) this.toplevels.splice(i, 1);
297
+ if (win.glctx?.chain?.gbm === this._currentSurface)
298
+ this._currentSurface = null;
299
+ }
300
+
301
+ windowById(id) {
302
+ for (const w of this.windows.values()) if (w.id === id) return w;
303
+ return null;
304
+ }
305
+
306
+ /**
307
+ * The window a new window hangs off: the explicit parent the tree passed,
308
+ * else the focused toplevel, else the one under the pointer, else the most
309
+ * recent — a popup opens from an interaction, and one of those is where it
310
+ * happened.
311
+ */
312
+ parentFor(attributes, { toplevelOnly = false } = {}) {
313
+ const explicit = attributes.parent;
314
+ if (explicit && (!toplevelOnly || !explicit.isPopup)) return explicit;
315
+ if (toplevelOnly) return null;
316
+ return (
317
+ this.input?.focusWindow ??
318
+ this.input?.pointerWindow ??
319
+ this.toplevels[this.toplevels.length - 1] ??
320
+ null
321
+ );
322
+ }
323
+
324
+ /** Called by windows when an output scale changes, and by outputs.js. */
325
+ noteScale() {
326
+ // Until a window exists the outputs are the only word on the scale, and
327
+ // the first window is laid out at whatever this says — so their
328
+ // densest one seeds it. Once a window has heard its own, the windows'
329
+ // is the truth: fractional-scale says 1.5 where `wl_output.scale`
330
+ // rounds up to 2.
331
+ let scale = this.windows.size === 0 ? (this.outputs?.maxScale ?? 1) : 1;
332
+ for (const w of this.windows.values()) scale = Math.max(scale, w.wl.scale);
333
+ const moved = scale !== this.scale;
334
+ this.scale = scale;
335
+ if (this._publishScale() || moved) this.emit('scale', scale);
336
+ }
337
+
338
+ /**
339
+ * What the renderer lays out with: device pixels per logical pixel, the
340
+ * number src/scale.js keeps for the tree. The compositor's scale, except
341
+ * where it is pinned — `REACT_X11_SCALE`, then `createRoot({ scale })`,
342
+ * absolute exactly as on X11 — or where the compositor says 1 over a
343
+ * retina-class grid it has no real millimetres for (`resolutionZoom`).
344
+ * A window's buffer is always in these pixels and its surface is them
345
+ * over the compositor's scale, so a zoom is a bigger window drawn at full
346
+ * resolution, never a smaller one stretched.
347
+ */
348
+ get layoutScale() {
349
+ if (this._pinnedScale) return this._pinnedScale.scale;
350
+ return this.scale === 1 ? this._zoom : this.scale;
351
+ }
352
+
353
+ /** Hand `layoutScale` to src/scale.js; whether it moved. */
354
+ _publishScale() {
355
+ const layout = this.layoutScale;
356
+ const source =
357
+ this._pinnedScale?.source ??
358
+ (this.scale === 1 && this._zoom !== 1 ? 'resolution' : 'wayland');
359
+ setScaleForTests(this, layout, source);
360
+ const moved = layout !== this._publishedScale;
361
+ this._publishedScale = layout;
362
+ return moved;
363
+ }
364
+
365
+ /**
366
+ * The scale's pin, then the outputs — awaited, because the first window
367
+ * is sized against them — then what they say about the scale. The middle
368
+ * of `open()`, apart so a test runs exactly this against a mock.
369
+ */
370
+ async _beginOutputs(conn, options = {}) {
371
+ this._pinnedScale = pinnedScale(options.scale);
372
+ this._publishScale();
373
+ this.outputs = new WaylandOutputs(conn, this);
374
+ await this.outputs.open();
375
+ this._zoomFromOutputs();
376
+ }
377
+
378
+ /** Once the outputs are known: the resolution class, unless pinned. */
379
+ _zoomFromOutputs() {
380
+ const pinned = this._pinnedScale;
381
+ if (pinned) {
382
+ traceScale(
383
+ `${pinned.scale}x from ` +
384
+ (pinned.source === 'option'
385
+ ? 'createRoot({ scale })'
386
+ : pinned.source) +
387
+ `, over the compositor's ${this.scale}x`,
388
+ );
389
+ } else {
390
+ const v = this.outputs?.resolutionZoom?.() ?? {
391
+ zoom: 1,
392
+ name: null,
393
+ reason: 'no outputs',
394
+ };
395
+ this._zoom = v.zoom;
396
+ traceScale(` ${v.name ?? 'output'}: ${v.zoom}x over it — ${v.reason}`);
397
+ traceScale(
398
+ `${this.layoutScale}x from ` +
399
+ (this.scale === 1 && v.zoom !== 1
400
+ ? 'the resolution class'
401
+ : `the compositor (${this.scale}x)`),
402
+ );
403
+ }
404
+ if (this._publishScale()) this.emit('scale', this.scale);
405
+ }
406
+
407
+ /** The desktop's frame style or colours changed: every frame repaints,
408
+ * and resizes if its titlebar did. */
409
+ _frameStyleChanged() {
410
+ for (const w of this.windows.values()) w._frameStyleChanged?.();
411
+ }
412
+
413
+ /** Hook for the input router: a discrete event may want a paint now. */
414
+ afterInput() {}
415
+
416
+ // ---- app-level requests -------------------------------------------------
417
+
418
+ createSurface(options) {
419
+ return new WaylandSurface(this, options);
420
+ }
421
+
422
+ /**
423
+ * What the machine can do, as a promise like the X11 and Cocoa answers
424
+ * (src/glbackend.js chains on it): here the answer was settled when the
425
+ * GPU context came up in `open()`.
426
+ */
427
+ async glCapabilities() {
428
+ const caps = {
429
+ direct: true,
430
+ indirect: false,
431
+ backend: 'gles',
432
+ version: this.gpu?.glVersion?.string ?? null,
433
+ };
434
+ this._glCapsResolved = caps;
435
+ return caps;
436
+ }
437
+
438
+ frameIntervalFor() {
439
+ // The compositor paces us through `wl_surface.frame`, and nothing in
440
+ // the shared renderer reads this (only the cocoa window does, to pace
441
+ // itself). The refresh rate is known — `win.output.refreshRate`, from
442
+ // the output's current mode — but an interval here would only invite a
443
+ // second clock beside the compositor's.
444
+ return 0;
445
+ }
446
+
447
+ findArgbVisual() {
448
+ return { depth: 32 };
449
+ }
450
+
451
+ /**
452
+ * What a `<glarea>` draws with: the window's own GLES context, into a rect
453
+ * of its backing target (glarea.js). A promise, as the X11 and Cocoa
454
+ * answers are — `GlAreaNode.realize` chains on it.
455
+ *
456
+ * `spec.api` is the `<glarea glx>` prop's rung: `'auto'` and `'gles'` are
457
+ * this; `'gl'` (desktop GL) and `'webgpu'` are named so the ladder is
458
+ * visible before it is built.
459
+ */
460
+ async chooseGLConfig(spec) {
461
+ const mode = this.options.glPolicy?.mode ?? 'auto';
462
+ if (mode === 'off') {
463
+ const err = new Error(
464
+ "glPolicy is 'off' on this connection, so no GL context is created at all",
465
+ );
466
+ err.code = 'GL_DISABLED';
467
+ throw err;
468
+ }
469
+ if (mode === 'indirect') {
470
+ const err = new Error(
471
+ "glPolicy is 'indirect', which is GLX — there is no X server here to speak it to. Use 'auto' or 'direct'.",
472
+ );
473
+ err.code = 'GL_POLICY_INDIRECT';
474
+ throw err;
475
+ }
476
+ const api = spec?.api ?? 'auto';
477
+ if (api !== 'auto' && api !== 'gles') {
478
+ const err = new Error(
479
+ api === 'gl' || api === 'webgpu'
480
+ ? `<glarea> api '${api}' is a named rung this backend has not built yet — today's rungs are auto, gles (GLES 3 on the window's own context)`
481
+ : `<glarea> api '${api}' is not a rung — expected one of auto, gles, gl, webgpu.`,
482
+ );
483
+ err.code = 'GL_API_UNAVAILABLE';
484
+ throw err;
485
+ }
486
+ return { backend: 'direct', api: 'gles', visual: GLAREA_VISUAL, depth: 32 };
487
+ }
488
+
489
+ /**
490
+ * The pane a `<glarea>`'s children are drawn on (src/gloverlay.js): an
491
+ * offscreen target the window blends over the surface, so the overlay is
492
+ * translucent here as on the Cocoa backend. Having this at all is how the
493
+ * overlay knows the backend composites.
494
+ */
495
+ createOverlayPane(attributes) {
496
+ return new WaylandOverlayPane(this, attributes);
497
+ }
498
+
499
+ /**
500
+ * Ask the compositor to activate a window. Stacking is the compositor's;
501
+ * xdg-activation lets a client ask with a token from a recent interaction,
502
+ * and the compositor decides whether the interaction was recent enough.
503
+ */
504
+ activate(win) {
505
+ if (!this.activation || !win?.wl?.surface) return;
506
+ const token = this.activation.$.get_activation_token();
507
+ token.on('done', (value) => {
508
+ this.activation.$.activate(value, win.wl.surface.id);
509
+ token.$.destroy();
510
+ });
511
+ token.$.set_surface(win.wl.surface.id);
512
+ if (this.seat.lastSerial)
513
+ token.$.set_serial(this.seat.lastSerial, this.seat.seat.id);
514
+ token.$.commit();
515
+ }
516
+
517
+ raiseWindow(win) {
518
+ this.activate(win);
519
+ }
520
+
521
+ requestAttention() {
522
+ // xdg-activation with a stale serial is exactly "attention, please":
523
+ // compositors that support it mark the window rather than focusing it.
524
+ const w = this.toplevels[0];
525
+ if (w) this.activate(w);
526
+ return 0;
527
+ }
528
+
529
+ cancelAttention() {}
530
+
531
+ async close() {
532
+ for (const w of [...this.windows.values()]) w.destroy();
533
+ this.windows.clear();
534
+ this.toplevels.length = 0;
535
+ this.textInput?.destroy();
536
+ this.frameStyle?.stop();
537
+ this._unwatchAppearance?.();
538
+ this.input?.destroy();
539
+ this.tablet?.destroy();
540
+ this.touch?.destroy();
541
+ this.screenCapture?.destroy();
542
+ this.seat?.destroy();
543
+ this.outputs?.destroy();
544
+ if (this._holder) {
545
+ try {
546
+ this._holder.destroy();
547
+ } catch {
548
+ /* gone */
549
+ }
550
+ this._holder = null;
551
+ }
552
+ this.X._closing = true;
553
+ this.conn.destroy();
554
+ }
555
+ }
556
+
557
+ /** What `Reconciler.js` imports. */
558
+ export async function createWaylandApp(options = {}) {
559
+ return await WaylandApp.open(options);
560
+ }