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,1123 @@
1
+ // One window as the renderer sees it: the object `app.createWindow()` hands
2
+ // back and `src/nodes/window/` drives.
3
+ //
4
+ // It wears the interface ntk's `Window` has — `getContext`, `on(...)`,
5
+ // `width`/`height`, `setTitle`, `requestAnimationFrame`, `destroy` and the
6
+ // rest — over a Wayland surface, a GL context and the client-side
7
+ // decorations. Where the X11 shape has no Wayland meaning the method is
8
+ // there and says so rather than silently doing nothing, because a stub that
9
+ // does nothing turns "not implemented" into a rendering bug two layers away.
10
+ //
11
+ // Two coordinate systems meet here and the rule is fixed: **everything the
12
+ // tree sees is content-relative and in device pixels**, as it is on the
13
+ // other backends. The surface is bigger than the content by the frame
14
+ // (decorations.js), and a compositor speaks logical pixels; the conversions
15
+ // happen at this boundary and nowhere else.
16
+ //
17
+ // The frame loop is the compositor's. `requestAnimationFrame` arms a
18
+ // `wl_surface.frame` callback (or paints straight away for the first frame,
19
+ // which nothing can pace), the callback runs the renderer's paint into the
20
+ // backing target, and the result is copied to a swapchain buffer and
21
+ // committed with the next frame request in the same commit.
22
+ //
23
+ // The frame is drawn here only until a compositor agrees to draw it:
24
+ // where xdg-decoration is offered the window asks for server-side
25
+ // decorations (ssd.js) and, granted them, switches its own off, which the
26
+ // tree sees as the content growing. And a window whose `windowType` is a
27
+ // dock, a wallpaper, a notification or a splash is not a toplevel at all
28
+ // where the compositor has layer-shell (layershell.js) — same surface, same
29
+ // loop, no frame.
30
+
31
+ import { EventEmitter } from 'node:events';
32
+ import { writeSync, writeFileSync } from 'node:fs';
33
+ import { snapshotPNG } from './readback.js';
34
+ import { WaylandWindow, TOPLEVEL_STATE } from './window.js';
35
+ import { WaylandGLContext } from './glcontext.js';
36
+ import { WaylandContext2D } from './context2d.js';
37
+ import { Decorations } from './decorations.js';
38
+ import { appearanceSnapshot } from '../appearance.js';
39
+ import { decorationPolicy } from './ssd.js';
40
+ import { layerRoleFor } from './layershell.js';
41
+
42
+ // What the X11 window has and this one does not — `getProperty`,
43
+ // `setProperty`, `selectXI2`, `grabKeyboard`, `setBackgroundPixel`,
44
+ // `reparent` — is *absent* rather than stubbed. That is the contract the
45
+ // tree already keeps for the cocoa backend: every caller probes
46
+ // (`wnd.setProperty?.(…)`, `typeof wnd.selectXI2 === 'function'`) and takes
47
+ // the absence as "not on this backend". A method that throws instead turns
48
+ // each of those probes into a crash two layers away (`_NET_WM_DESKTOP` was
49
+ // the first: src/windowstate.js reads it on every window).
50
+
51
+ const STATE_NAMES = {
52
+ [TOPLEVEL_STATE.MAXIMIZED]: ['maximized_vert', 'maximized_horz'],
53
+ [TOPLEVEL_STATE.FULLSCREEN]: ['fullscreen'],
54
+ [TOPLEVEL_STATE.ACTIVATED]: ['focused'],
55
+ [TOPLEVEL_STATE.SUSPENDED]: ['hidden'],
56
+ };
57
+
58
+ /** `REACT_X11_WAYLAND_TRACE=1`: a line on stderr per presented frame. */
59
+ const TRACE = Boolean(process.env.REACT_X11_WAYLAND_TRACE);
60
+ /**
61
+ * `REACT_X11_WAYLAND_SNAPSHOT=<file.png>`: the backing target of the first
62
+ * window, written at its present number `REACT_X11_WAYLAND_SNAPSHOT_AT`
63
+ * (default 30) — what the compositor was handed, read back from the GPU. The
64
+ * one way to see a frame on a desktop that offers no screenshot API.
65
+ */
66
+ const SNAPSHOT = process.env.REACT_X11_WAYLAND_SNAPSHOT || null;
67
+ const SNAPSHOT_AT = Number(process.env.REACT_X11_WAYLAND_SNAPSHOT_AT) || 30;
68
+
69
+ let nextId = 1;
70
+
71
+ const overlaps = (a, b) =>
72
+ a.x < b.x + b.width &&
73
+ b.x < a.x + a.width &&
74
+ a.y < b.y + b.height &&
75
+ b.y < a.y + a.height;
76
+
77
+ export class WaylandBackendWindow extends EventEmitter {
78
+ /**
79
+ * @param {import('./app.js').WaylandApp} app
80
+ * @param {object} attributes what `WindowNode.realize()` built
81
+ */
82
+ constructor(app, attributes = {}) {
83
+ super();
84
+ this.setMaxListeners(0);
85
+ this.app = app;
86
+ this.attributes = attributes;
87
+ this.id = nextId++;
88
+ this.X = app.X;
89
+ this.ownerDocument = null;
90
+ this.isPopup = attributes.overrideRedirect === true;
91
+ // A `<popup dragPreview>` is the picture of a drag. On Wayland it has
92
+ // nothing to be: a popup cannot follow a drag (start_drag's icon surface
93
+ // is the mechanism, and rendering the preview into one is a follow-up —
94
+ // src/wayland/dnd.js), so the preview is inert here — it presents no
95
+ // frame and holds no buffer, staying invisible rather than a static
96
+ // xdg_popup frozen where the drag began.
97
+ this.isDragPreview = attributes.dragPreview === true;
98
+ // A dock, a wallpaper, a notification: a layer surface where the
99
+ // compositor has layer-shell, an ordinary toplevel where it does not.
100
+ this.layerRole =
101
+ !this.isPopup && app.layerShell
102
+ ? layerRoleFor(attributes, { scale: app.scale })
103
+ : null;
104
+ this.isLayer = this.layerRole !== null;
105
+ /** the cursor the tree last asked for over the content */
106
+ this.treeCursor = 'default';
107
+ this._destroyed = false;
108
+ this._contexts = new Map();
109
+ this._raf = [];
110
+ this._frameArmed = false;
111
+ this._everPresented = false;
112
+ this._eagerFired = false;
113
+ this._presentInFlight = false;
114
+ this._frameSize = null;
115
+ this._frameDirty = true;
116
+ this._resized = true;
117
+ this._reactX11Node = null;
118
+ /** `<glarea>`s drawing into this window's target (glarea.js) */
119
+ this._surfaces = new Set();
120
+ /** the panes their children are painted on */
121
+ this._panes = new Set();
122
+ this._surfaceRaf = [];
123
+ /** rects (content px) a surface or pane changed since the last present */
124
+ this._surfaceDamage = [];
125
+
126
+ // Frame and content sizes. `attributes.width/height` are what the tree
127
+ // measured, in device pixels of content; the surface adds the frame.
128
+ // Popups and layer surfaces have none; a toplevel's is switched off once
129
+ // a compositor agrees to draw it (`_setDecorationMode`).
130
+ this._decorPolicy = decorationPolicy(
131
+ app.options.decorations,
132
+ attributes.decorations,
133
+ );
134
+ this.decor =
135
+ this.isPopup || this.isLayer
136
+ ? null
137
+ : new Decorations({ enabled: this._decorPolicy.draw });
138
+ if (this.decor) {
139
+ const decor = this.decor;
140
+ decor.title = attributes.title ?? 'react-x11';
141
+ decor.measure = app.frameText;
142
+ decor.setStyle(app.frameStyle?.value, appearanceSnapshot());
143
+ // the shell window's margins once it exists; the same answer before
144
+ decor.marginsOf = () =>
145
+ this.wl?.margins ?? Decorations.marginsFor(new Set(), decor.enabled);
146
+ decor.onNeedsRepaint = () => {
147
+ if (this._titleRetry) return;
148
+ this._titleRetry = setTimeout(() => {
149
+ this._titleRetry = null;
150
+ this.repaintFrame();
151
+ }, 60);
152
+ };
153
+ }
154
+ const scale = app.scale;
155
+ const insets = this.insets;
156
+ const contentW = Math.max(1, (attributes.width ?? 640) / scale);
157
+ const contentH = Math.max(1, (attributes.height ?? 480) / scale);
158
+ /** the content size the tree asked for, in logical pixels */
159
+ this._contentWish = { width: contentW, height: contentH };
160
+ const surfaceW = Math.round(contentW + insets.left + insets.right);
161
+ const surfaceH = Math.round(contentH + insets.top + insets.bottom);
162
+
163
+ if (this.isPopup) {
164
+ const parent = app.parentFor(attributes);
165
+ const pi = parent?.geometryInsets ?? { left: 0, top: 0 };
166
+ const ps = parent?.scale ?? scale;
167
+ this.wl = WaylandWindow.createPopupSync({
168
+ conn: app.conn,
169
+ compositor: app.compositor,
170
+ wmBase: app.wmBase,
171
+ parent: parent?.wl,
172
+ x: (attributes.x ?? 0) / ps + pi.left,
173
+ y: (attributes.y ?? 0) / ps + pi.top,
174
+ width: surfaceW,
175
+ height: surfaceH,
176
+ // the initial commit waits for the map — see map()
177
+ commit: false,
178
+ });
179
+ this.parentWindow = parent;
180
+ /** where the tree asked for the popup, in its parent's surface
181
+ * coordinates — the compositor's configure may have slid it */
182
+ this._popupAt = {
183
+ x: (attributes.x ?? 0) / ps + pi.left,
184
+ y: (attributes.y ?? 0) / ps + pi.top,
185
+ };
186
+ } else if (this.isLayer) {
187
+ this.wl = WaylandWindow.createLayerSync({
188
+ conn: app.conn,
189
+ compositor: app.compositor,
190
+ layerShell: app.layerShell,
191
+ width: surfaceW,
192
+ height: surfaceH,
193
+ ...this.layerRole,
194
+ });
195
+ } else {
196
+ this.wl = WaylandWindow.createSync({
197
+ conn: app.conn,
198
+ compositor: app.compositor,
199
+ wmBase: app.wmBase,
200
+ title: attributes.title ?? 'react-x11',
201
+ appId:
202
+ attributes.appId ??
203
+ attributes.class ??
204
+ app.options.appId ??
205
+ 'react-x11',
206
+ width: surfaceW,
207
+ height: surfaceH,
208
+ parent: app.parentFor(attributes, { toplevelOnly: true })?.wl ?? null,
209
+ decorations: app.decorationManager
210
+ ? { manager: app.decorationManager, prefer: this._decorPolicy.prefer }
211
+ : null,
212
+ });
213
+ }
214
+ this.wl.scale = scale;
215
+ this.wl.compositor = app.compositor;
216
+ if (this.decor) {
217
+ const decor = this.decor;
218
+ this.wl.marginsFor = (states) =>
219
+ Decorations.marginsFor(states, decor.enabled);
220
+ this.wl.margins = this.wl.marginsFor(this.wl.states);
221
+ }
222
+ this.wl.useScaling({
223
+ fractionalScaleManager: app.fractionalScale,
224
+ viewporter: app.viewporter,
225
+ });
226
+ // which monitor it is on, its bounds, and the output-scale fallback
227
+ app.outputs?.watchWindow(this.wl);
228
+ this.glctx = WaylandGLContext.createSync({
229
+ conn: app.conn,
230
+ window: this.wl,
231
+ dmabuf: app.dmabuf,
232
+ policy: app.options.glPolicy ?? {},
233
+ });
234
+ this._wire();
235
+ }
236
+
237
+ _wire() {
238
+ const wl = this.wl;
239
+ wl.on('resize', () => {
240
+ this._resized = true;
241
+ this._frameDirty = true;
242
+ this.emit('resize', {
243
+ width: this.width,
244
+ height: this.height,
245
+ x: 0,
246
+ y: 0,
247
+ });
248
+ });
249
+ wl.on('configure', ({ states }) => {
250
+ if (this.decor) {
251
+ const before = this.decor.insets().top;
252
+ this.decor.setState(states);
253
+ this._frameDirty = true;
254
+ if (this.decor.insets().top !== before) {
255
+ this._resized = true;
256
+ this.emit('resize', {
257
+ width: this.width,
258
+ height: this.height,
259
+ x: 0,
260
+ y: 0,
261
+ });
262
+ }
263
+ }
264
+ // Adopting a configure needs a frame even when nothing else changed:
265
+ // it is the commit after the ack that applies it. `_fireRaf` ends a
266
+ // frame with nothing due and nothing dirty without committing, so
267
+ // without this a popup's reposition — position only, no decorations
268
+ // to repaint — was never acked, and a tooltip stayed where it was
269
+ // first placed: the parent's top-left corner.
270
+ this._frameDirty = true;
271
+ this._armFrame();
272
+ });
273
+ wl.on('decorationmode', (mode) => this._setDecorationMode(mode));
274
+ wl.on('statechange', () => this.emit('statechange', this.getWmStates()));
275
+ wl.on('close', () => {
276
+ if (this.isPopup) this._dismissedByCompositor();
277
+ else this.requestClose();
278
+ });
279
+ wl.on('scale', () => {
280
+ this._resized = true;
281
+ this._frameDirty = true;
282
+ this.app.noteScale();
283
+ this.emit('resize', {
284
+ width: this.width,
285
+ height: this.height,
286
+ x: 0,
287
+ y: 0,
288
+ });
289
+ });
290
+ wl.on('error', (err) => this.emit('error', err));
291
+ }
292
+
293
+ /**
294
+ * The compositor's answer to who draws the frame. 'server' switches the
295
+ * client-side frame off: the insets go to zero, no titlebar is painted,
296
+ * and the content grows into the whole surface — a resize, as far as the
297
+ * tree is concerned, and `set_window_geometry` already covers the buffer.
298
+ * Before the first frame, where the compositor has not imposed a size,
299
+ * the surface is refitted to the content the tree asked for instead, so a
300
+ * floating window comes up the size it was declared rather than a
301
+ * titlebar taller. The answer can change later (a compositor's setting
302
+ * flips) and the same switch runs the other way.
303
+ */
304
+ _setDecorationMode(mode) {
305
+ if (!this.decor) return;
306
+ const enabled = this._decorPolicy.draw && mode !== 'server';
307
+ if (this.decor.enabled === enabled) return;
308
+ this.decor.enabled = enabled;
309
+ // no frame, no shadow: the margin goes with it, and the surface round
310
+ // the window with the margin
311
+ this.wl.setMargins(this.wl.marginsFor?.(this.wl.states));
312
+ if (!this._everPresented && !this.wl.sizeImposed) {
313
+ const i = this.insets;
314
+ const wish = this._contentWish;
315
+ this.wl.width = Math.max(1, Math.round(wish.width + i.left + i.right));
316
+ this.wl.height = Math.max(1, Math.round(wish.height + i.top + i.bottom));
317
+ }
318
+ this._resized = true;
319
+ this._frameDirty = true;
320
+ this.emit('resize', { width: this.width, height: this.height, x: 0, y: 0 });
321
+ }
322
+
323
+ // ---- geometry ---------------------------------------------------------
324
+
325
+ /** The frame's insets in logical pixels. */
326
+ get insets() {
327
+ return this.decor
328
+ ? this.decor.insets()
329
+ : { top: 0, left: 0, right: 0, bottom: 0 };
330
+ }
331
+
332
+ /**
333
+ * The content's offset inside the window geometry: the insets without the
334
+ * shadow's margin. What a popup's position is relative to — xdg-shell
335
+ * places popups in their parent's window geometry, not its surface.
336
+ */
337
+ get geometryInsets() {
338
+ const i = this.insets;
339
+ const m = this.wl?.margins ?? { left: 0, top: 0, right: 0, bottom: 0 };
340
+ return {
341
+ top: i.top - m.top,
342
+ left: i.left - m.left,
343
+ right: i.right - m.right,
344
+ bottom: i.bottom - m.bottom,
345
+ };
346
+ }
347
+
348
+ get scale() {
349
+ return this.wl.scale;
350
+ }
351
+
352
+ /**
353
+ * The monitor this window is on — the output the surface has entered
354
+ * (the densest one when it straddles two), as a `screens.js`-shaped
355
+ * record with the output's `name`, `description`, `scale` and
356
+ * `refreshRate`; null before the first present, when no compositor has
357
+ * said yet.
358
+ */
359
+ get output() {
360
+ return this.app.outputs?.monitorFor(this.wl.outputs) ?? null;
361
+ }
362
+
363
+ /** Content width in device pixels — what the tree lays out to. */
364
+ get width() {
365
+ const i = this.insets;
366
+ return Math.max(
367
+ 1,
368
+ Math.round((this.wl.width - i.left - i.right) * this.wl.scale),
369
+ );
370
+ }
371
+
372
+ get height() {
373
+ const i = this.insets;
374
+ return Math.max(
375
+ 1,
376
+ Math.round((this.wl.height - i.top - i.bottom) * this.wl.scale),
377
+ );
378
+ }
379
+
380
+ get x() {
381
+ return Math.round(this.wl.x * this.wl.scale);
382
+ }
383
+
384
+ get y() {
385
+ return Math.round(this.wl.y * this.wl.scale);
386
+ }
387
+
388
+ /** Content origin inside the buffer, in device pixels. */
389
+ get contentOrigin() {
390
+ const i = this.insets;
391
+ const s = this.wl.scale;
392
+ return { x: Math.round(i.left * s), y: Math.round(i.top * s) };
393
+ }
394
+
395
+ getClientRects() {
396
+ return [
397
+ { x: 0, y: 0, left: 0, top: 0, width: this.width, height: this.height },
398
+ ];
399
+ }
400
+
401
+ measure(callback) {
402
+ callback?.(0, 0, this.width, this.height, 0, 0);
403
+ return { width: this.width, height: this.height };
404
+ }
405
+
406
+ // ---- contexts ---------------------------------------------------------
407
+
408
+ /**
409
+ * The rendering context, by name. `'2d'` is the GPU context drawing into
410
+ * this window's backing target; `'gles'` is the raw GL entry points a
411
+ * `<glarea>` wants, with the backing target bound.
412
+ */
413
+ getContext(name = '2d') {
414
+ if (this._contexts.has(name)) return this._contexts.get(name);
415
+ let ctx;
416
+ if (name === '2d') {
417
+ if (!this.glctx.backing) this.glctx.beginFrame();
418
+ ctx = new WaylandContext2D(this.glctx.gl, {
419
+ fontManager: this.app.fonts,
420
+ target: this.glctx.backing,
421
+ });
422
+ this.app.makeCurrent();
423
+ ctx.init();
424
+ // The renderer creates its context lazily, inside its first paint —
425
+ // by which time this frame has begun, so the context is caught up.
426
+ if (this._frameSize) {
427
+ ctx.begin(
428
+ this._frameSize.width,
429
+ this._frameSize.height,
430
+ this._frameSize.time,
431
+ );
432
+ this._enterContent(ctx);
433
+ }
434
+ } else if (name === 'gles' || name === 'opengl' || name === 'webgl') {
435
+ ctx = this.glctx.gl;
436
+ } else {
437
+ throw new Error(
438
+ `react-x11 (wayland): no '${name}' rendering context — this backend has '2d' and 'gles'`,
439
+ );
440
+ }
441
+ this._contexts.set(name, ctx);
442
+ return ctx;
443
+ }
444
+
445
+ // ---- the frame loop ---------------------------------------------------
446
+
447
+ /**
448
+ * The frame clock in the shape the renderer uses. On X11 this is Present
449
+ * completions plus an estimator; here it is the compositor's own
450
+ * `wl_surface.frame`, which is both simpler and honest about occlusion —
451
+ * an invisible window stops being called back, and so stops painting.
452
+ */
453
+ requestAnimationFrame(fn) {
454
+ this._raf.push(fn);
455
+ this._armFrame();
456
+ return this._raf.length;
457
+ }
458
+
459
+ /** Ask for a repaint of the decorations at the next frame. */
460
+ /**
461
+ * A `<glarea>`'s frame: after the tree's paint, in the same frame of this
462
+ * window, so the GL goes over the 2D and out with the same present.
463
+ */
464
+ requestSurfaceFrame(fn) {
465
+ this._surfaceRaf.push(fn);
466
+ this._armFrame();
467
+ }
468
+
469
+ /** A surface drew: its rect is owed to the compositor. */
470
+ _surfaceDrew(area) {
471
+ this._surfaceDamage.push({ ...area.rect });
472
+ }
473
+
474
+ /** A pane was painted: it is composited over its surface this frame. */
475
+ _paneDrew(pane) {
476
+ this._surfaceDamage.push({ ...pane.rect });
477
+ }
478
+
479
+ repaintFrame() {
480
+ this._frameDirty = true;
481
+ this._armFrame();
482
+ }
483
+
484
+ /**
485
+ * Get a frame in flight.
486
+ *
487
+ * Three rules, each learned from a spin:
488
+ *
489
+ * - **A frame stays armed from its callback until its present has been
490
+ * handed over.** A callback that asks for the next frame (every
491
+ * animation does) queues it for the next vsync, as in a browser; it does
492
+ * not re-fire. The first version cleared the flag before running the
493
+ * callback, and an animation re-entered itself from inside the frame.
494
+ * - **The first frame cannot be paced**, because a compositor only
495
+ * schedules frame callbacks for a surface it is showing — so until the
496
+ * first buffer has been presented, frames run from a *timer*. Never from
497
+ * a microtask: presenting that first buffer needs the event loop (the
498
+ * dma-buf import is a round trip), and a microtask chain that keeps
499
+ * painting never yields to it. That was 100% of a core, nothing on
500
+ * screen, and Ctrl+C ignored.
501
+ * - **Post-present, an empty frame re-arms through the compositor**, so
502
+ * a renderer that keeps asking without drawing costs a commit per
503
+ * refresh, not a spin.
504
+ */
505
+ _armFrame() {
506
+ // A drag preview never presents (see the constructor): no frame loop, so
507
+ // no buffer is ever committed and the surface stays invisible.
508
+ if (this.isDragPreview) return;
509
+ // A closed connection arms nothing: a frame request on it rejects at
510
+ // once, the rejection re-arms, and that loop never yields.
511
+ if (this._frameArmed || this._destroyed || this.app.conn?.destroyed) return;
512
+ this._frameArmed = true;
513
+ this.wl.whenConfigured
514
+ .then(() => {
515
+ if (this._destroyed) return;
516
+ if (this._presentInFlight) return; // its vsync runs the next frame
517
+ if (!this._everPresented) {
518
+ const delay = this._eagerFired ? 16 : 0;
519
+ this._eagerFired = true;
520
+ setTimeout(() => this._fireRaf(0), delay);
521
+ return;
522
+ }
523
+ const vsync = this.wl.scheduleFrame();
524
+ this.wl.surface.$.commit();
525
+ vsync.then(
526
+ (t) => this._fireRaf(t),
527
+ () => this._frameIdle(),
528
+ );
529
+ })
530
+ .catch(() => this._frameIdle());
531
+ }
532
+
533
+ /** The frame is over with nothing presented; run again only if asked. */
534
+ _frameIdle() {
535
+ this._frameArmed = false;
536
+ if (!this._destroyed && (this._raf.length || this._frameDirty)) {
537
+ this._armFrame();
538
+ }
539
+ }
540
+
541
+ /** Translate into the content area and clip to it. */
542
+ _enterContent(ctx) {
543
+ const o = this.contentOrigin;
544
+ ctx.save();
545
+ ctx.translate(o.x, o.y);
546
+ ctx.beginPath();
547
+ // A floating window's bottom corners are rounded like its top ones, and
548
+ // the content is what fills them.
549
+ const r = this.decor?.enabled ? this.decor.radius * this.wl.scale : 0;
550
+ if (r > 0) ctx.roundRect(0, 0, this.width, this.height, [0, 0, r, r]);
551
+ else ctx.rect(0, 0, this.width, this.height);
552
+ ctx.clip();
553
+ }
554
+
555
+ /**
556
+ * Run the frame: open the GL frame, paint the decorations, let the
557
+ * renderer paint the content, close, and present.
558
+ *
559
+ * On X11 none of this is needed — an ntk 2d context is an XRender encoder
560
+ * writing straight at the window, so a paint *is* a present. Here a frame
561
+ * has a beginning and an end, and this is where they go, because the
562
+ * renderer's own paint is the callback in the middle.
563
+ */
564
+ _fireRaf(time) {
565
+ if (this._destroyed) return;
566
+ const due = this._raf.splice(0);
567
+ const frameDirty = this._frameDirty;
568
+ if (due.length === 0 && !frameDirty && this._surfaceRaf.length === 0) {
569
+ this._frameArmed = false;
570
+ return;
571
+ }
572
+
573
+ const size = this.glctx.beginFrame();
574
+ if (!size) return this._frameIdle();
575
+ // This window's GBM surface is current now, and stays the app's notion of
576
+ // "current" for the frame: an offscreen render, a pane, a context made
577
+ // lazily inside the paint — each asks `app.makeCurrent()` — must not
578
+ // switch to another window's surface mid-frame, or the blit lands on
579
+ // that window and `eglSwapBuffers` on this one is EGL_BAD_SURFACE (the
580
+ // popup crash: its first frame made its context, and `makeCurrent`
581
+ // reached for the toplevel's surface it had cached).
582
+ if (this.glctx.chain?.gbm) this.app._currentSurface = this.glctx.chain.gbm;
583
+ const resized = size.resized || this._resized;
584
+ this._resized = false;
585
+ this._frameSize = { width: size.width, height: size.height, time };
586
+ const ctx = this._contexts.get('2d');
587
+ if (ctx) {
588
+ ctx.begin(size.width, size.height, time);
589
+ this._paintFrame(ctx);
590
+ this._enterContent(ctx);
591
+ }
592
+
593
+ // `_frameArmed` stays set: a callback that asks for the next frame is
594
+ // queued for the vsync after this present, not run again now.
595
+ for (const fn of due) {
596
+ try {
597
+ fn(time);
598
+ } catch (err) {
599
+ this.emit('error', err);
600
+ }
601
+ }
602
+ // The input method hears about the focused field now, after the paint
603
+ // has laid the tree out and the caret rectangle is current (textinput.js).
604
+ this.app.textInput?.sync(this);
605
+
606
+ const painted = this._contexts.get('2d');
607
+ if (this._surfaces.size || this._surfaceRaf.length || this._panes.size) {
608
+ this._runSurfaces(painted, time, resized || frameDirty);
609
+ }
610
+ if (painted) {
611
+ painted.restore();
612
+ painted.end();
613
+ }
614
+ this._frameSize = null;
615
+
616
+ const drew =
617
+ painted?.drew || frameDirty || resized || this._surfaceDamage.length > 0;
618
+ if (!drew) return this._frameIdle();
619
+ this._frameDirty = false;
620
+ void this._present(resized || frameDirty ? 'all' : this._damageFor());
621
+ }
622
+
623
+ /**
624
+ * The `<glarea>`s' turn, after the tree has painted: flush the 2D under
625
+ * them, let each draw into its rect, put the 2d context's GL state back,
626
+ * and blend the children's panes over the surfaces.
627
+ *
628
+ * One thing a shared target needs that a child window never did: where
629
+ * the tree repainted *under* a surface that has no frame of its own this
630
+ * time — a background fill reaching under a static scene — the surface's
631
+ * pixels are gone with the repaint, so its node is asked for the frame
632
+ * now, before the present, rather than showing the fill for a frame.
633
+ */
634
+ _runSurfaces(ctx, time, wholeFrame) {
635
+ ctx?.flush();
636
+ for (const area of this._surfaces) area.drewThisFrame = false;
637
+ this._drainSurfaceFrames(time);
638
+ if (this._surfaces.size) {
639
+ const damage = wholeFrame ? null : this._reactX11Node?._lastDamageRects;
640
+ for (const area of this._surfaces) {
641
+ if (area.drewThisFrame || !area.mapped || area.destroyed) continue;
642
+ if (damage && !damage.some((r) => overlaps(r, area.rect))) continue;
643
+ area._reactX11Node?.requestFrame?.();
644
+ }
645
+ this._drainSurfaceFrames(time);
646
+ }
647
+ if (!ctx) return;
648
+ ctx.restoreGLState();
649
+ if (this._surfaceDamage.length === 0) return;
650
+ for (const pane of this._panes) {
651
+ if (pane.hidden || !pane.presented || pane.destroyed) continue;
652
+ ctx.drawImage(pane.target, pane.rect.x, pane.rect.y);
653
+ }
654
+ }
655
+
656
+ _drainSurfaceFrames(time) {
657
+ // a callback may queue the next frame; that one is for the next present
658
+ const due = this._surfaceRaf.splice(0);
659
+ for (const fn of due) {
660
+ try {
661
+ fn(time);
662
+ } catch (err) {
663
+ this.emit('error', err);
664
+ }
665
+ }
666
+ }
667
+
668
+ _paintFrame(ctx) {
669
+ if (!this.decor) return;
670
+ const s = this.wl.scale;
671
+ ctx.save();
672
+ ctx.scale(s, s);
673
+ this.decor.paint(ctx, this.wl.width, this.wl.height, this.app.frameText);
674
+ ctx.restore();
675
+ }
676
+
677
+ /**
678
+ * What the renderer repainted, as buffer-space rectangles. The window node
679
+ * keeps the rects its last flush painted; `null` means all of it.
680
+ */
681
+ _damageFor() {
682
+ const node = this._reactX11Node;
683
+ const rects = node?._lastDamageRects;
684
+ const extra = this._surfaceDamage.splice(0);
685
+ if (!rects) return 'all';
686
+ const o = this.contentOrigin;
687
+ return [...rects, ...extra].map((r) => ({
688
+ x: r.x + o.x,
689
+ y: r.y + o.y,
690
+ width: r.width,
691
+ height: r.height,
692
+ }));
693
+ }
694
+
695
+ /** Hand the painted buffer to the compositor and re-arm the clock. */
696
+ async _present(damage) {
697
+ if (this._destroyed) return;
698
+ this._presentInFlight = true;
699
+ if (SNAPSHOT && (this._presents ?? 0) + 1 === SNAPSHOT_AT) {
700
+ try {
701
+ this.glctx.bindBacking();
702
+ const { width, height } = this.glctx.backing;
703
+ writeFileSync(SNAPSHOT, snapshotPNG(this.glctx.gl, width, height));
704
+ writeSync(2, `react-x11 wayland: wrote ${SNAPSHOT}\n`);
705
+ } catch (err) {
706
+ writeSync(2, `react-x11 wayland: snapshot failed: ${err.message}\n`);
707
+ }
708
+ }
709
+ let vsync;
710
+ try {
711
+ vsync = await this.glctx.endFrame(damage);
712
+ } catch (err) {
713
+ this._presentInFlight = false;
714
+ // The connection going away rejects the frame request this present was
715
+ // waiting on (the library aborts in-flight callbacks on close). That is
716
+ // the app closing, not this window failing — and an 'error' nobody
717
+ // listens for throws: closing a window with a frame in flight, which is
718
+ // any window just resized, crashed with "wl_surface.frame: display
719
+ // closed".
720
+ if (this._destroyed || this.app.conn?.destroyed) return;
721
+ this.emit('error', err);
722
+ return this._frameIdle();
723
+ }
724
+ this._everPresented = true;
725
+ this.wl.mapped = true;
726
+ this._presents = (this._presents ?? 0) + 1;
727
+ if (TRACE) {
728
+ // a synchronous write: Bun buffers process.stderr to a file, and a
729
+ // buffer is what a SIGINT leaves behind
730
+ writeSync(
731
+ 2,
732
+ `react-x11 wayland: window ${this.id} present #${this._presents} ` +
733
+ `${damage === 'all' ? 'full' : damage.length + ' rect(s)'}\n`,
734
+ );
735
+ }
736
+ Promise.resolve(vsync).then(
737
+ (t) => {
738
+ this._presentInFlight = false;
739
+ if (!this._destroyed) this._fireRaf(t);
740
+ },
741
+ () => {
742
+ this._presentInFlight = false;
743
+ this._frameIdle();
744
+ },
745
+ );
746
+ }
747
+
748
+ // ---- window operations -------------------------------------------------
749
+
750
+ setTitle(title) {
751
+ if (this.decor) {
752
+ this.decor.title = String(title ?? '');
753
+ this.repaintFrame();
754
+ }
755
+ this.wl.setTitle(title);
756
+ return this;
757
+ }
758
+
759
+ setClass() {
760
+ // app_id is fixed at creation; xdg-shell has no later change
761
+ return this;
762
+ }
763
+
764
+ /**
765
+ * A size or a state. The renderer's size tracking calls this with
766
+ * `{ width, height }` (device pixels of content); a string is a WM state.
767
+ *
768
+ * A Wayland client *can* pick its own size — it simply commits a buffer
769
+ * that big — unless the compositor is imposing one (maximised, tiled, or
770
+ * mid-resize), in which case the wish is recorded and not acted on.
771
+ */
772
+ setState(state) {
773
+ if (state && typeof state === 'object') {
774
+ // A popup's rect is one placement: the tree places a popup through
775
+ // here whenever the window has a `setState`, and a tooltip that
776
+ // measured itself hidden at (0, 0) is moved to its trigger this way.
777
+ // Dropping `x`/`y` left it in its parent's corner.
778
+ if (this.isPopup && ('x' in state || 'y' in state)) {
779
+ this._placePopup(state);
780
+ return this;
781
+ }
782
+ if ('width' in state || 'height' in state)
783
+ this.resize(state.width ?? this.width, state.height ?? this.height);
784
+ return this;
785
+ }
786
+ if (state === 'maximized') this.wl.maximize(true);
787
+ else if (state === 'fullscreen') this.wl.fullscreen(true);
788
+ else if (state === 'minimized' || state === 'iconic') this.wl.minimize();
789
+ else if (state === 'normal') {
790
+ this.wl.maximize(false);
791
+ this.wl.fullscreen(false);
792
+ }
793
+ return this;
794
+ }
795
+
796
+ resize(width, height) {
797
+ const st = this.wl.states;
798
+ const imposed =
799
+ st.has(TOPLEVEL_STATE.MAXIMIZED) ||
800
+ st.has(TOPLEVEL_STATE.FULLSCREEN) ||
801
+ st.has(TOPLEVEL_STATE.RESIZING) ||
802
+ [...st].some(
803
+ (s) =>
804
+ s >= TOPLEVEL_STATE.TILED_LEFT && s <= TOPLEVEL_STATE.TILED_BOTTOM,
805
+ );
806
+ if (imposed && this.wl.configured) {
807
+ this._wanted = { width, height };
808
+ return this;
809
+ }
810
+ const i = this.insets;
811
+ const s = this.wl.scale;
812
+ let w = Math.max(1, Math.round(width / s + i.left + i.right));
813
+ let h = Math.max(1, Math.round(height / s + i.top + i.bottom));
814
+ if (this.isPopup) {
815
+ this.wl.reposition(this.app.wmBase, {
816
+ x: this._popupAt.x,
817
+ y: this._popupAt.y,
818
+ width: w,
819
+ height: h,
820
+ });
821
+ }
822
+ if (this.isLayer) {
823
+ // A stretched axis is the compositor's; the wish on it is not granted.
824
+ const role = this.wl.layer;
825
+ if (role.stretchX) w = this.wl.width;
826
+ if (role.stretchY) h = this.wl.height;
827
+ if (w !== this.wl.width || h !== this.wl.height) role.resize(w, h);
828
+ }
829
+ if (w === this.wl.width && h === this.wl.height) return this;
830
+ this.wl.width = w;
831
+ this.wl.height = h;
832
+ this._resized = true;
833
+ this._frameDirty = true;
834
+ this._armFrame();
835
+ return this;
836
+ }
837
+
838
+ move(x, y) {
839
+ if (this.isPopup) {
840
+ this._placePopup({ x, y });
841
+ return this;
842
+ }
843
+ // A toplevel cannot place itself. Not an error: the tree asks on every
844
+ // controlled position, and the compositor's answer is final.
845
+ return this;
846
+ }
847
+
848
+ /**
849
+ * Place a popup: `x`/`y` in its parent's content device pixels, `width`/
850
+ * `height` in its own, any of them omitted to keep what it has. One
851
+ * reposition for the whole rect, from the position the tree asked for —
852
+ * not the one the compositor reported, which it may have slid to keep the
853
+ * popup on screen, and which drifted further with every resize.
854
+ */
855
+ _placePopup({ x, y, width, height }) {
856
+ const p = this.parentWindow;
857
+ const pi = p?.geometryInsets ?? { left: 0, top: 0 };
858
+ const ps = p?.scale ?? this.scale;
859
+ if (x != null) this._popupAt.x = x / ps + pi.left;
860
+ if (y != null) this._popupAt.y = y / ps + pi.top;
861
+ const s = this.wl.scale;
862
+ const w =
863
+ width != null ? Math.max(1, Math.round(width / s)) : this.wl.width;
864
+ const h =
865
+ height != null ? Math.max(1, Math.round(height / s)) : this.wl.height;
866
+ this.wl.reposition(this.app.wmBase, {
867
+ x: this._popupAt.x,
868
+ y: this._popupAt.y,
869
+ width: w,
870
+ height: h,
871
+ });
872
+ if (w !== this.wl.width || h !== this.wl.height) {
873
+ this.wl.width = w;
874
+ this.wl.height = h;
875
+ this._resized = true;
876
+ this._frameDirty = true;
877
+ this._armFrame();
878
+ }
879
+ }
880
+
881
+ getWmStates() {
882
+ const out = [];
883
+ for (const s of this.wl.states)
884
+ for (const n of STATE_NAMES[s] ?? []) out.push(n);
885
+ return out;
886
+ }
887
+
888
+ setWmState(state, on = true) {
889
+ if (
890
+ state === 'maximized_vert' ||
891
+ state === 'maximized_horz' ||
892
+ state === 'maximized'
893
+ )
894
+ this.wl.maximize(on);
895
+ else if (state === 'fullscreen') this.wl.fullscreen(on);
896
+ else if (state === 'hidden' && on) this.wl.minimize();
897
+ return this;
898
+ }
899
+
900
+ setWindowType() {
901
+ return this;
902
+ }
903
+
904
+ setSizeHints(hints = {}) {
905
+ if (this.isPopup) return this;
906
+ const s = this.wl.scale;
907
+ const i = this.insets;
908
+ if (hints.minWidth || hints.minHeight) {
909
+ this.wl.setMinSize(
910
+ (hints.minWidth ?? 0) / s + i.left + i.right,
911
+ (hints.minHeight ?? 0) / s + i.top + i.bottom,
912
+ );
913
+ }
914
+ if (hints.maxWidth || hints.maxHeight) {
915
+ this.wl.setMaxSize(
916
+ (hints.maxWidth ?? 0) / s + i.left + i.right,
917
+ (hints.maxHeight ?? 0) / s + i.top + i.bottom,
918
+ );
919
+ }
920
+ return this;
921
+ }
922
+
923
+ setTransientFor(id) {
924
+ if (this.isPopup) return this;
925
+ const parent = id == null ? null : this.app.windowById(id);
926
+ this.wl.setParent(parent?.wl ?? null);
927
+ return this;
928
+ }
929
+
930
+ /**
931
+ * The drop side (src/wayland/dnd.js): the window node hands over its
932
+ * DropSession at realize (`_initDnd`), and from then on the app-wide
933
+ * `WaylandDnd` routes this surface's data-device events into it. The
934
+ * method's presence is what tells the tree this backend has drop machinery
935
+ * of its own.
936
+ */
937
+ attachDropTransport(session, node) {
938
+ this.app.dnd?.attach(this, session, node);
939
+ return this;
940
+ }
941
+
942
+ /**
943
+ * The source side: hand a `DragSession`'s gesture to a `wl_data_source`
944
+ * (see src/wayland/dnd.js). Called by `DragSession._start` once the
945
+ * threshold is crossed; returns at once, and the session reports back on
946
+ * the source's own events.
947
+ */
948
+ beginDrag(session) {
949
+ return this.app.dnd?.beginDrag(this, session) ?? null;
950
+ }
951
+
952
+ /** The tree's cursor over the content. */
953
+ setCursor(name) {
954
+ this.treeCursor = name ?? 'default';
955
+ if (
956
+ this.app.input?.pointerWindow === this &&
957
+ !this.app.input._frameCursor
958
+ ) {
959
+ this.app.seat.setCursor(this.treeCursor);
960
+ }
961
+ return this;
962
+ }
963
+
964
+ /**
965
+ * A popup's grab: the press outside it that should dismiss it arrives
966
+ * here rather than at whatever is under the pointer. Must precede the
967
+ * popup's first buffer, which is why the request is sent now and the
968
+ * callback answers on the next tick.
969
+ */
970
+ grabPointer(_opts, callback) {
971
+ let error = null;
972
+ if (this.isPopup) {
973
+ const serial = this.app.seat.lastPressSerial || this.app.seat.lastSerial;
974
+ try {
975
+ // Recorded for the initial commit, and false once that is over: a
976
+ // grab taken then holds for as long as the popup is mapped, and
977
+ // there is no second one to take.
978
+ this.wl.takeGrab(this.app.seat.seat, serial);
979
+ } catch (err) {
980
+ error = err;
981
+ }
982
+ }
983
+ queueMicrotask(() => callback?.(error));
984
+ return this;
985
+ }
986
+
987
+ ungrabPointer() {
988
+ // a popup's grab ends when the popup does
989
+ return this;
990
+ }
991
+
992
+ raise() {
993
+ return this;
994
+ }
995
+
996
+ lower() {
997
+ return this;
998
+ }
999
+
1000
+ map() {
1001
+ this._wantMapped = true;
1002
+ // A popup's initial commit waits for its map. The grab the tree takes
1003
+ // straight after mapping (`PopupNode._mapNow`) must reach the compositor
1004
+ // before that commit, and a popup the tree never maps — born `hidden`,
1005
+ // or anchored off screen — should never be configured, let alone shown.
1006
+ // A microtask, so that grab lands first.
1007
+ if (this.wl.initialCommitPending) {
1008
+ queueMicrotask(() => this.wl.commitInitial());
1009
+ }
1010
+ this._armFrame();
1011
+ return this;
1012
+ }
1013
+
1014
+ unmap() {
1015
+ this.wl.unmap();
1016
+ return this;
1017
+ }
1018
+
1019
+ /**
1020
+ * `xdg_popup.popup_done`: the compositor has dismissed this popup — a
1021
+ * press outside it, Escape, the parent losing focus — and already hidden
1022
+ * it. On X11 the grab brings that press here, outside the window, and the
1023
+ * event manager answers it with `onDismiss` (`_dismissOutside`); here the
1024
+ * compositor keeps the press, so the same press is made up, outside, for
1025
+ * the same answer. Without it the tree still thought the menu open while
1026
+ * the screen showed it gone, and the next click on its button closed
1027
+ * nothing.
1028
+ */
1029
+ _dismissedByCompositor() {
1030
+ this.emit('mousedown', {
1031
+ x: -1,
1032
+ y: -1,
1033
+ rootx: -1,
1034
+ rooty: -1,
1035
+ button: 1,
1036
+ buttons: 0,
1037
+ time: Date.now() >>> 0,
1038
+ dismissed: true,
1039
+ });
1040
+ }
1041
+
1042
+ /**
1043
+ * Where this popup's surface lies in the surface of the toplevel its chain
1044
+ * hangs from, in logical pixels: each configure's offset from its parent's
1045
+ * window geometry down the chain, plus the toplevel's shadow margin (a
1046
+ * popup's geometry is its whole surface). What turns a rectangle in the
1047
+ * toplevel into one in the popup — the text-input caret, when mutter has
1048
+ * given a grabbing popup the keyboard.
1049
+ */
1050
+ offsetInRoot() {
1051
+ let x = 0;
1052
+ let y = 0;
1053
+ let w = this;
1054
+ for (let i = 0; w?.isPopup && w.parentWindow && i < 16; i++) {
1055
+ x += w.wl?.x ?? 0;
1056
+ y += w.wl?.y ?? 0;
1057
+ w = w.parentWindow;
1058
+ }
1059
+ const m = w?.wl?.margins ?? { left: 0, top: 0 };
1060
+ return { x: x + m.left, y: y + m.top, root: w };
1061
+ }
1062
+
1063
+ /**
1064
+ * The desktop's frame style or its colours changed (app.js). The frame
1065
+ * repaints; if the titlebar's height moved with the title's font, the
1066
+ * content keeps its size where the compositor lets it and the surface
1067
+ * grows or shrinks round it, and the tree hears a resize where it does
1068
+ * not.
1069
+ */
1070
+ _frameStyleChanged() {
1071
+ if (!this.decor) return;
1072
+ const before = this.insets.top;
1073
+ this.decor.setStyle(this.app.frameStyle?.value, appearanceSnapshot());
1074
+ const delta = this.insets.top - before;
1075
+ this._frameDirty = true;
1076
+ if (delta) {
1077
+ if (!this.wl.sizeImposed) this.wl.height += delta;
1078
+ this._resized = true;
1079
+ this.emit('resize', {
1080
+ width: this.width,
1081
+ height: this.height,
1082
+ x: 0,
1083
+ y: 0,
1084
+ });
1085
+ }
1086
+ this._armFrame();
1087
+ }
1088
+
1089
+ /** The compositor (or the frame's close button) asked; the tree decides. */
1090
+ requestClose() {
1091
+ let prevented = false;
1092
+ this.emit('close', {
1093
+ preventDefault() {
1094
+ prevented = true;
1095
+ },
1096
+ get defaultPrevented() {
1097
+ return prevented;
1098
+ },
1099
+ });
1100
+ if (!prevented) this.app.emit('closeRequest', this);
1101
+ }
1102
+
1103
+ focus() {
1104
+ // Focus is the compositor's to give. xdg-activation can ask, with a token
1105
+ // from a recent interaction; without one the request is ignored.
1106
+ this.app.activate(this);
1107
+ return this;
1108
+ }
1109
+
1110
+ destroy() {
1111
+ if (this._destroyed) return;
1112
+ this._destroyed = true;
1113
+ this._raf.length = 0;
1114
+ this.app.dnd?.detach(this);
1115
+ this.app.makeCurrent();
1116
+ for (const ctx of this._contexts.values()) ctx.destroy?.();
1117
+ this._contexts.clear();
1118
+ this.glctx.destroy();
1119
+ this.app._forgetWindow(this);
1120
+ this.wl.destroy();
1121
+ this.emit('_destroyed');
1122
+ }
1123
+ }