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