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,601 @@
1
+ // The monitors: every `wl_output` the compositor advertises, published into
2
+ // `src/screens.js` so that `useScreens()`, `availableArea()` and everything
3
+ // that sizes a window against a monitor works with no X server in the path
4
+ // — the way `src/cocoa/app.js`'s `screenLayout()` publishes `NSScreen`.
5
+ //
6
+ // What a Wayland client can and cannot know about the screen shapes all of
7
+ // it:
8
+ //
9
+ // - **Outputs are plural globals.** Each monitor is its own `wl_output` in
10
+ // the registry, so the layout is a fold over every one of them rather
11
+ // than one query, and hot-plug is a `global`/`global_remove` on the
12
+ // registry — which is what connection.js's `Registry` view exists for.
13
+ // - **Position and size are logical.** `wl_output.geometry` places the
14
+ // output in the compositor's global space and `mode` gives the panel's
15
+ // pixels; the logical size is the mode over the scale, turned by the
16
+ // transform — unless `zxdg_output_manager_v1` is there, whose
17
+ // `logical_size` is exact under fractional scaling where that arithmetic
18
+ // is not. The rects are published in the renderer's device pixels
19
+ // (logical × `app.scale`), as cocoa converts points, so `monitorAt()` and
20
+ // the window rects agree; `useScreens()` divides them back.
21
+ // - **There is no primary.** The protocol has no notion of one. The output
22
+ // at the origin is the desktop's anchor on every compositor that lays
23
+ // monitors out from (0,0), and the leftmost stands in otherwise.
24
+ // - **There is no work area.** No `_NET_WORKAREA`, no per-output strut.
25
+ // What exists is `xdg_toplevel.configure_bounds`, sent to a window ahead
26
+ // of its configure: the size it is recommended to fit in — its monitor
27
+ // less the panels, on GNOME. So a window's bounds become the usable rect
28
+ // (`visible`, which `usable()` prefers) of the output the window is on,
29
+ // positioned at the monitor's own origin because the offset is
30
+ // unknowable and, on a backend where a client cannot place a window,
31
+ // irrelevant. Every monitor carries a `visible` — its whole rect until a
32
+ // window on it says otherwise — so that no head is ever clamped per axis
33
+ // by another's work area (the `_NET_WORKAREA` compromise, issue #453). A
34
+ // window that has bounds but no output yet — the bounds arrive before the
35
+ // first buffer, the output after — is put on the smallest monitor its
36
+ // bounds fit, which is the monitor they describe on any desk whose
37
+ // monitors differ. The primary's usable rect is the desktop's `workArea`.
38
+ // - **A surface learns its output by being told.** `wl_surface.enter` and
39
+ // `leave` name the outputs a surface overlaps. That is how `win.output`
40
+ // knows, and where a compositor with neither fractional-scale-v1 nor
41
+ // `wl_surface.preferred_buffer_scale` gets a window's buffer scale from.
42
+ //
43
+ // Published through `setScreensForTests`, as the cocoa backend does: it is
44
+ // the one seam `screens.js` has for a layout that did not come from X, and
45
+ // using it leaves the X11 path untouched.
46
+
47
+ import { EventEmitter } from 'node:events';
48
+ import { setScreensForTests } from '../screens.js';
49
+ import {
50
+ isVirtualDisplay,
51
+ monitorScaleFromMetadata,
52
+ monitorScalesOf,
53
+ } from '../scale.js';
54
+
55
+ /** `wl_output.mode` flags. */
56
+ const MODE_CURRENT = 0x1;
57
+
58
+ /** fractional-scale-v1's granularity; a mode-over-logical ratio snaps to it. */
59
+ const SCALE_DENOM = 120;
60
+
61
+ /** `wl_output.transform` → degrees. The flipped four are ignored as
62
+ * reflections, the way `screens.js` ignores RandR's reflection bits. */
63
+ export function degreesOf(transform) {
64
+ return (transform & 3) * 90;
65
+ }
66
+
67
+ /**
68
+ * Hz from `wl_output.mode`'s millihertz, to two decimals, or null where
69
+ * the compositor did not say: a virtual output reports 0, and anything
70
+ * outside the range a panel can refresh at is the same "unknown" in other
71
+ * clothes (`screens.js`'s `refreshRateOf` draws the line in the same place).
72
+ */
73
+ export function refreshHz(millihertz) {
74
+ if (!(millihertz > 0)) return null;
75
+ const hz = millihertz / 1000;
76
+ if (hz < 20 || hz > 1000) return null;
77
+ return Math.round(hz * 100) / 100;
78
+ }
79
+
80
+ /** The current mode's pixels with a quarter turn applied. */
81
+ function turnedMode(rec) {
82
+ const mode = rec.mode;
83
+ if (!mode) return null;
84
+ return rec.transform & 1
85
+ ? { width: mode.height, height: mode.width }
86
+ : { width: mode.width, height: mode.height };
87
+ }
88
+
89
+ /**
90
+ * The output's rect in logical pixels: xdg_output's where there is one,
91
+ * else geometry's position and the mode over the integer scale.
92
+ */
93
+ export function logicalRectOf(rec) {
94
+ const at = rec.logicalPosition ?? { x: rec.x, y: rec.y };
95
+ if (rec.logicalSize?.width > 0 && rec.logicalSize?.height > 0)
96
+ return { ...at, ...rec.logicalSize };
97
+ const mode = turnedMode(rec);
98
+ if (!mode) return null;
99
+ const s = rec.scale > 0 ? rec.scale : 1;
100
+ return {
101
+ ...at,
102
+ width: Math.max(1, Math.round(mode.width / s)),
103
+ height: Math.max(1, Math.round(mode.height / s)),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Device pixels per logical pixel on this output. `wl_output.scale` is an
109
+ * integer and rounds a fractional desktop up (150% reports 2); the mode
110
+ * against xdg_output's logical size is the fraction itself, snapped to the
111
+ * 1/120 fractional-scale-v1 speaks in so it compares equal to what a
112
+ * surface is told.
113
+ */
114
+ export function effectiveScaleOf(rec) {
115
+ const mode = turnedMode(rec);
116
+ const logical = rec.logicalSize;
117
+ if (mode?.width > 0 && logical?.width > 0) {
118
+ const ratio = mode.width / logical.width;
119
+ if (ratio > 0) return Math.round(ratio * SCALE_DENOM) / SCALE_DENOM;
120
+ }
121
+ return rec.scale > 0 ? rec.scale : 1;
122
+ }
123
+
124
+ /**
125
+ * The scale ladder's resolution class (src/scale.js, rung 5) for a
126
+ * compositor that scales nothing: `{ zoom: 2 }` where the largest output is
127
+ * a retina-class grid with no millimetres worth believing, `{ zoom: 1 }`
128
+ * everywhere else, with the verdict's reason for `REACT_X11_DEBUG_SCALE`.
129
+ *
130
+ * The case is a VM window over a retina panel. QEMU describes the guest's
131
+ * screen as a ~100dpi monitor, mutter believes it and settles on 100%, and
132
+ * every app laid out at the compositor's word comes up half the size it is
133
+ * on the host. The X11 ladder answers that screen 2x through Xwayland; this
134
+ * is the same rung giving the same answer here. A compositor that scales
135
+ * any output has decided for itself — 133% on that same VM is someone's
136
+ * choice — and credible millimetres were the compositor's own evidence, so
137
+ * both keep their word.
138
+ */
139
+ export function resolutionZoom(records) {
140
+ let pick = null;
141
+ for (const rec of records) {
142
+ const mode = turnedMode(rec);
143
+ if (!(mode?.width > 0 && mode?.height > 0)) continue;
144
+ const s = effectiveScaleOf(rec);
145
+ if (s !== 1) {
146
+ return {
147
+ zoom: 1,
148
+ name: rec.name,
149
+ reason: `the compositor scales it ${s}x`,
150
+ };
151
+ }
152
+ if (!pick || mode.width * mode.height > pick.w * pick.h)
153
+ pick = { rec, w: mode.width, h: mode.height };
154
+ }
155
+ if (!pick) return { zoom: 1, name: null, reason: 'no outputs yet' };
156
+ const { rec, w, h } = pick;
157
+ const verdict = monitorScaleFromMetadata({
158
+ name: rec.name ?? rec.xdgName ?? '',
159
+ width: w,
160
+ height: h,
161
+ widthMM: rec.physicalWidth,
162
+ heightMM: rec.physicalHeight,
163
+ edid: { virtual: isVirtualDisplay(rec.make || null, rec.model || null) },
164
+ });
165
+ return {
166
+ zoom: verdict.source === 'resolution' ? verdict.scale : 1,
167
+ name: rec.name ?? rec.xdgName ?? null,
168
+ reason: `${verdict.reason}; the compositor says 1x`,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * One output as a `screens.js` monitor record, at the app's scale. Pure,
174
+ * and exported for that reason: the layout arithmetic is the part worth
175
+ * pinning without a compositor.
176
+ *
177
+ * `rec` is what the tracker keeps per output: `x`, `y`, `physicalWidth`,
178
+ * `physicalHeight`, `make`, `model`, `transform` from `geometry`; `mode`
179
+ * `{ width, height, refresh }`; `scale`; `name`/`description` (wl_output
180
+ * v4, or xdg_output's `xdgName`/`xdgDescription`); `logicalPosition` and
181
+ * `logicalSize` from xdg_output.
182
+ */
183
+ export function monitorOf(rec, scale = 1) {
184
+ const logical = logicalRectOf(rec);
185
+ if (!logical) return null;
186
+ const px = (v) => Math.round(v * scale);
187
+ const name = rec.name ?? rec.xdgName ?? null;
188
+ return {
189
+ name,
190
+ outputs: name ? [name] : [],
191
+ description: rec.description ?? rec.xdgDescription ?? null,
192
+ x: px(logical.x),
193
+ y: px(logical.y),
194
+ width: Math.max(1, px(logical.width)),
195
+ height: Math.max(1, px(logical.height)),
196
+ primary: false,
197
+ widthMM: rec.physicalWidth > 0 ? rec.physicalWidth : null,
198
+ heightMM: rec.physicalHeight > 0 ? rec.physicalHeight : null,
199
+ refreshRate: refreshHz(rec.mode?.refresh),
200
+ rotation: degreesOf(rec.transform),
201
+ scale: effectiveScaleOf(rec),
202
+ make: rec.make || null,
203
+ model: rec.model || null,
204
+ };
205
+ }
206
+
207
+ /** `bounds` (logical) as a usable rect on `m` (device pixels), at its origin. */
208
+ function usableOf(m, bounds, scale) {
209
+ return {
210
+ x: m.x,
211
+ y: m.y,
212
+ width: Math.min(m.width, Math.max(1, Math.round(bounds.width * scale))),
213
+ height: Math.min(m.height, Math.max(1, Math.round(bounds.height * scale))),
214
+ };
215
+ }
216
+
217
+ /**
218
+ * The monitor a window with these bounds and no output yet is on: the
219
+ * smallest that fits them — `configure_bounds` is a monitor less its
220
+ * panels, so it fits its own and any larger one — the primary on a tie.
221
+ */
222
+ function bestFit(list, bounds, scale) {
223
+ const w = Math.round(bounds.width * scale);
224
+ const h = Math.round(bounds.height * scale);
225
+ let best = null;
226
+ for (const m of list) {
227
+ if (m.width < w || m.height < h) continue;
228
+ const area = m.width * m.height;
229
+ const bestArea = best ? best.width * best.height : Infinity;
230
+ if (area < bestArea || (area === bestArea && m.primary)) best = m;
231
+ }
232
+ return best;
233
+ }
234
+
235
+ /**
236
+ * Sort, pick a primary, and fold the windows' bounds in — everything the
237
+ * layout is beyond one monitor at a time. `bounds` is a list of
238
+ * `{ outputs: Set<id>, bounds: { width, height } }`, most recent last.
239
+ */
240
+ export function layoutOf(monitors, { scale = 1, bounds = [] } = {}) {
241
+ // Left to right, then top to bottom — the order the desktop is laid out
242
+ // in, not the order the registry announced them, which is neither.
243
+ const list = [...monitors].sort((a, b) => a.x - b.x || a.y - b.y);
244
+ const primary = list.find((m) => m.x === 0 && m.y === 0) ?? list[0] ?? null;
245
+ for (const m of list) {
246
+ m.primary = m === primary;
247
+ // Its own rect until a window on it says otherwise. Set on every
248
+ // monitor, so `usable()` takes each as the exact rect it is and never
249
+ // falls back to clamping a wider second head by the primary's.
250
+ m.visible = { x: m.x, y: m.y, width: m.width, height: m.height };
251
+ }
252
+ for (const b of bounds) {
253
+ if (!b.bounds) continue;
254
+ let targets = list.filter((m) => b.outputs.has(m.id));
255
+ if (!targets.length) {
256
+ const fit = bestFit(list, b.bounds, scale);
257
+ targets = fit ? [fit] : [];
258
+ }
259
+ for (const m of targets) m.visible = usableOf(m, b.bounds, scale);
260
+ }
261
+ return {
262
+ monitors: list,
263
+ workArea: primary ? { ...primary.visible } : null,
264
+ };
265
+ }
266
+
267
+ /** What `app.outputs` is. */
268
+ export class WaylandOutputs extends EventEmitter {
269
+ constructor(conn, app) {
270
+ super();
271
+ this.setMaxListeners(0);
272
+ this.conn = conn;
273
+ this.app = app;
274
+ /** registry name -> record */
275
+ this.byName = new Map();
276
+ /** wl_output proxy id -> record */
277
+ this.byId = new Map();
278
+ this.registry = null;
279
+ /** `zxdg_output_manager_v1`, or null on a compositor without it */
280
+ this.manager = null;
281
+ /** the shell windows (WaylandWindow) whose outputs and bounds count */
282
+ this._watched = new Map();
283
+ this._seq = 0;
284
+ this._monitors = [];
285
+ this._scheduled = false;
286
+ this._publishedKey = null;
287
+ this._lastScale = null;
288
+ this.destroyed = false;
289
+ }
290
+
291
+ /**
292
+ * Bind every output and publish the first layout. Two round trips — one
293
+ * for the registry's list, one for the outputs' state — which is the
294
+ * X11 backend's Xinerama tier in cost, and like it awaited by the app.
295
+ *
296
+ * The app assigns `app.outputs` *before* calling this: the first publish
297
+ * asks `app.noteScale()` to seed the scale from the outputs, and it reads
298
+ * them through that property.
299
+ */
300
+ async open() {
301
+ this.manager = await this.conn.bind('zxdg_output_manager_v1');
302
+ this.registry = await this.conn.registry();
303
+ for (const g of this.registry.of('wl_output')) this._add(g.name);
304
+ this._onGlobal = (name, iface) => {
305
+ if (iface === 'wl_output') this._add(name);
306
+ };
307
+ this._onGlobalRemove = (name) => this._remove(name);
308
+ this.registry.on('global', this._onGlobal);
309
+ this.registry.on('global_remove', this._onGlobalRemove);
310
+ // The app's factor multiplies every published rect, so it moving —
311
+ // the first window hearing a fractional scale — is a relayout.
312
+ this._onScale = () => {
313
+ if (this.app.scale !== this._lastScale) this._schedule();
314
+ };
315
+ this.app.on?.('scale', this._onScale);
316
+ // Every output's `done` has scheduled a publish by the time this is
317
+ // back; the explicit one is for a v1 output, which has no `done`.
318
+ await this.conn.roundtrip();
319
+ this.publish();
320
+ }
321
+
322
+ _add(name) {
323
+ if (this.byName.has(name)) return;
324
+ const proxy = this.registry.bind(name, 'wl_output');
325
+ if (!proxy) return;
326
+ const rec = {
327
+ global: name,
328
+ id: proxy.id,
329
+ proxy,
330
+ version: proxy.version,
331
+ xdg: null,
332
+ x: 0,
333
+ y: 0,
334
+ physicalWidth: 0,
335
+ physicalHeight: 0,
336
+ make: '',
337
+ model: '',
338
+ transform: 0,
339
+ mode: null,
340
+ scale: 1,
341
+ name: null,
342
+ description: null,
343
+ xdgName: null,
344
+ xdgDescription: null,
345
+ logicalPosition: null,
346
+ logicalSize: null,
347
+ };
348
+ this.byName.set(name, rec);
349
+ this.byId.set(proxy.id, rec);
350
+
351
+ const touched = () => this._touched(rec);
352
+ proxy.on('geometry', (x, y, pw, ph, _subpixel, make, model, transform) => {
353
+ Object.assign(rec, {
354
+ x,
355
+ y,
356
+ physicalWidth: pw,
357
+ physicalHeight: ph,
358
+ make,
359
+ model,
360
+ transform,
361
+ });
362
+ touched();
363
+ });
364
+ proxy.on('mode', (flags, width, height, refresh) => {
365
+ // A compositor may list every mode the panel has; the current one is
366
+ // the monitor, and failing a flag the last one listed.
367
+ const current = (flags & MODE_CURRENT) !== 0;
368
+ if (current || !rec.mode?.current)
369
+ rec.mode = { width, height, refresh, current };
370
+ touched();
371
+ });
372
+ proxy.on('scale', (factor) => {
373
+ rec.scale = factor > 0 ? factor : 1;
374
+ touched();
375
+ });
376
+ proxy.on('name', (n) => {
377
+ rec.name = n || null;
378
+ touched();
379
+ });
380
+ proxy.on('description', (d) => {
381
+ rec.description = d || null;
382
+ touched();
383
+ });
384
+ proxy.on('done', () => this._schedule());
385
+
386
+ if (this.manager) {
387
+ const xdg = this.manager.$.get_xdg_output(proxy.id);
388
+ xdg.setMaxListeners?.(0);
389
+ rec.xdg = xdg;
390
+ xdg.on('logical_position', (x, y) => {
391
+ rec.logicalPosition = { x, y };
392
+ touched();
393
+ });
394
+ xdg.on('logical_size', (width, height) => {
395
+ rec.logicalSize = { width, height };
396
+ touched();
397
+ });
398
+ xdg.on('name', (n) => {
399
+ rec.xdgName = n || null;
400
+ touched();
401
+ });
402
+ xdg.on('description', (d) => {
403
+ rec.xdgDescription = d || null;
404
+ touched();
405
+ });
406
+ // The atom below manager v3; from v3 on it is wl_output.done above.
407
+ xdg.on('done', () => this._schedule());
408
+ }
409
+ }
410
+
411
+ /** State moved; `done` is the atom, except on a v1 output that has none. */
412
+ _touched(rec) {
413
+ if (rec.version < 2) this._schedule();
414
+ }
415
+
416
+ _remove(name) {
417
+ const rec = this.byName.get(name);
418
+ if (!rec) return;
419
+ this.byName.delete(name);
420
+ this.byId.delete(rec.id);
421
+ this._release(rec);
422
+ this._schedule();
423
+ }
424
+
425
+ _release(rec) {
426
+ try {
427
+ rec.xdg?.$.destroy();
428
+ if (rec.version >= 3) rec.proxy.$.release();
429
+ else this.conn.display.deleteId(rec.id);
430
+ } catch {
431
+ /* the connection may already be gone */
432
+ }
433
+ }
434
+
435
+ /** One publish per turn, however many events a change arrived as. */
436
+ _schedule() {
437
+ if (this._scheduled || this.destroyed) return;
438
+ this._scheduled = true;
439
+ queueMicrotask(() => {
440
+ this._scheduled = false;
441
+ this.publish();
442
+ });
443
+ }
444
+
445
+ // ---- windows ------------------------------------------------------------
446
+
447
+ /**
448
+ * Follow a shell window: which outputs it enters (its monitor, and the
449
+ * buffer-scale fallback) and the bounds it is configured with (the work
450
+ * area). Called by the backend window at creation; forgets the window
451
+ * when it is destroyed.
452
+ */
453
+ watchWindow(wl) {
454
+ if (this._watched.has(wl)) return;
455
+ const entry = { seq: 0 };
456
+ this._watched.set(wl, entry);
457
+ const onOutputs = () => {
458
+ this._applyScale(wl);
459
+ this._schedule();
460
+ };
461
+ const onBounds = () => {
462
+ entry.seq = ++this._seq;
463
+ this._schedule();
464
+ };
465
+ wl.on('outputs', onOutputs);
466
+ wl.on('bounds', onBounds);
467
+ wl.once('destroyed', () => {
468
+ wl.off('outputs', onOutputs);
469
+ wl.off('bounds', onBounds);
470
+ this._watched.delete(wl);
471
+ this._schedule();
472
+ });
473
+ if (wl.bounds) entry.seq = ++this._seq;
474
+ if (wl.outputs.size) this._applyScale(wl);
475
+ }
476
+
477
+ _applyScale(wl) {
478
+ const s = this.scaleFor(wl.outputs);
479
+ if (s) wl.noteOutputScale(s);
480
+ }
481
+
482
+ /** The integer scale for a surface over these outputs: the densest, as
483
+ * every toolkit picks it, so nothing is upsampled. 0 for none known. */
484
+ scaleFor(ids) {
485
+ let s = 0;
486
+ for (const id of ids ?? []) {
487
+ const rec = this.byId.get(id);
488
+ if (rec) s = Math.max(s, rec.scale);
489
+ }
490
+ return s;
491
+ }
492
+
493
+ /** The monitor for a surface over these outputs (the densest), or null. */
494
+ monitorFor(ids) {
495
+ let best = null;
496
+ for (const id of ids ?? []) {
497
+ const m = this._monitors.find((mon) => mon.id === id);
498
+ if (m && (!best || m.scale > best.scale)) best = m;
499
+ }
500
+ return best ? publicMonitor(best) : null;
501
+ }
502
+
503
+ // ---- the layout ---------------------------------------------------------
504
+
505
+ /**
506
+ * The densest output's scale: what the app lays out at until a window
507
+ * has heard its own. The fraction where fractional-scale-v1 will hand
508
+ * the windows one, the integer where only `wl_output.scale` exists —
509
+ * seeding 1.5 on a compositor that can only answer 2 would be a layout
510
+ * corrected on the first frame.
511
+ */
512
+ get maxScale() {
513
+ const fractional = Boolean(this.app.fractionalScale);
514
+ let s = 1;
515
+ for (const rec of this.byName.values())
516
+ s = Math.max(s, fractional ? effectiveScaleOf(rec) : rec.scale || 1);
517
+ return s;
518
+ }
519
+
520
+ /** `resolutionZoom` over the outputs as they stand. */
521
+ resolutionZoom() {
522
+ return resolutionZoom([...this.byName.values()]);
523
+ }
524
+
525
+ /** The layout in `screens.js`'s shape, at the app's current scale. */
526
+ layout() {
527
+ const scale = this.app.scale > 0 ? this.app.scale : 1;
528
+ const monitors = [];
529
+ for (const rec of this.byName.values()) {
530
+ const m = monitorOf(rec, scale);
531
+ if (m) monitors.push({ id: rec.id, ...m });
532
+ }
533
+ const bounds = [...this._watched]
534
+ .filter(([wl]) => wl.bounds)
535
+ .sort((a, b) => a[1].seq - b[1].seq)
536
+ .map(([wl]) => ({ outputs: wl.outputs, bounds: wl.bounds }));
537
+ return layoutOf(monitors, { scale, bounds });
538
+ }
539
+
540
+ /** Every monitor, left to right, as `screens.js` sees it plus
541
+ * `description`, `scale`, `make` and `model`. */
542
+ list() {
543
+ return this._monitors.map(publicMonitor);
544
+ }
545
+
546
+ /**
547
+ * Publish what is known — to `screens.js`, to the per-monitor scale map
548
+ * `useScreens()` joins on, and to anyone listening for `change`. Skipped
549
+ * when nothing moved: a `configure_bounds` repeating the same numbers on
550
+ * every window must not re-render every `useScreens()` subscriber.
551
+ */
552
+ publish() {
553
+ if (this.destroyed) return;
554
+ // A denser output may have arrived before any window: the app's factor
555
+ // follows, and the rects below are computed at the new one.
556
+ this.app.noteScale?.();
557
+ this._lastScale = this.app.scale;
558
+ const { monitors, workArea } = this.layout();
559
+ this._monitors = monitors;
560
+ const published = monitors.map(publicMonitor);
561
+ const key = JSON.stringify({ published, workArea });
562
+ if (key === this._publishedKey) return;
563
+ this._publishedKey = key;
564
+
565
+ const scales = monitorScalesOf(this.app);
566
+ scales.clear();
567
+ for (const m of published) {
568
+ if (!m.name) continue;
569
+ scales.set(m.name, {
570
+ scale: m.scale,
571
+ source: 'wl_output',
572
+ primary: m.primary,
573
+ x: m.x,
574
+ y: m.y,
575
+ width: m.width,
576
+ height: m.height,
577
+ });
578
+ }
579
+ setScreensForTests(this.app, { monitors: published, workArea });
580
+ this.emit('change', published);
581
+ }
582
+
583
+ destroy() {
584
+ if (this.destroyed) return;
585
+ this.destroyed = true;
586
+ this.registry?.off('global', this._onGlobal);
587
+ this.registry?.off('global_remove', this._onGlobalRemove);
588
+ this.app.off?.('scale', this._onScale);
589
+ for (const rec of this.byName.values()) this._release(rec);
590
+ this.byName.clear();
591
+ this.byId.clear();
592
+ this._watched.clear();
593
+ }
594
+ }
595
+
596
+ /** A monitor record without the proxy id the tracker keys on. */
597
+ function publicMonitor(m) {
598
+ // eslint-disable-next-line no-unused-vars
599
+ const { id, ...rest } = m;
600
+ return rest;
601
+ }
@@ -0,0 +1 @@
1
+ [{"name":"wp_cursor_shape_manager_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_pointer","args":[{"name":"cursor_shape_device","type":"new_id","interface":"wp_cursor_shape_device_v1"},{"name":"pointer","type":"object","interface":"wl_pointer"}]},{"name":"get_tablet_tool_v2","args":[{"name":"cursor_shape_device","type":"new_id","interface":"wp_cursor_shape_device_v1"},{"name":"tablet_tool","type":"object","interface":"zwp_tablet_tool_v2"}]}],"events":[],"enums":{}},{"name":"wp_cursor_shape_device_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"set_shape","args":[{"name":"serial","type":"uint"},{"name":"shape","type":"uint","enum":"shape"}]}],"events":[],"enums":{"shape":{"entries":[{"name":"default","value":1},{"name":"context_menu","value":2},{"name":"help","value":3},{"name":"pointer","value":4},{"name":"progress","value":5},{"name":"wait","value":6},{"name":"cell","value":7},{"name":"crosshair","value":8},{"name":"text","value":9},{"name":"vertical_text","value":10},{"name":"alias","value":11},{"name":"copy","value":12},{"name":"move","value":13},{"name":"no_drop","value":14},{"name":"not_allowed","value":15},{"name":"grab","value":16},{"name":"grabbing","value":17},{"name":"e_resize","value":18},{"name":"n_resize","value":19},{"name":"ne_resize","value":20},{"name":"nw_resize","value":21},{"name":"s_resize","value":22},{"name":"se_resize","value":23},{"name":"sw_resize","value":24},{"name":"w_resize","value":25},{"name":"ew_resize","value":26},{"name":"ns_resize","value":27},{"name":"nesw_resize","value":28},{"name":"nwse_resize","value":29},{"name":"col_resize","value":30},{"name":"row_resize","value":31},{"name":"all_scroll","value":32},{"name":"zoom_in","value":33},{"name":"zoom_out","value":34},{"name":"dnd_ask","value":35},{"name":"all_resize","value":36}]},"error":{"entries":[{"name":"invalid_shape","value":1}]}}}]
@@ -0,0 +1 @@
1
+ [{"name":"ext_idle_notifier_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_idle_notification","args":[{"name":"id","type":"new_id","interface":"ext_idle_notification_v1"},{"name":"timeout","type":"uint"},{"name":"seat","type":"object","interface":"wl_seat"}]},{"name":"get_input_idle_notification","since":2,"args":[{"name":"id","type":"new_id","interface":"ext_idle_notification_v1"},{"name":"timeout","type":"uint"},{"name":"seat","type":"object","interface":"wl_seat"}]}],"events":[],"enums":{}},{"name":"ext_idle_notification_v1","version":2,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"idled","args":[]},{"name":"resumed","args":[]}],"enums":{}}]
@@ -0,0 +1 @@
1
+ [{"name":"ext_image_capture_source_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{}},{"name":"ext_output_image_capture_source_manager_v1","version":1,"requests":[{"name":"create_source","args":[{"name":"source","type":"new_id","interface":"ext_image_capture_source_v1"},{"name":"output","type":"object","interface":"wl_output"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{}},{"name":"ext_foreign_toplevel_image_capture_source_manager_v1","version":1,"requests":[{"name":"create_source","args":[{"name":"source","type":"new_id","interface":"ext_image_capture_source_v1"},{"name":"toplevel_handle","type":"object","interface":"ext_foreign_toplevel_handle_v1"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{}}]
@@ -0,0 +1 @@
1
+ [{"name":"ext_image_copy_capture_manager_v1","version":1,"requests":[{"name":"create_session","args":[{"name":"session","type":"new_id","interface":"ext_image_copy_capture_session_v1"},{"name":"source","type":"object","interface":"ext_image_capture_source_v1"},{"name":"options","type":"uint","enum":"options"}]},{"name":"create_pointer_cursor_session","args":[{"name":"session","type":"new_id","interface":"ext_image_copy_capture_cursor_session_v1"},{"name":"source","type":"object","interface":"ext_image_capture_source_v1"},{"name":"pointer","type":"object","interface":"wl_pointer"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[],"enums":{"error":{"entries":[{"name":"invalid_option","value":1}]},"options":{"entries":[{"name":"paint_cursors","value":1}]}}},{"name":"ext_image_copy_capture_session_v1","version":1,"requests":[{"name":"create_frame","args":[{"name":"frame","type":"new_id","interface":"ext_image_copy_capture_frame_v1"}]},{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"buffer_size","args":[{"name":"width","type":"uint"},{"name":"height","type":"uint"}]},{"name":"shm_format","args":[{"name":"format","type":"uint","enum":"wl_shm.format"}]},{"name":"dmabuf_device","args":[{"name":"device","type":"array"}]},{"name":"dmabuf_format","args":[{"name":"format","type":"uint"},{"name":"modifiers","type":"array"}]},{"name":"done","args":[]},{"name":"stopped","args":[]}],"enums":{"error":{"entries":[{"name":"duplicate_frame","value":1}]}}},{"name":"ext_image_copy_capture_frame_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"attach_buffer","args":[{"name":"buffer","type":"object","interface":"wl_buffer"}]},{"name":"damage_buffer","args":[{"name":"x","type":"int"},{"name":"y","type":"int"},{"name":"width","type":"int"},{"name":"height","type":"int"}]},{"name":"capture","args":[]}],"events":[{"name":"transform","args":[{"name":"transform","type":"uint","enum":"wl_output.transform"}]},{"name":"damage","args":[{"name":"x","type":"int"},{"name":"y","type":"int"},{"name":"width","type":"int"},{"name":"height","type":"int"}]},{"name":"presentation_time","args":[{"name":"tv_sec_hi","type":"uint"},{"name":"tv_sec_lo","type":"uint"},{"name":"tv_nsec","type":"uint"}]},{"name":"ready","args":[]},{"name":"failed","args":[{"name":"reason","type":"uint","enum":"failure_reason"}]}],"enums":{"error":{"entries":[{"name":"no_buffer","value":1},{"name":"invalid_buffer_damage","value":2},{"name":"already_captured","value":3}]},"failure_reason":{"entries":[{"name":"unknown","value":0},{"name":"buffer_constraints","value":1},{"name":"stopped","value":2}]}}},{"name":"ext_image_copy_capture_cursor_session_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_capture_session","args":[{"name":"session","type":"new_id","interface":"ext_image_copy_capture_session_v1"}]}],"events":[{"name":"enter","args":[]},{"name":"leave","args":[]},{"name":"position","args":[{"name":"x","type":"int"},{"name":"y","type":"int"}]},{"name":"hotspot","args":[{"name":"x","type":"int"},{"name":"y","type":"int"}]}],"enums":{"error":{"entries":[{"name":"duplicate_session","value":1}]}}}]
@@ -0,0 +1 @@
1
+ [{"name":"wp_fractional_scale_manager_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"get_fractional_scale","args":[{"name":"id","type":"new_id","interface":"wp_fractional_scale_v1"},{"name":"surface","type":"object","interface":"wl_surface"}]}],"events":[],"enums":{"error":{"entries":[{"name":"fractional_scale_exists","value":0}]}}},{"name":"wp_fractional_scale_v1","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"preferred_scale","args":[{"name":"scale","type":"uint"}]}],"enums":{}}]