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/dbus.md ADDED
@@ -0,0 +1,205 @@
1
+ # D-Bus
2
+
3
+ Everything the desktop wants from an app beyond drawing — a native file
4
+ dialog, the light/dark preference, notifications, a
5
+ [global menu](globalmenu.md), a tray icon
6
+ — is a D-Bus call. This page is the connection those features sit on, and
7
+ nothing else: no service is spoken to here, and the only traffic this layer
8
+ generates is its own handshake.
9
+
10
+ ```jsx
11
+ import { useSessionBus } from 'react-x11';
12
+
13
+ function ColourScheme() {
14
+ const { bus } = useSessionBus();
15
+ if (!bus) return null; // no bus here — nothing to read a preference from
16
+ // ...call org.freedesktop.portal.Settings through `bus`
17
+ }
18
+ ```
19
+
20
+ There is **nothing to wrap**. No provider, no prop drilling, no setup step
21
+ for the app author — a component library can call this.
22
+
23
+ ## The rule everything else is subordinate to
24
+
25
+ **Nothing throws at import time, and nothing crashes an app running where
26
+ there is no session bus** — XQuartz, a bare `startx`, CI, an ssh session on a
27
+ headless server, a container, Node 20 where the transport is not installed.
28
+ **"No bus" is a first-class configuration, not a degraded one.**
29
+
30
+ So `sessionBus()` answers `null` rather than rejecting, and `useSessionBus()`
31
+ reports `'unavailable'` rather than throwing, when:
32
+
33
+ - `dbus-native` is not installed,
34
+ - `$DBUS_SESSION_BUS_ADDRESS` is unset and there is no `$XDG_RUNTIME_DIR/bus`,
35
+ - the connect fails, authentication fails, or the platform has no bus at all.
36
+
37
+ ## The hooks
38
+
39
+ ```ts
40
+ function useSessionBus(): BusHandle;
41
+ function useSystemBus(): BusHandle;
42
+
43
+ interface BusHandle {
44
+ readonly bus: MessageBus | null; // null unless status === 'ready'
45
+ readonly uniqueName: string | null;
46
+ readonly status: 'connecting' | 'ready' | 'unavailable' | 'closed';
47
+ readonly cause?: BusUnavailableError; // set when status === 'unavailable'
48
+ retry(): void;
49
+ }
50
+ ```
51
+
52
+ | `status` | |
53
+ | --------------- | -------------------------------------------------------- |
54
+ | `'connecting'` | the handshake is in flight — and also the first render |
55
+ | `'ready'` | `bus` and `uniqueName` are set |
56
+ | `'unavailable'` | no daemon, socket or permission here; `cause` says which |
57
+ | `'closed'` | the connection died under you; `retry()` dials a new one |
58
+
59
+ **The terse path is correct on its own.** `const { bus } = useSessionBus()`
60
+ followed by `if (!bus) return fallback` is right, with no reference to
61
+ `status`. The states are for diagnostics ("why is my tray icon missing") and
62
+ for an app that is itself a service.
63
+
64
+ **`'unavailable'` is a snapshot, not a verdict.** Failure is not cached: a
65
+ session bus can appear later, and the next acquisition retries. Do not
66
+ permanently disable a feature on seeing it.
67
+
68
+ **`'closed'` does not auto-retry.** A new connection is a new unique name (see
69
+ [the sharing contract](#the-sharing-contract)), so a hook that knows how to
70
+ rebuild its bus-side state calls `retry()` itself.
71
+
72
+ ## The imperative pair
73
+
74
+ ```ts
75
+ function sessionBus(opts?: { required?: boolean }): Promise<BusRef | null>;
76
+ function systemBus(opts?: { required?: boolean }): Promise<BusRef | null>;
77
+ function closeBus(kind: 'session' | 'system'): Promise<void>;
78
+
79
+ interface BusRef {
80
+ readonly bus: MessageBus; // the full dbus-native surface, shared
81
+ readonly uniqueName: string; // this connection's unique name, e.g. ':1.42'
82
+ release(): Promise<void>; // drop this consumer's reference; idempotent
83
+ [Symbol.asyncDispose](): Promise<void>;
84
+ }
85
+ ```
86
+
87
+ For host-side code with no component to hang off, and for a click handler that
88
+ wants to resolve at click time rather than render time:
89
+
90
+ ```jsx
91
+ <Button
92
+ onClick={async () => {
93
+ await using ref = await sessionBus({ required: true });
94
+ await ref.bus.proxy(/* … */);
95
+ }}
96
+ />
97
+ ```
98
+
99
+ `required: true` converts unavailability into a rejection carrying the cause —
100
+ for an app whose purpose is the bus. The default never rejects.
101
+
102
+ There is **no `busAddress` parameter**: the connection is shared, so the
103
+ address comes from `$DBUS_SESSION_BUS_ADDRESS`, falling back to
104
+ `$XDG_RUNTIME_DIR/bus`. On **macOS** it is looked up from launchd
105
+ (`launchctl getenv`), asynchronously and once per process, so it does not
106
+ block the first frame
107
+ ([desktop.md](desktop.md#not-blocking-the-first-frame-on-the-bus)).
108
+
109
+ ## One connection, on purpose
110
+
111
+ Every consumer in the process — the global menu, a tray, your own exported
112
+ service — shares **one socket and one unique name**, so the desktop sees one
113
+ application. That is also why these hooks need no provider: two roots on two X
114
+ displays still share one session bus.
115
+
116
+ ## Lifecycle: connect on first use, stay connected
117
+
118
+ The ref count drives the socket's **event-loop hold**, not its existence:
119
+
120
+ | refs | socket | process exit |
121
+ | ---- | --------------------- | --------------------------------------------------------- |
122
+ | `>0` | connected, `ref()`d | held open — someone is using the bus |
123
+ | `0` | connected, `unref()`d | not held; the connection stays warm and costs one idle FD |
124
+
125
+ Releasing the last ref does **not** close the connection, so a dialog that
126
+ mounts and unmounts (or StrictMode's double mount) does not reconnect and get
127
+ a new unique name each time.
128
+
129
+ `closeBus(kind)` is the way out for an app that genuinely wants the connection
130
+ gone. Rare, explicit, and never the automatic consequence of an unmount.
131
+
132
+ ## The sharing contract
133
+
134
+ - **Never close a shared bus.** `release()` is the only lifecycle verb a ref
135
+ holder has. `ref.bus.close()` or `bus.connection.end()` tears the connection
136
+ out from under every other holder.
137
+ - **react-x11 owns the connection's one `'error'` listener.** Do not attach
138
+ your own.
139
+ - **A dead connection is not resurrected.** Refs on it fail their calls with
140
+ `ConnectionClosedError` and `release()` harmlessly; the next `sessionBus()`
141
+ dials fresh (a new unique name), and mounted hooks move to `'closed'`.
142
+ - **App-global stays app-global.** Well-known names (`RequestName` has no
143
+ per-consumer refcount), exported object paths (re-exporting the same path
144
+ and interface replaces silently) and `setValueShapes()` (which flips reply
145
+ shapes for _every_ holder) are process-level resources. Owning them is app
146
+ policy; a `BusRef` does not scope them, and react-x11 will never call
147
+ `setValueShapes()` on the shared bus.
148
+
149
+ ## Being a service, not just a client
150
+
151
+ An app that wants to _be_ on the bus gets the full `dbus-native` surface
152
+ through `ref.bus`, on the **same connection and unique name** as react-x11's
153
+ own plumbing:
154
+
155
+ ```js
156
+ const ref = await sessionBus({ required: true });
157
+ await ref.bus.requestName('org.example.MyApp', 0);
158
+ ref.bus.exportInterface(impl, '/org/example/MyApp', description);
159
+ // hold the ref for as long as you serve: it is what keeps the process alive
160
+ ```
161
+
162
+ Release the ref and nothing keeps the process running.
163
+
164
+ The one service react-x11 exports for you is `org.freedesktop.Application`, so
165
+ that a `myapp://…` link opens the app that is already running —
166
+ [uri-schemes.md](uri-schemes.md). It is on this same connection, so the app's
167
+ name, its global menu and its portal requests share one identity.
168
+
169
+ ## Installing, and Node 20
170
+
171
+ ```json
172
+ "optionalDependencies": { "dbus-native": "^0.16.0" }
173
+ ```
174
+
175
+ `dbus-native` declares `engines: ">=22.12.0"` where react-x11 declares
176
+ `">=20.19"`, so **npm skips it on Node 20** and the install still succeeds. No
177
+ compiler and no install script either way.
178
+
179
+ On Node 20, `useSessionBus()` reports `'unavailable'` with a `cause` that says
180
+ the transport is missing — the same path as an ssh session or a Mac without a
181
+ bus. To have it on Node 20 anyway, install it explicitly:
182
+
183
+ ```bash
184
+ npm i dbus-native
185
+ ```
186
+
187
+ The declarations never name `dbus-native`, so they type-check on an install
188
+ where it is absent. `MessageBus` is a structural type covering the members
189
+ documented here, with an index signature over the rest of the package's
190
+ surface — code that wants `dbus-native`'s own checked types can import them
191
+ directly and cast; it is the same object.
192
+
193
+ ## Known sharp edges
194
+
195
+ - **A daemon that authenticates and never answers `Hello`** crashes the
196
+ process from inside `dbus-native`, 25 s in, where nothing can catch it. A
197
+ small window, tracked upstream.
198
+
199
+ ## Seeing it
200
+
201
+ Point `DBUS_SESSION_BUS_ADDRESS` at nothing: a well-built consumer renders the
202
+ `'unavailable'` state rather than failing to start. That is the thing to check
203
+ in your own app.
204
+
205
+ Design notes: [design/dbus.md](../design/dbus.md).
@@ -0,0 +1,371 @@
1
+ # Runtime diagnostics
2
+
3
+ Three switches answer the three questions a slow or misbehaving frame
4
+ raises: _what went on the wire_ (`REACT_X11_TRACE`), _what got repainted_
5
+ (`REACT_X11_DEBUG_PAINT`), and _why_ (invalidation reasons, printed by
6
+ both). They complement [devtools.md](devtools.md) (component tree) and
7
+ `REACT_X11_DEBUG_LAYOUT=1` (outline every drawn node, colour by depth).
8
+
9
+ The failure mode they exist for is the silent full-window repaint: correct
10
+ pixels, quietly costing the whole window every frame. X11 is a network
11
+ protocol even on a local socket, and damage rects are invisible at runtime
12
+ without these.
13
+
14
+ Everything here costs nothing when off. The environment switches are read
15
+ **once at startup** unless a section says otherwise, and the `NO_…` and
16
+ `STRICT_…` switches answer only to `1` — any other value leaves the default
17
+ in place. The tracer module is not imported unless `REACT_X11_TRACE` is set
18
+ or you import `react-x11/debug` yourself, and stacks are captured only under
19
+ `REACT_X11_DEBUG_PAINT=full` or `+stacks`.
20
+
21
+ ## Protocol tracing — `REACT_X11_TRACE`
22
+
23
+ ```sh
24
+ REACT_X11_TRACE=summary npm run examples:dashboard
25
+ # at exit:
26
+ # react-x11 trace: 4211 requests (312.4KB out), 89 replies, 41 events, 0 errors (48.1KB in)
27
+ # 1780 Render.CompositeGlyphs32
28
+ # 512 Render.Composite
29
+ # ...
30
+ ```
31
+
32
+ - `summary` — request/byte totals and an opcode histogram, printed to
33
+ stderr when the process exits.
34
+ - `requests` — one stderr line per request as it is sent
35
+ (`x11 → Render.FillRectangles 36B`), plus one line per painted frame
36
+ (`frame 412: 320x24@8,40 reasons=style-state landed=16.4ms`). X errors are
37
+ decoded and name the failing request. `landed` is how long the previous
38
+ frame took to be answered. A window presents by default, so this is
39
+ time-to-display and reads about **one refresh period** — 16ms on a 60Hz
40
+ screen is the system working. On `frameClock: 'fence'` it is a server
41
+ round trip instead, and a paint that builds in 0.3ms against 20ms there
42
+ _is_ a server-side problem (software-fallback RENDER ops, a virtualized
43
+ GPU) rather than a renderer one. A frame the frame pacer held
44
+ ([elements.md](elements.md#framerate--pacing-the-frames-under-a-flood))
45
+ carries `waited=18.0ms` — how long the claim waited before the clock got
46
+ it.
47
+ - `chrome:/tmp/trace.json` — [Chrome Trace Event
48
+ JSON](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU),
49
+ written at exit. Open it in Perfetto or `about:tracing`: React commits
50
+ and painted frames are slices, requests and errors are instant events —
51
+ so "this interaction sent 4000 requests" becomes "3600 of them were
52
+ `CompositeGlyphs32` inside one commit".
53
+
54
+ Append `+stacks` to `requests` or `chrome` to capture a JS stack per
55
+ request. An X error arrives long after the call that caused it returned,
56
+ and the captured stack is the only thing that maps it back to your code. It
57
+ costs an `Error` capture per request, so it is opt-in.
58
+
59
+ The tracer attaches to a connection _after_ its handshake, so the
60
+ connection-setup packet — the one carrying the X auth cookie — is never
61
+ seen, and payload bytes are never recorded: only opcode, length and
62
+ direction.
63
+
64
+ ### `startTrace()` — the same thing as an API
65
+
66
+ ```js
67
+ import { startTrace } from 'react-x11/debug';
68
+
69
+ const trace = startTrace(); // sink: 'summary' | 'requests' | 'chrome'
70
+ await interaction();
71
+ const { requests, bytesOut, replies, byOpcode } = trace.stop();
72
+ ```
73
+
74
+ With no `app` the trace follows every connection the renderer has open or
75
+ opens later; pass `startTrace({ app })` to trace one specific connection.
76
+ `byOpcode` maps decoded request names — `'CopyArea'`,
77
+ `'Render.CompositeGlyphs32'` — to counts.
78
+
79
+ ## Repaint flashing — `REACT_X11_DEBUG_PAINT`
80
+
81
+ ```sh
82
+ REACT_X11_DEBUG_PAINT=1 npm run examples:dashboard
83
+ ```
84
+
85
+ Every painted frame strokes its damage rects in a colour that rotates per
86
+ frame. A region repainting every frame strobes; one that painted once
87
+ leaves a single quiet outline. This is the browser's "paint flashing" for
88
+ this renderer.
89
+
90
+ ```sh
91
+ REACT_X11_DEBUG_PAINT=full npm run examples:dashboard
92
+ ```
93
+
94
+ Additionally warns — with the invalidation's reasons and the stack of the
95
+ `invalidate()` call that made the frame unbounded — whenever a frame
96
+ degrades to a full-window repaint:
97
+
98
+ ```
99
+ react-x11: full-window repaint (800x600) reasons=props
100
+ Error: invalidated here
101
+ at WindowNode.invalidate (src/nodes/invalidate.js:…)
102
+ at ...
103
+ ```
104
+
105
+ Full repaints are the main performance bug to look for. Expected ones exist
106
+ too — a resize, the first frame after a mount, the backing store being
107
+ invalidated — and their reasons say so.
108
+
109
+ A bounded frame can still be doing the work of an unbounded one: an element
110
+ that draws a whole **scene** into one node — a graph view, a chart, an
111
+ editor — is one node to the damage machinery, so a tight outline around
112
+ what moved can sit over a pane that redrew all of itself inside it. Such an
113
+ element reads the pass's rect and claims its own commits
114
+ ([extending.md](extending.md#drawing-a-scene-into-one-node)).
115
+
116
+ ## `REACT_X11_NO_SCOPED_FLOORS=1`
117
+
118
+ Measures the content floors over the whole tree on every layout change,
119
+ instead of measuring a change inside an absolutely positioned box with its
120
+ own width and height in that box alone, or a change down a column whose
121
+ height is its content's (a document, a log, a feed in a scroll pane) alone
122
+ with the column summed. For comparing the two on the same build, and as
123
+ first aid if a box ever comes out the wrong size after its content changes.
124
+
125
+ ## `REACT_X11_NO_UNREAD_WIDTHS=1`
126
+
127
+ Measures every box's min-content width in the width passes — over the whole
128
+ tree, and over a box measured alone — instead of only the widths a floor is
129
+ written from. By default a leaf whose width nothing reads (a paragraph down
130
+ a column in a document) is not set a word to a line to find one, which
131
+ matters for a large first frame. For comparing the two, and as first aid if
132
+ something in a row ever comes out narrower than its content after the first
133
+ frame.
134
+
135
+ ## `REACT_X11_NO_REFIT_HOLD=1`
136
+
137
+ By default, the frame on which an `'auto'`-sized window's content asks for a
138
+ new size waits for the server's answer ([elements.md](elements.md#natural-size)).
139
+ This switch lays it out at the size it has on that frame instead, so on X11
140
+ that frame shows the new content at the old size — a label that grew wraps,
141
+ and is cut off — for the round trip before the resize lands. For comparing
142
+ the two, and as first aid if an `'auto'` window ever stops repainting after
143
+ its content changes.
144
+
145
+ ## `REACT_X11_NO_SCROLL_BLIT=1`
146
+
147
+ Disables the scroll-blit fast path (a pure scroll copies the surviving band
148
+ and repaints only the exposed strip), so a scroll frame repaints its whole
149
+ viewport again. For measuring the blit against the plain path on the same
150
+ build, and as first aid if a scroll ever misrenders.
151
+
152
+ It also covers the other paths that move retained pixels instead of
153
+ repainting them:
154
+
155
+ - [`scrollContents`](extending.md#panning-a-scene-you-drew), which a pane
156
+ that pans a scene it drew itself calls — so a scene that misrenders while
157
+ panning is one variable away from being told apart from one that misdraws;
158
+ - the `<glarea>` overlay's copy, where a child of a surface that only moved
159
+ is moved on its pane rather than repainted ([elements.md](elements.md#glarea));
160
+ - the window's own copy of a subtree that only moved: a card with a
161
+ background nothing shows through, dragged across a pane, is copied where
162
+ it went and the frame paints the strip it uncovered
163
+ ([extending.md](extending.md#an-element-that-covers-its-box)).
164
+
165
+ With this set, each of them repaints instead.
166
+
167
+ ## `REACT_X11_NO_PANE_SHAPE=1`
168
+
169
+ Leaves every X11 pane over a `<glarea>` the rectangle of its child's reach,
170
+ where by default a pane is cut with the SHAPE extension to what its child
171
+ actually paints ([elements.md](elements.md#glarea)). A rectangle covers the
172
+ GL frame with the surface's `clearColor` wherever the child's reach goes
173
+ past its ink — a `<Flow>` graph's edges stop short of every card and lose
174
+ their arrowheads — so this is for measuring the cut against the rectangle,
175
+ and as first aid if a pane's cut is ever wrong.
176
+
177
+ ## `REACT_X11_STRICT_TOKENS=1`
178
+
179
+ Makes an unresolvable `$token` fatal instead of reported.
180
+
181
+ By default a token the palette in force does not define is dropped, named
182
+ loudly on stderr — token, element, owning component, and every token the
183
+ palette _does_ have — and counted as a failure through `process.exitCode`.
184
+ The widget paints one property short, which is visible, and the rest of the
185
+ app keeps running. (A throw there could not be caught by most boundaries:
186
+ the check runs as a node's ancestry completes, often while React completes
187
+ the `<window>` or from a desktop light/dark switch with no React on the
188
+ stack.)
189
+
190
+ Set this when you would rather stop than paint something wrong — a kiosk, or
191
+ a CI job that should fail at the first bad token rather than at the
192
+ screenshot diff:
193
+
194
+ ```sh
195
+ REACT_X11_STRICT_TOKENS=1 npm run examples:theming
196
+ ```
197
+
198
+ The error is raised from the offending node's own `commitMount`, on its own
199
+ fiber, so an [error boundary](react-features.md) at any depth inside the
200
+ window catches it and renders a fallback for that subtree rather than
201
+ losing the window. With no boundary above it, it reaches `onUncaughtError`
202
+ and the process stops. The one path that cannot be routed is the desktop
203
+ appearance switch; there strict mode crashes.
204
+
205
+ ## `REACT_X11_NO_BOUNDS_CACHE=1`
206
+
207
+ Disables the cached paint reach — the rect of a node unioned with its
208
+ descendants', which a bounded frame asks of every subtree on the way to its
209
+ rect, and which is kept until layout, a child list change, a style swap, a
210
+ state flip or the node's own `invalidate` moves it. With this set every walk
211
+ is fresh, so if a node ever goes missing from a bounded repaint — culled by
212
+ a reach nobody updated — this is the first thing to try, and a fix that
213
+ lands here is a change nobody announced through `invalidate`.
214
+
215
+ ## `REACT_X11_NO_STROKE_CHUNKING=1` (macOS, Windows)
216
+
217
+ The 2d context the Cocoa and win32 backends share splits a stroke with many
218
+ subpaths into several platform calls, which makes a stroke of thousands of
219
+ subpaths many times faster; the antialiased fringe where two subpaths'
220
+ strokes overlap is then half-covered twice rather than covered once, and
221
+ reads a little lighter. This switch hands every stroke to the platform in
222
+ one call. Set it to compare the two on the same build, or as first aid if a
223
+ drawing that strokes thousands of subpaths ever looks wrong at its seams.
224
+ `ctx.strokeChunking = false` is the same switch for one context.
225
+
226
+ Nothing on X11 reads it: there a stroke is one coverage mask, so splitting
227
+ would be a loss.
228
+
229
+ ## `REACT_X11_NO_TRANSPARENCY=1`
230
+
231
+ Makes every display answer the way one with no 32-bit visual does:
232
+ [`transparent`](elements.md#transparent--rounded-corners-and-translucency)
233
+ is ignored, windows are created on the ordinary visual,
234
+ `'@supports transparency'` ([styling.md](styling.md#capability-queries))
235
+ never matches, and `useSupports('transparency')` is false.
236
+
237
+ ```sh
238
+ REACT_X11_NO_TRANSPARENCY=1 npm run examples:tooltips
239
+ ```
240
+
241
+ It lets you see the fallback design — rounded menus, the tooltip's arrow,
242
+ any `@supports` block of your own — without stopping the compositor, which
243
+ would take every other window on the desktop with it. Both halves answer
244
+ this switch at once, so a window never takes the ARGB visual under a
245
+ `useSupports` that said no.
246
+
247
+ What it models is a display with **no 32-bit visual** — XQuartz, a plain
248
+ remote server. The other degraded case, an ARGB window with _nothing
249
+ compositing it_ (a cleared corner comes out black rather than showing the
250
+ desktop), needs a real session with the compositor off, or
251
+ `setCompositingForTests`. Both should render the same.
252
+
253
+ Unlike the switches above it is read per call rather than once at startup —
254
+ when a window is created and when a `@supports` block resolves, never on the
255
+ frame path — so a test can turn it on around a single mount.
256
+
257
+ ## The paint cache — `REACT_X11_NO_PAINT_CACHE`, `REACT_X11_PAINT_CACHE=verify`
258
+
259
+ `<svg>` renders its content once and composites the result on
260
+ later repaints, keyed on _what is drawn_ rather than on which node drew it —
261
+ so a wall of 400 cells holding eight distinct icons keeps eight rendered
262
+ copies, and a repaint of unchanged content is one composite per cell. Your own
263
+ drawings opt in with [`<canvas cacheKey>`](elements.md#cachekey--draw-it-once).
264
+
265
+ Three switches, all read once at startup:
266
+
267
+ - `REACT_X11_NO_PAINT_CACHE=1` disables it entirely, so everything paints
268
+ live. For measuring against the cached path on the same build, and as first
269
+ aid if anything ever renders stale — same role as `REACT_X11_NO_SCROLL_BLIT`.
270
+ - `REACT_X11_PAINT_CACHE=verify` re-renders every cache hit and compares a
271
+ digest of the drawing calls against the one recorded when the entry was
272
+ made. A cache key that fails to name something the drawing reads then says
273
+ so, loudly, at the moment it would otherwise have shown a stale pixel:
274
+
275
+ ```
276
+ react-x11: paint cache key does not cover the paint of <canvas>.
277
+ key: canvas|64x24@1|spark:3
278
+ The drawing changed while the key did not, so a cached frame would show
279
+ stale pixels. Add whatever changed to the key.
280
+ ```
281
+
282
+ Slow by construction — it does all the work the cache exists to avoid, plus
283
+ the comparison. Worth leaving on while developing a `cacheKey`.
284
+
285
+ - `REACT_X11_DEBUG_PAINT_CACHE=1` prints a line per frame: entries, bytes
286
+ held, and hits/misses/renders/evictions. Rising `renders` on a still screen
287
+ means keys that change when they should not; rising `evictions` means the
288
+ working set does not fit the budget.
289
+
290
+ ## Rounded boxes — the corner-glyph fast path
291
+
292
+ On X11, a `fill()` or `stroke()` whose path is exactly one axis-aligned
293
+ `roundRect` on integer geometry — what every `borderRadius` box draws — goes
294
+ out as four cached corner glyphs plus rectangles, with nothing rasterized or
295
+ uploaded in steady state. A box that falls back still renders, identically,
296
+ as a polygon, which on a server without accelerated trapezoids (a
297
+ virtualized GPU) can cost an order of magnitude more per frame. The fallback
298
+ is silent, so the counters below are how you notice it.
299
+
300
+ Design notes and measurements:
301
+ [design/debugging.md](../design/debugging.md#rounded-boxes--the-corner-glyph-fast-path).
302
+
303
+ ### Seeing which boxes took it
304
+
305
+ `REACT_X11_TRACE=summary` ends its histogram with the tally:
306
+
307
+ ```sh
308
+ REACT_X11_TRACE=summary npm run examples:tasks
309
+ # react-x11 trace: 83 requests (15.6KB out), 32 replies, ...
310
+ # 6 Render.CompositeGlyphs8
311
+ # 6 Render.FillRectangles
312
+ # rounded boxes: 6 fast (glyph+rect), 2 fell back (fractional 2)
313
+ ```
314
+
315
+ The same tally is on `trace.stop().shapes` as `{ hits, misses }`, and ntk
316
+ prints a process-wide version under `NTK_DEBUG_SHAPES=1`. In the opcode
317
+ histogram the fast path shows as `Render.CompositeGlyphs8` and
318
+ `Render.FillRectangles`; the fallback as `Render.Trapezoids`,
319
+ `Render.AddTraps` or a `PutImage`-heavy `Render.Composite`.
320
+
321
+ ### Why a box falls back
322
+
323
+ `misses` counts by reason. In rough order of how often they bite:
324
+
325
+ - `fractional` — the box, or the stroke's band, is not on whole pixels:
326
+ usually a box laid out on a half-pixel. Any border width, odd or even,
327
+ takes the fast path.
328
+ - `radius-cap` — the corner radius is over `shapePolicy.maxRadius` (64 by
329
+ default). Also what the off switch reports.
330
+ - `gradient` — the fill or stroke style is not a solid colour.
331
+ - `clip-mask` — a non-rectangular clip is in effect. Rectangular clips are
332
+ fine, and so, on X11, is a rounded box's own `overflow` clip: everything
333
+ inside a rounded card or pane stays on the fast path. The exceptions are a
334
+ box that is corner all the way along a side (a pill), one laid out off the
335
+ pixel grid, and a clip an element sets itself.
336
+ - `dashes` — `borderStyle: 'dashed'`.
337
+ - `transform` — the context's matrix is not translate-only.
338
+ - `composite-op`, `geometry`, `radii-mix`, `join` — a non-`source-over`
339
+ operation, a degenerate or oversized box, mixed per-corner radii on a
340
+ stroke, a non-miter join on a square one.
341
+
342
+ ### Turning it off
343
+
344
+ `app.shapePolicy` sits beside `rasterPolicy` and `textPolicy`:
345
+
346
+ ```js
347
+ const root = await createRoot();
348
+ root.app.shapePolicy = { maxRadius: 0 }; // off; every box goes back to polygons
349
+ root.app.shapePolicy = { maxRadius: 32, cacheBytes: 128 << 10 };
350
+ ```
351
+
352
+ `NTK_NO_SHAPE_GLYPHS=1` does the same from the environment. Both are for
353
+ A/B measurement and for cornering a suspected rendering difference — an
354
+ application should not need to turn it off. The two routes differ only in
355
+ corner antialiasing (under 4/255 for fills, up to 48/255 on a stroked
356
+ corner); every pixel outside the corners is exact, and translucent colours
357
+ are safe.
358
+
359
+ ## Invalidation reasons
360
+
361
+ Every internal `invalidate()` call names why it ran, from a small closed
362
+ set: `props`, `position`, `style-state`, `shadow`, `outline`, `theme`,
363
+ `ink`, `direction`, `scale`, `units`, `animation`, `scroll`, `text`,
364
+ `selection`, `content`, `measure`, `child-list`, `focus`, `caret`,
365
+ `resize`, `mount`, `expose`, `highlight`, `trace-updates`, `capabilities`,
366
+ `layout` (`INVALIDATE_REASONS` in `src/nodes/damage.js` says what each one
367
+ means). A frame's collected reasons are what `REACT_X11_TRACE=requests`
368
+ frame lines, the chrome trace's frame slices and the full-repaint warning
369
+ print. After a frame they are readable on the window node as
370
+ `root._lastReasons` (an instrumentation surface, like
371
+ `root._lastDamageRects` — not public API).