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
package/docs/frame.md ADDED
@@ -0,0 +1,436 @@
1
+ # Panes in their own process: `<Frame>`
2
+
3
+ ```jsx
4
+ import { Frame } from 'react-x11';
5
+
6
+ <Frame
7
+ src={new URL('./charts.pane.js', import.meta.url)}
8
+ props={{ rows, range, onPick }}
9
+ style={{ flexGrow: 1, backgroundColor: '$surface' }}
10
+ fallback={({ error, restart }) => <Crashed error={error} onRetry={restart} />}
11
+ />;
12
+ ```
13
+
14
+ `<Frame>` mounts a module of your application in a **process of its own** and
15
+ embeds its output here, laid out like any other child — this package's
16
+ iframe, on X11, Cocoa and win32. On the [Wayland backend](wayland-backend.md)
17
+ a `<Frame>` fails at once: `fallback` renders and no pane is started.
18
+
19
+ - **X11** — the pane renders into a real window of its own, which the host
20
+ embeds over [`<foreign>`](embedding.md).
21
+ - **Cocoa** — the pane has no `NSWindow` and runs no AppKit: it paints into
22
+ shared `IOSurface`s the host shows in a layer of its window, and every other
23
+ window it makes (a menu, a dropdown's sheet, a tooltip, a dialog) the host
24
+ shows in an `NSWindow` of its own.
25
+ - **Windows** — the same, with a DirectComposition surface; the pane has no
26
+ HWND.
27
+
28
+ The host owns layout and hit testing, the pane its drawing: **CPU
29
+ offloading, not isolation**. What the process boundary buys:
30
+
31
+ - **Parallelism.** The pane's reconciliation, layout, text shaping and
32
+ painting run on another core; a pane that grinds for 400ms does not delay
33
+ the shell's input (`npm run examples:frame` shows it with a ticker).
34
+ - **Crash isolation.** An uncaught throw, a native-module crash, an OOM: the
35
+ pane dies, `fallback` renders, `restart()` respawns.
36
+ - **Leak isolation.** A leaking pane is a killable leak, bounded by the heap
37
+ its `execArgv` gives it, and its GC pauses are its own.
38
+
39
+ **It is not a security boundary** ([security.md](security.md)). On X11 the
40
+ pane holds a full-privilege connection to the same X server — it can read
41
+ keys, take screenshots, move windows — and on Cocoa it is an ordinary child
42
+ process with your file system and your network. `<Frame>` contains a pane's
43
+ _failures_, not its intentions.
44
+
45
+ | prop | |
46
+ | --------------------- | ------------------------------------------------------------------------ |
47
+ | `src` | the pane module: a `URL` or an absolute path |
48
+ | `props` | data for the pane; functions in it become stubs |
49
+ | `style` | the pane's rect; `backgroundColor` shows while no pane frame does |
50
+ | `fallback` | element or `({ error, restart }) => …` for a pane that died or never ran |
51
+ | `bridge` | bridged contexts: `true` (default), `false`, or a list of keys |
52
+ | `display` | X display for the pane; default the inherited `$DISPLAY` |
53
+ | `execArgv`, `env` | flags and environment for the pane's process |
54
+ | `watchdog` | ms unanswered before the pane counts as stuck; 15000, or `false` |
55
+ | `focusable` | whether the pane joins the tab order, like any `<foreign>` |
56
+ | `onStarted`, `onExit` | the pane mounted; its process ended |
57
+ | `transport` | replaces the fork |
58
+ | `ref` | `{ restart(), pid }` |
59
+
60
+ Design notes (the display boundary per backend, and how a Cocoa pane's
61
+ frames, windows and videos travel): [design/frame.md](../design/frame.md).
62
+
63
+ ## The pane module
64
+
65
+ `src` names a module whose **default export is the pane's root component**:
66
+
67
+ ```jsx
68
+ // charts.pane.js
69
+ export default function ChartsPane({ rows, range, onPick }) {
70
+ return <box style={{ flexGrow: 1 }}>…</box>;
71
+ }
72
+ ```
73
+
74
+ Pass `src` as `new URL('./charts.pane.js', import.meta.url)` or an absolute
75
+ path — a relative string is refused, because it would resolve against
76
+ react-x11's own files rather than yours.
77
+
78
+ A pane module is an ordinary component module. The same file can run inline
79
+ (`import ChartsPane from './charts.pane.js'` and render it), be its own
80
+ application, or be a pane — `isFramed()` answers which, and the examples'
81
+ autorun guard is where to ask:
82
+
83
+ ```js
84
+ import { isFramed } from 'react-x11';
85
+
86
+ if (!isFramed() && !process.env.REACT_X11_NO_AUTORUN) {
87
+ const root = await createRoot();
88
+ root.render(<ChartsPane {...defaults} />);
89
+ }
90
+ ```
91
+
92
+ The pane process inherits the parent's `execArgv`, which carries a dev loader
93
+ (tsx, the refresh loader) into it during development; a bundled application
94
+ forks plain JavaScript and needs none. It connects to the display the host
95
+ names in `display`, or `$DISPLAY`. What else it is started with is the
96
+ `<Frame>`'s to say — see
97
+ [The pane process](#the-pane-process-flags-and-environment).
98
+
99
+ On macOS:
100
+
101
+ - A pane stays on its process's main thread, where an app moves onto a
102
+ worker ([macos.md](macos.md#js-on-a-worker-a-ui-thread-of-the-bridges-own)):
103
+ it has no AppKit for that thread to keep, and its IPC channel is that
104
+ thread's (a worker has no `process.send`). That holds under
105
+ `--import react-x11/cocoa-main` too, which a pane inherits from its host.
106
+ - A pane launches with AppKit's `prohibited` activation policy: no Dock tile,
107
+ no ⌘-Tab entry, never the active app — its host's window has the focus and
108
+ forwards what the pane is owed. A root that names `cocoa.activationPolicy`
109
+ itself still has the one it names.
110
+
111
+ ## Props: a bag of data, and stubs for the functions in it
112
+
113
+ `props` crosses by **structured clone** — Dates, Maps, TypedArrays and
114
+ cycles survive; component instances, refs and React elements do not. One
115
+ message goes out per parent commit that changed the bag (a shallow
116
+ `Object.is` pass decides "changed", so a parent re-rendering without
117
+ touching the pane's inputs sends nothing), and props and bridged context
118
+ travel in the **same message**, so a theme flip and the state change that
119
+ caused it land in the pane as one commit rather than a torn pair.
120
+
121
+ Functions anywhere in the bag become **fire-and-forget stubs** on the pane
122
+ side: calling one sends its arguments back and returns `undefined`. Functions
123
+ inside the arguments are dropped and the data around them kept, so a pane can
124
+ call `onPick(item, ev)` the way it would call any handler, though `ev` is
125
+ full of methods.
126
+
127
+ Callback identity follows function identity: a handler the parent keeps
128
+ stable (`useCallback`) keeps its id across updates, and an id from the
129
+ previous update still lands — one update of grace, which covers a click
130
+ racing a props change. A stub stashed outside the props flow and called two
131
+ updates later is dropped with a warning, not delivered to the wrong function.
132
+
133
+ Two rules of thumb that keep the wire small:
134
+
135
+ - **The pane owns its data.** Props are for queries, ids, ranges — the pane
136
+ fetches its own rows, the way an iframe fetches its own resources.
137
+ Shipping a dataset through `props` on every commit works, and is the
138
+ slowest possible way to have the pane know it.
139
+ - **The pane owns its animation.** An update is an IPC hop plus a pane
140
+ commit: the right price for "show this now", the wrong one for driving a
141
+ spinner at 60fps from the host.
142
+
143
+ ## Context: bridged explicitly, theme by default
144
+
145
+ React context cannot cross a process boundary, and most of it need not:
146
+ appearance, locale, desktop settings and fonts resolve identically on both
147
+ sides. The rule: **bridge what the app defines; let each process resolve what
148
+ the desktop defines.**
149
+
150
+ The theme crosses **by default**: a `<ThemeProvider>` above a `<Frame>` wraps
151
+ the pane's **window** as a real `ThemeProvider` — `useTheme()` and `$token`
152
+ styles alike — from the pane's very first commit, so the pane's background
153
+ and static text follow the app's palette and its toggles, not the pane
154
+ process's desktop. With no provider above the frame, nothing is bridged and
155
+ the pane follows the desktop by itself. A pane's own provider wins below the
156
+ bridged one, as an inner provider always does.
157
+
158
+ For a context of your own, create it with `createFrameContext` in a module
159
+ **both sides import** — which the pane already does, since its components
160
+ read the context from it. The import is what registers the key, so identity
161
+ needs no wiring:
162
+
163
+ ```js
164
+ // contexts.js — imported by the app and by the pane's components
165
+ import { createFrameContext } from 'react-x11';
166
+ export const Session = createFrameContext('session', null);
167
+ ```
168
+
169
+ ```jsx
170
+ // app side: an ordinary provider that also publishes to frames below it
171
+ <Session.Provider value={{ user, workspace }}>
172
+ <Frame src={paneUrl} … />
173
+ </Session.Provider>;
174
+
175
+ // pane side: nothing to wire
176
+ const { user } = Session.use();
177
+ ```
178
+
179
+ In-process it is an ordinary React context. Across a frame the value is
180
+ snapshotted, sent with the props, and recreated as a provider around the
181
+ pane — providers nest in the parent's own order, and a frame inside a pane
182
+ re-bridges automatically, because the pane's providers republish what they
183
+ received.
184
+
185
+ **Bridged values are data.** A value that will not structured-clone is
186
+ dropped with a warning naming the key. That is deliberate: a bridged
187
+ `{ state, dispatch }` would hand live RPC stubs to every pane under the
188
+ provider, invisibly, and a restarted pane would leave it unclear which
189
+ incarnation holds which stub. Dispatchers travel in `props`, where the wiring
190
+ is visible. `serialize`/`revive` options on `createFrameContext` cover a
191
+ value that is data in spirit but a class in shape.
192
+
193
+ `bridge` is the off switch: `bridge={false}` bridges nothing,
194
+ `bridge={['react-x11:theme', 'session']}` is an allowlist.
195
+
196
+ The pane commits an IPC hop after the host, so a theme flip repaints the
197
+ shell a frame before the panes.
198
+
199
+ ## Lifecycle, from both sides
200
+
201
+ The host's view:
202
+
203
+ - **`onStarted({ pid, windowId })`** — the pane mounted; its window is
204
+ being embedded.
205
+ - **`onExit({ code, signal, expected })`** — the pane process ended.
206
+ `expected` is `true` when this side asked (unmount, `restart()`, a `src`
207
+ change) — and the event still fires after the frame unmounted, because
208
+ the exit a graceful close ends in always arrives after the unmount that
209
+ asked for it.
210
+ - **`fallback`** — what the rect shows when the pane crashed or could not
211
+ start: an element, or `({ error, restart }) => …`. `error.phase` says
212
+ where it went wrong: `spawn`, `load` (the commonest — a bad `src`),
213
+ `connect`, `handshake` (version skew), `runtime`, `send`, `embed`,
214
+ `unresponsive` (see [A pane that stops answering](#a-pane-that-stops-answering)),
215
+ or plain `exit`.
216
+ - **`ref`** — `{ restart(), pid }`.
217
+
218
+ The pane's view:
219
+
220
+ - **`useFrameClose(handler)`** — the host is letting the pane go: the
221
+ `<Frame>` unmounted, or the host app is exiting. The handler may return a
222
+ promise and may still call callback props — flushing through one is what
223
+ it is for. The patience is bounded: the host asks by message, escalates to
224
+ SIGTERM at 1.5s, SIGKILL at 4s. Not called on a crash; anything that must
225
+ survive a crash belongs on disk before the close.
226
+ - **`isFramed()`** — see [The pane module](#the-pane-module).
227
+ - A host that dies without asking takes the IPC channel with it, and the
228
+ pane exits on the disconnect — no orphans either way.
229
+
230
+ ## Sizing, stacking, input
231
+
232
+ The embedded pane is a [`<foreign>`](embedding.md), and its rules apply. The
233
+ rect comes from `style` and yoga like any child — there is no content-driven
234
+ sizing across the boundary. On X11 the pane's window is a real X child
235
+ window, so **host-drawn content cannot overlap it**; a HUD or a
236
+ "reconnecting…" banner belongs in a sibling `<popup>`. Pointer events over
237
+ the pane are the pane's without the host ever seeing them; keys follow
238
+ [embedding.md's focus rules](embedding.md#focus-and-who-gets-the-keys) — the
239
+ host's handlers see every key first, and what they do not consume is
240
+ forwarded.
241
+
242
+ On Cocoa and Windows the pane has no window for the pointer to be over: the
243
+ host hit-tests the frame's box and forwards the input. Keys go over, with
244
+ their modifiers, as the box's default action, as `<foreign>` forwards them, so
245
+ a chord the host answered above the frame never reaches the pane. The cursor
246
+ the pane's tree names is the box's default cursor, so a `cursor` in the
247
+ frame's `style` still wins. On X11 the pane's own window carries its cursor.
248
+
249
+ **Tab goes through a pane as through any subtree.** The frame is one stop in
250
+ the host's order, and the pane's own order hangs off it: Tab arriving at the
251
+ frame goes on into the pane at its first stop, or its last for Shift+Tab;
252
+ Tab while the frame has the focus is the pane's; and the pane running off an
253
+ end of its order hands Tab back, and the host goes on from the frame. The
254
+ pane lets go of its focus as it does, so the stop it was at shows no ring
255
+ while the host's next one does. A modal open in the pane keeps Tab going
256
+ round inside it.
257
+
258
+ **A pane is drawn at the host's scale.** On Cocoa and Windows the host tells
259
+ the pane the scale its window is drawn at, and the pane's window, fonts and
260
+ the windows it opens are made at that scale from the first commit — even if
261
+ the screens changed between the host's start and the pane's — and laid out
262
+ again when it changes. A `<box scale>` around the `<Frame>` is not part of
263
+ it: the frame's box grows with the zoom, and the pane lays out at the
264
+ window's scale in the room it was given.
265
+
266
+ **A pane's other windows.** On X11 a `<popup>` in a pane is a real window on
267
+ the pane's own connection. On Cocoa the host shows each window the pane makes
268
+ besides its own — a `<popup>`, a menu, a `<Select>`'s list, a `<Dialog>` — in
269
+ an `NSWindow` of its own:
270
+
271
+ - A popup the pane anchors to one of its nodes is placed against it exactly
272
+ as a window's popup is, and placed again when the frame's box or the window
273
+ moves.
274
+ - A grab is the host's, so a press anywhere in the host's windows reaches the
275
+ menu outside its bounds, which is the dismissal. A `<Dialog>` gets the
276
+ user's moves and resizes and its close button.
277
+ - The host window's focus is sent to the pane as `focus`/`blur`, so a menu
278
+ that closes when its window loses focus (`useDismissOnWindowBlur`) closes
279
+ when the user goes to another application.
280
+ - A pane that exits or unmounts takes every window the host shows for it,
281
+ and their grab.
282
+
283
+ **A pane's videos.** On Cocoa the host plays a pane's `<video src>` for it, so
284
+ `useSupports('mediaPlayback')` is true in a pane whose host can play. A pane's
285
+ video is always drawn — it has no layer to lift it onto — so it costs the copy
286
+ a covered video costs in a window, twice: into a shared ring and out. A host
287
+ over a bridge with no player plays nothing, and the pane refuses `src` as X11
288
+ does. A pane that exits takes its players, and their sound, with it.
289
+
290
+ **A pane resized.** On Cocoa the pane's frame at a new size comes a pane frame
291
+ after the host's window lays out. What shows in between is set by
292
+ `createRoot({ cocoa: { paneStretch } })` or `REACT_X11_COCOA_PANE_STRETCH`:
293
+
294
+ - **`'behind'`** (the default) — while the pane keeps within the window's
295
+ live-resize budget, `createRoot({ cocoa: { resizeWait } })` (50 ms by
296
+ default), its last frame is shown at its size from the top left: cropped on
297
+ an axis the layer shrank on, its last column or row carried across one it
298
+ grew on. A pane two frames in a row later than the budget, or with none for
299
+ longer than it, is stretched across to its new width until a frame lands
300
+ well inside the budget; down the pane it stays at its size. Each gesture
301
+ starts out at its size, and `resizeWait: 0` stretches always, both ways.
302
+ - **`'never'`** — at its size, its edge carried over, however late the pane
303
+ is.
304
+ - **`'always'`** — stretched both ways.
305
+
306
+ A carried edge usually continues the page's background; where it crosses
307
+ content, that content runs down the strip in thin lines for a frame. The
308
+ pane's window's `liveResizing` is the host's, so content floors are measured
309
+ once the drag ends, as in a window of its own, and an element can put off
310
+ work until then too.
311
+
312
+ `backgroundColor` in the frame's `style` is what shows before the pane's
313
+ first frame and after one dies — the same server-painted rectangle
314
+ `<foreign>` documents.
315
+
316
+ ## The pane process: flags and environment
317
+
318
+ `execArgv` and `env` say what a pane's process is started with, beyond what
319
+ it inherits:
320
+
321
+ ```jsx
322
+ <Frame
323
+ src={new URL('./page.pane.js', import.meta.url)}
324
+ // a page that grows without bound costs its own process, at 256 MB
325
+ execArgv={['--max-old-space-size=256']}
326
+ env={{ PAGE_LOG: 'quiet', NODE_OPTIONS: undefined }}
327
+ />
328
+ ```
329
+
330
+ - **`execArgv`** comes after the flags the pane inherits, so a dev loader
331
+ still follows it in and a flag it names is the one in force where the two
332
+ disagree. `--max-old-space-size` is the one a pane wants most: it bounds
333
+ the pane's heap, which nothing running inside the pane can, and a pane
334
+ that passes it is ended — and its `fallback` shown — rather than the
335
+ machine swapping. The flags go to the runtime that forks the pane, and are
336
+ its flags: Bun 1.4 takes node's V8 flags and bounds nothing by them, so
337
+ under Bun this is not a heap bound.
338
+ - **`env`** is merged over this process's environment, and a variable named
339
+ with `undefined` is taken away. `DISPLAY` is `display`'s, and
340
+ `REACT_X11_FRAME` is how a pane knows it is one (`isFramed`), so the two
341
+ are set after it and cannot be overridden here.
342
+
343
+ Either is read by value: the same flags in a new array — one written in
344
+ place, as above, at every render — keep the pane it was, and flags or
345
+ variables that say something else restart it, as a new `src` does. Something
346
+ a fork cannot take — a flag that is not a string — fails as a `spawn` error
347
+ and starts nothing.
348
+
349
+ ## A pane that stops answering
350
+
351
+ A pane whose event loop is stuck — a loop that never ends, a page's script
352
+ with no timeout around it — would otherwise stay `running`: it is sent
353
+ updates it never reads, its window keeps showing its last frame, and on X11
354
+ it holds the keyboard while the pointer is over it.
355
+
356
+ So the host asks. About once a second it sends the pane a `ping`, which the
357
+ pane answers from its event loop. A pane that has not answered for
358
+ `watchdog` ms — 15 seconds by default — goes the way a crashed one goes: the
359
+ frame fails with phase `unresponsive`, `fallback` is shown and can
360
+ `restart()` it, and the process is ended from outside, SIGTERM and then
361
+ SIGKILL, since it cannot read an unmount. `onExit` reports it with `expected: false`.
362
+
363
+ ```jsx
364
+ <Frame
365
+ src={new URL('./page.js', import.meta.url)}
366
+ watchdog={10000}
367
+ fallback={({ error, restart }) =>
368
+ error?.phase === 'unresponsive' ? (
369
+ <PageStopped onReload={restart} />
370
+ ) : (
371
+ <PageCrashed error={error} onReload={restart} />
372
+ )
373
+ }
374
+ />
375
+ ```
376
+
377
+ Three things keep it from ending a pane that is fine:
378
+
379
+ - **The host's own late tick is no news of the pane's.** Where the host's
380
+ timer comes late — the machine slept, or the host itself was stuck — the
381
+ count starts again from that tick.
382
+ - **A pane under the inspector is not watched.** `--inspect`,
383
+ `--inspect-brk` or `--inspect-wait`, in its own `execArgv`, in this
384
+ process's that it inherits, or in `NODE_OPTIONS`: a debugger stops a pane
385
+ on purpose. The flags that only set the inspector up, which Node's test
386
+ runner hands every test process, do not count.
387
+ - **`watchdog={false}`** watches nothing, for a pane whose work is long
388
+ and synchronous by design.
389
+
390
+ A pane of an older react-x11 that does not know the ping fails the handshake,
391
+ which says why, rather than being ended for not answering.
392
+
393
+ ## What a pane costs
394
+
395
+ A pane is a node process with React and ntk loaded: roughly 40–70MB and
396
+ 100–300ms of module graph before its first frame, plus an X connection.
397
+ That is the price of an event loop of one's own — right for a handful of
398
+ heavy panes, wrong as a general componentization strategy. A hundred
399
+ buttons belong in your process; the log viewer that parses gigabytes does
400
+ not.
401
+
402
+ ## The transport seam
403
+
404
+ `transport` replaces the fork wholesale: it is the door to running a pane
405
+ somewhere other than a child process without `<Frame>` learning about it
406
+ (react-x11's own tests run a real pane in-process over a loopback pair this
407
+ way). A transport is handed what the fork would be — `{ src, display,
408
+ execArgv, env }` — and returns the parent's end of the wire: `send`,
409
+ `onMessage`, `onExit`, and optionally `kill` and `pid` (`FrameTransport` in
410
+ the types).
411
+
412
+ A transport that still forks the pane, its own way (under a sandbox, with
413
+ another runtime), forks the entry the default one does, which is on the
414
+ exports map to be resolved: `fileURLToPath(import.meta.resolve(
415
+ 'react-x11/frame/child'))`. It is a process's entry, not a module to import:
416
+ outside a fork it says so and exits.
417
+
418
+ ## Known gaps
419
+
420
+ - **Accessibility**: a pane registers on the AT-SPI bus as its own
421
+ application. Orca reads it, and Tab crosses the boundary, but the tree is
422
+ not unified under the host's.
423
+ - **DevTools**: a pane is its own React tree; `REACT_X11_DEVTOOLS=1` in the
424
+ pane process attaches a separate DevTools, not a merged one.
425
+ - **No intrinsic size**: the host decides the rect.
426
+ - **A window a pane opens in its first commit is placed against a guess**,
427
+ since the host has not laid the pane out yet. A popup anchored to a node is
428
+ placed again before anyone sees it; a `<Dialog>` open from the start is
429
+ centred where the pane guessed it was.
430
+ - **Windows** panes do not know where their frame is on the screen yet: a
431
+ popup a Windows pane anchors is placed against the screen's corner. Not run
432
+ there.
433
+ - **A pane hears its window's focus, not its frame's.** Tab out of a pane
434
+ takes its focus with it, but a click on the host does not: the stop the
435
+ pane had focused keeps its focus, and its ring, while the host's is
436
+ elsewhere.
package/docs/gl.md ADDED
@@ -0,0 +1,173 @@
1
+ # 3D: `<glarea>` and the three ways underneath it
2
+
3
+ react-x11 gives you a **GL surface in the layout** and nothing above it. A
4
+ scene graph — meshes, materials, lights, post-processing — is
5
+ [`@react-x11/components/three`](ecosystem.md), which brings its own
6
+ reconciler and renders through the surface described here.
7
+
8
+ ```jsx
9
+ <glarea
10
+ style={{ flexGrow: 1 }}
11
+ clearColor="#0b1021"
12
+ frameLoop="always"
13
+ onCreated={(gl) => gl.Enable(gl.DEPTH_TEST)}
14
+ onDraw={(gl, { width, height }) => {
15
+ /* one frame */
16
+ }}
17
+ />
18
+ ```
19
+
20
+ `examples/viewer3d.jsx` is the worked example: a model viewer that orbits,
21
+ and the display-list discipline the indirect backend demands.
22
+
23
+ ## The three ways
24
+
25
+ `<glarea>` and `onDraw` are the same everywhere. What is underneath is not:
26
+ these are different APIs, not three spellings of one, and which one a
27
+ connection has decides what `onDraw` can do.
28
+
29
+ | | **direct** (X11) | **indirect** (X11) | **Cocoa** |
30
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
31
+ | how it draws | OpenGL ES 2 on the GPU (DRI3 + Present on Linux, Apple-DRI on macOS/XQuartz) | GL 1.x commands encoded into the X connection | CGL — OpenGL 4.1 core on Metal — presented as a sublayer |
32
+ | shaders | **yes**, GLSL ES 1.00 | none | **yes** |
33
+ | render targets | **yes** — framebuffer objects | none | **yes** |
34
+ | geometry | vertex buffers on the GPU | immediate mode compiled into display lists | vertex buffers on the GPU |
35
+ | lighting | per fragment | per vertex | per fragment |
36
+ | where it runs | a local connection, plus ntk's optional `x11-dri` addon: a Linux server with DRI3, or macOS/XQuartz with Apple-DRI (ntk 8.4.0) | any server that allows indirect contexts, including over a network | any Mac, on the Cocoa backend — it needs the same optional `x11-dri` addon and nothing else |
37
+
38
+ The Cocoa column hands `onDraw` the same WebGL-shaped `gl` table as X11
39
+ direct. `glPolicy: 'indirect'` is an error there, and says so: there is no X
40
+ server to speak GLX to.
41
+
42
+ **win32** and **Wayland** have no column because they hand `onDraw` the same
43
+ direct API and report `gl.backend === 'direct'` too. On win32 a `<glarea>` is
44
+ a WGL context composited under the window's own visual, so it composites with
45
+ the window's alpha where an X11 surface does not. On Wayland it is drawn into
46
+ the window's own backing target, between the tree's 2D paint and its
47
+ children's panes, rather than being a surface of its own.
48
+
49
+ On X11 the default is indirect. Turn direct on per app:
50
+
51
+ ```jsx
52
+ const root = await createRoot({ glPolicy: 'auto' });
53
+ ```
54
+
55
+ `'auto'` uses direct where it is available and indirect otherwise, which is
56
+ usually what you want: most modern desktops **refuse** indirect GLX — Xorg
57
+ 1.17 and later, and Xwayland, ship with it off — and those are exactly the
58
+ machines where direct works. `'direct'` and `'off'` are the strict forms, and
59
+ `'indirect'` is the default. One run can be switched without touching code:
60
+
61
+ ```sh
62
+ NTK_GL_POLICY=direct npm start
63
+ ```
64
+
65
+ Which flavor a connection got is `app.glCapabilities().flavor` — `'dri3'` or
66
+ `'appledri'`. Both spell the context the same way, so nothing above the
67
+ policy has to branch on it; it is worth reading when a machine that should
68
+ have direct does not.
69
+
70
+ Everything about how the backend is chosen and why it might be unavailable —
71
+ the `GLError` codes, `app.glCapabilities()`, the addon — is ntk's, and is
72
+ documented in [ntk's context-gles.md](https://github.com/sidorares/ntk/blob/master/docs/context-gles.md).
73
+
74
+ `npm run labs:direct-gl` is the shader path on a real display, and reports
75
+ both the flavor and which backend actually drew.
76
+
77
+ ## Raw GL through `onDraw`
78
+
79
+ `onDraw(gl, { width, height, node })` hands you the context itself, and
80
+ there are two spellings of GL to write against — camelCase ES 2 against
81
+ PascalCase OpenGL 1.x. Nothing translates between them. Branch on
82
+ `gl.backend`, which is `'direct'` or `'indirect'`; the Cocoa path reports
83
+ `'direct'`, because that is the API it hands you:
84
+
85
+ ```jsx
86
+ <glarea
87
+ onDraw={(gl) => {
88
+ if (gl.backend === 'direct') gl.clear(gl.COLOR_BUFFER_BIT);
89
+ else gl.Clear(gl.COLOR_BUFFER_BIT);
90
+ }}
91
+ />
92
+ ```
93
+
94
+ Code written against one spelling will not run on the other, which is why
95
+ the default X11 policy does not switch under an app that never asked for it.
96
+ A scene written for the direct API runs unchanged on the Cocoa backend; one
97
+ written for indirect GLX does not run there at all.
98
+
99
+ **On the indirect backend, geometry belongs in a display list.** Every
100
+ immediate-mode vertex is a command on the wire, so a mesh re-sent per frame
101
+ costs kilobytes per frame (about 96 KB for a 1 000-triangle mesh) while a
102
+ compiled list costs one `CallList`. Names are yours to choose — `GenLists` is
103
+ a round trip, and this is the backend where round trips are the thing to
104
+ avoid.
105
+
106
+ ### What indirect GLX encodes
107
+
108
+ Indirect GLX is a fixed-function OpenGL 1.x pipeline, and these are the
109
+ commands it can carry:
110
+
111
+ | available | |
112
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
113
+ | immediate mode | `Begin`/`End`, `Vertex3f`, `Normal3f`, `Color3f/4f`, `TexCoord2f` |
114
+ | matrices | `MatrixMode`, `LoadIdentity`, `LoadMatrixf`, `MultMatrixf`, `Push/PopMatrix`, `Translatef`, `Rotatef`, `Scalef`, `Frustum`, `Ortho` |
115
+ | lighting | `Lightfv`, `LightModelf`, `Materialfv`, `Materialf`, `ColorMaterial`, `ShadeModel` |
116
+ | texturing | `TexImage2D`, `BindTexture`, `TexParameter*`, `TexEnv*`, `TexGen*` |
117
+ | state | `Enable`/`Disable`, `Clear*`, `DepthFunc`, `DepthMask`, `BlendFunc`, `AlphaFunc`, `CullFace`, `FrontFace`, `PolygonMode`, `Viewport`, `Scissor`, `Fog*` |
118
+ | display lists | `CallList`, `ListBase`, `NewList`/`EndList`/`DeleteLists` |
119
+
120
+ There are **no GLSL shaders, no VBOs, no vertex arrays** (`DrawArrays` and
121
+ `VertexPointer` are not encoded), **no framebuffer objects and no
122
+ instancing**, and ARB assembly programs are not supported. These are limits
123
+ of the protocol, not of an implementation: shader materials and
124
+ post-processing need the direct backend.
125
+
126
+ ## Input
127
+
128
+ The pointer over a surface is the tree's, on every backend: `onMouseDown`,
129
+ `onMouseMove`, `onMouseUp`, `onClick`, `onWheel` and the rest fire at the
130
+ `<glarea>` — or at a child drawn over it (below) — and bubble, with
131
+ `ev.localX`/`localY` measured from the target's corner, which is what a
132
+ scene picks with. Nothing needs to listen on the surface's own window, and
133
+ nothing should: on X11 a listener there takes the event away from the tree
134
+ ([elements.md](elements.md#glarea) says why).
135
+
136
+ ## 2D over the surface
137
+
138
+ A `<glarea>`'s children are drawn above it — a legend, a toolbar, a label
139
+ over the scene — laid out in its box and hit before it. They are ordinary
140
+ elements. On the Cocoa backend they composite with the GL frame,
141
+ translucency and all. On X11 each is drawn on an opaque pane over the
142
+ surface, so what a child leaves unpainted shows the surface's `clearColor`
143
+ ([elements.md](elements.md#glarea) has the table).
144
+ `useSupports('glOverlay')` asks whether a connection draws them;
145
+ `examples/labs/gl-overlay.jsx` runs one on both backends.
146
+
147
+ The answer is no on XQuartz: the macOS window server composites every GL
148
+ surface there above everything the X server draws. The children are laid out
149
+ and not drawn, and a development build warns once. On macOS the Cocoa backend
150
+ is the one that composites a HUD over GL.
151
+
152
+ ## When there is no surface at all
153
+
154
+ `onError(err)` fires when no GL context could be made: no GLX, indirect
155
+ disabled, no matching visual. `err.code` is one of ntk's `GLXError` values,
156
+ `GLX_INDIRECT_DISABLED` being the usual one. Without a handler the failure is
157
+ a console warning and the element draws nothing, which looks like a bug in
158
+ your scene rather than a fact about the machine — so handle it and say what
159
+ the reader can do.
160
+
161
+ ## Testing
162
+
163
+ Test a GL app on **what it emits**, not on pixels: GL renders where `GetImage`
164
+ cannot read it (on XQuartz a capture reads back white), so a screenshot of a
165
+ window with a `<glarea>` has a hole where the scene is. On the direct
166
+ backend, `gl.readPixels` inside `onDraw` is the only way to see the frame.
167
+
168
+ This repo's `test/glarea.test.js` shows the pattern: node-x11's in-process X
169
+ server with its GLX emulator registered, so a frame's GL calls land on a
170
+ `RecordingBackend` — no display, no GPU, no addon.
171
+
172
+ Design notes: [design/gl.md](../design/gl.md), and the indirect backend's
173
+ design in [design/glx.md](../design/glx.md).