react-x11 2.11.0 → 2.13.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 (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +12 -4
  3. package/src/Reconciler.js +19 -31
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/appcontext.js +59 -30
  7. package/src/bootstrap.js +14 -0
  8. package/src/clientmessage.js +1 -1
  9. package/src/cocoa/app.js +303 -49
  10. package/src/cocoa/bezels.js +175 -30
  11. package/src/cocoa/dnd.js +27 -13
  12. package/src/cocoa/fonts.js +3 -3
  13. package/src/cocoa/glarea.js +24 -5
  14. package/src/cocoa/main.d.ts +8 -0
  15. package/src/cocoa/main.js +43 -0
  16. package/src/cocoa/overlay.js +159 -0
  17. package/src/cocoa/panehost.js +15 -5
  18. package/src/cocoa/presenter.js +13 -9
  19. package/src/cocoa/promotion.js +17 -7
  20. package/src/cocoa/relaunch.js +207 -0
  21. package/src/cocoa/threaded.js +246 -0
  22. package/src/cocoa/window.js +256 -42
  23. package/src/components/Select.js +2 -2
  24. package/src/components/anchor.js +3 -3
  25. package/src/components/native.js +12 -7
  26. package/src/components/theme.js +2 -2
  27. package/src/debug.js +1 -1
  28. package/src/decorations.js +1 -1
  29. package/src/editmenu.js +2 -2
  30. package/src/embedding.js +31 -0
  31. package/src/errors.js +46 -0
  32. package/src/events.js +78 -18
  33. package/src/foreignnodes.js +59 -5
  34. package/src/frames.js +2 -2
  35. package/src/glnodes.js +172 -41
  36. package/src/gloverlay.js +383 -0
  37. package/src/grid.js +1653 -0
  38. package/src/host.d.ts +230 -1
  39. package/src/host.js +11 -3
  40. package/src/imagesource.js +1 -1
  41. package/src/index.d.ts +34 -4
  42. package/src/index.js +9 -1
  43. package/src/layouts.js +721 -0
  44. package/src/node.d.ts +16 -3
  45. package/src/node.js +19 -21
  46. package/src/nodes/animation.js +644 -0
  47. package/src/nodes/box.js +21 -0
  48. package/src/nodes/boxpaint.js +473 -0
  49. package/src/nodes/canvas.js +269 -0
  50. package/src/nodes/cascade.js +600 -0
  51. package/src/nodes/damage.js +183 -0
  52. package/src/nodes/edithistory.js +124 -0
  53. package/src/nodes/editmenupopup.js +260 -0
  54. package/src/nodes/hittest.js +185 -0
  55. package/src/nodes/image.js +266 -0
  56. package/src/nodes/install.js +75 -0
  57. package/src/nodes/invalidate.js +465 -0
  58. package/src/nodes/kinds.js +31 -0
  59. package/src/nodes/layout.js +439 -0
  60. package/src/nodes/layouthost.js +949 -0
  61. package/src/nodes/node.js +868 -0
  62. package/src/nodes/paint.js +466 -0
  63. package/src/nodes/position.js +366 -0
  64. package/src/nodes/preedit.js +127 -0
  65. package/src/nodes/queries.js +330 -0
  66. package/src/nodes/rects.js +102 -0
  67. package/src/nodes/scrollable.js +891 -0
  68. package/src/nodes/scrollbars.js +138 -0
  69. package/src/nodes/scrollblit.js +1034 -0
  70. package/src/nodes/selectable.js +142 -0
  71. package/src/nodes/styling.js +225 -0
  72. package/src/nodes/text.js +649 -0
  73. package/src/nodes/textarea.js +391 -0
  74. package/src/nodes/textinput.js +1146 -0
  75. package/src/nodes/util.js +17 -0
  76. package/src/nodes/window/anchoring.js +161 -0
  77. package/src/nodes/window/capabilities.js +190 -0
  78. package/src/nodes/window/debugpaint.js +83 -0
  79. package/src/nodes/window/droptarget.js +145 -0
  80. package/src/nodes/window/floors.js +577 -0
  81. package/src/nodes/window/flush.js +369 -0
  82. package/src/nodes/window/hints.js +482 -0
  83. package/src/nodes/window/listeners.js +222 -0
  84. package/src/nodes/window/popup.js +71 -0
  85. package/src/nodes/window/size.js +591 -0
  86. package/src/nodes/window/window.js +954 -0
  87. package/src/palette.js +1 -1
  88. package/src/registry.js +7 -3
  89. package/src/styles.js +137 -15
  90. package/src/svgnodes.js +2 -1
  91. package/src/testing/harness.js +2 -2
  92. package/src/textselection.js +5 -3
  93. package/src/trace-registry.js +1 -1
  94. package/src/types/components.d.ts +38 -6
  95. package/src/types/elements.d.ts +26 -14
  96. package/src/types/nodes.d.ts +17 -2
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. package/src/nodes.js +0 -13120
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.11.0",
3
+ "version": "2.13.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -15,6 +15,8 @@
15
15
  "examples:app": "tsx examples/app.jsx",
16
16
  "examples:theming": "tsx examples/theming.jsx",
17
17
  "examples:container-queries": "tsx examples/container-queries.jsx",
18
+ "examples:schedule": "tsx examples/schedule.jsx",
19
+ "examples:custom-layout": "tsx examples/custom-layout.jsx",
18
20
  "examples:appearance": "tsx examples/appearance.jsx",
19
21
  "examples:animation": "tsx examples/animation.jsx",
20
22
  "examples:simple": "tsx examples/simple.jsx",
@@ -58,6 +60,7 @@
58
60
  "examples:frame": "tsx examples/frame.jsx",
59
61
  "labs:text-baseline": "tsx examples/labs/text-baseline.jsx",
60
62
  "labs:direct-gl": "tsx examples/labs/direct-gl.jsx",
63
+ "labs:gl-overlay": "tsx examples/labs/gl-overlay.jsx",
61
64
  "examples:viewer3d": "tsx examples/viewer3d.jsx",
62
65
  "examples:transparent": "tsx examples/transparent.jsx",
63
66
  "examples:windows": "tsx examples/windows.jsx",
@@ -74,7 +77,8 @@
74
77
  "docs:build": "npm --prefix website run build",
75
78
  "docs:test": "npm --prefix website test",
76
79
  "bench:presenters": "node --import tsx scripts/bench/presenters.js",
77
- "bench:touched": "node scripts/bench/touched.js"
80
+ "bench:touched": "node scripts/bench/touched.js",
81
+ "bench:grid": "node --import tsx scripts/bench/grid/bench.mjs"
78
82
  },
79
83
  "repository": {
80
84
  "type": "git",
@@ -100,9 +104,9 @@
100
104
  "yoga-layout": "^3.2.1"
101
105
  },
102
106
  "optionalDependencies": {
103
- "@windowkit/appkit": "^0.9.0",
107
+ "@windowkit/appkit": "^0.10.0",
104
108
  "dbus-native": "^0.15.1",
105
- "x11-dri": "^0.7.0"
109
+ "x11-dri": "^0.8.0"
106
110
  },
107
111
  "peerDependencies": {
108
112
  "@babel/core": "^8.0.0",
@@ -206,6 +210,10 @@
206
210
  "./keysyms": {
207
211
  "types": "./src/keysyms.d.ts",
208
212
  "default": "./src/keysyms.js"
213
+ },
214
+ "./cocoa-main": {
215
+ "types": "./src/cocoa/main.d.ts",
216
+ "default": "./src/cocoa/main.js"
209
217
  }
210
218
  },
211
219
  "types": "./src/index.d.ts"
package/src/Reconciler.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // The react-reconciler host config plus the public render entry points.
2
- // Host instances are the retained nodes from nodes.js; only <window> and
2
+ // Host instances are the retained nodes from src/nodes/; only <window> and
3
3
  // <popup> map to real X11 windows (see NEXT_STEPS.md), and those windows
4
4
  // are created top-down in the commit phase (WindowNode.realize) so every
5
5
  // CreateWindow names its actual parent from the start — createInstance
@@ -18,21 +18,19 @@ import {
18
18
  } from './priority.js';
19
19
  import {
20
20
  WindowNode,
21
- PopupNode,
22
- BoxNode,
23
- TextNode,
24
- TextChunkNode,
25
- ImageNode,
26
- CanvasNode,
27
- TextInputNode,
28
- TextAreaNode,
29
- appearanceChanged,
30
21
  beginWindowMaps,
31
22
  flushWindowMaps,
32
23
  flushWindowRestacks,
33
- windowAttributes,
34
- setTextStripBelow,
35
- } from './nodes.js';
24
+ } from './nodes/window/window.js';
25
+ import { PopupNode } from './nodes/window/popup.js';
26
+ import { BoxNode } from './nodes/box.js';
27
+ import { TextNode, TextChunkNode, setTextStripBelow } from './nodes/text.js';
28
+ import { ImageNode } from './nodes/image.js';
29
+ import { CanvasNode } from './nodes/canvas.js';
30
+ import { TextInputNode } from './nodes/textinput.js';
31
+ import { TextAreaNode } from './nodes/textarea.js';
32
+ import { appearanceChanged } from './nodes/cascade.js';
33
+ import { windowAttributes } from './nodes/window/hints.js';
36
34
  import { setFrameRateDefault } from './pacing.js';
37
35
  import { hasDropProps } from './dnd.js';
38
36
  import { AppProvider } from './appcontext.js';
@@ -140,17 +138,18 @@ const HostConfig = {
140
138
  return {
141
139
  isInsideText: false,
142
140
  isInsideSvg: false,
143
- isInside3d: false,
144
141
  };
145
142
  },
146
143
 
147
144
  getChildHostContext(parentHostContext, type) {
145
+ // A `<glarea>`'s children are drawn nodes like any box's — 2D content
146
+ // above the surface (src/gloverlay.js) — so it opens no context of its
147
+ // own. A scene graph over the surface is `@react-x11/components/three`,
148
+ // with a reconciler of its own.
148
149
  return {
149
150
  isInsideText: parentHostContext.isInsideText || type === 'text',
150
151
  // <svg> children are declarative SVG elements, not react-x11 nodes
151
152
  isInsideSvg: parentHostContext.isInsideSvg || type === 'svg',
152
- // inside <glarea> the children are scene nodes, not drawn nodes
153
- isInside3d: parentHostContext.isInside3d || type === 'glarea',
154
153
  };
155
154
  },
156
155
 
@@ -162,7 +161,7 @@ const HostConfig = {
162
161
 
163
162
  prepareForCommit() {
164
163
  // …so that a <window> realized during the mutation phase waits to be
165
- // mapped until React has finished hiding whatever it hides (nodes.js,
164
+ // mapped until React has finished hiding whatever it hides (nodes/window/window.js,
166
165
  // beginWindowMaps)
167
166
  beginWindowMaps();
168
167
  traceHooks.commitStart?.();
@@ -196,17 +195,6 @@ const HostConfig = {
196
195
  '<text> spans and strings are.',
197
196
  );
198
197
  }
199
- if (hostContext.isInside3d) {
200
- // `<glarea>` is a leaf here: it owns the surface, the frame clock and
201
- // the swap, and `onDraw` is the escape hatch. A *scene graph* over it
202
- // — meshes, materials, lights, post-processing, on either backend —
203
- // is `@react-x11/components/three`, which brings its own reconciler.
204
- throw new Error(
205
- `react-x11: <${type}> is not an element — <glarea> takes no ` +
206
- 'children. Draw through `onDraw`, or use ' +
207
- '`@react-x11/components/three` for a scene graph. See docs/gl.md.',
208
- );
209
- }
210
198
  let node;
211
199
  switch (type) {
212
200
  case 'window':
@@ -313,7 +301,7 @@ const HostConfig = {
313
301
  // Under REACT_X11_STRICT_TOKENS every token-styled node asks for one as
314
302
  // well, since a bad token is only *found* once the node is attached —
315
303
  // which is after this ran — and commitMount is the first moment React
316
- // holds that node's own fiber (nodes.js `_tokenProblem`). Gated on the
304
+ // holds that node's own fiber (nodes/cascade.js `_tokenProblem`). Gated on the
317
305
  // flag so the default mount pays nothing for a debugging mode.
318
306
  return (
319
307
  type === 'popup' ||
@@ -328,7 +316,7 @@ const HostConfig = {
328
316
  // first, and before any of the work below: the tree is on its way out.
329
317
  // `false` afterwards marks this instance's one commitMount spent, so a
330
318
  // later re-attach throws at once rather than deferring to a call that
331
- // will never come (nodes.js `_tokenProblem`).
319
+ // will never come (nodes/cascade.js `_tokenProblem`).
332
320
  const tokenError = instance._tokenError;
333
321
  instance._tokenError = false;
334
322
  if (tokenError) throw tokenError;
@@ -793,7 +781,7 @@ export async function createRoot(options = {}) {
793
781
  beginCompose(app, rest.compose);
794
782
 
795
783
  // Under what size a paragraph is painted as a strip of its ink instead
796
- // of as glyphs (nodes.js, `TextNode._paintsStrip`): six logical pixels by
784
+ // of as glyphs (nodes/text.js, `TextNode._paintsStrip`): six logical pixels by
797
785
  // default, 0 for glyphs at every size.
798
786
  setTextStripBelow(app, rest.textStripBelow);
799
787
 
package/src/a11y.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // The accessibility model: standard `role` / `aria-*` props resolved against
2
2
  // the retained node tree, plus the hook slots the renderer polls.
3
3
  //
4
- // This file is the cheap half. It is imported unconditionally by nodes.js,
4
+ // This file is the cheap half. It is imported unconditionally by src/nodes/,
5
5
  // events.js and Reconciler.js, so it must cost nothing when accessibility is
6
6
  // off: no D-Bus, no node builtins, no side effects — the hot paths pay one
7
7
  // property read per hook (`hooks.focus?.(…)`), exactly the trace-registry
@@ -941,7 +941,7 @@ export function a11yAttributes(node) {
941
941
  * **The one thing "activatable" means**: there is a click here to make.
942
942
  *
943
943
  * Three input routes stand on this — the pointer, an AT's `DoAction`, and
944
- * the keyboard's Space/Enter (`Node.defaultKeyDown`, nodes.js) — and they
944
+ * the keyboard's Space/Enter (`Node.defaultKeyDown`, nodes/node.js) — and they
945
945
  * all dispatch the same click, so a control cannot answer one of them and
946
946
  * not another. It lives here, beside the bridge's rule, because the bridge
947
947
  * is the layer that already had to write the rule down.
package/src/anchor.js CHANGED
@@ -8,7 +8,8 @@
8
8
  // `realize()`, between the measurement and `CreateWindow`, which is after
9
9
  // the last moment React could have computed a position for it. So the
10
10
  // window places *itself* from the same functions (`WindowNode._followAnchor`,
11
- // nodes.js), and the two paths agree because they are the same code.
11
+ // nodes/window/anchoring.js), and the two paths agree because they are the
12
+ // same code.
12
13
 
13
14
  import { availableArea } from './screens.js';
14
15
 
@@ -48,7 +49,7 @@ export function screenRect(node) {
48
49
  /** Where the node's owner window is on the screen. Exported for the one
49
50
  * caller that anchors to a *point* rather than to a node's rect — the edit
50
51
  * menu opens at the pointer, whose coordinates arrive window-relative on the
51
- * event (`openEditMenu`, nodes.js). */
52
+ * event (`openEditMenu`, nodes/editmenupopup.js). */
52
53
  export function windowOrigin(node) {
53
54
  const win = node?.root?.window;
54
55
  return win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
@@ -93,9 +94,10 @@ function deviceAt(at, s) {
93
94
 
94
95
  /**
95
96
  * Has the thing this popup points at scrolled out of view? The check paint
96
- * culling uses (`Node._offscreen`), asked about the sub-rect rather than
97
- * about the node: an editor scrolls its own text, so the caret leaves the
98
- * viewport a long time before the editor does.
97
+ * culling uses (`Node._offscreen`), without the pixel paint allows for ink
98
+ * past a box, and asked about the sub-rect rather than about the node: an
99
+ * editor scrolls its own text, so the caret leaves the viewport a long time
100
+ * before the editor does.
99
101
  *
100
102
  * A degenerate rect counts as its own thinnest visible version — a caret is
101
103
  * a line with no width and a `{x, y}` anchor is a point with neither, and
package/src/appcontext.js CHANGED
@@ -26,7 +26,9 @@ import {
26
26
  compositingActive,
27
27
  watchCompositing,
28
28
  } from './compositing.js';
29
+ import { canEmbed } from './embedding.js';
29
30
  import { hasDirectGL, watchDirectGL } from './glbackend.js';
31
+ import { canOverlay } from './gloverlay.js';
30
32
 
31
33
  const AppContext = createContext(null);
32
34
 
@@ -57,17 +59,29 @@ export function useApp() {
57
59
  return app;
58
60
  }
59
61
 
60
- const SUPPORTS_FEATURES = new Set([
61
- 'transparency',
62
- 'shaders',
63
- 'nativeControls',
64
- ]);
65
-
66
- // 'nativeControls' is a property of the backend, decided before the first
67
- // render and never changing after — so its subscription has nothing to
68
- // deliver and its snapshot is a property test.
62
+ // 'nativeControls', 'embedding' and 'glOverlay' are properties of the
63
+ // backend, decided before the first render and never changing after — so
64
+ // their subscription has nothing to deliver and their snapshot is a property
65
+ // test.
69
66
  const NEVER_CHANGES = () => () => {};
70
67
 
68
+ // What `useSupports` watches and reads, per feature. `read` answers a
69
+ // boolean, so the snapshot is stable for a given state — returning the
70
+ // visual object for 'transparency' would tear on every render.
71
+ const FEATURES = {
72
+ transparency: {
73
+ watch: watchCompositing,
74
+ read: (app) => compositingActive(app) && Boolean(argbVisual(app)),
75
+ },
76
+ shaders: { watch: watchDirectGL, read: hasDirectGL },
77
+ nativeControls: {
78
+ watch: NEVER_CHANGES,
79
+ read: (app) => Boolean(app.nativeBezels),
80
+ },
81
+ embedding: { watch: NEVER_CHANGES, read: canEmbed },
82
+ glOverlay: { watch: NEVER_CHANGES, read: canOverlay },
83
+ };
84
+
71
85
  /**
72
86
  * Can this **display** do something, as a value a component can branch on?
73
87
  *
@@ -127,40 +141,55 @@ const NEVER_CHANGES = () => () => {};
127
141
  * first render already reads the final answer. A policy raised after
128
142
  * connecting has missed that probe, and re-renders its readers when it
129
143
  * settles rather than leaving them with two different answers.
144
+ *
145
+ * `'embedding'` is true when this connection can take another process's
146
+ * window into its own — the X11 backend; never Cocoa, which has no such
147
+ * primitive, and never the headless mock. It is the question to ask before
148
+ * rendering a `<foreign>`, which refuses with one `onError` where the answer
149
+ * is no:
150
+ *
151
+ * ```jsx
152
+ * const embedding = useSupports('embedding');
153
+ * <box style={{ flexGrow: 1 }}>
154
+ * {embedding ? <foreign onReady={spawnInto} /> : <text>X11 only</text>}
155
+ * </box>
156
+ * ```
157
+ *
158
+ * Like `'nativeControls'`, it is a property of the backend and never changes.
159
+ *
160
+ * `'glOverlay'` is true when the children of a `<glarea>` are drawn above its
161
+ * GL surface on this connection — laid out in its box, painted on panes over
162
+ * the surface, hit before it. Both backends draw them; what differs is
163
+ * translucency, composited by Core Animation on the Cocoa backend and opaque
164
+ * on X11 (docs/elements.md says exactly how). It is the question to ask
165
+ * before handing a surface its HUD rather than drawing that some other way:
166
+ *
167
+ * ```jsx
168
+ * const overlay = useSupports('glOverlay');
169
+ * <glarea onDraw={drawMap}>{overlay && <Legend />}</glarea>
170
+ * ```
171
+ *
172
+ * A property of the backend too, and it never changes.
130
173
  */
131
174
  export function useSupports(feature) {
132
175
  const app = useApp();
133
- if (!SUPPORTS_FEATURES.has(feature)) {
176
+ const spec = Object.hasOwn(FEATURES, feature) ? FEATURES[feature] : null;
177
+ if (!spec) {
134
178
  throw new Error(
135
179
  `react-x11: useSupports(${JSON.stringify(feature)}) — unknown feature ` +
136
- `(expected one of ${[...SUPPORTS_FEATURES].join(', ')})`,
180
+ `(expected one of ${Object.keys(FEATURES).join(', ')})`,
137
181
  );
138
182
  }
139
- // Both features go through the same store, so the hooks below run in the
183
+ // Every feature goes through the same store, so the hooks below run in the
140
184
  // same order whatever is being asked about. Where compositing comes and
141
185
  // goes for as long as the app runs, the backend settles at most once — and
142
186
  // watching that one moment is what keeps two components rendered either
143
187
  // side of it from disagreeing (see watchDirectGL).
144
188
  const subscribe = useCallback(
145
- (onChange) =>
146
- feature === 'nativeControls'
147
- ? NEVER_CHANGES()
148
- : feature === 'shaders'
149
- ? watchDirectGL(app, onChange)
150
- : watchCompositing(app, onChange),
151
- [app, feature],
152
- );
153
- // a boolean, so the snapshot is stable for a given state — returning the
154
- // visual object here would tear on every render
155
- const snapshot = useCallback(
156
- () =>
157
- feature === 'nativeControls'
158
- ? Boolean(app.nativeBezels)
159
- : feature === 'shaders'
160
- ? hasDirectGL(app)
161
- : compositingActive(app) && Boolean(argbVisual(app)),
162
- [app, feature],
189
+ (onChange) => spec.watch(app, onChange),
190
+ [app, spec],
163
191
  );
192
+ const snapshot = useCallback(() => spec.read(app), [app, spec]);
164
193
  return useSyncExternalStore(subscribe, snapshot, snapshot);
165
194
  }
166
195
 
@@ -0,0 +1,14 @@
1
+ // What runs the moment react-x11 is imported, before any of the app's own
2
+ // code: each backend's chance to put the app where its platform wants it.
3
+ // Only the cocoa backend has one — AppKit keeps the process main thread, so
4
+ // an app on macOS moves onto a worker (src/cocoa/relaunch.js), and on that
5
+ // worker the same import sets it up. A Windows backend's UI thread will be
6
+ // the addon's own, and X11 has none, so neither needs this step
7
+ // (docs/windows.md §"Three shapes on Windows"). Imported first by
8
+ // src/index.js, so that it runs before the rest of the package.
9
+ import { isMainThread, workerData } from 'node:worker_threads';
10
+
11
+ import { bootstrapWorker, relaunchOnImport } from './cocoa/relaunch.js';
12
+
13
+ if (isMainThread) relaunchOnImport(import.meta.url);
14
+ else if (workerData?.reactX11State) bootstrapWorker(workerData.reactX11State);
@@ -75,7 +75,7 @@ function atomName(X, id) {
75
75
  * hands it to `dispatch` in arrival order.
76
76
  *
77
77
  * `dispatch` is the caller's, so the priority and the paint stay with the
78
- * other window events in nodes.js; what lives here is the naming and the
78
+ * other window events in nodes/window/; what lives here is the naming and the
79
79
  * ordering it has to preserve.
80
80
  */
81
81
  export function createClientMessages(node, dispatch) {