react-x11 1.2.0 → 2.0.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 +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// The pane process, from hello to exit. `src/frame/child.js` is the file
|
|
2
|
+
// the parent actually forks; everything it does beyond `process.send`
|
|
3
|
+
// plumbing is here, behind an injectable transport — so the tests run a
|
|
4
|
+
// real pane (module, root, window, props, callbacks, close handlers) in
|
|
5
|
+
// process, over a loopback pair, against the in-process X server. The fork
|
|
6
|
+
// boundary itself is the only thing this seam cannot cover, and the only
|
|
7
|
+
// thing the forked entry adds.
|
|
8
|
+
//
|
|
9
|
+
// Order matters three times in this file:
|
|
10
|
+
//
|
|
11
|
+
// - The **update listener goes up before the module import starts.** The
|
|
12
|
+
// import is the slow part of booting a pane, the parent sends updates
|
|
13
|
+
// whenever its commits produce them, and a listener scoped to the
|
|
14
|
+
// mounted tree would drop everything that arrived in between. So the
|
|
15
|
+
// listener is a store the tree subscribes to later: the last update
|
|
16
|
+
// always wins, however early it came.
|
|
17
|
+
//
|
|
18
|
+
// - The module is imported **before** the root is created. A `src` that
|
|
19
|
+
// does not resolve is the commonest way a frame fails, and failing it
|
|
20
|
+
// should not cost an X connection — or worse, report a connection error
|
|
21
|
+
// when the truth is a typo in a path.
|
|
22
|
+
//
|
|
23
|
+
// - `ready` is sent only once the pane's window has an id, which is after
|
|
24
|
+
// the commit that realizes it. The parent embeds on `ready`; an id sent
|
|
25
|
+
// early would have the embedder reparenting a window that does not
|
|
26
|
+
// exist.
|
|
27
|
+
//
|
|
28
|
+
// The window is `<window embeddable>`: created unmapped, because a window
|
|
29
|
+
// waiting to be embedded is unmapped — that is what waiting looks like —
|
|
30
|
+
// and the embedder maps it once it is reparented (ntk's XEmbedSocket).
|
|
31
|
+
|
|
32
|
+
import { isAbsolute } from 'node:path';
|
|
33
|
+
import { pathToFileURL } from 'node:url';
|
|
34
|
+
|
|
35
|
+
import React, { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
36
|
+
|
|
37
|
+
import { createRoot } from '../index.js';
|
|
38
|
+
import { windowIdOf } from '../windowid.js';
|
|
39
|
+
import { registeredFrameContext } from './env.js';
|
|
40
|
+
import { markFramed, runCloseHandlers } from './lifecycle.js';
|
|
41
|
+
import { PROTOCOL, reviveCallbacks } from './protocol.js';
|
|
42
|
+
|
|
43
|
+
const h = React.createElement;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {object} ChildTransport The child's end of the wire.
|
|
47
|
+
* @property {(msg: object) => void} send
|
|
48
|
+
* @property {(cb: (msg: object) => void) => () => void} onMessage
|
|
49
|
+
* @property {(cb: () => void) => () => void} onDisconnect the parent's end
|
|
50
|
+
* closed — there is nobody left to talk to
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** Recreate the bridged providers around the pane's window, outermost
|
|
54
|
+
* first — the parent's own nesting order, which the env Map records and
|
|
55
|
+
* structured clone preserves. A key the pane never registered wraps
|
|
56
|
+
* nothing: no module in this process reads it. Providers registered
|
|
57
|
+
* `innermost` (the theme) wrap directly around the window, inside the
|
|
58
|
+
* rest — see registerFrameProvider (src/frame/env.js) for why the
|
|
59
|
+
* adjacency matters. */
|
|
60
|
+
function wrapEnv(env, inner) {
|
|
61
|
+
const wrap = (tree, [key, value]) => {
|
|
62
|
+
const registered = registeredFrameContext(key);
|
|
63
|
+
if (!registered) return tree;
|
|
64
|
+
return registered.render
|
|
65
|
+
? registered.render(value, tree)
|
|
66
|
+
: h(
|
|
67
|
+
registered.Context.Provider,
|
|
68
|
+
{ value: registered.revive ? registered.revive(value) : value },
|
|
69
|
+
tree,
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
const entries = [...env];
|
|
73
|
+
const isInnermost = ([key]) =>
|
|
74
|
+
registeredFrameContext(key)?.innermost === true;
|
|
75
|
+
let tree = inner;
|
|
76
|
+
for (const entry of entries.filter(isInnermost).reverse()) {
|
|
77
|
+
tree = wrap(tree, entry);
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries.filter((e) => !isInnermost(e)).reverse()) {
|
|
80
|
+
tree = wrap(tree, entry);
|
|
81
|
+
}
|
|
82
|
+
return tree;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function Bridge({ Component, store, rect, invoke, onReady }) {
|
|
86
|
+
const wire = useSyncExternalStore(
|
|
87
|
+
(cb) => {
|
|
88
|
+
store.listeners.add(cb);
|
|
89
|
+
return () => store.listeners.delete(cb);
|
|
90
|
+
},
|
|
91
|
+
() => store.value,
|
|
92
|
+
);
|
|
93
|
+
const snapshot = useMemo(
|
|
94
|
+
() => ({
|
|
95
|
+
props: reviveCallbacks(wire.props ?? {}, invoke),
|
|
96
|
+
env: new Map(wire.env ?? []),
|
|
97
|
+
}),
|
|
98
|
+
[wire, invoke],
|
|
99
|
+
);
|
|
100
|
+
const ref = useRef(null);
|
|
101
|
+
|
|
102
|
+
// The window realizes in the commit this effect follows — but `ready`
|
|
103
|
+
// must carry its id, so a beat where the ref is still empty retries on a
|
|
104
|
+
// fresh task rather than reporting a pane with no window.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
const report = (tries) => {
|
|
108
|
+
if (cancelled) return;
|
|
109
|
+
const windowId = windowIdOf(ref);
|
|
110
|
+
if (windowId) onReady(windowId);
|
|
111
|
+
else if (tries > 0) setTimeout(() => report(tries - 1), 10);
|
|
112
|
+
else onReady(null);
|
|
113
|
+
};
|
|
114
|
+
report(200);
|
|
115
|
+
return () => {
|
|
116
|
+
cancelled = true;
|
|
117
|
+
};
|
|
118
|
+
}, [onReady]);
|
|
119
|
+
|
|
120
|
+
// The bridged providers wrap the *window*, not the pane component — the
|
|
121
|
+
// same position they held in the host. ThemeProvider plants the palette
|
|
122
|
+
// on a window it finds among its children (theme.js, `planted`), and the
|
|
123
|
+
// window is where it has to land: the window's own background follows the
|
|
124
|
+
// palette, and it is the top of the node tree every `$token` beneath
|
|
125
|
+
// resolves through. Mounted inside the window, the palette reached a box
|
|
126
|
+
// and the window kept resolving against the pane process's own desktop —
|
|
127
|
+
// a dark-desktop pane in a light-themed app, wrong in both directions.
|
|
128
|
+
return wrapEnv(
|
|
129
|
+
snapshot.env,
|
|
130
|
+
h(
|
|
131
|
+
'window',
|
|
132
|
+
{
|
|
133
|
+
ref,
|
|
134
|
+
embeddable: true,
|
|
135
|
+
width: Math.max(1, rect?.width ?? 400),
|
|
136
|
+
height: Math.max(1, rect?.height ?? 300),
|
|
137
|
+
},
|
|
138
|
+
h(Component, snapshot.props),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Run the pane: handshake, load `src`, mount its default export, follow
|
|
145
|
+
* updates, and exit when told to (or when the parent goes away).
|
|
146
|
+
*
|
|
147
|
+
* `exit` and `rootOptions` are the test seams: a test passes a loopback
|
|
148
|
+
* transport, a borrowed in-process connection (`rootOptions.app`) and an
|
|
149
|
+
* `exit` that resolves instead of killing the runner.
|
|
150
|
+
*/
|
|
151
|
+
export async function runFrameChild(transport, options = {}) {
|
|
152
|
+
const {
|
|
153
|
+
exit = (code) => process.exit(code),
|
|
154
|
+
rootOptions,
|
|
155
|
+
// The pane process's last resort: an async throw nothing else caught is
|
|
156
|
+
// reported before the exit it forces. Off in the in-process tests,
|
|
157
|
+
// where "the process" is the test runner and trapping its errors would
|
|
158
|
+
// convert a failing test into a politely-reported pane crash.
|
|
159
|
+
trapProcessErrors = true,
|
|
160
|
+
} = options;
|
|
161
|
+
|
|
162
|
+
const fatal = (phase, err) => {
|
|
163
|
+
try {
|
|
164
|
+
transport.send({
|
|
165
|
+
type: 'fatal',
|
|
166
|
+
phase,
|
|
167
|
+
message: err?.message ?? String(err),
|
|
168
|
+
stack: err?.stack,
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
// the channel is gone; the exit code still says what happened
|
|
172
|
+
}
|
|
173
|
+
console.error(`react-x11 frame pane (${phase}):`, err);
|
|
174
|
+
exit(1);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
if (trapProcessErrors) {
|
|
178
|
+
process.on('uncaughtException', (err) => fatal('runtime', err));
|
|
179
|
+
process.on('unhandledRejection', (err) => fatal('runtime', err));
|
|
180
|
+
}
|
|
181
|
+
markFramed();
|
|
182
|
+
|
|
183
|
+
const hello = await new Promise((resolve) => {
|
|
184
|
+
const off = transport.onMessage((msg) => {
|
|
185
|
+
if (msg?.type !== 'hello') return;
|
|
186
|
+
off();
|
|
187
|
+
resolve(msg);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
if (hello.protocol !== PROTOCOL) {
|
|
191
|
+
return fatal(
|
|
192
|
+
'handshake',
|
|
193
|
+
new Error(
|
|
194
|
+
`frame protocol ${hello.protocol} from the host, ${PROTOCOL} here — ` +
|
|
195
|
+
'host and pane are running different react-x11 versions',
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// From here every update is kept, mounted tree or not: last one wins.
|
|
201
|
+
const store = {
|
|
202
|
+
value: { props: hello.props ?? {}, env: hello.env ?? [] },
|
|
203
|
+
listeners: new Set(),
|
|
204
|
+
};
|
|
205
|
+
transport.onMessage((msg) => {
|
|
206
|
+
if (msg?.type !== 'update') return;
|
|
207
|
+
store.value = { props: msg.props ?? {}, env: msg.env ?? [] };
|
|
208
|
+
for (const listener of [...store.listeners]) listener();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
let Component;
|
|
212
|
+
try {
|
|
213
|
+
const url = isAbsolute(hello.src)
|
|
214
|
+
? pathToFileURL(hello.src).href
|
|
215
|
+
: hello.src;
|
|
216
|
+
const mod = await import(url);
|
|
217
|
+
Component = mod.default;
|
|
218
|
+
if (typeof Component !== 'function' && typeof Component !== 'object') {
|
|
219
|
+
throw new Error(`${hello.src} has no default export to mount`);
|
|
220
|
+
}
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return fatal('load', err);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let root;
|
|
226
|
+
try {
|
|
227
|
+
root = await createRoot({
|
|
228
|
+
...(hello.display ? { display: hello.display } : {}),
|
|
229
|
+
...rootOptions,
|
|
230
|
+
onUncaughtError: (err) => fatal('runtime', err),
|
|
231
|
+
});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return fatal('connect', err);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let closing = false;
|
|
237
|
+
const close = async () => {
|
|
238
|
+
if (closing) return;
|
|
239
|
+
closing = true;
|
|
240
|
+
try {
|
|
241
|
+
await runCloseHandlers();
|
|
242
|
+
await root.unmount();
|
|
243
|
+
} catch {
|
|
244
|
+
// exiting is the point; nothing downstream of it to protect
|
|
245
|
+
}
|
|
246
|
+
exit(0);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
transport.onMessage((msg) => {
|
|
250
|
+
if (msg?.type === 'unmount') close();
|
|
251
|
+
});
|
|
252
|
+
transport.onDisconnect(close);
|
|
253
|
+
|
|
254
|
+
root.render(
|
|
255
|
+
h(Bridge, {
|
|
256
|
+
Component,
|
|
257
|
+
store,
|
|
258
|
+
rect: hello.rect,
|
|
259
|
+
invoke: (id, args) => {
|
|
260
|
+
try {
|
|
261
|
+
transport.send({ type: 'invoke', id, args });
|
|
262
|
+
} catch {
|
|
263
|
+
// the host is going away; its shutdown handles the rest
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
onReady: (windowId) => {
|
|
267
|
+
if (windowId) transport.send({ type: 'ready', windowId });
|
|
268
|
+
else fatal('runtime', new Error('the pane window never realized'));
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return { root, close };
|
|
274
|
+
}
|
package/src/frame/env.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// The frame environment: which context values cross into a `<Frame>`.
|
|
2
|
+
//
|
|
3
|
+
// React context cannot cross a process boundary — a context object is
|
|
4
|
+
// process-local, and React has no API for enumerating "the providers above
|
|
5
|
+
// this node". Both halves of that problem are answered here with one
|
|
6
|
+
// structure:
|
|
7
|
+
//
|
|
8
|
+
// - **Identity** is a string key plus a module both sides import. A pane's
|
|
9
|
+
// components read a context by importing the module that created it, and
|
|
10
|
+
// creating it (`createFrameContext`) is what registers it — so by the
|
|
11
|
+
// time the child bridge has a value to recreate, the context it belongs
|
|
12
|
+
// to is in the registry *by construction*. A key that arrives with no
|
|
13
|
+
// registration means the pane never imported the module, which means
|
|
14
|
+
// nothing in the pane reads it: skipped, silently.
|
|
15
|
+
//
|
|
16
|
+
// - **Enumeration** is an accumulator: every bridging provider also merges
|
|
17
|
+
// its value into `FrameEnv`, a single internal context holding a
|
|
18
|
+
// `Map(key → value)`. `<Frame>` reads that one context at its own tree
|
|
19
|
+
// position and gets exactly the values an in-process child would have
|
|
20
|
+
// seen there — and re-renders when any of them changes, which is the
|
|
21
|
+
// entire subscription mechanism. Map insertion order is the parent's
|
|
22
|
+
// provider nesting order (outermost first), v8's structured clone
|
|
23
|
+
// preserves it, and the child recreates providers in that order.
|
|
24
|
+
//
|
|
25
|
+
// What deliberately does not cross: functions. Props run the callback
|
|
26
|
+
// bridge because props are visible per-frame wiring; a context is ambient,
|
|
27
|
+
// and bridging a `{ state, dispatch }` pair would hand live RPC stubs to
|
|
28
|
+
// every pane under the provider — invisible IPC, with an unanswerable
|
|
29
|
+
// question about which incarnation of a restarted child holds which stub.
|
|
30
|
+
// A bridged value that fails structured clone is dropped with a warning
|
|
31
|
+
// naming the key (src/frame/index.js), and dispatchers travel in `props`.
|
|
32
|
+
//
|
|
33
|
+
// The other line this file draws: bridge what the **app** defines, and let
|
|
34
|
+
// each process resolve what the **desktop** defines. Appearance, locale,
|
|
35
|
+
// desktop settings and fonts are read from the environment identically on
|
|
36
|
+
// both sides of the boundary, so bridging them would only add a stale copy.
|
|
37
|
+
// The theme is the one ambient thing the app authors, which is why
|
|
38
|
+
// `ThemeProvider` publishes here by default (src/components/theme.js).
|
|
39
|
+
|
|
40
|
+
import React, { useContext, useMemo } from 'react';
|
|
41
|
+
|
|
42
|
+
const h = React.createElement;
|
|
43
|
+
|
|
44
|
+
const EMPTY = new Map();
|
|
45
|
+
|
|
46
|
+
/** `Map(key → serialized value)` of every bridged context above here. */
|
|
47
|
+
export const FrameEnv = React.createContext(EMPTY);
|
|
48
|
+
|
|
49
|
+
/** The env a `<Frame>` at this position would hand its child. */
|
|
50
|
+
export function useFrameEnv() {
|
|
51
|
+
return useContext(FrameEnv);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* key → how the child recreates the provider:
|
|
56
|
+
* `{ Context, revive }` from `createFrameContext`, or `{ render }` from
|
|
57
|
+
* `registerFrameProvider`. Last registration wins, which is what lets a
|
|
58
|
+
* hot-reloaded module re-register itself without a guard.
|
|
59
|
+
*/
|
|
60
|
+
const registry = new Map();
|
|
61
|
+
|
|
62
|
+
/** The child bridge's lookup (src/frame/childmain.js). */
|
|
63
|
+
export function registeredFrameContext(key) {
|
|
64
|
+
return registry.get(key) ?? null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Register how the child side recreates `key` — for a provider that is more
|
|
69
|
+
* than `Context.Provider`, like `ThemeProvider`, which also plants the
|
|
70
|
+
* palette on a node so `$token` styles resolve. `render(value, children)`
|
|
71
|
+
* returns the wrapped element.
|
|
72
|
+
*
|
|
73
|
+
* `innermost: true` puts the provider directly around the pane's window,
|
|
74
|
+
* inside every other bridged provider. ThemeProvider needs the adjacency:
|
|
75
|
+
* it plants the palette on a window it finds among its *direct* children,
|
|
76
|
+
* and the window is where the palette must land — the window's own
|
|
77
|
+
* background follows it, and the node tree under it is what every `$token`
|
|
78
|
+
* resolves through. Plain context providers have no such constraint and
|
|
79
|
+
* keep the host's nesting order outside.
|
|
80
|
+
*/
|
|
81
|
+
export function registerFrameProvider(key, render, { innermost = false } = {}) {
|
|
82
|
+
registry.set(key, { render, innermost });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Publish one `key → value` into the frame environment. Used by
|
|
87
|
+
* `createFrameContext`'s Provider and by `ThemeProvider`; not part of the
|
|
88
|
+
* public API.
|
|
89
|
+
*/
|
|
90
|
+
export function EnvValue({ k, value, children }) {
|
|
91
|
+
const env = useContext(FrameEnv);
|
|
92
|
+
const next = useMemo(() => new Map(env).set(k, value), [env, k, value]);
|
|
93
|
+
return h(FrameEnv.Provider, { value: next }, children);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* A React context whose value follows the app into its `<Frame>` panes.
|
|
98
|
+
*
|
|
99
|
+
* ```js
|
|
100
|
+
* // contexts.js — imported by the app *and* by the pane's components
|
|
101
|
+
* export const Session = createFrameContext('session', null);
|
|
102
|
+
*
|
|
103
|
+
* // app side
|
|
104
|
+
* <Session.Provider value={{ user, database }}>
|
|
105
|
+
* <Frame src={paneUrl} … />
|
|
106
|
+
* </Session.Provider>
|
|
107
|
+
*
|
|
108
|
+
* // pane side — no wiring; the value was in force where the Frame stood
|
|
109
|
+
* const { user } = Session.use();
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* In-process it is an ordinary context. Across a `<Frame>` the value is
|
|
113
|
+
* snapshotted with structured clone, sent with the frame's props, and
|
|
114
|
+
* recreated as a real Provider around the pane — one direction, data only.
|
|
115
|
+
*
|
|
116
|
+
* `key` names the value on the wire and must be unique per app;
|
|
117
|
+
* `serialize`/`revive` are for values structured clone cannot carry
|
|
118
|
+
* (a class instance), and default to identity.
|
|
119
|
+
*/
|
|
120
|
+
export function createFrameContext(key, defaultValue, options = {}) {
|
|
121
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
122
|
+
throw new Error('react-x11: createFrameContext needs a string key');
|
|
123
|
+
}
|
|
124
|
+
const { serialize, revive } = options;
|
|
125
|
+
const Context = React.createContext(defaultValue);
|
|
126
|
+
registry.set(key, { Context, revive });
|
|
127
|
+
function Provider({ value, children }) {
|
|
128
|
+
return h(
|
|
129
|
+
EnvValue,
|
|
130
|
+
{ k: key, value: serialize ? serialize(value) : value },
|
|
131
|
+
h(Context.Provider, { value }, children),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
key,
|
|
136
|
+
Context,
|
|
137
|
+
Provider,
|
|
138
|
+
use: () => useContext(Context),
|
|
139
|
+
};
|
|
140
|
+
}
|