react-x11 2.52.0 → 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 (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
@@ -25,26 +25,19 @@ function RecordButton() {
25
25
 
26
26
  ## The ladder
27
27
 
28
- The [file dialog](filedialog.md)'s shape again, with one rung that answers and
29
- one that only points:
28
+ Like the [file dialog](filedialog.md)'s, chosen for you — one rung that answers
29
+ and one that only points:
30
30
 
31
31
  | | | |
32
32
  | --- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
33
33
  | 1 | **the platform's authorization** | The [cocoa backend](macos.md): macOS's privacy (TCC) status per kind, and the system's own prompt where a framework offers one — `AVCaptureDevice` for the camera and microphone, `CoreLocation`, and the rest. |
34
34
  | 2 | **the Settings pane** | `open x-apple.systempreferences:…` — any Mac, bridge or not. It cannot read a status or raise a prompt; it puts the user in front of the switch, which is all `openPrivacySettings` promises. |
35
35
 
36
- **On Linux nothing answers yet.** The freedesktop counterparts are the
37
- per-device portals (`org.freedesktop.portal.Camera`, `.Location`, …), which
38
- need no bridge and are
39
- [#466](https://github.com/sidorares/react-x11/issues/466)'s remaining half.
40
- Until then a status query says `'unknown'` and a request rejects with a
41
- **typed** `NoPermissionServiceError` — the signal to keep the feature behind
42
- the answer, not a crash. `usePermission().available` is that branch as render
43
- state.
44
-
45
- The top rung is never chosen by naming a backend: the call asks the connection
46
- the tree renders through whether it has an authorization API, and the cocoa
47
- backend says so by carrying one.
36
+ **On Linux nothing answers yet** (the per-device portals,
37
+ `org.freedesktop.portal.Camera`, `.Location`, …, are not wired up). A status
38
+ query says `'unknown'` and a request rejects with a **typed**
39
+ `NoPermissionServiceError` — the signal to keep the feature behind the answer,
40
+ not a crash. `usePermission().available` is that branch as render state.
48
41
 
49
42
  ## The vocabulary
50
43
 
@@ -146,3 +139,5 @@ the request never prompts, or TCC ends the process. See
146
139
  - **`'unknown'` is not `'prompt'`.** An app that treats them alike will show
147
140
  an "Allow" button on Linux that rejects when pressed. Branch on `available`
148
141
  first.
142
+
143
+ Design notes: [design/permissions.md](../design/permissions.md).
@@ -1,17 +1,12 @@
1
1
  # React features
2
2
 
3
- You already know React. This page is about the parts where that knowledge
4
- transfers unchanged, the parts where it transfers with a caveat, and the
5
- four places where a habit from the DOM will quietly not work.
3
+ **Everything in React core behaves as documented** — hooks, context, error
4
+ boundaries, `<Suspense>`, `lazy`, `use`, transitions, `<Profiler>`,
5
+ `<StrictMode>`. react-x11 is a normal React 19 renderer, and where React's
6
+ behaviour does not depend on the DOM, it is the same behaviour. This page is
7
+ the caveats, and the four places where a DOM habit quietly does not work.
6
8
 
7
- The short version: **everything in React core behaves exactly as documented.**
8
- Hooks, context, error boundaries, `<Suspense>`, `lazy`, `use`, transitions,
9
- `<Profiler>`, `<StrictMode>` — all of it. react-x11 is a normal React 19
10
- renderer, and where React's behaviour does not depend on the DOM, it is the
11
- same behaviour.
12
-
13
- Everything on this page is also something you can drive by hand — in the
14
- apps that depend on it rather than in a panel of its own:
9
+ The examples that exercise it:
15
10
 
16
11
  ```sh
17
12
  npm run examples:monitor # priority: a filter over ~780 live rows
@@ -19,12 +14,6 @@ npm run examples:chat # <Activity>, useOptimistic, <Suspense>
19
14
  npm run examples:timer # error boundaries, and where a crash is logged
20
15
  ```
21
16
 
22
- Those replaced an `examples/react-features.jsx` that had a panel per feature.
23
- A panel proves the API exists; it cannot show you the choice, because nothing
24
- in it costs anything. `monitor`'s field is expensive because a process list
25
- is expensive, and that is the only condition under which deferring it is a
26
- decision rather than a demonstration.
27
-
28
17
  What differs is anything the DOM used to define for you:
29
18
 
30
19
  - **Paint timing.** A commit does not put pixels on screen. Frames go out on
@@ -37,25 +26,26 @@ What differs is anything the DOM used to define for you:
37
26
 
38
27
  ## At a glance
39
28
 
40
- | | |
41
- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
42
- | `useState`, `useReducer`, `useContext`, `useMemo`, `useCallback`, `useRef` | unchanged |
43
- | `useImperativeHandle`, `forwardRef`, `ref` as a prop, ref cleanup | unchanged |
44
- | `use(promise)`, `use(context)`, `React.lazy` | unchanged |
45
- | `useOptimistic`, `useActionState`, `<Profiler>`, custom hooks | unchanged |
46
- | `useSyncExternalStore` | unchanged, and the best way to bridge non-React code |
47
- | `useTransition`, `startTransition`, `useDeferredValue` | work, and really do stay responsive |
48
- | `useLayoutEffect` vs `useEffect` | the difference is real — one paint versus two |
49
- | `<Suspense>` | works; read [Suspense and Activity](#suspense-and-activity) for `<window>` |
50
- | `<Activity>` | works; `mode="hidden"` around a `<window>` unmaps it |
51
- | Error boundaries | work — but **where you put one** decides if the window survives |
52
- | `<StrictMode>` | safe to use; effects are not double-invoked on the first mount |
53
- | `useId` | works; there is very little here to use it for |
54
- | `createPortal` | use [`<popup>`](elements.md) instead — see [Portals](#portals) |
55
- | `flushSync` | not exported; you almost certainly do not need it |
56
- | `useFormStatus` | not wired up |
57
- | React Server Components | not available |
58
- | anything imported from `react-dom` | not available — see [Not available](#not-available) |
29
+ | | |
30
+ | ----------------------------------------------------------------- | ----------------------------------------------------------------- |
31
+ | `useState`, `useReducer`, `useContext`, `useRef` | unchanged |
32
+ | `useMemo`, `useCallback`, custom hooks | unchanged |
33
+ | `useImperativeHandle`, `forwardRef`, `ref` as a prop, ref cleanup | unchanged |
34
+ | `use(promise)`, `use(context)`, `React.lazy` | unchanged |
35
+ | `useOptimistic`, `useActionState`, `<Profiler>` | unchanged |
36
+ | `useSyncExternalStore` | unchanged, and the best way to bridge non-React code |
37
+ | `useTransition`, `startTransition`, `useDeferredValue` | work, and really do stay responsive |
38
+ | `useLayoutEffect` vs `useEffect` | the difference is real — one paint versus two |
39
+ | `<Suspense>` | works; [around a `<window>`](#suspense-and-activity) it unmaps it |
40
+ | `<Activity>` | works; `mode="hidden"` around a `<window>` unmaps it |
41
+ | Error boundaries | work — but **where you put one** decides if the window survives |
42
+ | `<StrictMode>` | safe; effects are not double-invoked on the first mount |
43
+ | `useId` | works; there is very little here to use it for |
44
+ | `createPortal` | use [`<popup>`](elements.md) instead — see [Portals](#portals) |
45
+ | `flushSync` | not exported; you almost certainly do not need it |
46
+ | `useFormStatus` | not wired up |
47
+ | React Server Components | not available |
48
+ | anything imported from `react-dom` | not available — see [Not available](#not-available) |
59
49
 
60
50
  ## Effects, and the paint boundary
61
51
 
@@ -68,27 +58,24 @@ it has a directly observable consequence:
68
58
  | `useLayoutEffect` | one frame, with the corrected value |
69
59
  | `useEffect` | one frame with the old value, then a frame with the new |
70
60
 
71
- So an adjustment made in `useEffect` flickers. This is not a timing race you
72
- can get lucky with — updates from a layout effect are folded into the frame
73
- being prepared, and updates from a passive effect are not.
61
+ So an adjustment made in `useEffect` flickers, every time: updates from a
62
+ layout effect are folded into the frame being prepared, and updates from a
63
+ passive effect are not.
74
64
 
75
65
  That holds for a correction computed from React state. One computed from
76
66
  **geometry** is a layout behind: a layout effect runs after the commit and
77
67
  before the layout pass, so `abs` there is still the previous frame's rect
78
- (zeros, on mount), and anything measured from it describes the frame
79
- before. For a size, let the element report it with `onLayout`
68
+ (zeros, on mount). For a size, let the element report it with `onLayout`
80
69
  ([below](#measuring-a-node)); for a style that depends on one, write a
81
70
  container query ([styling.md](styling.md#container-queries)), which the
82
- pass answers in the same frame. `scrollIntoView` measures nothing when you
83
- call it, and neither does the part of a `scrollTo` past the extent the last
84
- pass measured, which is what `scrollTo(Infinity)` asks for when it follows
85
- rows this commit added. The pass answers both against the geometry it
86
- produces, so they are safe here ([elements.md](elements.md#scrolling)).
71
+ pass answers in the same frame. `scrollIntoView` and `scrollTo` — including
72
+ `scrollTo(Infinity)` to follow rows this commit added — are safe here: the
73
+ pass answers them against the geometry it produces
74
+ ([elements.md](elements.md#scrolling)).
87
75
 
88
- The same boundary is why an event handler feels instant: react-x11 lands the
89
- React update caused by a click or a keystroke **before** the frame goes out,
90
- so the response and the default action paint together rather than a frame
91
- apart.
76
+ The same boundary is why an event handler feels instant: the React update
77
+ caused by a click or a keystroke lands **before** the frame goes out, so the
78
+ response and the default action paint together.
92
79
 
93
80
  ```jsx
94
81
  // good — `tooTall` and `fits` are React state, so the corrected rows paint
@@ -101,9 +88,8 @@ useLayoutEffect(() => {
101
88
  ## Measuring a node
102
89
 
103
90
  **There is no forced synchronous layout.** Nothing you can call will make
104
- layout run so you can read the result. This is the DOM habit most likely to
105
- break on the way over, because in a browser `getBoundingClientRect()` both
106
- reads _and_ triggers.
91
+ layout run so you can read the result, as `getBoundingClientRect()` does in a
92
+ browser.
107
93
 
108
94
  A node's `abs` rect (and `getClientRects()`) reports what the **last** layout
109
95
  produced. On the very first render of a node, that is nothing yet.
@@ -179,11 +165,10 @@ say (see [Known gaps](#known-gaps)).
179
165
  really do keep the UI responsive — React will interrupt a transition render
180
166
  to handle an incoming click or keystroke.
181
167
 
182
- What is worth knowing is that **transitions are the mechanism that does
183
- this**. An ordinary `setState` — from a handler, a timer, a socket callback —
184
- renders in one uninterruptible pass, however much work it is. If a render is
185
- big enough to drop a frame, wrapping it in `startTransition` (or deferring
186
- its input with `useDeferredValue`) is not a micro-optimisation, it is the
168
+ **Transitions are the only mechanism that does this.** An ordinary
169
+ `setState` — from a handler, a timer, a socket callback — renders in one
170
+ uninterruptible pass, however much work it is. If a render is big enough to
171
+ drop a frame, `startTransition` (or `useDeferredValue` on its input) is the
187
172
  difference between a responsive window and a frozen one.
188
173
 
189
174
  ```jsx
@@ -203,17 +188,13 @@ Two smaller notes:
203
188
  - A `startTransition` inside a click handler is still a transition. It does
204
189
  not inherit the handler's urgency.
205
190
 
206
- react-x11 already classifies X11 input for you: presses, releases, keys,
207
- focus changes, window close and drops are urgent; pointer motion and
208
- drag-over are not, which is what keeps a motion burst from flooding the
209
- renderer. You do not have to do anything to get this.
210
-
211
- [`examples/monitor.jsx`](../examples/monitor.jsx) is this paragraph with a
212
- keyboard on it: a filter over hundreds of live rows, where one keystroke
213
- changes every row on screen. Without the deferral the caret waits for the
214
- table; with it the field stays under the fingers and the rows arrive a beat
215
- later. Two things there are worth copying, and both are one-line mistakes in
216
- the other direction: the expensive subtree is
191
+ Input is already classified for you: presses, releases, keys, focus
192
+ changes, window close and drops are urgent; pointer motion and drag-over are
193
+ not, which keeps a motion burst from flooding the renderer.
194
+
195
+ [`examples/monitor.jsx`](../examples/monitor.jsx) is the worked case: a
196
+ filter over hundreds of live rows, where one keystroke changes every row on
197
+ screen. Two things there are worth copying: the expensive subtree is
217
198
  `memo`ised (or the urgent render rebuilds it anyway, and deferring buys
218
199
  nothing), and no prop that changes on every update — not even a "stale"
219
200
  border — is passed into it.
@@ -221,22 +202,18 @@ border — is passed into it.
221
202
  ### Bridging non-React code
222
203
 
223
204
  `useSyncExternalStore` is the right tool for anything that lives outside
224
- React — a timer, a D-Bus signal, a socket, an ntk event. It is also faster
225
- here than a hand-rolled `setState` bridge: a store notification is applied
226
- urgently and lands in the next microtask, where the equivalent `setState`
227
- waits a turn longer.
228
-
205
+ React — a timer, a D-Bus signal, a socket, an ntk event — and faster than a
206
+ hand-rolled `setState` bridge: a store notification is applied urgently, in
207
+ the next microtask, where the equivalent `setState` waits a turn longer.
229
208
  Every mainstream store (zustand, jotai, valtio, redux, XState) works with no
230
- adapter for this reason. The [window-manager
231
- example](../examples/wm.jsx) is the worked case: the WM core is a plain
232
- store, and the UI subscribes to it.
209
+ adapter. The [window-manager example](../examples/wm.jsx) is the worked
210
+ case: the WM core is a plain store, and the UI subscribes to it.
233
211
 
234
212
  ## Suspense and Activity
235
213
 
236
- `<Suspense>` works. The one thing to plan for is layout: a hidden subtree
237
- gives up its space entirely, so the fallback lays out as if the real content
238
- were not there. **Size your fallback deliberately**, or the window collapses
239
- to the fallback and jumps back on reveal.
214
+ `<Suspense>` works. Plan for layout: a hidden subtree gives up its space
215
+ entirely, so **size your fallback deliberately**, or the window collapses to
216
+ the fallback and jumps back on reveal.
240
217
 
241
218
  ```jsx
242
219
  <Suspense fallback={<box style={{ height: 240 }} />}>
@@ -251,12 +228,11 @@ something else took focus meanwhile, so a boundary that re-suspends
251
228
  mid-edit does not drop the user out of what they were typing. See
252
229
  [events.md](events.md#focus-and-visibility) for the rules and the way out.
253
230
 
254
- **Scroll positions come back too.** What a hidden boundary holds is not
255
- placed while it is hidden — the same as under `display: 'none'` — so a scroll
256
- pane in it is not clamped to the start of content it no longer lays out,
257
- reports no `onViewport` or `onScroll` until it is shown, and is shown at the
258
- offset it was left at. A tab kept mounted under `<Activity>` reopens on the
259
- row the user was reading.
231
+ **Scroll positions come back too.** A hidden boundary's content is not laid
232
+ out while hidden (as under `display: 'none'`), so a scroll pane in it is not
233
+ clamped, reports no `onViewport` or `onScroll` until shown, and reappears at
234
+ the offset it was left at — a tab kept under `<Activity>` reopens on the row
235
+ the user was reading.
260
236
 
261
237
  If a `<window>` is inside the boundary, hiding it **unmaps the real window**.
262
238
  The window manager treats the reveal as a new window: it may re-place it,
@@ -267,12 +243,11 @@ lost. Prefer suspending _inside_ a window over suspending the window itself.
267
243
  `import()` gets inlined — you keep lazy _evaluation_ but get no code
268
244
  splitting; see [packaging.md](packaging.md).
269
245
 
270
- `<Activity>` works, and `mode="hidden"` around a toplevel `<window>` reads
271
- the same way `<Suspense>` does: the window is unmapped, and one mounted
272
- hidden is never mapped at all — it is created, laid out and kept, but the
273
- window manager only ever hears about it on the reveal. The same caveat
274
- applies: what the user did to the window before it was hidden is the window
275
- manager's to remember, and it may not.
246
+ `<Activity>` works, and `mode="hidden"` around a toplevel `<window>` unmaps
247
+ it the same way; one mounted hidden is created and laid out but never
248
+ mapped until the reveal. The same caveat applies: what the user did to the
249
+ window before it was hidden is the window manager's to remember, and it may
250
+ not.
276
251
 
277
252
  **Testing Suspense**: a promise that settles outside `act()` is not always
278
253
  picked up by an `await act()`, because React throttles the commit when a
@@ -281,11 +256,10 @@ see [testing.md](testing.md).
281
256
 
282
257
  ## Portals
283
258
 
284
- **Use `<popup>`.** It is what you want in almost every case where the DOM
285
- would reach for `createPortal`: a `<popup>` is written as an ordinary child
286
- in your JSX — so it sees context, state and props exactly where you wrote it
287
- — but it is a real top-level X window, so it escapes its parent's clipping
288
- and can extend past the window edge.
259
+ **Use `<popup>`** where the DOM would reach for `createPortal`. A `<popup>`
260
+ is an ordinary child in your JSX — it sees context, state and props where
261
+ you wrote it — but a real top-level window, so it escapes its parent's
262
+ clipping and can extend past the window edge.
289
263
 
290
264
  ```jsx
291
265
  <box>
@@ -323,8 +297,9 @@ yourself wanting it, you almost certainly want a second `<window>` or a
323
297
 
324
298
  ## Where to put an error boundary
325
299
 
326
- Boundaries work exactly as documented. The X11-specific decision is
327
- **placement**, and it decides whether the user's window survives:
300
+ Boundaries work exactly as documented. The decision specific to a desktop
301
+ renderer is **placement**, and it decides whether the user's window
302
+ survives:
328
303
 
329
304
  ```jsx
330
305
  // good — the window and everything the WM knows about it survives
@@ -403,12 +378,12 @@ and the [hot-reload example](../examples/tasks.jsx), which is
403
378
  `flushSync`, `findDOMNode`, `hydrateRoot` and `unstable_batchedUpdates` are
404
379
  not part of react-x11.
405
380
 
406
- The trap is not the import error — it is that `react-dom` is a required peer
407
- of some libraries, so it can end up installed anyway. Then the import
408
- resolves, and `flushSync()` runs your callback **without flushing anything**,
409
- because it is talking to a renderer you are not using. If a library depends
410
- on synchronous flushing, check `npm ls react-dom`. The
411
- [ecosystem register](ecosystem.md) records which packages hit this.
381
+ The trap: `react-dom` is a required peer of some libraries, so it can end up
382
+ installed anyway. Then the import resolves, and `flushSync()` runs your
383
+ callback **without flushing anything**, because it talks to a renderer you
384
+ are not using. If a library depends on synchronous flushing, check
385
+ `npm ls react-dom`; the [ecosystem register](ecosystem.md) records which
386
+ packages hit this.
412
387
 
413
388
  **Server Components** are not available: there is no Flight client wired up.
414
389
 
package/docs/remote.md CHANGED
@@ -1,12 +1,9 @@
1
1
  # Remote display
2
2
 
3
- This is the case the architecture is categorically better at, and the one
4
- nothing else on the page has told you about.
5
-
6
3
  A react-x11 app **draws over the wire**. It does not render pixels and ship
7
4
  them; it sends X11 drawing requests — rectangles, glyph runs, composites —
8
- and the X server on the far end turns them into pixels. So "running the app
9
- somewhere else" is not a feature bolted on top. It is what the protocol is,
5
+ and the X server on the far end turns them into pixels. So running the app
6
+ somewhere else is not a feature bolted on top: it is what the protocol is,
10
7
  and the program does not know the difference.
11
8
 
12
9
  ```sh
@@ -18,13 +15,12 @@ Nothing in the app changes. No VNC, no browser, no streaming daemon, no port
18
15
  to forward, no second copy of the UI.
19
16
 
20
17
  **This is the X11 backend**, and it is the reason X11 stays the flagship
21
- target rather than becoming the legacy one: the Cocoa, win32 and Wayland
22
- backends have no wire at all, so nothing on this page applies to them. That
23
- matters in one specific case — **an app running on a Mac or on Windows and
24
- drawing somewhere else**. `$DISPLAY` in the environment does not choose the
25
- backend there; `backend: 'auto'` sees darwin and picks Cocoa, or win32 and
26
- picks the win32 backend, and the window opens on the local machine. Say
27
- which you want:
18
+ target: the Cocoa, win32 and Wayland backends have no wire at all, so
19
+ nothing on this page applies to them. That matters in one case — **an app
20
+ running on a Mac or on Windows and drawing somewhere else**. `$DISPLAY` in
21
+ the environment does not choose the backend there; `backend: 'auto'` sees
22
+ darwin and picks Cocoa, or win32 and picks the win32 backend, and the window
23
+ opens on the local machine. Say which you want:
28
24
 
29
25
  ```sh
30
26
  REACT_X11_BACKEND=x11 DISPLAY=thin-client:0 npm start
@@ -37,67 +33,31 @@ and drawing to a Mac's XQuartz is not affected: both ends are X11 already.
37
33
 
38
34
  ## What it costs, measured
39
35
 
40
- The honest comparison is with pixel streaming (VNC, RDP, a
41
- `--remote-debugging` browser), because that is what people reach for
42
- instead. The trade is bytes-per-frame against round-trips-per-interaction.
43
-
44
- From `scripts/bench/baseline.json`, which measures against node-x11's
45
- in-process server — so these are exact protocol costs, not estimates:
46
-
47
- | scenario | requests | bytes out | replies | stalls |
48
- | ---------------------------------- | -------- | --------- | ------- | ------ |
49
- | mount: a window, 40 boxes + labels | 110 | 4.2 kB | 32 | 0 |
50
- | 50 filled rounded boxes | 122 | 6.6 kB | 7 | 0 |
51
- | a paragraph of text | 39 | 6.0 kB | 7 | 0 |
52
- | 10 wheel notches over 500 rows | 224 | 11.2 kB | 22 | 4 |
53
- | 5 drag steps of a card of widgets | 72 | 4.3 kB | 11 | 0 |
54
-
55
- **A whole window appears for about four kilobytes.** A single 1280×800 frame
56
- of VNC, even well compressed, is tens to hundreds of kilobytes, and it is
57
- sent again every time anything moves. Scrolling a 500-row table ten notches
58
- costs 11 kB here because only the damaged strip is redrawn (the
59
- [scroll-blit path](../AGENTS.md), issue #138); the same scroll under pixel
60
- streaming is ten full frames. Dragging a card with a background of its own
61
- is the same trade: each step copies the card where it went and draws the
62
- strip it uncovered, so its widgets are not drawn again
63
- ([extending.md](extending.md#an-element-that-covers-its-box), issue #681) —
64
- the five steps in the table would be 24 kB drawn.
65
-
66
- What react-x11 pays instead is **latency sensitivity**. Bandwidth is not the
67
- scarce resource on a link like this — round trips are. A request that waits
68
- for a reply stalls the pipeline for a full RTT, so on a 60 ms link every
69
- reply is 60 ms of nothing.
70
-
71
- The last two columns are how that is watched, and the difference between
72
- them is the whole point. `replies` counts requests that carry an answer;
73
- `stalls` counts the ones whose reply arrived with **nothing pipelined behind
74
- them** — the request was the last thing sent, so the connection really did
75
- sit idle for a round trip. A pipelined reply costs a reply and no time: the
76
- mount's 32 are almost all `InternAtom`, fired off together while the window
77
- is built, and it stalls on none of them. (The bench's own `settle()` fences
78
- are a fixed floor in that column, so read growth rather than the absolute
79
- number.)
80
-
81
- So the rule — "avoid round trips", rule 4 in
82
- [AGENTS.md](../AGENTS.md#protocol-efficiency) — is really "avoid _blocking_
83
- round trips", which is why atoms, geometry and glyph pages are cached rather
84
- than re-asked, and why `stalls` is the column to watch on a slow link.
36
+ Bandwidth is small: **a whole window appears for about four kilobytes** of
37
+ requests, and scrolling a 500-row table ten notches costs 11 kB, because
38
+ only the damaged strip is redrawn — where a single frame of pixel streaming
39
+ (VNC, RDP) is tens to hundreds of kilobytes, sent again whenever anything
40
+ moves.
41
+
42
+ What react-x11 pays instead is **latency**. A request that waits for a
43
+ reply with nothing pipelined behind it stalls the connection for a full
44
+ round trip, so on a 60 ms link every such reply is 60 ms of nothing. That is
45
+ why atoms, geometry and glyph pages are cached rather than re-asked, and
46
+ why the column to watch on a slow link is `stalls`, not `replies`:
85
47
  `npm run bench -- --hotspots` attributes them, request by request.
86
48
 
87
- Rules of thumb, from the shape of the protocol rather than from a benchmark
88
- we have not run:
49
+ Rules of thumb, reasoned from the request and reply counts rather than
50
+ measured over a shaped link:
89
51
 
90
52
  - **Under ~20 ms RTT** (a LAN, a nearby VPS): indistinguishable from local.
91
53
  - **20–80 ms** (a continent away): typing and scrolling stay responsive;
92
54
  window mapping and font setup have a visible beat.
93
55
  - **Above ~150 ms** or on a lossy link: X11's chattiness starts to show, and
94
56
  a pixel-streaming protocol designed for high latency will feel better.
95
- This is not a react-x11 limitation, it is X11's, and it is the honest place
96
- to say so.
57
+ This is X11's limit, not react-x11's.
97
58
 
98
- `unverified:` those bands are reasoning from the request/reply counts above,
99
- not measurements over a shaped link. Measuring them properly means `tc
100
- netem` between two hosts; if you do it, the numbers belong in this table.
59
+ The per-scenario numbers are in the
60
+ [design notes](../design/remote.md#what-it-costs-measured).
101
61
 
102
62
  ## `ssh -X` and `ssh -Y`
103
63
 
@@ -114,8 +74,7 @@ that does not":
114
74
 
115
75
  **Prefer `-X`.** react-x11 should work under it — it draws into windows it
116
76
  created, which is what untrusted mode is for. If something does not, that is
117
- a bug worth filing rather than a reason to reach for `-Y`. Most toolkits'
118
- documentation says "use `-Y` if `-X` misbehaves"; that advice hands every
77
+ a bug worth filing rather than a reason to reach for `-Y`, which hands every
119
78
  program on the remote host the ability to read your screen and log your
120
79
  keystrokes.
121
80
 
@@ -178,15 +137,12 @@ the compositor is willing to expose), and `WM_TRANSIENT_FOR` and the
178
137
  `_NET_WM_STATE` work are among the things that survive the trip — see
179
138
  [elements.md](elements.md#transientfor--a-window-that-belongs-to-another).
180
139
 
181
- **Native Wayland: not remotely**, and the reason is not effort. Wayland has
182
- no drawing protocol. A Wayland client allocates a buffer, renders pixels into
183
- it _itself_, and hands the compositor a handle — there is nothing to send over
184
- a wire, and no server-side rendering to ask for. The entire premise of this
185
- page, that drawing crosses the connection, does not exist there. The Wayland
186
- backend (`backend: 'wayland'`, [wayland-backend.md](wayland-backend.md)) is
187
- therefore a different program under the same API — it rasterizes locally —
188
- which is why it is opt-in only and never what `'auto'` picks: on Linux the
189
- default stays X11, and with it everything on this page.
140
+ **Native Wayland: not remotely.** Wayland has no drawing protocol: a client
141
+ renders pixels into a buffer itself and hands the compositor a handle, so
142
+ there is no drawing to send over a wire. The
143
+ [Wayland backend](wayland-backend.md) rasterizes locally, which is why it is
144
+ opt-in and never what `'auto'` picks: on Linux the default stays X11, and
145
+ with it everything on this page.
190
146
 
191
147
  **XQuartz notes**, since it is the most common macOS path:
192
148
 
@@ -196,7 +152,7 @@ default stays X11, and with it everything on this page.
196
152
  - Put `/opt/X11/bin` first on `PATH`. Font family resolution shells out to
197
153
  `fc-match`, and Homebrew's fontconfig — which is usually found first —
198
154
  ships no macOS system-font aliases, so `sans-serif` resolves to a CJK
199
- face. Issue [#86](https://github.com/sidorares/react-x11/issues/86).
155
+ face.
200
156
 
201
157
  ## Making it feel better
202
158