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
@@ -984,7 +984,9 @@ function useMenuAccelerators(
984
984
  *
985
985
  * The wrapper is focusable and takes focus when the menu opens, because the
986
986
  * popup is override-redirect and never gets focus itself — the same trick
987
- * `Select` uses to keep arrow keys working while its menu is open.
987
+ * `Select` uses to keep arrow keys working while its menu is open. It is not
988
+ * a Tab stop: it has no role or name, so Tab landing on it would announce
989
+ * nothing, and the controls inside are stops of their own.
988
990
  */
989
991
  export function ContextMenu({
990
992
  items = [],
@@ -1087,7 +1089,10 @@ export function ContextMenu({
1087
1089
  'box',
1088
1090
  {
1089
1091
  ref,
1090
- focusable: true,
1092
+ // focusable by a press and by `focus()` above, but out of the Tab
1093
+ // cycle: a stop here is a nameless "filler" around content whose own
1094
+ // controls are already reachable
1095
+ tabIndex: -1,
1091
1096
  onMouseDown: (ev) => {
1092
1097
  if (ev.button !== 3 && rect) close();
1093
1098
  },
@@ -98,7 +98,7 @@ export function Switch({
98
98
  );
99
99
 
100
100
  // NSSwitch's own pixels, on/off as two cached bezels. The knob slide is
101
- // the fidelity this loses for now (docs/macos.md flags the switch as the
101
+ // the fidelity this loses for now (design/macos.md flags the switch as the
102
102
  // one control worth revisiting if that disappoints); the press keeps its
103
103
  // answer as the wash, since NSSwitch renders no pressed state offscreen.
104
104
  if (nativeControls) {
@@ -1,4 +1,4 @@
1
- // The native half of the widget set (docs/macos.md §Native controls).
1
+ // The native half of the widget set (design/macos.md §Native controls).
2
2
  //
3
3
  // House rule: **default native, seam out.** On a backend that renders the
4
4
  // platform's own control bezels — today the Cocoa backend — the core
@@ -8,7 +8,8 @@
8
8
  //
9
9
  // `appearance` the light/dark, accent, contrast and reduced-motion ladder
10
10
  // (src/appearance.js) — the settings portal, then macOS, then
11
- // XSETTINGS
11
+ // XSETTINGS — and, on the Wayland backend, the interaction
12
+ // settings read off the same portal (src/desktopsettings.js)
12
13
  // `a11y` the AT-SPI bridge, started from `createRoot()`
13
14
  // (src/a11y.js, docs/accessibility.md)
14
15
  // `globalMenu` a `MenuBar` handing its menu to the panel instead of drawing
@@ -11,17 +11,30 @@
11
11
  //
12
12
  // `appearance.js` climbs three sources for colour scheme and accent, because
13
13
  // those are answered by three different things and the portal is the modern
14
- // one. Nothing here has a portal equivalent: `org.freedesktop.appearance` has
15
- // four keys and none of them is a timing. XSETTINGS is the whole story, and
16
- // where there is no settings daemon — a bare `startx`, most window managers,
17
- // XQuartz — the defaults below stand.
14
+ // one. Nothing here has a *standard* portal equivalent:
15
+ // `org.freedesktop.appearance` has four keys and none of them is a timing.
16
+ // On X11, XSETTINGS is the whole story, and where there is no settings
17
+ // daemon — a bare `startx`, most window managers, XQuartz — the defaults
18
+ // below stand.
18
19
  //
19
- // So this reads the map `xsettings.js` already maintains, rather than
20
- // standing up a source of its own. The one exception is macOS, which has no
21
- // XSETTINGS and answers exactly one of these — "reduce motion", through
22
- // `NSWorkspace` — so the Cocoa app exposes that behind a two-method seam
23
- // (`accessibilityDisplayOptions`, `watchAccessibilityDisplay`) and this
24
- // module reads it the way it reads the map: synchronously, cached, live.
20
+ // So on X11 this reads the map `xsettings.js` already maintains, rather than
21
+ // standing up a source of its own. There are two exceptions, one per backend
22
+ // that has no X server to ask:
23
+ //
24
+ // - **macOS** answers exactly one of these — "reduce motion", through
25
+ // `NSWorkspace` — so the Cocoa app exposes that behind a two-method seam
26
+ // (`accessibilityDisplayOptions`, `watchAccessibilityDisplay`) and this
27
+ // module reads it the way it reads the map: synchronously, cached, live.
28
+ //
29
+ // - **The Wayland backend** has no XSETTINGS either, but the settings portal
30
+ // is not limited to `org.freedesktop.appearance`: the GNOME and GTK portal
31
+ // backends pass whole GSettings schemas through it, and
32
+ // `org.gnome.desktop.interface` and `org.gnome.desktop.peripherals.mouse`
33
+ // are exactly what gsd-xsettings translates into the XSETTINGS keys this
34
+ // reads on X11. Same numbers, one hop closer to the source. Read once at
35
+ // startup and again on `SettingChanged`; a desktop whose portal does not
36
+ // carry them (KDE, a bare compositor) keeps the defaults, as a bare
37
+ // `startx` does.
25
38
  //
26
39
  // ## Two of these are read synchronously, which is what shapes the module
27
40
  //
@@ -34,8 +47,26 @@
34
47
  // desktop's cadence from the next focus onward; that is the one seam, and it
35
48
  // is worth more than the round trips on every app's startup path.
36
49
 
50
+ import { sessionBus } from './bus.js';
51
+ import { desktopIntegrationEnabled } from './desktopintegration.js';
52
+ import { PORTAL_NAME, PORTAL_PATH } from './portal.js';
37
53
  import { beginXSettings, watchXSettings, xsettings } from './xsettings.js';
38
54
 
55
+ const SETTINGS_IFACE = 'org.freedesktop.portal.Settings';
56
+ const INTERFACE_NS = 'org.gnome.desktop.interface';
57
+ const MOUSE_NS = 'org.gnome.desktop.peripherals.mouse';
58
+
59
+ /** The keys a `SettingChanged` has to name for a re-read to be worth it —
60
+ * `org.gnome.desktop.interface` also carries the clock format, the font
61
+ * and the icon theme, and none of those is a reason to re-render. */
62
+ const WATCHED = new Set([
63
+ `${INTERFACE_NS}/cursor-blink`,
64
+ `${INTERFACE_NS}/cursor-blink-time`,
65
+ `${INTERFACE_NS}/enable-animations`,
66
+ `${MOUSE_NS}/double-click`,
67
+ `${MOUSE_NS}/drag-threshold`,
68
+ ]);
69
+
39
70
  /**
40
71
  * What is true with no settings daemon to ask.
41
72
  *
@@ -71,24 +102,15 @@ const sessions = new WeakMap();
71
102
  * export a key with a string value or a nonsense one, and a caret with a
72
103
  * blink period of `'fast'` would never come back on. */
73
104
  function positive(map, key) {
74
- const value = map?.get(key);
105
+ return positiveValue(map?.get(key));
106
+ }
107
+
108
+ function positiveValue(value) {
75
109
  return typeof value === 'number' && Number.isFinite(value) && value > 0
76
110
  ? value
77
111
  : undefined;
78
112
  }
79
113
 
80
- /**
81
- * An XSETTINGS map → the settings. Pure, and exported for the test: the
82
- * in-process X server has no settings daemon, so this is the part of the
83
- * path worth pinning without one.
84
- *
85
- * **`Net/CursorBlinkTime` is a full cycle**, on and off together — that is
86
- * what GTK means by `gtk-cursor-blink-time` and what every daemon writes —
87
- * and `caretBlinkMs` is how long the caret stays in *each* state, because
88
- * that is the number that goes into a timer. Halving it is the whole
89
- * conversion, and forgetting to is a caret that blinks at half speed and
90
- * looks broken rather than wrong.
91
- */
92
114
  /**
93
115
  * macOS's accessibility display options → the settings. Only `animations`
94
116
  * comes from the system there: it is the one of these macOS publishes at all
@@ -105,6 +127,18 @@ export function fromMacOS(options) {
105
127
  });
106
128
  }
107
129
 
130
+ /**
131
+ * An XSETTINGS map → the settings. Pure, and exported for the test: the
132
+ * in-process X server has no settings daemon, so this is the part of the
133
+ * path worth pinning without one.
134
+ *
135
+ * **`Net/CursorBlinkTime` is a full cycle**, on and off together — that is
136
+ * what GTK means by `gtk-cursor-blink-time` and what every daemon writes —
137
+ * and `caretBlinkMs` is how long the caret stays in *each* state, because
138
+ * that is the number that goes into a timer. Halving it is the whole
139
+ * conversion, and forgetting to is a caret that blinks at half speed and
140
+ * looks broken rather than wrong.
141
+ */
108
142
  export function fromXSettings(map) {
109
143
  if (!map) return DEFAULTS;
110
144
  const cycle = positive(map, 'Net/CursorBlinkTime');
@@ -134,6 +168,39 @@ export function fromXSettings(map) {
134
168
  });
135
169
  }
136
170
 
171
+ /**
172
+ * The settings portal's GNOME namespaces → the settings. Pure, and exported
173
+ * for the test. `all` is a `ReadAll` reply, namespace → key → value.
174
+ *
175
+ * The same keys gsd-xsettings turns into the XSETTINGS ones above, so the
176
+ * same rules: `cursor-blink-time` is a full cycle and is halved, only an
177
+ * explicit `false` turns blinking or animation off, and a value of the wrong
178
+ * type is no answer. GNOME has no double-click *distance*; GTK's is a
179
+ * constant there, so the default stands.
180
+ *
181
+ * `null` when neither namespace came back — a portal that does not pass
182
+ * GSettings through has not answered, and the source stays null.
183
+ */
184
+ export function fromGnomePortal(all) {
185
+ const iface = all?.[INTERFACE_NS];
186
+ const mouse = all?.[MOUSE_NS];
187
+ if (!iface && !mouse) return null;
188
+ const cycle = positiveValue(iface?.['cursor-blink-time']);
189
+ return Object.freeze({
190
+ ...DEFAULTS,
191
+ caretBlink: iface?.['cursor-blink'] !== false,
192
+ caretBlinkMs: cycle
193
+ ? Math.max(1, Math.round(cycle / 2))
194
+ : DEFAULTS.caretBlinkMs,
195
+ doubleClickMs:
196
+ positiveValue(mouse?.['double-click']) ?? DEFAULTS.doubleClickMs,
197
+ dragThreshold:
198
+ positiveValue(mouse?.['drag-threshold']) ?? DEFAULTS.dragThreshold,
199
+ animations: iface?.['enable-animations'] !== false,
200
+ source: 'portal',
201
+ });
202
+ }
203
+
137
204
  /**
138
205
  * The settings for a connection, **synchronously**. Always a complete answer:
139
206
  * the defaults before the daemon has been read, and on a display with none.
@@ -148,7 +215,9 @@ export function desktopSettings(app) {
148
215
  if (!session.snapshot) {
149
216
  session.snapshot = session.macos
150
217
  ? fromMacOS(app.accessibilityDisplayOptions())
151
- : fromXSettings(xsettings(app));
218
+ : session.wayland
219
+ ? (fromGnomePortal(session.portal) ?? DEFAULTS)
220
+ : fromXSettings(xsettings(app));
152
221
  }
153
222
  return session.snapshot;
154
223
  }
@@ -173,6 +242,20 @@ export function beginDesktopSettings(app) {
173
242
  });
174
243
  return;
175
244
  }
245
+ // The Wayland backend: no X server, so no XSETTINGS — the portal instead,
246
+ // behind the same switch as the appearance ladder that reads the portal
247
+ // too. `createRoot({ desktop: false })` keeps the defaults, which is what a
248
+ // test or an embedder that asked for determinism wants.
249
+ if (app?.backend === 'wayland') {
250
+ session.wayland = true;
251
+ if (desktopIntegrationEnabled('appearance')) {
252
+ portalSettings(app, session).catch(() => {
253
+ // no bus, no portal, a portal without the GNOME namespaces: the
254
+ // defaults stand, which is what they are for
255
+ });
256
+ }
257
+ return;
258
+ }
176
259
  beginXSettings(app).then(
177
260
  () => {
178
261
  if (!sessions.has(app)) return;
@@ -190,6 +273,92 @@ export function beginDesktopSettings(app) {
190
273
  );
191
274
  }
192
275
 
276
+ /**
277
+ * Read the GNOME namespaces off the settings portal, and again whenever one
278
+ * of the keys this cares about changes.
279
+ *
280
+ * The order is `appearance.js`'s portal rung, for the same reasons: the
281
+ * match rule and the handler go in **before** the first read, so a change
282
+ * that lands while it is in flight is not lost; and reads are sequenced, so
283
+ * a slow reply never publishes over a newer one. The bus ref is released as
284
+ * soon as the first read settles — the subscription outlives it, and holding
285
+ * it would keep the process alive for as long as the app cared how fast its
286
+ * caret blinks.
287
+ */
288
+ async function portalSettings(app, session) {
289
+ const ref = await sessionBus();
290
+ if (!ref) return;
291
+ const { bus } = ref;
292
+ let sub = null;
293
+ let handler = null;
294
+ const key = bus.mangle(PORTAL_PATH, SETTINGS_IFACE, 'SettingChanged');
295
+ const stop = () => {
296
+ if (handler) bus.signals.removeListener(key, handler);
297
+ handler = null;
298
+ sub?.remove().catch(() => {});
299
+ sub = null;
300
+ };
301
+ try {
302
+ sub = await bus.watch(
303
+ `type='signal',sender='${PORTAL_NAME}',` +
304
+ `interface='${SETTINGS_IFACE}',member='SettingChanged'`,
305
+ );
306
+
307
+ let started = 0;
308
+ let published = 0;
309
+ const refresh = async () => {
310
+ const mine = ++started;
311
+ const all = await bus.invoke(
312
+ {
313
+ destination: PORTAL_NAME,
314
+ path: PORTAL_PATH,
315
+ interface: SETTINGS_IFACE,
316
+ member: 'ReadAll',
317
+ signature: 'as',
318
+ body: [[INTERFACE_NS, MOUSE_NS]],
319
+ },
320
+ { timeout: 5_000 },
321
+ );
322
+ if (mine <= published || sessions.get(app) !== session) return;
323
+ published = mine;
324
+ const next = fromGnomePortal(all);
325
+ if (!next) return;
326
+ const before = desktopSettings(app);
327
+ session.portal = all;
328
+ session.snapshot = null;
329
+ // A re-read that changed nothing this reads notifies nobody: the hook
330
+ // re-renders on identity, and so would every subscriber.
331
+ if (!sameSettings(before, desktopSettings(app))) notify(session);
332
+ else session.snapshot = before;
333
+ };
334
+
335
+ handler = ([namespace, name]) => {
336
+ if (!WATCHED.has(`${namespace}/${name}`)) return;
337
+ refresh().catch(() => {
338
+ // the portal went away mid-session; the last answer stands
339
+ });
340
+ };
341
+ bus.signals.on(key, handler);
342
+
343
+ // Torn down while the bus was connecting: undo, and publish nothing.
344
+ if (sessions.get(app) !== session) return stop();
345
+ session.stop = stop;
346
+ await refresh();
347
+ } catch (err) {
348
+ stop();
349
+ throw err;
350
+ } finally {
351
+ await ref.release();
352
+ }
353
+ }
354
+
355
+ function sameSettings(a, b) {
356
+ for (const key of Object.keys(DEFAULTS)) {
357
+ if (a[key] !== b[key]) return false;
358
+ }
359
+ return true;
360
+ }
361
+
193
362
  function notify(session) {
194
363
  for (const fn of [...session.listeners]) {
195
364
  try {
package/src/dnd.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // XDND drag and drop, the drop-target half: react-x11 windows accept drags
2
2
  // from other X11 applications (file managers, editors, browsers).
3
3
  //
4
- // Design: docs/architecture/drag-and-drop.md; protocol: XDND version 5.
4
+ // Design: design/architecture/drag-and-drop.md; protocol: XDND version 5.
5
5
  // The advertisement (`XdndAware`) is a 4-byte property written
6
6
  // unconditionally on every top-level window at realize time — it creates no
7
7
  // X windows, so it is orthogonal to the windowed/windowless split.
@@ -1080,7 +1080,7 @@ export class DropSession {
1080
1080
  // ---------------------------------------------------------------------------
1081
1081
  // The drag source.
1082
1082
  //
1083
- // One drag session, two transports (docs/architecture/drag-and-drop.md §6.3):
1083
+ // One drag session, two transports (design/architecture/drag-and-drop.md §6.3):
1084
1084
  // while the pointer is over this process's own windows the drag is local —
1085
1085
  // no wire, live JS payloads, the DropSession above delivering to the same
1086
1086
  // handlers with `e.source === 'internal'`. The moment it leaves them, the
package/src/filedialog.js CHANGED
@@ -46,14 +46,14 @@ import {
46
46
  RESPONSE_CANCELLED,
47
47
  RESPONSE_OK,
48
48
  hasService,
49
- parentWindowHandle,
50
49
  pathBytes,
50
+ portalParentFor,
51
51
  portalRequest,
52
52
  variant,
53
53
  } from './portal.js';
54
54
  import { sessionBus } from './bus.js';
55
55
  import { liveApps } from './trace-registry.js';
56
- import { windowIdOf, windowOf } from './windowid.js';
56
+ import { windowOf } from './windowid.js';
57
57
 
58
58
  const FILE_CHOOSER = 'org.freedesktop.portal.FileChooser';
59
59
 
@@ -149,7 +149,7 @@ async function portalDialog(kind, opts, ref) {
149
149
  const { response, results } = await portalRequest(ref, {
150
150
  iface: FILE_CHOOSER,
151
151
  member,
152
- parentWindow: parentWindowHandle(windowIdOf(opts.parentWindow)),
152
+ parentWindow: await portalParentFor(opts.parentWindow),
153
153
  title: opts.title ?? defaultTitle(kind),
154
154
  options: await portalOptions(opts, {
155
155
  save: kind === 'save',
package/src/glnodes.js CHANGED
@@ -95,7 +95,7 @@ export function glxConfig(app, spec) {
95
95
  process.platform === 'win32'
96
96
  ? 'react-x11: <glarea> is not built on the win32 backend yet — it ' +
97
97
  'needs ANGLE (EGL and GLES over Direct3D 11), which ' +
98
- 'docs/windows.md plans as an optional dependency the way ' +
98
+ 'design/windows.md plans as an optional dependency the way ' +
99
99
  'x11-dri is on X11. Everything else on this backend works ' +
100
100
  'without it; for GL content today, use the X11 backend.'
101
101
  : 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLConfig)',
package/src/grid.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // CSS grid, built in (docs/styling.md "Grid"; the design record is
2
- // docs/architecture/grid-layout.md). A box with `display: 'grid'` — or
2
+ // design/architecture/grid-layout.md). A box with `display: 'grid'` — or
3
3
  // `layout: 'grid'`, the same request under the name every layout goes by —
4
4
  // arranges its children by CSS Grid's placement and track sizing algorithms
5
5
  // (css-grid-2 §8.5 and §12), inside the pass that lays the window out. It is
@@ -32,6 +32,18 @@
32
32
  // it answers atoms that have to be interned back one round trip each, and
33
33
  // what it returns is a description like `English (US)` where an indicator in
34
34
  // a status bar wants `us`.
35
+ //
36
+ // ## A backend with no X server
37
+ //
38
+ // The native Wayland backend has no XKB extension to ask, but its seat holds
39
+ // the same three answers: the locked modifiers and the group from
40
+ // `wl_keyboard.modifiers`, and the layout codes from the name of the keymap
41
+ // the compositor sent (src/wayland/xkb.js, `layoutsFromKeymap`). It hands
42
+ // them over through a two-method seam — `keyboardState()` and
43
+ // `watchKeyboardState(fn)` — and this module publishes what it says the way
44
+ // it publishes a StateNotify. The one difference is the protocol's:
45
+ // a compositor tells only the focused client, so the locks are as of the last
46
+ // time this app had the keyboard.
35
47
 
36
48
  import { requireExtension } from './extensions.js';
37
49
 
@@ -71,6 +83,7 @@ class KeyboardSession {
71
83
  this.armed = false;
72
84
  this.stopped = false;
73
85
  this._handler = null;
86
+ this._unwatch = null;
74
87
  }
75
88
 
76
89
  publish(values) {
@@ -106,6 +119,8 @@ class KeyboardSession {
106
119
 
107
120
  stop() {
108
121
  this.stopped = true;
122
+ this._unwatch?.();
123
+ this._unwatch = null;
109
124
  if (this._handler && this.app?.X?.off) {
110
125
  try {
111
126
  this.app.X.off('event', this._handler);
@@ -157,6 +172,19 @@ async function arm(session) {
157
172
  if (session.armed) return;
158
173
  session.armed = true;
159
174
  const app = session.app;
175
+
176
+ // A backend that answers for itself (the Wayland seat): subscribe, then
177
+ // read, for the same reason the XKB path below does.
178
+ if (typeof app?.watchKeyboardState === 'function') {
179
+ const read = () => {
180
+ const state = !session.stopped && app.keyboardState?.();
181
+ if (state) session.publish(state);
182
+ };
183
+ session._unwatch = app.watchKeyboardState(read);
184
+ read();
185
+ return;
186
+ }
187
+
160
188
  const xkb = await requireExtension(app, 'xkb');
161
189
  if (!xkb || session.stopped) return;
162
190
 
package/src/layouts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Layout algorithms and positions from outside the renderer — the two seams
2
- // docs/architecture/custom-layout.md is the design record for.
2
+ // design/architecture/custom-layout.md is the design record for.
3
3
  //
4
4
  // A **layout** arranges a box's children. It is asked how big the box is for
5
5
  // the room on offer, and then where each child goes in the room the box got.
package/src/node.d.ts CHANGED
@@ -39,12 +39,13 @@ export interface ImageDataLike {
39
39
  }
40
40
 
41
41
  /**
42
- * The 2d context a node paints into — the canvas-shaped subset **both**
43
- * backends implement: ntk's `RenderingContext2D` over XRender on X11, and
44
- * `BackendContext2D` over CoreGraphics on macOS. Declared as the contract an
45
- * element may rely on rather than as either class: what is here is on
46
- * both, and a member one backend has and the other does not is optional
47
- * here or absent. Coordinates are device pixels in the owning window's
42
+ * The 2d context a node paints into — the canvas-shaped subset **every**
43
+ * backend implements: ntk's `RenderingContext2D` over XRender on X11, the
44
+ * Wayland backend's GLES rasterizer, and `BackendContext2D` over CoreGraphics
45
+ * on macOS and Direct2D on Windows. Declared as the contract an element may
46
+ * rely on rather than as any one class: what is here is on all of them, and
47
+ * a member some backends have and others do not is optional here or
48
+ * absent. Coordinates are device pixels in the owning window's
48
49
  * space — `abs`, `contentBox()` — unless a transform says otherwise.
49
50
  * Anything ntk documents beyond this is reachable at runtime and is
50
51
  * ntk's to change.
@@ -67,10 +68,9 @@ export interface Context2D {
67
68
  shadowOffsetY: number;
68
69
  shadowColor: string;
69
70
  /**
70
- * X11 only: XRender's compositing operator — `'source-over'` (the
71
- * default), `'copy'`, and the rest ntk documents. Absent on the cocoa
72
- * backend, whose bridge has no blend-mode verb yet; test for it with
73
- * `'globalCompositeOperation' in ctx`.
71
+ * The compositing operator — `'source-over'` (the default), `'copy'`,
72
+ * and the rest ntk documents on X11. A native context (macOS, Windows)
73
+ * takes only the modes its bridge draws: assign and read back to know.
74
74
  */
75
75
  globalCompositeOperation?: string;
76
76
  /**
@@ -310,7 +310,7 @@ export class NodeAnimation {
310
310
  // loop-stop rule and `sameAnimation` all see it — but it contributes no
311
311
  // value to the style, is skipped by the tick, and keeps the node out of the
312
312
  // window's animating set. The X11 path has none of these hooks and is
313
- // byte-identical (docs/architecture/animation.md §4).
313
+ // byte-identical (design/architecture/animation.md §4).
314
314
 
315
315
  _offload(prop, entry) {
316
316
  const wnd = this.root?.window;
@@ -361,7 +361,7 @@ export class WindowInvalidate {
361
361
  }
362
362
  // A retained presenter keeps a per-node diff instead of damage rects,
363
363
  // and this is the one channel every change already announces itself on
364
- // (docs/macos.md §"One renderer, two presenters"). Feature-detected: an
364
+ // (design/macos.md §"One renderer, two presenters"). Feature-detected: an
365
365
  // ntk window has no ear here and the X11 path is byte-identical. A
366
366
  // presenter that answers `true` has taken the claim onto a layer of its
367
367
  // own (src/cocoa/promotion.js): the bitmap owes nothing for it, and the
@@ -2,7 +2,7 @@
2
2
  // in layouts.js arranges its children itself. Each child is a yoga root of
3
3
  // its own, and the host is a measured leaf in its parent — so what the
4
4
  // algorithm decides is on screen in the frame that asked.
5
- // docs/architecture/custom-layout.md is the design record.
5
+ // design/architecture/custom-layout.md is the design record.
6
6
 
7
7
  import {
8
8
  createLayoutNode,
@@ -1,6 +1,6 @@
1
1
  // <video>: frames an application decodes (`frames`, a VideoFrames sink), or a
2
2
  // file or URL the platform plays (`src`, where there is a player), fitted
3
- // into the node's box (docs/architecture/video.md §6–7).
3
+ // into the node's box (design/architecture/video.md §6–7).
4
4
  //
5
5
  // A drawn node like `<image>`: it joins yoga, it is in the paint order and it
6
6
  // is hit like anything else — which is the whole point. A video in a form
@@ -18,9 +18,10 @@
18
18
  // - **drawn** — everywhere else, and in every frame the presenter declines
19
19
  // the part (something is painted over it, it fades, a rounded card does
20
20
  // not hold it): the newest frame is copied into a surface of the node's
21
- // own at paint, natively where the backend can (`app.writeVideoFrame`) and
22
- // through `frameToRGBA` where it cannot, and composited at the picture's
23
- // rect like an `<image>` whose pixels changed. A push claims the picture.
21
+ // own at paint, natively where the backend can (`writeVideoFrame`,
22
+ // src/videobackend.js) and through `frameToRGBA` where it cannot, and
23
+ // composited at the picture's rect like an `<image>` whose pixels
24
+ // changed. A push claims the picture.
24
25
  //
25
26
  // `src` needs a platform player, which this backend has when
26
27
  // `useSupports('mediaPlayback')` says so — the app's `createPlayer`, on macOS
@@ -41,6 +42,7 @@ import {
41
42
  } from '../imagesource.js';
42
43
  import { canPlayMedia, NoMediaPlaybackError } from '../mediaplayback.js';
43
44
  import { Surface } from '../offscreen.js';
45
+ import { videoBackendOf } from '../videobackend.js';
44
46
  import { frameToRGBA, isVideoFrames } from '../videoframes.js';
45
47
  import { fitRect, objectFitOf } from './fit.js';
46
48
  import { intrinsicSize } from './layout.js';
@@ -592,10 +594,18 @@ export class VideoNode extends Node {
592
594
  }
593
595
 
594
596
  _upload(surface, sink, frame) {
595
- // natively where the backend converts in its own code (the Cocoa
596
- // bridge's writeVideoSurface: a copy for BGRA, VideoToolbox for YCbCr,
597
- // colour-matched to what a layer shows)
598
- if (this.app?.writeVideoFrame?.(surface, sink, frame)) return;
597
+ // natively where the backend converts in its own code: the Cocoa
598
+ // bridge's writeVideoSurface (a copy for BGRA, VideoToolbox for YCbCr,
599
+ // colour-matched to what a layer shows), the Wayland context's shader,
600
+ // an Xv port on X11 (src/videobackend.js)
601
+ if (
602
+ videoBackendOf(this.app)?.writeVideoFrame?.(surface, sink, frame, this)
603
+ ) {
604
+ // a frame converted in JavaScript earlier — the first, on X11, before
605
+ // the window was mapped — left a buffer the native path does not need
606
+ this._rgba = null;
607
+ return;
608
+ }
599
609
  const w = sink.width;
600
610
  const h = sink.height;
601
611
  if (this._rgba?.width !== w || this._rgba?.height !== h) {
@@ -632,6 +642,7 @@ export class VideoNode extends Node {
632
642
  this._surface = null;
633
643
  this._rgba = null;
634
644
  this._drawn = -1;
645
+ if (surface) videoBackendOf(this.app)?.releaseVideoFrame?.(surface);
635
646
  try {
636
647
  surface?.destroy();
637
648
  } catch {
@@ -1,4 +1,4 @@
1
- // The window's half of XDND (docs/architecture/drag-and-drop.md): the drop
1
+ // The window's half of XDND (design/architecture/drag-and-drop.md): the drop
2
2
  // targets registered under it and the types they accept.
3
3
 
4
4
  import {
package/src/pacing.js CHANGED
@@ -49,7 +49,7 @@
49
49
  // Off by default: `'display'` is the built-in, and paints every frame the
50
50
  // clock gives, after React's own batching — a UI answers its input as
51
51
  // quickly as it can unless it says otherwise. `'adaptive'` is what a window
52
- // that streams asks for. docs/architecture/frame-pacing.md is the account.
52
+ // that streams asks for. design/architecture/frame-pacing.md is the account.
53
53
 
54
54
  export const DEFAULT_FRAME_RATE = 'display';
55
55
 
package/src/portal.js CHANGED
@@ -29,6 +29,7 @@
29
29
  // re-solve.
30
30
 
31
31
  import { loadTransport, sessionBus } from './bus.js';
32
+ import { windowIdOf, windowOf } from './windowid.js';
32
33
 
33
34
  export const PORTAL_NAME = 'org.freedesktop.portal.Desktop';
34
35
  export const PORTAL_PATH = '/org/freedesktop/portal/desktop';
@@ -439,6 +440,36 @@ export function parentWindowHandle(xid) {
439
440
  return typeof xid === 'number' && xid > 0 ? `x11:${xid.toString(16)}` : '';
440
441
  }
441
442
 
443
+ /**
444
+ * The `parent_window` handle for whatever names a window — a ref, a node, a
445
+ * backend window, an XID — on whichever backend it belongs to.
446
+ *
447
+ * - **X11**: `x11:<xid>`, as `parentWindowHandle` spells it.
448
+ * - **Wayland**: `wayland:<handle>`, from an xdg-foreign export of the
449
+ * toplevel (src/wayland/app.js, `exportHandle`) — what the portal needs to
450
+ * make the dialog modal to the window and stack it there. `''` where the
451
+ * compositor has no xdg-foreign, or the window is a popup; never an `x11:`
452
+ * handle, because a Wayland window's id names no X window, and on a desktop
453
+ * running Xwayland it could name somebody else's.
454
+ *
455
+ * `app` is the fallback for a target that resolves to no window object — a
456
+ * bare number on Wayland is that app's id for one of its own windows.
457
+ *
458
+ * @param {any} target
459
+ * @param {any} [app]
460
+ * @returns {Promise<string>}
461
+ */
462
+ export async function portalParentFor(target, app = null) {
463
+ let win = windowOf(target);
464
+ const owner = win?.app ?? app;
465
+ if (owner?.backend !== 'wayland') {
466
+ return parentWindowHandle(windowIdOf(target));
467
+ }
468
+ if (!win && typeof target === 'number') win = owner.windowById?.(target);
469
+ const handle = win ? await owner.exportHandle?.(win) : null;
470
+ return handle ? `wayland:${handle}` : '';
471
+ }
472
+
442
473
  /**
443
474
  * `Variant`, from the one copy of the transport the bus layer loaded.
444
475
  *
@@ -1,6 +1,7 @@
1
1
  // react-x11/refresh — the runtime half of state-preserving hot reload
2
2
  // (Fast Refresh). The loader half is ./loader.js; `react-x11/refresh/register`
3
- // wires both up for `node --import`. See docs/ecosystem/dev-tooling.md.
3
+ // wires both up for `node --import`. See docs/ecosystem/dev-tooling.md, and
4
+ // design/ecosystem/dev-tooling.md §"How it reloads" for the loader's rules.
4
5
  //
5
6
  // Import order is what this module exists to own:
6
7
  //