react-x11 2.51.0 → 2.52.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 +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +10 -3
- 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/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -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/xkb.js +137 -0
package/docs/frame.md
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
# Panes in their own process: `<Frame>`
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
import { Frame } from 'react-x11';
|
|
5
|
+
|
|
6
|
+
<Frame
|
|
7
|
+
src={new URL('./charts.pane.js', import.meta.url)}
|
|
8
|
+
props={{ rows, range, onPick }}
|
|
9
|
+
style={{ flexGrow: 1, backgroundColor: '$surface' }}
|
|
10
|
+
fallback={({ error, restart }) => <Crashed error={error} onRetry={restart} />}
|
|
11
|
+
/>;
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`<Frame>` mounts a module of your application in a **process of its own** and
|
|
15
|
+
embeds its output here, laid out like any other child. It is this package's
|
|
16
|
+
iframe: the same composition of a display boundary and a process boundary,
|
|
17
|
+
behind one element, and it works on X11, Cocoa and win32. The
|
|
18
|
+
[Wayland backend](wayland-backend.md) has neither of the mechanisms below, so
|
|
19
|
+
a `<Frame>` there fails at once: `fallback` renders and no pane is started.
|
|
20
|
+
|
|
21
|
+
How the display boundary is drawn differs, and it is worth knowing which one
|
|
22
|
+
you are on when reading a trace:
|
|
23
|
+
|
|
24
|
+
- **X11** — the pane renders into a real window of its own and the host
|
|
25
|
+
embeds it over [`<foreign>`](embedding.md). Two boundaries that already
|
|
26
|
+
existed separately, with `<Frame>` as the contract between them.
|
|
27
|
+
- **Cocoa** — there is no server to share, so the pane paints into
|
|
28
|
+
`IOSurface`s created shared and presents by message; the host points one
|
|
29
|
+
sublayer of its window at whichever surface the pane last presented. The
|
|
30
|
+
pane has no `NSWindow` at all, and runs no AppKit. Every other window it
|
|
31
|
+
makes — a menu, a dropdown's sheet, a tooltip, a dialog — it paints the
|
|
32
|
+
same way, and the host shows in an `NSWindow` of its own (see "Sizing,
|
|
33
|
+
stacking, input").
|
|
34
|
+
- **Windows** — the same shape as Cocoa with a **DirectComposition surface
|
|
35
|
+
handle** in the IOSurface's place: the pane makes the handle, draws into it
|
|
36
|
+
through a composition swapchain, and duplicates it into the host, which
|
|
37
|
+
gives it to a visual in its own window. The pane has no HWND.
|
|
38
|
+
[windows-embedding.md](windows-embedding.md) has the reasoning; the one
|
|
39
|
+
thing worth knowing from here is that the handle is named **once**, not per
|
|
40
|
+
frame — after that the compositor scans out of whatever the pane last
|
|
41
|
+
presented, so a pane frame costs no message, no fence and no copy.
|
|
42
|
+
|
|
43
|
+
Either way the host owns layout and hit testing and the pane owns its
|
|
44
|
+
drawing, which is the line that makes this **CPU offloading, not
|
|
45
|
+
isolation**.
|
|
46
|
+
|
|
47
|
+
What the process boundary buys is the part no amount of careful coding buys
|
|
48
|
+
in-process:
|
|
49
|
+
|
|
50
|
+
- **Parallelism where this framework is CPU-bound.** The pane's
|
|
51
|
+
reconciliation, layout, text shaping and painting run on another core. A
|
|
52
|
+
pane that grinds for 400ms does not delay the shell's input handling by a
|
|
53
|
+
microsecond — `npm run examples:frame` makes the difference visible with a
|
|
54
|
+
ticker you can watch freeze, or not.
|
|
55
|
+
- **Crash isolation.** An uncaught throw, a native-module crash, an OOM: the
|
|
56
|
+
pane dies, the `fallback` renders, `restart()` respawns. The shell never
|
|
57
|
+
knew.
|
|
58
|
+
- **Leak isolation.** A pane that leaks is a killable leak, bounded by the
|
|
59
|
+
heap its `execArgv` gives it
|
|
60
|
+
([The pane process](#the-pane-process-flags-and-environment)), and its GC
|
|
61
|
+
pauses are its own.
|
|
62
|
+
|
|
63
|
+
And what it does not buy, said once here and again in
|
|
64
|
+
[security.md](security.md): **it is not a security boundary.** On X11 the
|
|
65
|
+
pane holds a full-privilege connection to the same X server — it can read
|
|
66
|
+
keys, take screenshots, move windows — and on Cocoa it is an ordinary child
|
|
67
|
+
process of your application with your file system and your network.
|
|
68
|
+
`<Frame>` contains a pane's _failures_, not its intentions; code you would
|
|
69
|
+
not run in-process is code you should not run at all.
|
|
70
|
+
|
|
71
|
+
## The pane module
|
|
72
|
+
|
|
73
|
+
`src` names a module whose **default export is the pane's root component**:
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
// charts.pane.js
|
|
77
|
+
export default function ChartsPane({ rows, range, onPick }) {
|
|
78
|
+
return <box style={{ flexGrow: 1 }}>…</box>;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pass `src` as `new URL('./charts.pane.js', import.meta.url)` or an absolute
|
|
83
|
+
path — a relative string is refused, because it would resolve against
|
|
84
|
+
react-x11's own files rather than yours.
|
|
85
|
+
|
|
86
|
+
A pane module is an ordinary component module. The same file can run inline
|
|
87
|
+
(`import ChartsPane from './charts.pane.js'` and render it), be its own
|
|
88
|
+
application, or be a pane — `isFramed()` answers which, and the examples'
|
|
89
|
+
autorun guard is where to ask:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { isFramed } from 'react-x11';
|
|
93
|
+
|
|
94
|
+
if (!isFramed() && !process.env.REACT_X11_NO_AUTORUN) {
|
|
95
|
+
const root = await createRoot();
|
|
96
|
+
root.render(<ChartsPane {...defaults} />);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The pane process inherits the parent's `execArgv`, which is what carries a
|
|
101
|
+
dev loader (tsx, the refresh loader) into it during development; a bundled
|
|
102
|
+
application forks plain JavaScript and needs none. It connects to the
|
|
103
|
+
display the host names in `display`, or `$DISPLAY` like everything else.
|
|
104
|
+
What else it is started with is the `<Frame>`'s to say — see
|
|
105
|
+
[The pane process](#the-pane-process-flags-and-environment).
|
|
106
|
+
|
|
107
|
+
On macOS a pane stays on its process's main thread, where an app moves onto
|
|
108
|
+
a worker ([macos.md](macos.md#js-on-a-worker-a-ui-thread-of-the-bridges-own)):
|
|
109
|
+
it has no AppKit for that thread to keep, and the IPC channel it talks to
|
|
110
|
+
its host over is that thread's — a worker has no `process.send`. That holds
|
|
111
|
+
under `--import react-x11/cocoa-main` too, which a pane inherits whenever its
|
|
112
|
+
host was started under it.
|
|
113
|
+
|
|
114
|
+
Nor is a macOS pane an app of its own: it launches with AppKit's
|
|
115
|
+
`prohibited` activation policy, so it has no Dock tile and no ⌘-Tab entry
|
|
116
|
+
and is never the active app — its host's window has the focus and forwards
|
|
117
|
+
what the pane is owed. Launched with the bridge's default, `regular`, every
|
|
118
|
+
pane was another `node` in the Dock beside its host, one for each tab of the
|
|
119
|
+
components' browser example. A root that names `cocoa.activationPolicy`
|
|
120
|
+
itself still has the one it names.
|
|
121
|
+
|
|
122
|
+
## Props: a bag of data, and stubs for the functions in it
|
|
123
|
+
|
|
124
|
+
`props` crosses by **structured clone** — Dates, Maps, TypedArrays and
|
|
125
|
+
cycles survive; component instances, refs and React elements do not. One
|
|
126
|
+
message goes out per parent commit that changed the bag (a shallow
|
|
127
|
+
`Object.is` pass decides "changed", so a parent re-rendering without
|
|
128
|
+
touching the pane's inputs sends nothing), and props and bridged context
|
|
129
|
+
travel in the **same message**, so a theme flip and the state change that
|
|
130
|
+
caused it land in the pane as one commit rather than a torn pair.
|
|
131
|
+
|
|
132
|
+
Functions anywhere in the bag become **fire-and-forget stubs** on the pane
|
|
133
|
+
side: calling one sends its arguments back and returns `undefined`. The
|
|
134
|
+
arguments are sanitized on the way — functions inside them are dropped, the
|
|
135
|
+
data around them kept — because a pane calls `onPick(item, ev)` the way it
|
|
136
|
+
would call any handler, and `ev` is full of methods.
|
|
137
|
+
|
|
138
|
+
Callback identity follows function identity: a handler the parent keeps
|
|
139
|
+
stable (`useCallback`) keeps its id across updates, and an id from the
|
|
140
|
+
previous update still lands — one update of grace, which is what covers a
|
|
141
|
+
click racing a props change. A stub stashed outside the props flow and
|
|
142
|
+
called two updates later is dropped with a warning, not delivered to the
|
|
143
|
+
wrong function.
|
|
144
|
+
|
|
145
|
+
Two rules of thumb that keep the wire small:
|
|
146
|
+
|
|
147
|
+
- **The pane owns its data.** Props are for queries, ids, ranges — the pane
|
|
148
|
+
fetches its own rows, the way an iframe fetches its own resources.
|
|
149
|
+
Shipping a dataset through `props` on every commit works, and is the
|
|
150
|
+
slowest possible way to have the pane know it.
|
|
151
|
+
- **The pane owns its animation.** An update is an IPC hop plus a pane
|
|
152
|
+
commit: the right price for "show this now", the wrong one for driving a
|
|
153
|
+
spinner at 60fps from the host.
|
|
154
|
+
|
|
155
|
+
## Context: bridged explicitly, theme by default
|
|
156
|
+
|
|
157
|
+
React context cannot cross a process boundary, and most of it should not
|
|
158
|
+
try: appearance, locale, desktop settings and fonts are resolved from the
|
|
159
|
+
desktop identically on both sides, so each process just asks. The rule the
|
|
160
|
+
bridge implements: **bridge what the app defines; let each process resolve
|
|
161
|
+
what the desktop defines.**
|
|
162
|
+
|
|
163
|
+
The theme is the one ambient thing the app authors, so it crosses **by
|
|
164
|
+
default**: a `<ThemeProvider>` above a `<Frame>` reaches the pane as a real
|
|
165
|
+
`ThemeProvider` around it — both routes, `useTheme()` and `$token` styles —
|
|
166
|
+
and it is in the pane's very first commit, so there is no frame of default
|
|
167
|
+
palette first. The bridged provider wraps the pane's **window** itself, the
|
|
168
|
+
position it held in the host: the window's own background follows the app's
|
|
169
|
+
palette rather than the pane process's desktop, and the window is the top
|
|
170
|
+
of the node tree every `$token` beneath it resolves through — so a theme
|
|
171
|
+
toggle in the host moves the pane's background and its static text, not
|
|
172
|
+
only what re-renders through `useTheme()`. With no provider above the
|
|
173
|
+
frame, nothing is bridged and the pane follows the desktop by itself: same
|
|
174
|
+
answer, no bridge. A pane that wants its own look mounts its own provider,
|
|
175
|
+
which wins below the bridged one the way an inner provider always wins.
|
|
176
|
+
|
|
177
|
+
For a context of your own, create it with `createFrameContext` in a module
|
|
178
|
+
**both sides import** — which the pane already does, since its components
|
|
179
|
+
read the context from it. The import is what registers the key, so identity
|
|
180
|
+
needs no wiring:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
// contexts.js — imported by the app and by the pane's components
|
|
184
|
+
import { createFrameContext } from 'react-x11';
|
|
185
|
+
export const Session = createFrameContext('session', null);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```jsx
|
|
189
|
+
// app side: an ordinary provider that also publishes to frames below it
|
|
190
|
+
<Session.Provider value={{ user, workspace }}>
|
|
191
|
+
<Frame src={paneUrl} … />
|
|
192
|
+
</Session.Provider>;
|
|
193
|
+
|
|
194
|
+
// pane side: nothing to wire
|
|
195
|
+
const { user } = Session.use();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
In-process it is an ordinary React context. Across a frame the value is
|
|
199
|
+
snapshotted, sent with the props, and recreated as a provider around the
|
|
200
|
+
pane — providers nest in the parent's own order, and a frame inside a pane
|
|
201
|
+
re-bridges automatically, because the pane's providers republish what they
|
|
202
|
+
received.
|
|
203
|
+
|
|
204
|
+
**Bridged values are data.** A value that will not structured-clone is
|
|
205
|
+
dropped with a warning naming the key, and that is deliberate: a bridged
|
|
206
|
+
`{ state, dispatch }` would hand live RPC stubs to every pane under the
|
|
207
|
+
provider — invisible wiring, with an unanswerable question about which
|
|
208
|
+
incarnation of a restarted pane holds which stub. Dispatchers travel in
|
|
209
|
+
`props`, where the wiring is visible. `serialize`/`revive` options on
|
|
210
|
+
`createFrameContext` cover a value that is data in spirit but a class in
|
|
211
|
+
shape.
|
|
212
|
+
|
|
213
|
+
`bridge` is the off switch the default owes: `bridge={false}` bridges
|
|
214
|
+
nothing, `bridge={['react-x11:theme', 'session']}` is an allowlist.
|
|
215
|
+
|
|
216
|
+
One honest limitation: the pane commits an IPC hop after the host, so a
|
|
217
|
+
theme flip repaints the shell a frame before the panes. Every asynchronous
|
|
218
|
+
boundary has this; nothing here pretends otherwise.
|
|
219
|
+
|
|
220
|
+
## Lifecycle, from both sides
|
|
221
|
+
|
|
222
|
+
The host's view:
|
|
223
|
+
|
|
224
|
+
- **`onStarted({ pid, windowId })`** — the pane mounted; its window is
|
|
225
|
+
being embedded.
|
|
226
|
+
- **`onExit({ code, signal, expected })`** — the pane process ended.
|
|
227
|
+
`expected` is `true` when this side asked (unmount, `restart()`, a `src`
|
|
228
|
+
change) — and the event still fires after the frame unmounted, because
|
|
229
|
+
the exit a graceful close ends in always arrives after the unmount that
|
|
230
|
+
asked for it.
|
|
231
|
+
- **`fallback`** — what the rect shows when the pane crashed or could not
|
|
232
|
+
start: an element, or `({ error, restart }) => …`. `error.phase` says
|
|
233
|
+
where it went wrong: `spawn`, `load` (the commonest — a bad `src`),
|
|
234
|
+
`connect`, `handshake` (version skew), `runtime`, `send`, `embed`,
|
|
235
|
+
`unresponsive` (see [A pane that stops answering](#a-pane-that-stops-answering)),
|
|
236
|
+
or plain `exit`.
|
|
237
|
+
- **`ref`** — `{ restart(), pid }`.
|
|
238
|
+
|
|
239
|
+
The pane's view:
|
|
240
|
+
|
|
241
|
+
- **`useFrameClose(handler)`** — the host is letting the pane go: the
|
|
242
|
+
`<Frame>` unmounted, or the host app is exiting. The handler may return a
|
|
243
|
+
promise and may still call callback props — flushing through one is what
|
|
244
|
+
it is for. The patience is real but bounded: the host asks by message,
|
|
245
|
+
escalates to SIGTERM at 1.5s, SIGKILL at 4s. Not called on a crash; a
|
|
246
|
+
close handler is a courtesy, and anything that must survive a crash
|
|
247
|
+
belongs on disk before the close.
|
|
248
|
+
- **`isFramed()`** — see above.
|
|
249
|
+
- A host that dies without asking takes the IPC channel with it, and the
|
|
250
|
+
pane exits on the disconnect — no orphans either way.
|
|
251
|
+
|
|
252
|
+
## Sizing, stacking, input
|
|
253
|
+
|
|
254
|
+
The embedded pane is a [`<foreign>`](embedding.md), and its rules apply
|
|
255
|
+
verbatim. The rect comes from `style` and yoga like any child — there is no
|
|
256
|
+
content-driven sizing across the boundary. The pane's window is a real X
|
|
257
|
+
child window, so **host-drawn content cannot overlap it**; a HUD or a
|
|
258
|
+
"reconnecting…" banner belongs in a sibling `<popup>`. Pointer events over
|
|
259
|
+
the pane are the pane's without the host ever seeing them; keys follow
|
|
260
|
+
[embedding.md's focus rules](embedding.md#focus-and-who-gets-the-keys) —
|
|
261
|
+
the host's handlers see every key first, and what they do not consume is
|
|
262
|
+
forwarded.
|
|
263
|
+
|
|
264
|
+
On Cocoa and Windows the pointer can never be over a pane's window,
|
|
265
|
+
because the pane has none. The host hit-tests the frame's box and forwards
|
|
266
|
+
the input over the channel. The cursor travels the other way. The pane's
|
|
267
|
+
tree names its cursor as a window's would, the pane sends it
|
|
268
|
+
(`pane-cursor`), and the host shows it over the box. It is the box's
|
|
269
|
+
default cursor, so a `cursor` in the frame's `style` still wins. On X11 the
|
|
270
|
+
pane's own window carries its cursor, as any window does. Keys go over as
|
|
271
|
+
the box's default action, as `<foreign>` forwards them, so a chord the host
|
|
272
|
+
answered above the frame never reaches the pane, and with their modifiers.
|
|
273
|
+
|
|
274
|
+
**Tab goes through a pane as through any subtree.** The frame is one stop in
|
|
275
|
+
the host's order, and the pane's own order hangs off it: Tab arriving at the
|
|
276
|
+
frame goes on into the pane at its first stop, or its last for Shift+Tab;
|
|
277
|
+
Tab while the frame has the focus is the pane's; and the pane running off an
|
|
278
|
+
end of its order hands Tab back, and the host goes on from the frame. The
|
|
279
|
+
pane lets go of its focus as it does, so the stop it was at shows no ring
|
|
280
|
+
while the host's next one does. A modal open in the pane keeps Tab going
|
|
281
|
+
round inside it. On X11 this is XEmbed's own chain — `FOCUS_IN` with
|
|
282
|
+
`FOCUS_FIRST` or `FOCUS_LAST` one way, `FOCUS_NEXT` or `FOCUS_PREV` the other,
|
|
283
|
+
which the pane's window answers and sends as an XEmbed client
|
|
284
|
+
(`_initXEmbedTab`). On Cocoa and Windows it is the same two messages over the
|
|
285
|
+
channel, `tabenter` and `pane-tab-out` (`EventManager._tabEnter`,
|
|
286
|
+
`_tabOut`). Before it, the host sent Tab on and cycled its own focus as
|
|
287
|
+
well, so Tab went between one of the pane's stops and one of the host's, and
|
|
288
|
+
on X11 Tab went round the pane's stops for good.
|
|
289
|
+
|
|
290
|
+
**A pane is drawn at the host's scale.** A pane process reads the screens
|
|
291
|
+
when it starts, and the host read them when it did. A desk that changed in
|
|
292
|
+
between gave the two different answers: a page forked for a new tab after
|
|
293
|
+
the desk went to 1x (a mirror set for a projector) was laid out at 1x in a
|
|
294
|
+
frame the host showed at 2x. Everything in it came out half the size of the
|
|
295
|
+
tabs beside it, and every click it was sent landed twice as far in. So the
|
|
296
|
+
host says what scale its window is drawn at, in the hello and in every
|
|
297
|
+
`pane-rect` (`displayScale`), and a Cocoa or Windows pane takes that one
|
|
298
|
+
instead of its own. Its window, its fonts and the windows it opens are made
|
|
299
|
+
at it from the first commit, and a later `pane-rect` that names another
|
|
300
|
+
scale lays the pane out again in the new unit. A `<box scale>` around the
|
|
301
|
+
`<Frame>` is not part of it: the frame's box grows with the zoom, and the
|
|
302
|
+
pane lays out at the window's scale in the room it was given.
|
|
303
|
+
|
|
304
|
+
**A pane's other windows.** On X11 a `<popup>` in a pane is a real window on
|
|
305
|
+
the pane's own connection. A Cocoa pane process runs no AppKit, so a window
|
|
306
|
+
made there exists and is never shown — a `<Select>` in a pane took the press
|
|
307
|
+
and opened nothing (#824). So the pane paints each window it makes besides
|
|
308
|
+
its own into a shared ring, as it paints itself (`CocoaPaneSubwindow`,
|
|
309
|
+
`src/cocoa/panewindow.js`), and the host makes the `NSWindow` and shows the
|
|
310
|
+
ring in it (`src/cocoa/panehost.js`):
|
|
311
|
+
|
|
312
|
+
- **Where it goes is the pane's to say.** Every `pane-rect` carries
|
|
313
|
+
`screen`, where the frame's box is on the screen in device pixels, sent
|
|
314
|
+
again when the box or the window moves. A popup the pane anchors to one of
|
|
315
|
+
its nodes is placed against it exactly as a window's popup is
|
|
316
|
+
(`windowOrigin`, src/anchor.js), and the host puts the window where the
|
|
317
|
+
pane says.
|
|
318
|
+
- **The window's life goes up, its input comes down.** `pane-window`
|
|
319
|
+
messages make, map, move, resize, grab with and destroy it, and its
|
|
320
|
+
`pane-present`s and `pane-cursor`s name it. Input on the host's window for
|
|
321
|
+
it goes back as `pane-event`s naming it. A grab is the host's, so a press
|
|
322
|
+
anywhere in the host's windows reaches the menu outside its bounds, which
|
|
323
|
+
is the dismissal. A managed window — a `<Dialog>` — also sends back the
|
|
324
|
+
user's moves and resizes and its close button.
|
|
325
|
+
- **The window's focus goes down too.** The host's window gaining or losing
|
|
326
|
+
it is sent to the pane as `focus`/`blur`, so a menu that closes when its
|
|
327
|
+
window loses focus (`useDismissOnWindowBlur`) closes when the user goes to
|
|
328
|
+
another application.
|
|
329
|
+
- **Nothing waits on the host until it is listening.** These messages are
|
|
330
|
+
held until the host's first `pane-rect`: it hears a pane from the moment
|
|
331
|
+
it has a `<Frame>`, and knows what to do with them only once the pane is
|
|
332
|
+
laid out in it. A window is then announced where it is, after that rect
|
|
333
|
+
has placed what is anchored to the pane again.
|
|
334
|
+
- **The pane's windows go with it.** A pane that exits or unmounts takes
|
|
335
|
+
every window the host shows for it, and their grab.
|
|
336
|
+
|
|
337
|
+
**A pane's videos.** A Cocoa pane runs no AppKit, so a player it made would
|
|
338
|
+
open the item and never hear AVFoundation say a word — a `<video src>` in a
|
|
339
|
+
pane sat on its poster and could not be started. So the host plays it
|
|
340
|
+
(`src/cocoa/paneplayer.js`): the hello tells the pane the host plays for
|
|
341
|
+
it, which is what makes `useSupports('mediaPlayback')` true there, and the
|
|
342
|
+
pane's `<video>` holds a player whose verbs go up as `pane-player` messages
|
|
343
|
+
and whose events come back down. The picture comes down the way the pane's
|
|
344
|
+
own goes up, the other way round: the host copies the frame showing into
|
|
345
|
+
one of a ring of three shared IOSurfaces and names it, and the pane copies
|
|
346
|
+
it out into the element's surface. A pane has no layers to lift a part
|
|
347
|
+
onto, so its video is always drawn, which costs the copy a covered video
|
|
348
|
+
costs in a window — the frame once into the ring, once out of it. A host
|
|
349
|
+
over a bridge with no player says nothing in the hello, and the pane
|
|
350
|
+
refuses `src` as X11 does. A pane that exits takes its players, and their
|
|
351
|
+
sound, with it.
|
|
352
|
+
|
|
353
|
+
**A pane resized.** On Cocoa the host gives the pane's layer its new size as
|
|
354
|
+
the window lays out, and the pane's frame of that size comes a pane frame
|
|
355
|
+
later. Core Animation's default stretched the frame the layer had to the new
|
|
356
|
+
bounds in between, so a page in a pane had its left column drawn scaled a
|
|
357
|
+
little at every step of a window dragged wider, and then drawn back at its
|
|
358
|
+
size. A pane whose frames of a new size come within the window's
|
|
359
|
+
live-resize budget, `createRoot({ cocoa: { resizeWait } })` (50 ms by
|
|
360
|
+
default), has its last frame shown at its size from the top left instead:
|
|
361
|
+
cropped on an axis the layer shrank on, and on one it grew on, its last
|
|
362
|
+
pixel carried over the rest — the last column across a window dragged
|
|
363
|
+
wider, the last row down one dragged taller. A page's edge is mostly its
|
|
364
|
+
background, so the strip continues it, a gradient included, where the
|
|
365
|
+
`<Frame>`'s own background showed through before. Where the edge crosses
|
|
366
|
+
content, a bottom row through a line of text, that content runs down the
|
|
367
|
+
strip in thin lines for the frame it lasts. One that falls behind the
|
|
368
|
+
budget is stretched across, which is the better picture of a long wait: a
|
|
369
|
+
width moves where a page's lines break, and a squeezed page is nearer to the
|
|
370
|
+
page laid out again than one cut off with a strip beside it. Down the pane
|
|
371
|
+
it stays at its size, cropped or its last row carried down, since a height
|
|
372
|
+
moves nothing a page lays out, and stretched down it was the page drawn too
|
|
373
|
+
tall for as long as the wait lasted. Behind is
|
|
374
|
+
judged by the frames that land: two in a row later than the budget, or none
|
|
375
|
+
at all for longer than it, and a frame well inside it shows the next at its
|
|
376
|
+
size again. A size the pane is never sent, because a newer one replaced it
|
|
377
|
+
while a frame was being painted, is no frame late, and a fast drag of a
|
|
378
|
+
page that keeps up stays at its size rather than taking one look at every
|
|
379
|
+
step and the other at every frame. Each gesture starts out at its size, and
|
|
380
|
+
`resizeWait: 0` stretches always, both ways, as before. An app can choose
|
|
381
|
+
apart from the budget, which is also how long AppKit holds the window's
|
|
382
|
+
edge for a frame: `createRoot({ cocoa: { paneStretch } })`, or
|
|
383
|
+
`REACT_X11_COCOA_PANE_STRETCH`, is `behind` for all of the above, `never`
|
|
384
|
+
for a last frame kept at its size and its edge carried over however late
|
|
385
|
+
the pane is, and `always` for one stretched both ways. And the pane hears that the window is being resized
|
|
386
|
+
live — AppKit's drag of its edge, from its begin to its end, which a pane
|
|
387
|
+
running no AppKit has no window to hear — with every size and on its own
|
|
388
|
+
where only that changes (`pane-rect`'s `live`, `pane-live`): its window's
|
|
389
|
+
`liveResizing` is the host's, so core defers measuring its content floors
|
|
390
|
+
for the drag's length and catches up once it ends, as a window of its own
|
|
391
|
+
does, and an element can put off what it would rather do once the drag is
|
|
392
|
+
over.
|
|
393
|
+
|
|
394
|
+
**A pane's frames are named by id, and the id outlives nothing.** On Cocoa
|
|
395
|
+
a present carries the IOSurface's id, and the host looks it up when it
|
|
396
|
+
reads the message. A resize has the pane make a new ring of buffers, and
|
|
397
|
+
the system gives a freed id to the next surface anyone makes — the pane's
|
|
398
|
+
next ring, another tab's, the host's own window. A present still in the
|
|
399
|
+
channel when its ring was freed was looked up as whichever surface had
|
|
400
|
+
the id by then: the pane's next frame cleared or half drawn, or another
|
|
401
|
+
tab's page, on glass for a frame at every few steps of a drag. So the host
|
|
402
|
+
answers each present it has looked up (`pane-shown`), and the pane keeps a
|
|
403
|
+
buffer it retired until the present naming it is answered: that is one
|
|
404
|
+
buffer as a rule, and never more than a ring of them, the oldest freed
|
|
405
|
+
first if the host stops answering. A pane's video comes down the same way
|
|
406
|
+
round: the pane answers each frame once it has looked its buffer up, and
|
|
407
|
+
the host keeps a buffer of a ring it retired — a stream that switched
|
|
408
|
+
renditions — until the frame naming it is answered. A lookup that finds a
|
|
409
|
+
surface of another size than its frame says is not copied out of at all.
|
|
410
|
+
|
|
411
|
+
**A pane draws only into a buffer the host is done with.** The ring holds
|
|
412
|
+
three buffers so that one present can be on its way while the next frame
|
|
413
|
+
draws: one on the layer, one named in the channel, one free. Each is made
|
|
414
|
+
the first time a frame takes it, copied over from the frame before: a drag
|
|
415
|
+
gives each size one frame, and a ring made whole at each was two buffers
|
|
416
|
+
made and cleared for nothing, 4.4 ms of every 12 ms frame of a drag of Zen
|
|
417
|
+
Garden 101 in the browser example on a Mac. A host two
|
|
418
|
+
presents behind — busy with a long frame of its own, a breakpoint's layout,
|
|
419
|
+
a window resize — left the next frame nothing but the buffer on the layer or
|
|
420
|
+
the one a present still in the channel named, and the frame drew into it
|
|
421
|
+
anyway: the host then showed a frame half drawn. So a frame takes a buffer
|
|
422
|
+
only once the host has answered a present after the last one that named it,
|
|
423
|
+
and waits on the clock for the next answer when there is none. An answer
|
|
424
|
+
says the host has looked the present up, not that the screen shows it, and
|
|
425
|
+
a host back from a long frame has the buffer it showed throughout still on
|
|
426
|
+
glass until its UI thread commits: so of the buffers the host is done with,
|
|
427
|
+
a frame takes one the WindowServer has let go of too, and waits for one, a
|
|
428
|
+
frame interval at most after the answer. A host that has not answered for
|
|
429
|
+
250 ms no longer holds the pane up.
|
|
430
|
+
|
|
431
|
+
The same answer gates the frame an input gets on the spot
|
|
432
|
+
(`flushPendingFrames`): a pane paints on an input only once its last present
|
|
433
|
+
is answered and a frame interval has passed since it went, and otherwise the
|
|
434
|
+
next frame on the clock answers it. The interval costs an input a few
|
|
435
|
+
milliseconds at the display's rate, and is what keeps a pane at one frame a
|
|
436
|
+
refresh: without it, input faster than the display had a frame each — 245 a
|
|
437
|
+
second on a 120Hz panel, at 250 inputs a second — and the frames that went
|
|
438
|
+
faster than the WindowServer let go of buffers drew into ones it still read.
|
|
439
|
+
|
|
440
|
+
`backgroundColor` in the frame's `style` is what shows before the pane's
|
|
441
|
+
first frame and after one dies — the same server-painted rectangle
|
|
442
|
+
`<foreign>` documents.
|
|
443
|
+
|
|
444
|
+
## The pane process: flags and environment
|
|
445
|
+
|
|
446
|
+
`execArgv` and `env` say what a pane's process is started with, beyond what
|
|
447
|
+
it inherits:
|
|
448
|
+
|
|
449
|
+
```jsx
|
|
450
|
+
<Frame
|
|
451
|
+
src={new URL('./page.pane.js', import.meta.url)}
|
|
452
|
+
// a page that grows without bound costs its own process, at 256 MB
|
|
453
|
+
execArgv={['--max-old-space-size=256']}
|
|
454
|
+
env={{ PAGE_LOG: 'quiet', NODE_OPTIONS: undefined }}
|
|
455
|
+
/>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
- **`execArgv`** comes after the flags the pane inherits, so a dev loader
|
|
459
|
+
still follows it in and a flag it names is the one in force where the two
|
|
460
|
+
disagree. `--max-old-space-size` is the one a pane wants most: it bounds
|
|
461
|
+
the pane's heap, which nothing running inside the pane can, and a pane
|
|
462
|
+
that passes it is ended — and its `fallback` shown — rather than the
|
|
463
|
+
machine swapping. The flags go to the runtime that forks the pane, and are
|
|
464
|
+
its flags: Bun 1.4 takes node's V8 flags and bounds nothing by them, so
|
|
465
|
+
under Bun this is not a heap bound.
|
|
466
|
+
- **`env`** is merged over this process's environment, and a variable named
|
|
467
|
+
with `undefined` is taken away. `DISPLAY` is `display`'s, and
|
|
468
|
+
`REACT_X11_FRAME` is how a pane knows it is one (`isFramed`), so the two
|
|
469
|
+
are set after it and cannot be overridden here.
|
|
470
|
+
|
|
471
|
+
Either is read by value: a pane started with the same flags in a new array
|
|
472
|
+
— one written in place, as above, at every render — is the pane it was, and
|
|
473
|
+
flags or variables that say something else restart it, as a new `src` does.
|
|
474
|
+
Something a fork cannot take — a flag that is not a string — fails as a
|
|
475
|
+
`spawn` error and starts nothing.
|
|
476
|
+
|
|
477
|
+
## A pane that stops answering
|
|
478
|
+
|
|
479
|
+
A pane whose event loop is stuck — a loop that never ends, a page's script
|
|
480
|
+
with no timeout around it — stays `running` as far as anything else can
|
|
481
|
+
tell: it is sent updates it never reads, its window stays up showing its
|
|
482
|
+
last frame, and on X11 it holds the keyboard while the pointer is over it.
|
|
483
|
+
Nothing about it fails, so nothing says so.
|
|
484
|
+
|
|
485
|
+
So the host asks. About once a second it sends the pane a `ping`, and the
|
|
486
|
+
pane answers from its event loop, which is the thing asked about. A pane
|
|
487
|
+
that has not answered for `watchdog` ms — 15 seconds by default — has
|
|
488
|
+
stopped, and goes the way a crashed one goes: the frame fails with phase
|
|
489
|
+
`unresponsive`, `fallback` is shown and can `restart()` it, and the process
|
|
490
|
+
is ended from outside, SIGTERM and then SIGKILL, since it cannot read an
|
|
491
|
+
unmount. `onExit` reports it with `expected: false`.
|
|
492
|
+
|
|
493
|
+
```jsx
|
|
494
|
+
<Frame
|
|
495
|
+
src={new URL('./page.js', import.meta.url)}
|
|
496
|
+
watchdog={10000}
|
|
497
|
+
fallback={({ error, restart }) =>
|
|
498
|
+
error?.phase === 'unresponsive' ? (
|
|
499
|
+
<PageStopped onReload={restart} />
|
|
500
|
+
) : (
|
|
501
|
+
<PageCrashed error={error} onReload={restart} />
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
/>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Three things keep it from ending a pane that is fine:
|
|
508
|
+
|
|
509
|
+
- **The host's own late tick is no news of the pane's.** Where the host's
|
|
510
|
+
timer comes late — the machine slept, or the host itself was stuck — the
|
|
511
|
+
count starts again from that tick, rather than taking the time nobody
|
|
512
|
+
was asking for as time the pane did not answer.
|
|
513
|
+
- **A pane under the inspector is not watched.** `--inspect`,
|
|
514
|
+
`--inspect-brk` or `--inspect-wait`, in its own `execArgv`, in this
|
|
515
|
+
process's that it inherits, or in `NODE_OPTIONS`: a debugger stops a pane
|
|
516
|
+
on purpose. The flags that only set the inspector up, which Node's test
|
|
517
|
+
runner hands every test process, are not it.
|
|
518
|
+
- **`watchdog={false}`** watches nothing, for a pane whose work is long
|
|
519
|
+
and synchronous by design.
|
|
520
|
+
|
|
521
|
+
The ping and the pong are messages of the protocol, so a pane of an older
|
|
522
|
+
react-x11 fails the handshake, which says why, rather than being ended for
|
|
523
|
+
not answering a question it does not know.
|
|
524
|
+
|
|
525
|
+
## What a pane costs
|
|
526
|
+
|
|
527
|
+
A pane is a node process with React and ntk loaded: roughly 40–70MB and
|
|
528
|
+
100–300ms of module graph before its first frame, plus an X connection.
|
|
529
|
+
That is the price of an event loop of one's own — right for a handful of
|
|
530
|
+
heavy panes, wrong as a general componentization strategy. A hundred
|
|
531
|
+
buttons belong in your process; the log viewer that parses gigabytes does
|
|
532
|
+
not.
|
|
533
|
+
|
|
534
|
+
## The transport seam
|
|
535
|
+
|
|
536
|
+
Everything between the host and the pane is eight message types over an
|
|
537
|
+
injectable transport, and `transport` replaces the fork wholesale — it is
|
|
538
|
+
how the tests run a real pane (module, root, window, props, callbacks,
|
|
539
|
+
close handlers) in-process over a loopback pair against the in-process X
|
|
540
|
+
server, with `structuredClone` standing in for the fork's serialization
|
|
541
|
+
(`test/helpers/frame-loopback.js`, and `runFrameChild` in
|
|
542
|
+
`src/frame/childmain.js` is the pane's whole bootstrap behind the same
|
|
543
|
+
seam). It is also the door to running a pane somewhere other than a child
|
|
544
|
+
process without `<Frame>` learning about it.
|
|
545
|
+
|
|
546
|
+
A transport is handed what the fork would be — `{ src, display, execArgv,
|
|
547
|
+
env }` — and a transport that still forks the pane, its own way (under a
|
|
548
|
+
sandbox, with another runtime), forks the entry the default one does, which
|
|
549
|
+
is on the exports map to be resolved: `fileURLToPath(import.meta.resolve(
|
|
550
|
+
'react-x11/frame/child'))`. It is a process's entry, not a module to import:
|
|
551
|
+
outside a fork it says so and exits.
|
|
552
|
+
|
|
553
|
+
## Known gaps
|
|
554
|
+
|
|
555
|
+
Named so they are not rediscovered:
|
|
556
|
+
|
|
557
|
+
- **Accessibility**: a pane registers on the AT-SPI bus as its own
|
|
558
|
+
application. Orca reads it, and Tab crosses the boundary, but the tree is
|
|
559
|
+
not unified under the host's.
|
|
560
|
+
- **DevTools**: a pane is its own React tree; `REACT_X11_DEVTOOLS=1` in the
|
|
561
|
+
pane process attaches a separate DevTools, not a merged one.
|
|
562
|
+
- **No intrinsic size**: the host decides the rect; a `measure` protocol
|
|
563
|
+
could exist and does not yet.
|
|
564
|
+
- **A window a pane opens in its first commit is placed against a guess.**
|
|
565
|
+
The host has not laid the pane out yet, so the pane does not know where
|
|
566
|
+
it is. A popup anchored to a node is placed again when the first
|
|
567
|
+
`pane-rect` comes, before anyone sees it; a `<Dialog>` open from the start
|
|
568
|
+
is centred where the pane guessed it was, as it is not anchored.
|
|
569
|
+
- **Windows** panes do not read `screen` yet: a popup a Windows pane
|
|
570
|
+
anchors is placed against the screen's corner, the second half of what
|
|
571
|
+
#824 fixed on Cocoa. Not run there.
|
|
572
|
+
- **A pane hears its window's focus, not its frame's.** Tab out of a pane
|
|
573
|
+
takes its focus with it, but a click on the host does not: the stop the
|
|
574
|
+
pane had focused keeps its focus, and its ring, while the host's is
|
|
575
|
+
elsewhere. On X11 the `<window embeddable>` underneath (created unmapped,
|
|
576
|
+
waiting for an embedder — see [embedding.md](embedding.md)) speaks
|
|
577
|
+
XEmbed's Tab and no more of its focus handshake — `FOCUS_CURRENT`,
|
|
578
|
+
`FOCUS_OUT` and window activation are left to the forwarding rules above —
|
|
579
|
+
and on Cocoa and Windows the pane is sent the host window's `focus` and
|
|
580
|
+
`blur` alone.
|