react-x11 2.51.1 → 2.53.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 (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. package/src/xvideo.js +189 -0
@@ -65,12 +65,11 @@ import {
65
65
  PortalCancelledError,
66
66
  RESPONSE_OK,
67
67
  hasService,
68
- parentWindowHandle,
68
+ portalParentFor,
69
69
  portalRequest,
70
70
  portalVersion,
71
71
  } from './portal.js';
72
72
  import { liveApps } from './trace-registry.js';
73
- import { windowIdOf } from './windowid.js';
74
73
 
75
74
  export const SCREENSHOT_IFACE = 'org.freedesktop.portal.Screenshot';
76
75
  /** `PickColor` arrived in version 2 of the Screenshot interface. */
@@ -220,14 +219,10 @@ async function portalPick(opts, ref) {
220
219
  // options)` — no title — which is the whole reason portalRequest takes a
221
220
  // signature now.
222
221
  signature: 'sa{sv}',
223
- // The handle names an X window. A Wayland toplevel has none to name —
224
- // an xdg-foreign export would be the equivalent — so there the dialog
225
- // floats, which the portal allows.
226
- args: [
227
- appFor(opts)?.backend === 'wayland'
228
- ? ''
229
- : parentWindowHandle(windowIdOf(opts.parentWindow)),
230
- ],
222
+ // An X window's handle on X11, an xdg-foreign export on Wayland, and
223
+ // nothing — a floating dialog, which the portal allows — where neither
224
+ // can be had.
225
+ args: [await portalParentFor(opts.parentWindow, appFor(opts))],
231
226
  options: {},
232
227
  signal: opts.signal,
233
228
  });
@@ -253,16 +253,18 @@ export interface DesktopSettings {
253
253
  /** How far a press moves before it is a drag rather than a click. */
254
254
  readonly dragThreshold: number;
255
255
  /** **False means hold still** — the desktop asked for less motion:
256
- * `Gtk/EnableAnimations` off over XSETTINGS, reduce motion on macOS.
256
+ * `Gtk/EnableAnimations` off over XSETTINGS, `enable-animations` off on
257
+ * the settings portal (Wayland), reduce motion on macOS.
257
258
  * Core's loops do not start then (docs/styling.md, "Loops"); an element
258
259
  * that animates on a timer of its own holds still too, and draws its
259
260
  * resting frame. True wherever the desktop said nothing. */
260
261
  readonly animations: boolean;
261
- /** `'xsettings'`; `'macos'` on the Cocoa backend, where only `animations`
262
- * comes from the system (reduce motion) and the rest are the defaults; or
263
- * null where no settings daemon answered and these are the renderer's own
264
- * defaults. */
265
- readonly source: 'xsettings' | 'macos' | 'test' | null;
262
+ /** `'xsettings'`; `'portal'` on the Wayland backend, from the settings
263
+ * portal's GNOME namespaces; `'macos'` on the Cocoa backend, where only
264
+ * `animations` comes from the system (reduce motion) and the rest are the
265
+ * defaults; or null where nothing answered and these are the renderer's
266
+ * own defaults. */
267
+ readonly source: 'xsettings' | 'portal' | 'macos' | 'test' | null;
266
268
  }
267
269
 
268
270
  /**
@@ -69,8 +69,11 @@ export class VideoFrames {
69
69
  readonly colorSpace: VideoColorSpace;
70
70
  readonly range: VideoRange;
71
71
  /** This display's formats, cheapest first: what a decoder that can choose
72
- * should emit (`ffmpeg -pix_fmt nv12`). NV12 leads where a frame goes on
73
- * a layer as it is — macOS — and BGRA wherever a 2D context draws it. */
72
+ * should emit (`ffmpeg -pix_fmt nv12`). NV12 leads wherever something
73
+ * native takes it as it is — a layer on macOS, a shader on Wayland, an Xv
74
+ * port on X11 — and BGRA where a frame is converted in JavaScript. On X11
75
+ * it is BGRA, and not kept, until the port has answered, a few round
76
+ * trips after the first sink is made. */
74
77
  readonly preferredFormats: readonly VideoFormat[];
75
78
  /** Frames pushed so far. */
76
79
  readonly version: number;
@@ -0,0 +1,28 @@
1
+ // Who writes a `<video>`'s frames natively on an app, if anyone — the seam
2
+ // `VideoFrames.preferredFormats` and the drawn presentation ask through
3
+ // (design/architecture/video.md §6.3).
4
+ //
5
+ // The app itself where its backend has the verbs: the Cocoa app over the
6
+ // bridge's video surfaces, the Wayland app over its own GL context. On X11
7
+ // the app is ntk's, which knows nothing of sinks, so the answer is the Xv
8
+ // writer react-x11 keeps for the connection (src/xvideo.js). A feature test,
9
+ // not a question about which backend this is — `typeof app.xv` is an ntk
10
+ // with XVideo discovery. Null where nothing will, and every frame is
11
+ // converted in JavaScript (`frameToRGBA`, src/videoframes.js).
12
+ import { xvVideoFor } from './xvideo.js';
13
+
14
+ /**
15
+ * `{ videoFormats(), writeVideoFrame(surface, sink, frame, node),
16
+ * releaseVideoFrame?(surface), prepare?(sink) }` for `app`, or null.
17
+ */
18
+ export function videoBackendOf(app) {
19
+ if (!app) return null;
20
+ if (
21
+ typeof app.writeVideoFrame === 'function' ||
22
+ typeof app.videoFormats === 'function'
23
+ ) {
24
+ return app;
25
+ }
26
+ if (typeof app.xv === 'function') return xvVideoFor(app);
27
+ return null;
28
+ }
@@ -1,5 +1,5 @@
1
1
  // `VideoFrames`: a sink an application pushes decoded video frames into,
2
- // and a `<video frames>` shows (docs/architecture/video.md §6.3).
2
+ // and a `<video frames>` shows (design/architecture/video.md §6.3).
3
3
  //
4
4
  // react-x11 decodes nothing. Where the bytes come from — an `ffmpeg` child
5
5
  // writing rawvideo to a pipe, a WASM decoder, an addon over libmpv — is the
@@ -33,12 +33,16 @@
33
33
  // a decoder that can choose (`ffmpeg -pix_fmt`); one that cannot still
34
34
  // works, with the conversion charged where it happens.
35
35
  //
36
- // Pure: no node, no backend. The app is asked for its formats
37
- // (`app.videoFormats()`, the Cocoa app's answer from the bridge) and nothing
38
- // else.
36
+ // No node and no backend: the one question asked of the display is its
37
+ // formats, through `videoBackendOf` (src/videobackend.js) — the Cocoa app's
38
+ // answer from the bridge, the Wayland app's own, and on X11 the Xv writer's,
39
+ // which a sink starts asking for when it is made.
40
+
41
+ import { videoBackendOf } from './videobackend.js';
39
42
 
40
43
  export const VIDEO_FORMATS = Object.freeze(['NV12', 'I420', 'BGRA']);
41
44
  const COLOR_SPACES = ['bt709', 'bt601', 'bt2020'];
45
+ const BGRA_ONLY = Object.freeze(['BGRA']);
42
46
  const RANGES = ['video', 'full'];
43
47
  // The largest side a frame may have: a surface's limit on every backend, and
44
48
  // a number past which a size is a bug rather than a video.
@@ -209,22 +213,30 @@ export class VideoFrames {
209
213
  this._frame = null;
210
214
  this._listeners = new Set();
211
215
  this._preferred = null;
216
+ // On X11 the formats are a port's, and asking for one is round trips:
217
+ // started here, so they are known by the time a decoder that waits for
218
+ // anything at all — a probe of the file, a spawn — reads them.
219
+ videoBackendOf(app)?.prepare?.(this);
212
220
  }
213
221
 
214
222
  /**
215
223
  * This display's formats, cheapest first: what a decoder that can choose
216
- * should emit. NV12 first where a frame can go on a layer as it is
217
- * (Cocoa); BGRA first wherever a 2D context draws it.
224
+ * should emit. YCbCr first wherever something native takes it as it is —
225
+ * a layer on Cocoa, a shader on Wayland, an Xv port on X11 — and BGRA
226
+ * first where a frame is converted in JavaScript on its way to a 2D
227
+ * context. On X11 the answer is BGRA until the port has answered, a few
228
+ * round trips after the first sink is made, and is not kept until then.
218
229
  */
219
230
  get preferredFormats() {
220
- if (this._preferred === null) {
221
- const asked = this.app?.videoFormats?.();
222
- this._preferred = Object.freeze(
223
- Array.isArray(asked) && asked.length !== 0
224
- ? asked.filter((f) => VIDEO_FORMATS.includes(f))
225
- : ['BGRA'],
226
- );
227
- }
231
+ if (this._preferred !== null) return this._preferred;
232
+ const asked = videoBackendOf(this.app)?.videoFormats?.();
233
+ // null: still being asked — answer for now, and ask again next time
234
+ if (asked === null) return BGRA_ONLY;
235
+ this._preferred = Object.freeze(
236
+ Array.isArray(asked) && asked.length !== 0
237
+ ? asked.filter((f) => VIDEO_FORMATS.includes(f))
238
+ : ['BGRA'],
239
+ );
228
240
  return this._preferred;
229
241
  }
230
242
 
@@ -281,8 +293,9 @@ export const isVideoFrames = (v) => v instanceof VideoFrames;
281
293
 
282
294
  // --- converting a frame where nothing native will ----------------------------
283
295
  //
284
- // The drawn path on a backend with no native write (X11, win32, Wayland, a
285
- // Cocoa bridge older than its video surfaces): the frame converted here to
296
+ // The drawn path where nothing native writes the frame (win32, an X server
297
+ // with no Xv adaptor, a Cocoa bridge older than its video surfaces, and on
298
+ // any backend a frame the native writer declines): the frame converted here to
286
299
  // the straight RGBA `putImageData` takes. Not free — two million pixels a
287
300
  // frame at 1080p — which is why `preferredFormats` says BGRA first there.
288
301
  // The matrix and the range are the frame's; no transfer function is
@@ -30,6 +30,7 @@ import { releaseDevice } from './device.js';
30
30
  import { WaylandSeat } from './seat.js';
31
31
  import { WaylandBackendWindow } from './backendwindow.js';
32
32
  import { WaylandSurface } from './surface.js';
33
+ import { releaseVideoFrame, writeVideoFrame } from './video.js';
33
34
  import { InputRouter } from './input.js';
34
35
  import { WaylandTouch } from './touch.js';
35
36
  import { WaylandTablet } from './tablet.js';
@@ -104,6 +105,9 @@ export class WaylandApp extends EventEmitter {
104
105
  this.viewporter = null;
105
106
  this.fractionalScale = null;
106
107
  this.activation = null;
108
+ /** xdg-foreign's exporter: a toplevel's handle for another client to
109
+ * parent to (`exportHandle`); null where the compositor has none */
110
+ this.exporter = null;
107
111
  /** the monitors, published into src/screens.js (outputs.js) */
108
112
  this.outputs = null;
109
113
  /** xdg-decoration, layer-shell, shm: null where the compositor has none */
@@ -153,6 +157,7 @@ export class WaylandApp extends EventEmitter {
153
157
  app.viewporter = await conn.bind('wp_viewporter');
154
158
  app.fractionalScale = await conn.bind('wp_fractional_scale_manager_v1');
155
159
  app.activation = await conn.bind('xdg_activation_v1');
160
+ app.exporter = await conn.bind('zxdg_exporter_v2');
156
161
  // The scale session first, so the outputs have a per-monitor map to
157
162
  // fill; then the outputs, awaited like the X11 backend's Xinerama tier:
158
163
  // an auto-sized window clamps to a monitor synchronously inside
@@ -304,6 +309,9 @@ export class WaylandApp extends EventEmitter {
304
309
  }
305
310
 
306
311
  _forgetWindow(win) {
312
+ // before the surface goes: the export is a handle *to* it
313
+ win._exported?.proxy.$.destroy();
314
+ win._exported = null;
307
315
  if (this.windows.get(win.wl.surface.id) === win)
308
316
  this.windows.delete(win.wl.surface.id);
309
317
  const i = this.toplevels.indexOf(win);
@@ -433,6 +441,32 @@ export class WaylandApp extends EventEmitter {
433
441
  return new WaylandSurface(this, options);
434
442
  }
435
443
 
444
+ /**
445
+ * The formats a `VideoFrames` sink is cheapest in here
446
+ * (`preferredFormats`, src/videoframes.js): all three, YCbCr first. Every
447
+ * one goes up as textures as it is and is converted by a shader
448
+ * (src/wayland/video.js), and NV12 or I420 is less than half of BGRA's
449
+ * bytes on the way.
450
+ */
451
+ videoFormats() {
452
+ return ['NV12', 'I420', 'BGRA'];
453
+ }
454
+
455
+ /**
456
+ * A `<video>`'s frame into `surface`, its drawn presentation, on the GPU:
457
+ * planes as textures, the matrix in a fragment shader. False where the
458
+ * frame cannot be — a size past the GPU's textures — and the node
459
+ * converts it in JavaScript instead (src/nodes/video.js).
460
+ */
461
+ writeVideoFrame(surface, sink, frame) {
462
+ return writeVideoFrame(this, surface, sink, frame);
463
+ }
464
+
465
+ /** The node let `surface` go: its plane textures go too. */
466
+ releaseVideoFrame(surface) {
467
+ releaseVideoFrame(this, surface);
468
+ }
469
+
436
470
  /**
437
471
  * What the machine can do, as a promise like the X11 and Cocoa answers
438
472
  * (src/glbackend.js chains on it): here the answer was settled when the
@@ -510,6 +544,37 @@ export class WaylandApp extends EventEmitter {
510
544
  return new WaylandOverlayPane(this, attributes);
511
545
  }
512
546
 
547
+ /**
548
+ * A handle another client can parent a window of its own to — the
549
+ * portal's `parent_window`, spelled `wayland:<handle>` there — or null.
550
+ *
551
+ * xdg-foreign's `export_toplevel`, made once per window on first ask and
552
+ * kept for its lifetime: the handle is valid for as long as the export
553
+ * object lives, and a file dialog opened twice from one window should name
554
+ * it the same way both times. Only an xdg_toplevel can be exported — a
555
+ * popup or a layer surface is a protocol error, so those answer null, as
556
+ * does a compositor without the protocol. The dialog then floats, which the
557
+ * portal allows; what it must never get is an `x11:` handle naming a
558
+ * Wayland window, which is what it got before this existed.
559
+ */
560
+ exportHandle(win) {
561
+ if (!this.exporter || !win?.wl?.surface || win.isPopup || win.isLayer) {
562
+ return Promise.resolve(null);
563
+ }
564
+ if (win._destroyed) return Promise.resolve(null);
565
+ if (!win._exported) {
566
+ const proxy = this.exporter.$.export_toplevel(win.wl.surface.id);
567
+ const handle = new Promise((resolve) => {
568
+ proxy.on('handle', (value) => resolve(value || null));
569
+ // the answer is one round trip away; a compositor that never sends
570
+ // it is a floating dialog, not a hung one
571
+ setTimeout(() => resolve(null), 1000).unref?.();
572
+ });
573
+ win._exported = { proxy, handle };
574
+ }
575
+ return win._exported.handle;
576
+ }
577
+
513
578
  /**
514
579
  * Ask the compositor to activate a window. Stacking is the compositor's;
515
580
  * xdg-activation lets a client ask with a token from a recent interaction,
@@ -542,6 +607,27 @@ export class WaylandApp extends EventEmitter {
542
607
 
543
608
  cancelAttention() {}
544
609
 
610
+ /**
611
+ * `useKeyboardState()`'s source on this backend: there is no XKB extension
612
+ * to ask, and the seat has the same answers from `wl_keyboard`
613
+ * (src/keyboardstate.js, `WaylandSeat.keyboardState`).
614
+ */
615
+ keyboardState() {
616
+ return this.seat?.keyboardState() ?? null;
617
+ }
618
+
619
+ /** Call `fn` when the locks, the group or the keymap change. */
620
+ watchKeyboardState(fn) {
621
+ const seat = this.seat;
622
+ if (!seat) return () => {};
623
+ seat.on('modifiers', fn);
624
+ seat.on('keymap', fn);
625
+ return () => {
626
+ seat.off('modifiers', fn);
627
+ seat.off('keymap', fn);
628
+ };
629
+ }
630
+
545
631
  async close() {
546
632
  for (const w of [...this.windows.values()]) w.destroy();
547
633
  this.windows.clear();
@@ -67,17 +67,22 @@ class Selection {
67
67
  /** our own source while we own the selection */
68
68
  this.source = null;
69
69
  this.ownData = null;
70
- this._pendingOffers = new Map(); // offer id -> types
70
+ this._pendingOffers = new Map(); // offer id -> { offer, types }
71
71
  /** `watch()` handlers, told when the selection changes hands */
72
72
  this.watchers = new Set();
73
73
 
74
74
  device.on('data_offer', (offer) => {
75
75
  const types = [];
76
- this._pendingOffers.set(offer.id, types);
76
+ this._pendingOffers.set(offer.id, { offer, types });
77
77
  offer.on('offer', (mime) => types.push(mime));
78
78
  });
79
- device.on('selection', (offer) => {
80
- const id = offer?.id ?? offer;
79
+ // The offer arrives as an id: an `object` argument is a number off the
80
+ // wire, where `data_offer`'s `new_id` was a proxy. The proxy is what a
81
+ // read calls `receive` on, so it is found by the id — keeping the number
82
+ // made every read throw, and nothing another application copied could
83
+ // be pasted here.
84
+ device.on('selection', (offerArg) => {
85
+ const id = offerArg?.id ?? offerArg ?? 0;
81
86
  if (this.offer && this.offer.id !== id) {
82
87
  try {
83
88
  this.offer.$.destroy();
@@ -85,15 +90,17 @@ class Selection {
85
90
  /* gone */
86
91
  }
87
92
  }
88
- if (!offer || offer === 0) {
93
+ const pending = id ? this._pendingOffers.get(id) : null;
94
+ this._pendingOffers.delete(id);
95
+ const offer = pending?.offer ?? (offerArg?.$ ? offerArg : null);
96
+ if (!offer) {
89
97
  this.offer = null;
90
98
  this.offerTypes = [];
91
99
  this._notify('closed');
92
100
  return;
93
101
  }
94
102
  this.offer = offer;
95
- this.offerTypes = this._pendingOffers.get(id) ?? [];
96
- this._pendingOffers.delete(id);
103
+ this.offerTypes = pending?.types ?? [];
97
104
  // an offer from our own source means we still own it; anyone else's
98
105
  // means we lost it
99
106
  if (this.source && !this._isOurs(offer)) this._dropSource();
@@ -65,6 +65,9 @@ export const PROTOCOLS = [
65
65
  'cursor-shape-v1',
66
66
  'tablet-v2',
67
67
  'xdg-activation-v1',
68
+ // a handle another client can parent a dialog to — the portal's
69
+ // `parent_window` (src/portal.js, `portalParentFor`)
70
+ 'xdg-foreign-unstable-v2',
68
71
  'xdg-decoration-unstable-v1',
69
72
  // the older KDE protocol that does the same job, for the compositors that
70
73
  // have only that one (ssd.js picks between them)
@@ -67,6 +67,8 @@ const MODE_RRECT = 3;
67
67
  const MODE_GRADIENT = 4;
68
68
  /** A texture read through a projection: `vParams` is (s/w, t/w, 1/w). */
69
69
  const MODE_PROJECTIVE = 5;
70
+ /** A pattern: an image laid down from the fragment's device position. */
71
+ const MODE_PATTERN = 6;
70
72
 
71
73
  /**
72
74
  * The stencil buffer, shared between two uses: bit 7 is the clip (set where
@@ -132,6 +134,12 @@ uniform highp vec2 uViewport;
132
134
  uniform int uGradKind;
133
135
  uniform vec4 uGradA;
134
136
  uniform vec4 uGradB;
137
+ // a pattern: device position to its pixels (the inverse transform it is
138
+ // filled under, a b c d and then e f), its size, and which axes repeat (1 x,
139
+ // 2 y) and whether its rows run bottom up (4: a render target)
140
+ uniform highp vec4 uPatInv;
141
+ uniform highp vec4 uPatOff;
142
+ uniform int uPatFlags;
135
143
 
136
144
  float sdRoundBox(vec2 p, vec2 b, float r) {
137
145
  vec2 q = abs(p) - b + r;
@@ -167,6 +175,28 @@ void main() {
167
175
  // Interpolated across the quad in the plane of the screen, s/w, t/w and
168
176
  // 1/w are each exact, and so is their quotient: a texture in perspective.
169
177
  gl_FragColor = vColor * sampleTex(vParams.xy / vParams.z);
178
+ } else if (uMode == 6) {
179
+ // A pattern, from the fragment's device position as a gradient is, so
180
+ // any shape can be filled with one: back through the transform it was
181
+ // filled under to its own pixels, wrapped along the axes it repeats on
182
+ // and clear off the ones it does not, and read at the centre of the
183
+ // texel it lands in, so a tile drawn at its own size lands pixel for
184
+ // pixel — a grid's dots do not smear across a seam.
185
+ highp vec2 p = vec2(gl_FragCoord.x, uViewport.y - gl_FragCoord.y);
186
+ highp vec2 q = vec2(uPatInv.x * p.x + uPatInv.z * p.y + uPatOff.x,
187
+ uPatInv.y * p.x + uPatInv.w * p.y + uPatOff.y);
188
+ highp vec2 size = uPatOff.zw;
189
+ highp vec2 t = q / size;
190
+ float flags = float(uPatFlags);
191
+ if (mod(flags, 2.0) >= 1.0) t.x = fract(t.x);
192
+ if (mod(floor(flags / 2.0), 2.0) >= 1.0) t.y = fract(t.y);
193
+ if (t.x < 0.0 || t.x >= 1.0 || t.y < 0.0 || t.y >= 1.0) {
194
+ gl_FragColor = vec4(0.0);
195
+ } else {
196
+ t = (floor(t * size) + 0.5) / size;
197
+ if (flags >= 4.0) t.y = 1.0 - t.y;
198
+ gl_FragColor = vColor * sampleTex(t);
199
+ }
170
200
  } else {
171
201
  // Gradients are evaluated per fragment from its device position, so any
172
202
  // shape — a cover quad over a path included — can be filled with one.
@@ -415,6 +445,43 @@ export function parseFont(font) {
415
445
  */
416
446
  const fontIds = new WeakMap();
417
447
  let nextFontId = 1;
448
+ const REPETITIONS = new Set(['repeat', 'repeat-x', 'repeat-y', 'no-repeat']);
449
+
450
+ /**
451
+ * A pattern: an image or a surface, and the axes it repeats along. Its
452
+ * texture is looked up as it is filled rather than made here, so a surface
453
+ * drawn on again after the pattern was made fills with what it holds then,
454
+ * as a canvas pattern of a canvas does.
455
+ */
456
+ class Pattern {
457
+ constructor(source, repetition) {
458
+ this.source = source;
459
+ this.repetition = repetition;
460
+ /** its own transform, under the one it is filled with (`setTransform`) */
461
+ this.m = null;
462
+ }
463
+
464
+ setTransform(m) {
465
+ this.m = m
466
+ ? [m.a ?? 1, m.b ?? 0, m.c ?? 0, m.d ?? 1, m.e ?? 0, m.f ?? 0]
467
+ : null;
468
+ }
469
+ }
470
+
471
+ function invert(m) {
472
+ const det = m[0] * m[3] - m[1] * m[2];
473
+ if (!det) return null;
474
+ const a = m[3] / det;
475
+ const b = -m[1] / det;
476
+ const c = -m[2] / det;
477
+ const d = m[0] / det;
478
+ return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
479
+ }
480
+
481
+ /** A paint that is not a colour: drawn from the fragment's position. */
482
+ const isPaint = (style) =>
483
+ style instanceof Gradient || style instanceof Pattern;
484
+
418
485
  function fontKey(font) {
419
486
  let id = fontIds.get(font);
420
487
  if (!id) fontIds.set(font, (id = `f${nextFontId++}`));
@@ -571,6 +638,9 @@ export class WaylandContext2D {
571
638
  uGradKind: u('uGradKind'),
572
639
  uGradA: u('uGradA'),
573
640
  uGradB: u('uGradB'),
641
+ uPatInv: u('uPatInv'),
642
+ uPatOff: u('uPatOff'),
643
+ uPatFlags: u('uPatFlags'),
574
644
  };
575
645
  shared.buffer = gl.createBuffer();
576
646
  gl.bindBuffer(gl.ARRAY_BUFFER, shared.buffer);
@@ -847,9 +917,23 @@ export class WaylandContext2D {
847
917
  return new Gradient('radial', [x0, y0, r0, x1, y1, r1]);
848
918
  }
849
919
 
850
- createPattern() {
851
- // Patterns need a repeating texture mode; not yet.
852
- return null;
920
+ /**
921
+ * A pattern of `image` — an ImageData, an ntk `Image`, a surface or a
922
+ * render target. Null for one with nothing in it, as canvas answers for an
923
+ * image it cannot use; `repetition` is canvas's, null and `''` meaning
924
+ * `'repeat'`, and anything else a `SyntaxError`.
925
+ */
926
+ createPattern(image, repetition) {
927
+ const rep =
928
+ repetition == null || repetition === '' ? 'repeat' : String(repetition);
929
+ if (!REPETITIONS.has(rep)) {
930
+ throw new SyntaxError(
931
+ `createPattern: ${JSON.stringify(rep)} is not a repetition — ` +
932
+ "expected 'repeat', 'repeat-x', 'repeat-y' or 'no-repeat'.",
933
+ );
934
+ }
935
+ if (!((image?.width | 0) > 0 && (image?.height | 0) > 0)) return null;
936
+ return new Pattern(image, rep);
853
937
  }
854
938
 
855
939
  // ---- paths ------------------------------------------------------------
@@ -1088,7 +1172,7 @@ export class WaylandContext2D {
1088
1172
 
1089
1173
  _rect(x, y, w, h, radius, style, strokeWidth) {
1090
1174
  if (!(w > 0 && h > 0)) return;
1091
- if (style instanceof Gradient) {
1175
+ if (isPaint(style)) {
1092
1176
  if (radius <= 0 && strokeWidth === 0)
1093
1177
  return this._gradientQuad(x, y, w, h, style);
1094
1178
  // rounded/stroked gradient: stencil the shape, cover with the gradient
@@ -1226,8 +1310,40 @@ export class WaylandContext2D {
1226
1310
  }
1227
1311
  }
1228
1312
 
1313
+ /** Bind a gradient or a pattern for the next quads; false where there is
1314
+ * nothing to draw with — a pattern of an image that has gone. */
1315
+ _usePaint(paint) {
1316
+ if (paint instanceof Pattern) return this._usePattern(paint);
1317
+ this._useGradient(paint);
1318
+ return true;
1319
+ }
1320
+
1321
+ /** Bind a pattern's texture, and the way from a device pixel to one of
1322
+ * its own, as the current batch's uniforms. */
1323
+ _usePattern(pat) {
1324
+ const src = this._textureFor(pat.source);
1325
+ if (!src) return false;
1326
+ const inv = invert(pat.m ? mul(this._m, pat.m) : this._m);
1327
+ if (!inv) return false;
1328
+ const gl = this.gl;
1329
+ this._flush();
1330
+ this._mode = MODE_PATTERN;
1331
+ this._texture = src.tex;
1332
+ this._swizzle = src.swizzle;
1333
+ const rep = pat.repetition;
1334
+ const flags =
1335
+ (rep === 'repeat' || rep === 'repeat-x' ? 1 : 0) +
1336
+ (rep === 'repeat' || rep === 'repeat-y' ? 2 : 0) +
1337
+ (src.flipY ? 4 : 0);
1338
+ gl.useProgram(this._program);
1339
+ gl.uniform4f(this._loc.uPatInv, inv[0], inv[1], inv[2], inv[3]);
1340
+ gl.uniform4f(this._loc.uPatOff, inv[4], inv[5], src.w, src.h);
1341
+ gl.uniform1i(this._loc.uPatFlags, flags);
1342
+ return true;
1343
+ }
1344
+
1229
1345
  _gradientQuad(x, y, w, h, grad) {
1230
- this._useGradient(grad);
1346
+ if (!this._usePaint(grad)) return;
1231
1347
  const m = this._m;
1232
1348
  const alpha = this.globalAlpha;
1233
1349
  this._quad(
@@ -1277,7 +1393,7 @@ export class WaylandContext2D {
1277
1393
  // only once the device has been claimed.
1278
1394
  this._claim();
1279
1395
  let color = null;
1280
- if (!(style instanceof Gradient)) {
1396
+ if (!isPaint(style)) {
1281
1397
  color = this._color(style);
1282
1398
  if (color[3] === 0) return;
1283
1399
  if (this._fillCoverage(polys, rule, bounds, color)) return;
@@ -1297,15 +1413,18 @@ export class WaylandContext2D {
1297
1413
  const { x, y, w, h } = bounds;
1298
1414
  const saved = this._m;
1299
1415
  this._m = IDENTITY;
1300
- if (style instanceof Gradient) {
1301
- this._m = saved; // gradient endpoints are in user space
1302
- this._useGradient(style);
1416
+ if (isPaint(style)) {
1417
+ this._m = saved; // a gradient's endpoints, a pattern's origin: user space
1418
+ const painted = this._usePaint(style);
1303
1419
  this._m = IDENTITY;
1304
1420
  const a = this.globalAlpha;
1421
+ // a pattern with nothing to draw with still covers, clear, so the
1422
+ // counts the winding pass left are zeroed as the cover always does
1423
+ if (!painted) this._setMode(MODE_SOLID, null, SWIZZLE_RGBA);
1305
1424
  this._quad(
1306
1425
  [x, y, x + w, y, x + w, y + h, x, y + h],
1307
1426
  UV_UNIT,
1308
- [a, a, a, a],
1427
+ painted ? [a, a, a, a] : CLEAR,
1309
1428
  ZERO4,
1310
1429
  );
1311
1430
  } else {
@@ -127,5 +127,11 @@
127
127
  "keyboard-shortcuts-inhibit-unstable-v1": [
128
128
  "zwp_keyboard_shortcuts_inhibit_manager_v1",
129
129
  "zwp_keyboard_shortcuts_inhibitor_v1"
130
+ ],
131
+ "xdg-foreign-unstable-v2": [
132
+ "zxdg_exporter_v2",
133
+ "zxdg_importer_v2",
134
+ "zxdg_exported_v2",
135
+ "zxdg_imported_v2"
130
136
  ]
131
137
  }
@@ -0,0 +1 @@
1
+ [{"name":"zxdg_exporter_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"export_toplevel","args":[{"name":"id","type":"new_id","interface":"zxdg_exported_v2"},{"name":"surface","type":"object","interface":"wl_surface"}]}],"events":[],"enums":{"error":{"entries":[{"name":"invalid_surface","value":0}]}}},{"name":"zxdg_importer_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"import_toplevel","args":[{"name":"id","type":"new_id","interface":"zxdg_imported_v2"},{"name":"handle","type":"string"}]}],"events":[],"enums":{}},{"name":"zxdg_exported_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]}],"events":[{"name":"handle","args":[{"name":"handle","type":"string"}]}],"enums":{}},{"name":"zxdg_imported_v2","version":1,"requests":[{"name":"destroy","type":"destructor","args":[]},{"name":"set_parent_of","args":[{"name":"surface","type":"object","interface":"wl_surface"}]}],"events":[{"name":"destroyed","args":[]}],"enums":{"error":{"entries":[{"name":"invalid_surface","value":0}]}}}]