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.
Files changed (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. 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
+ }
@@ -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
+ }