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
package/src/anchor.js ADDED
@@ -0,0 +1,360 @@
1
+ // Popup geometry: where to put a `<popup>` that hangs off something else.
2
+ //
3
+ // Core rather than `src/components/`, because two callers need it and only
4
+ // one of them is a widget. A widget that knows its own size measures the
5
+ // rect itself and passes it as `x`/`y` (`useAnchor`, `useAnchorTracking` —
6
+ // `src/components/anchor.js`, which re-exports everything here). A `<popup>`
7
+ // that sizes itself from its content cannot: its size is settled inside
8
+ // `realize()`, between the measurement and `CreateWindow`, which is after
9
+ // the last moment React could have computed a position for it. So the
10
+ // window places *itself* from the same functions (`WindowNode._followAnchor`,
11
+ // nodes.js), and the two paths agree because they are the same code.
12
+
13
+ import { availableArea } from './screens.js';
14
+
15
+ /**
16
+ * A node's laid-out rect in **screen** coordinates: the owner window's
17
+ * position plus the node's own box.
18
+ *
19
+ * `_screenOrigin` is what the server says the window is at; `x`/`y` are
20
+ * frame-relative under a reparenting window manager and are only a fallback
21
+ * (the headless mock has no server to ask).
22
+ *
23
+ * Exported because a popup sometimes has to know where its *trigger* is and
24
+ * not only where to put itself — a tooltip's arrow points at the middle of
25
+ * the thing it annotates, which stops being the middle of the tooltip as
26
+ * soon as a screen edge slides one of them.
27
+ */
28
+ export function screenRect(node) {
29
+ if (!node?.abs) return null;
30
+ const origin = windowOrigin(node);
31
+ return {
32
+ x: origin.x + node.abs.x,
33
+ y: origin.y + node.abs.y,
34
+ width: node.abs.width,
35
+ height: node.abs.height,
36
+ };
37
+ }
38
+
39
+ /** Where the node's owner window is on the screen. Exported for the one
40
+ * caller that anchors to a *point* rather than to a node's rect — the edit
41
+ * menu opens at the pointer, whose coordinates arrive window-relative on the
42
+ * event (`openEditMenu`, nodes.js). */
43
+ export function windowOrigin(node) {
44
+ const win = node?.root?.window;
45
+ return win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
46
+ }
47
+
48
+ /**
49
+ * The rect within a node that a popup is anchored to, in the node's own
50
+ * coordinates — a caret, a table cell, a chart datapoint, the span under a
51
+ * text-range tooltip. `width`/`height` default to 0, so `{x, y}` alone is a
52
+ * point.
53
+ *
54
+ * Node-relative rather than screen-relative because the caller measures it
55
+ * against its own content, and because it then survives everything that
56
+ * moves the node: the window being dragged, an ancestor scrolling, the
57
+ * node's own layout shifting. That is what lets `useAnchorTracking` follow a
58
+ * caret with no extra work — it re-reads the options and the node, and the
59
+ * offset between them is still true.
60
+ */
61
+ export function subRect(node, at) {
62
+ if (!node?.abs) return null;
63
+ if (!at) return node.abs;
64
+ return {
65
+ x: node.abs.x + (at.x ?? 0),
66
+ y: node.abs.y + (at.y ?? 0),
67
+ width: at.width ?? 0,
68
+ height: at.height ?? 0,
69
+ };
70
+ }
71
+
72
+ /** `at` in logical pixels (it comes off an `anchor` prop) → the same rect in
73
+ * the device pixels `node.abs` and everything downstream are in. */
74
+ function deviceAt(at, s) {
75
+ if (!at || s === 1) return at;
76
+ return {
77
+ ...at,
78
+ ...(typeof at.x === 'number' && { x: at.x * s }),
79
+ ...(typeof at.y === 'number' && { y: at.y * s }),
80
+ ...(typeof at.width === 'number' && { width: at.width * s }),
81
+ ...(typeof at.height === 'number' && { height: at.height * s }),
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Has the thing this popup points at scrolled out of view? The check paint
87
+ * culling uses (`Node._offscreen`), asked about the sub-rect rather than
88
+ * about the node: an editor scrolls its own text, so the caret leaves the
89
+ * viewport a long time before the editor does.
90
+ *
91
+ * A degenerate rect counts as its own thinnest visible version — a caret is
92
+ * a line with no width and a `{x, y}` anchor is a point with neither, and
93
+ * having no area is not the same as being off screen.
94
+ */
95
+ export function anchorOffscreen(node, at) {
96
+ const rect = subRect(node, deviceAt(at, node?.scale ?? 1));
97
+ if (!rect || typeof node._offscreen !== 'function') return false;
98
+ return node._offscreen({
99
+ x: rect.x,
100
+ y: rect.y,
101
+ width: Math.max(rect.width, 1),
102
+ height: Math.max(rect.height, 1),
103
+ });
104
+ }
105
+
106
+ /**
107
+ * The area a popup anchored to this node may be placed in: the usable part
108
+ * of the monitor the node is on.
109
+ *
110
+ * The same answer `<window width="auto">` is capped by (`src/screens.js`) —
111
+ * per-monitor, minus the panels — rather than `screen.pixel_width`, which is
112
+ * the whole virtual desktop. On a two-head setup that difference is the
113
+ * difference between a menu flipping at the edge of the monitor it is on and
114
+ * a menu that only flips at the far edge of the *other* monitor, having
115
+ * opened halfway across the seam. `null` where there is nothing to ask,
116
+ * which is the headless mock and a server with no Xinerama and no screen:
117
+ * placement then neither flips nor clamps.
118
+ */
119
+ export function anchorArea(node) {
120
+ const area = deviceAnchorArea(node);
121
+ const s = node?.scale ?? 1;
122
+ if (!area || s === 1) return area;
123
+ // Public callers size popups from this — `maxHeight` styles, widths —
124
+ // and those are logical like every style, so the answer is too.
125
+ return {
126
+ x: area.x / s,
127
+ y: area.y / s,
128
+ width: area.width / s,
129
+ height: area.height / s,
130
+ };
131
+ }
132
+
133
+ /** The same area in device pixels, for placement math that runs against
134
+ * `abs` rects and window origins — here and in the edit menu's. */
135
+ export function deviceAnchorArea(node) {
136
+ const app = node?.app;
137
+ if (!app) return null;
138
+ const at = screenRect(node);
139
+ return availableArea(app, at ? { x: at.x, y: at.y } : null);
140
+ }
141
+
142
+ /**
143
+ * Where to put a `<popup>` anchored to a drawn node, in **screen**
144
+ * coordinates: the owner window's position plus the node's laid-out rect.
145
+ *
146
+ * `placement` is a preference, not a promise — a menu near the bottom of
147
+ * the screen flips above its trigger rather than opening off-screen, and
148
+ * the result is clamped into the screen either way. The chosen side comes
149
+ * back as `placement` so the caller can style accordingly.
150
+ *
151
+ * `at` anchors to a rect **inside** the node instead of to the node itself
152
+ * (see `subRect`): the caret in an editor, a cell in a table, a point on a
153
+ * chart. Everything else then reads that rect — the side it flips on, the
154
+ * edge it aligns to, the gap `offset` leaves — so a popup anchored to a
155
+ * caret behaves exactly like one anchored to a small widget that happens to
156
+ * be there.
157
+ *
158
+ * `alignTo` takes the two axes from **different** nodes: the placement edge
159
+ * from `node`, the alignment from `alignTo`. A submenu is the case that
160
+ * needs it — it belongs against the outer edge of the menu it comes out of,
161
+ * but lined up with the row that spawned it, and that row is inset by the
162
+ * menu's border and padding. Anchoring both to the row opens the submenu
163
+ * *over* its parent by exactly that inset. Both nodes must be in the same
164
+ * window, which is what lets one origin serve both.
165
+ *
166
+ * `alignOffset` shifts the result along the *alignment* axis, where `offset`
167
+ * moves it along the placement one — and it is applied before the clamp, so
168
+ * a popup nudged towards a screen edge is still brought back from it. What
169
+ * needs it is the difference between lining up a **surface** and lining up
170
+ * what is drawn **in** it: a submenu whose top edge is level with the row
171
+ * that opened it has its first item a border and a padding lower down, and
172
+ * the eye lines up the items, not the boxes.
173
+ *
174
+ * ## Which side is which
175
+ *
176
+ * `placement: 'start'` and `'end'` are the **logical** sides, and they are
177
+ * what a submenu wants: a menu opens away from the edge its rows begin at, so
178
+ * it goes out to the right in an LTR menu and out to the left in an RTL one.
179
+ * `'left'`/`'right'` stay available and stay physical, for the rare placement
180
+ * that really is about the screen. `align: 'start'`/`'end'` mirror the same
181
+ * way — but only when the popup is above or below its trigger, since with a
182
+ * popup beside it the alignment axis is vertical and nothing about a vertical
183
+ * axis mirrors.
184
+ *
185
+ * The direction comes from the anchoring node, so a menu inside a mirrored
186
+ * panel opens the mirrored way without its widget being told.
187
+ */
188
+ export function anchorRect(node, options = {}) {
189
+ if (!node?.abs) return null;
190
+ // Options are logical pixels — they come from application code, like
191
+ // every length — and so is the returned rect, which is headed for a
192
+ // popup's `x`/`y` props. The math between runs in device pixels, because
193
+ // `abs`, the window origin and the monitor area are (src/scale.js).
194
+ const s = node.scale ?? 1;
195
+ const {
196
+ placement = 'bottom',
197
+ align = 'start',
198
+ alignOffset: logicalAlignOffset = 0,
199
+ offset: logicalOffset = 2,
200
+ at,
201
+ alignTo,
202
+ direction = node.direction,
203
+ } = options;
204
+ const alignOffset = logicalAlignOffset * s;
205
+ const offset = logicalOffset * s;
206
+ const rtl = direction === 'rtl';
207
+
208
+ // The anchor is the sub-rect where there is one, all the way through:
209
+ // the side that flips, the edge that aligns, and — since a popup with no
210
+ // size of its own is as wide as the thing it hangs off — the default
211
+ // width.
212
+ const anchor = subRect(node, deviceAt(at, s));
213
+ const width = options.width !== undefined ? options.width * s : anchor.width;
214
+ const height = options.height !== undefined ? options.height * s : 0;
215
+
216
+ const origin = windowOrigin(node);
217
+ const ax = origin.x + anchor.x;
218
+ const ay = origin.y + anchor.y;
219
+ const aw = anchor.width;
220
+ const ah = anchor.height;
221
+ // the rect the *alignment* reads, which is the anchor's own unless the
222
+ // caller split the two axes — one origin serves both, since both nodes are
223
+ // in the same window
224
+ const cross = alignTo ? (subRect(alignTo) ?? anchor) : anchor;
225
+ const cx = origin.x + cross.x;
226
+ const cy = origin.y + cross.y;
227
+
228
+ const area = deviceAnchorArea(node);
229
+ const left = area?.x ?? 0;
230
+ const top = area?.y ?? 0;
231
+ const right = area ? area.x + area.width : null;
232
+ const bottom = area ? area.y + area.height : null;
233
+
234
+ // `mirrored` is only ever true on the horizontal axis: `alignAlong` serves
235
+ // both, and a vertically-aligned popup's `start` is the top in every
236
+ // direction there is.
237
+ const alignAlong = (start, size, extent, mirrored) => {
238
+ const edge =
239
+ align === 'center'
240
+ ? 'center'
241
+ : (align === 'end') !== mirrored
242
+ ? 'end'
243
+ : 'start';
244
+ return (
245
+ alignOffset +
246
+ (edge === 'center'
247
+ ? start + (size - extent) / 2
248
+ : edge === 'end'
249
+ ? start + size - extent
250
+ : start)
251
+ );
252
+ };
253
+
254
+ let side =
255
+ placement === 'start'
256
+ ? rtl
257
+ ? 'right'
258
+ : 'left'
259
+ : placement === 'end'
260
+ ? rtl
261
+ ? 'left'
262
+ : 'right'
263
+ : placement;
264
+ let x;
265
+ let y;
266
+
267
+ if (side === 'bottom' || side === 'top') {
268
+ const below = ay + ah + offset;
269
+ const above = ay - height - offset;
270
+ if (
271
+ side === 'bottom' &&
272
+ bottom != null &&
273
+ below + height > bottom &&
274
+ above >= top
275
+ ) {
276
+ side = 'top';
277
+ } else if (
278
+ side === 'top' &&
279
+ above < top &&
280
+ (bottom == null || below + height <= bottom)
281
+ ) {
282
+ side = 'bottom';
283
+ }
284
+ y = side === 'bottom' ? below : above;
285
+ x = alignAlong(cx, cross.width, width, rtl);
286
+ } else {
287
+ const after = ax + aw + offset;
288
+ const before = ax - width - offset;
289
+ if (
290
+ side === 'right' &&
291
+ right != null &&
292
+ after + width > right &&
293
+ before >= left
294
+ ) {
295
+ side = 'left';
296
+ } else if (
297
+ side === 'left' &&
298
+ before < left &&
299
+ (right == null || after + width <= right)
300
+ ) {
301
+ side = 'right';
302
+ }
303
+ x = side === 'right' ? after : before;
304
+ y = alignAlong(cy, cross.height, height, false);
305
+ }
306
+
307
+ if (right != null) x = Math.max(left, Math.min(x, right - width));
308
+ if (bottom != null && height) y = Math.max(top, Math.min(y, bottom - height));
309
+
310
+ // Back to logical on the way out. Rounded in *device* pixels first, so
311
+ // the placement still lands on the device grid it was computed on.
312
+ return {
313
+ x: Math.round(x) / s,
314
+ y: Math.round(y) / s,
315
+ width: width / s,
316
+ height: height / s,
317
+ placement: side,
318
+ };
319
+ }
320
+
321
+ /**
322
+ * Where to put a `<popup>` of this size **centred over the owner window**,
323
+ * in screen coordinates, clamped into the screen. A dialog is anchored to
324
+ * the window rather than to a widget, which is the one placement
325
+ * `anchorRect` cannot express.
326
+ */
327
+ export function centerRect(node, { width: logicalW, height: logicalH }) {
328
+ if (!node) return null;
329
+ // Logical in, logical out, device in between — anchorRect's contract,
330
+ // for the same caller: the result becomes a popup's `x`/`y` props.
331
+ const s = node.scale ?? 1;
332
+ const width = logicalW * s;
333
+ const height = logicalH * s;
334
+ const win = node.root?.window;
335
+ const ww = win?.width ?? width;
336
+ const wh = win?.height ?? height;
337
+ // `_screenOrigin` for the same reason `anchorRect` uses it: `x`/`y` come
338
+ // from ConfigureNotify, and under a reparenting window manager those can be
339
+ // relative to the *frame* rather than the root — which would centre the
340
+ // dialog off by the size of the decoration. The server's own answer is
341
+ // right whatever the WM did. (quartz-wm happens to report root-relative
342
+ // coordinates, so the two agree there; that is not something to rely on.)
343
+ // The raw coordinates stay as the fallback, since the headless mock has no
344
+ // server to ask.
345
+ const origin = win?._screenOrigin ?? { x: win?.x ?? 0, y: win?.y ?? 0 };
346
+ let x = origin.x + (ww - width) / 2;
347
+ let y = origin.y + (wh - height) / 2;
348
+
349
+ const area = deviceAnchorArea(node);
350
+ if (area) {
351
+ x = Math.max(area.x, Math.min(x, area.x + area.width - width));
352
+ y = Math.max(area.y, Math.min(y, area.y + area.height - height));
353
+ }
354
+ return {
355
+ x: Math.round(x) / s,
356
+ y: Math.round(y) / s,
357
+ width: logicalW,
358
+ height: logicalH,
359
+ };
360
+ }
@@ -0,0 +1,161 @@
1
+ // Reaching the X connection from a component.
2
+ //
3
+ // `createRoot()` hands the app back to whoever called it, which is fine for
4
+ // an entry point and useless three components down — and a component
5
+ // library has no route to it at all without prop-drilling. So `render()`
6
+ // wraps the tree in a provider and `useApp()` reads it.
7
+ //
8
+ // Context rather than the ref walk `useWindowId`/`useAnchor` use: those
9
+ // answer *per-node* questions, and the ref is the question. The connection
10
+ // is app-scoped, so demanding a host ref to find it would be busywork with
11
+ // no information in it. A module-level global would be simpler still and
12
+ // wrong — one process can drive several roots on several connections, which
13
+ // the test suite does routinely.
14
+
15
+ import {
16
+ createContext,
17
+ useCallback,
18
+ useContext,
19
+ useMemo,
20
+ useSyncExternalStore,
21
+ } from 'react';
22
+
23
+ import { createClipboard } from './clipboard.js';
24
+ import {
25
+ argbVisual,
26
+ compositingActive,
27
+ watchCompositing,
28
+ } from './compositing.js';
29
+ import { hasDirectGL, watchDirectGL } from './glbackend.js';
30
+
31
+ const AppContext = createContext(null);
32
+
33
+ /** Wraps the rendered element; not exported to applications. */
34
+ export const AppProvider = AppContext.Provider;
35
+
36
+ /** `useApp()` without the throw, for components that can still render
37
+ * something useful outside a tree. Not public. */
38
+ export function useAppOrNull() {
39
+ return useContext(AppContext);
40
+ }
41
+
42
+ /**
43
+ * The ntk connection this tree is rendering onto.
44
+ *
45
+ * Everything ntk exposes hangs off it — `app.fonts`, `app.cursors`,
46
+ * `app.X` for raw protocol — so this is the escape hatch as much as it is
47
+ * an API. Throws outside a react-x11 tree rather than returning null,
48
+ * because every caller would have had to check.
49
+ */
50
+ export function useApp() {
51
+ const app = useContext(AppContext);
52
+ if (!app) {
53
+ throw new Error(
54
+ 'react-x11: useApp() must be called inside a tree rendered by createRoot()',
55
+ );
56
+ }
57
+ return app;
58
+ }
59
+
60
+ const SUPPORTS_FEATURES = new Set(['transparency', 'shaders']);
61
+
62
+ /**
63
+ * Can this **display** do something, as a value a component can branch on?
64
+ *
65
+ * ```jsx
66
+ * const canBlend = useSupports('transparency');
67
+ * // a shadow has to be painted into a margin the popup owns, so the
68
+ * // decision has to be made before the window is sized
69
+ * const margin = canBlend ? 26 : 0;
70
+ * ```
71
+ *
72
+ * `'transparency'` is true when the server has a 32-bit visual to draw on
73
+ * *and* a compositor is running to blend it. It re-renders when a compositor
74
+ * starts or stops. `REACT_X11_NO_TRANSPARENCY=1` answers false whatever the
75
+ * display can do, so the fallback design can be looked at without stopping
76
+ * the compositor for the whole session.
77
+ *
78
+ * The companion is the `'@supports transparency'` style block, and the two
79
+ * answer deliberately different questions. This one is about the display, so
80
+ * it can be asked before any window exists — which is what a caller sizing a
81
+ * popup needs. The style block is about the window the node is actually in,
82
+ * so a component nested in a plain `<window>` gets the opaque design there
83
+ * and the translucent one inside a `<popup transparent>`, without being
84
+ * told which it is. Reach for the style block first; this is for decisions
85
+ * that are not styling.
86
+ *
87
+ * `'shaders'` is true when 3D can run your own GLSL — that is, when this
88
+ * connection draws through the **direct** rendering backend. It is the
89
+ * question to ask before rendering a `<shaderMaterial>`, which throws where
90
+ * there is no pipeline to compile it:
91
+ *
92
+ * ```jsx
93
+ * const shaders = useSupports('shaders');
94
+ * <mesh>
95
+ * <boxGeometry args={[1, 1, 1]} />
96
+ * {shaders ? <shaderMaterial {...glsl} /> : <meshPhongMaterial color="#e0533d" />}
97
+ * </mesh>
98
+ * ```
99
+ *
100
+ * It needs `createRoot({ glPolicy: 'auto' })`: the default policy is the
101
+ * indirect backend, which has no shaders at all, so this is false under it
102
+ * whatever the machine could do. That is the line between this and
103
+ * `app.glCapabilities()` — the hook answers which backend *this connection*
104
+ * got, the capabilities answer what the machine could offer, and they
105
+ * disagree exactly when the policy did not ask. The capabilities also say
106
+ * *why* this is false; see docs/gl.md.
107
+ *
108
+ * Unlike `'transparency'`, which comes and goes with the compositor, this
109
+ * settles once and then holds still: under a policy that could pick direct,
110
+ * `createRoot()` waits for ntk's probe before handing the app back, so the
111
+ * first render already reads the final answer. A policy raised after
112
+ * connecting has missed that probe, and re-renders its readers when it
113
+ * settles rather than leaving them with two different answers.
114
+ */
115
+ export function useSupports(feature) {
116
+ const app = useApp();
117
+ if (!SUPPORTS_FEATURES.has(feature)) {
118
+ throw new Error(
119
+ `react-x11: useSupports(${JSON.stringify(feature)}) — unknown feature ` +
120
+ `(expected one of ${[...SUPPORTS_FEATURES].join(', ')})`,
121
+ );
122
+ }
123
+ // Both features go through the same store, so the hooks below run in the
124
+ // same order whatever is being asked about. Where compositing comes and
125
+ // goes for as long as the app runs, the backend settles at most once — and
126
+ // watching that one moment is what keeps two components rendered either
127
+ // side of it from disagreeing (see watchDirectGL).
128
+ const subscribe = useCallback(
129
+ (onChange) =>
130
+ feature === 'shaders'
131
+ ? watchDirectGL(app, onChange)
132
+ : watchCompositing(app, onChange),
133
+ [app, feature],
134
+ );
135
+ // a boolean, so the snapshot is stable for a given state — returning the
136
+ // visual object here would tear on every render
137
+ const snapshot = useCallback(
138
+ () =>
139
+ feature === 'shaders'
140
+ ? hasDirectGL(app)
141
+ : compositingActive(app) && Boolean(argbVisual(app)),
142
+ [app, feature],
143
+ );
144
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
145
+ }
146
+
147
+ /**
148
+ * The clipboard, scoped to this tree's connection.
149
+ *
150
+ * ```jsx
151
+ * const clipboard = useClipboard();
152
+ * await clipboard.writeText(selection); // stamped with the keystroke
153
+ * const files = await clipboard.readFiles(); // parsed, or []
154
+ * ```
155
+ *
156
+ * Stable for as long as the app is, so it is safe in a dependency array.
157
+ */
158
+ export function useClipboard() {
159
+ const app = useApp();
160
+ return useMemo(() => createClipboard(app), [app]);
161
+ }