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.
- package/README.md +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +86 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/video.js +376 -0
- package/src/wayland/xkb.js +137 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- 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).
|