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,336 @@
1
+ // XSETTINGS — the desktop's settings channel that predates D-Bus, and the
2
+ // only one that exists on a plain X session with no portal running.
3
+ //
4
+ // A settings daemon owns the `_XSETTINGS_S<screen>` selection and publishes
5
+ // everything as one binary property, `_XSETTINGS_SETTINGS`, on its own
6
+ // manager window. Clients read that property and watch it for changes. GTK
7
+ // has read it since 2.0, Qt reads it, and on a GNOME session it is live even
8
+ // under Wayland — gnome-settings-daemon keeps it up to date for Xwayland
9
+ // clients, which is how it is reachable from here at all.
10
+ //
11
+ // ## What it is worth
12
+ //
13
+ // For **appearance** it is a fallback and a thin one: `Net/ThemeName` is a
14
+ // theme *name*, so "is this desktop dark" comes down to reading `Yaru-dark`
15
+ // and believing the suffix. That guess is exactly what the settings portal
16
+ // was invented to replace, and it is only ever consulted here when the portal
17
+ // answered nothing.
18
+ //
19
+ // For **font rendering** it is the only source there is — `Xft/RGBA`,
20
+ // `Xft/HintStyle`, `Xft/DPI`, `Gtk/FontName` have no portal equivalent — so
21
+ // this module reads and publishes the whole map rather than the three keys
22
+ // the appearance ladder happens to want (#86 is where the rest lands).
23
+
24
+ import { requireExtension } from './extensions.js';
25
+
26
+ /** Per-connection sessions, like `compositing.js`. */
27
+ const sessions = new WeakMap();
28
+
29
+ const SETTINGS_PROPERTY = '_XSETTINGS_SETTINGS';
30
+ const PROPERTY_NOTIFY = 28;
31
+ const PROPERTY_CHANGE_MASK = 4194304; // x11.eventMask.PropertyChange
32
+
33
+ /** The three value types in the spec; anything else ends the walk. */
34
+ const TYPE_INTEGER = 0;
35
+ const TYPE_STRING = 1;
36
+ const TYPE_COLOR = 2;
37
+
38
+ /**
39
+ * Decode a `_XSETTINGS_SETTINGS` payload into a `Map` of name → value:
40
+ * a number for integers, a string for strings, and `[r, g, b, a]` of 16-bit
41
+ * channels for colours.
42
+ *
43
+ * ```
44
+ * CARD8 byte-order 0 = LSBFirst, 1 = MSBFirst
45
+ * CARD8 pad[3]
46
+ * CARD32 serial
47
+ * CARD32 n_settings
48
+ * CARD8 type per setting
49
+ * CARD8 pad
50
+ * CARD16 name-len
51
+ * STRING8 name padded to a 4-byte boundary
52
+ * CARD32 last-change-serial
53
+ * <value> strings padded to 4 as well
54
+ * ```
55
+ *
56
+ * **Two things make a naive reader desynchronise**, and both are silent: the
57
+ * byte-order byte at the front — every multi-byte field after it follows the
58
+ * *daemon's* order, not the platform's — and the padding after each name and
59
+ * each string. Miss either and the walk reads a length out of the middle of a
60
+ * name and produces plausible garbage rather than an error.
61
+ *
62
+ * Never throws. A payload that is truncated or carries a type from a future
63
+ * version stops the walk and returns everything read so far, because a
64
+ * fallback rung that throws is worse than one that answers less.
65
+ */
66
+ export function parseXSettings(buffer) {
67
+ const out = new Map();
68
+ if (!buffer || buffer.length < 12) return out;
69
+
70
+ const be = buffer[0] === 1;
71
+ const u16 = (o) => (be ? buffer.readUInt16BE(o) : buffer.readUInt16LE(o));
72
+ const u32 = (o) => (be ? buffer.readUInt32BE(o) : buffer.readUInt32LE(o));
73
+ const i32 = (o) => (be ? buffer.readInt32BE(o) : buffer.readInt32LE(o));
74
+ const pad = (o) => o + ((4 - (o % 4)) % 4);
75
+
76
+ const count = u32(8);
77
+ let at = 12;
78
+
79
+ for (let i = 0; i < count; i++) {
80
+ if (at + 4 > buffer.length) break;
81
+ const type = buffer[at];
82
+ const nameLength = u16(at + 2);
83
+ let p = at + 4;
84
+ if (p + nameLength > buffer.length) break;
85
+ const name = buffer.toString('latin1', p, p + nameLength);
86
+ p = pad(p + nameLength);
87
+ p += 4; // the per-setting last-change serial, which no client needs
88
+
89
+ if (type === TYPE_INTEGER) {
90
+ if (p + 4 > buffer.length) break;
91
+ out.set(name, i32(p));
92
+ p += 4;
93
+ } else if (type === TYPE_STRING) {
94
+ if (p + 4 > buffer.length) break;
95
+ const length = u32(p);
96
+ if (p + 4 + length > buffer.length) break;
97
+ out.set(name, buffer.toString('utf8', p + 4, p + 4 + length));
98
+ p = pad(p + 4 + length);
99
+ } else if (type === TYPE_COLOR) {
100
+ if (p + 8 > buffer.length) break;
101
+ out.set(name, [u16(p), u16(p + 2), u16(p + 4), u16(p + 6)]);
102
+ p += 8;
103
+ } else {
104
+ // A type this version does not know. The walk cannot continue past a
105
+ // value whose length it cannot compute, so it stops with what it has.
106
+ break;
107
+ }
108
+ at = p;
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * What one connection knows. `values` is null until the first read answers —
115
+ * and stays null on a display with no settings daemon, which is the normal
116
+ * state on a bare `startx`, under most window managers, and on XQuartz.
117
+ */
118
+ class XSettingsSession {
119
+ constructor(app) {
120
+ this.app = app;
121
+ this.values = null;
122
+ /** Whether the first read has landed — `values` is null both before it
123
+ * and on a display with no daemon, and those are different states. */
124
+ this.answered = false;
125
+ this.owner = 0;
126
+ this.listeners = new Set();
127
+ this.stopped = false;
128
+ this._selectionAtom = 0;
129
+ this._propertyAtom = 0;
130
+ }
131
+
132
+ _set(values) {
133
+ this.values = values;
134
+ this.answered = true;
135
+ for (const fn of [...this.listeners]) {
136
+ try {
137
+ fn(values);
138
+ } catch {
139
+ // one subscriber throwing must not take the others with it, nor the
140
+ // X event loop this runs on
141
+ }
142
+ }
143
+ }
144
+
145
+ subscribe(fn) {
146
+ this.listeners.add(fn);
147
+ return () => this.listeners.delete(fn);
148
+ }
149
+
150
+ stop() {
151
+ this.stopped = true;
152
+ this.listeners.clear();
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Start reading XSETTINGS on `app`. Resolves once the first answer is in —
158
+ * a `Map`, or `null` where no daemon owns the selection.
159
+ *
160
+ * Never rejects. A server with no settings manager, an ntk too old to reach
161
+ * the raw connection, and a headless mock all answer `null` and stop there.
162
+ */
163
+ export async function beginXSettings(app, screen = 0) {
164
+ let session = sessions.get(app);
165
+ if (session) return session;
166
+ session = new XSettingsSession(app);
167
+ sessions.set(app, session);
168
+
169
+ const X = app?.X;
170
+ if (!X || typeof X.GetSelectionOwner !== 'function') {
171
+ session._set(null);
172
+ return session;
173
+ }
174
+
175
+ try {
176
+ // Two name lookups that know nothing about each other, so they go out
177
+ // together — the property atom is not read until the owner answers.
178
+ [session._selectionAtom, session._propertyAtom] = await Promise.all([
179
+ internAtom(X, `_XSETTINGS_S${screen}`),
180
+ internAtom(X, SETTINGS_PROPERTY),
181
+ ]);
182
+ await latch(session);
183
+ // Live updates are a bonus: without XFixes the startup answer stands for
184
+ // the life of the connection, which is what every pre-2005 client did —
185
+ // and a bonus does not belong on the chain the first window waits behind.
186
+ void watchOwner(session).catch(() => {});
187
+ } catch {
188
+ if (!session.answered) session._set(null);
189
+ }
190
+ return session;
191
+ }
192
+
193
+ /**
194
+ * Find the manager window, read it, and start watching it.
195
+ *
196
+ * Called again whenever the selection changes hands, which is what happens
197
+ * when the settings daemon is restarted — the old window is destroyed and
198
+ * every property read from it after that answers nothing.
199
+ */
200
+ async function latch(session) {
201
+ const X = session.app.X;
202
+ const owner = await selectionOwner(X, session._selectionAtom);
203
+ session.owner = owner;
204
+ if (!owner) {
205
+ session._set(null);
206
+ return;
207
+ }
208
+ // PropertyChange on a window we do not own. Legal and shared — unlike
209
+ // SubstructureRedirect, any number of clients may select it, which is how
210
+ // GTK and Qt watch this same window at the same time.
211
+ X.ChangeWindowAttributes(owner, { eventMask: PROPERTY_CHANGE_MASK }, () => {
212
+ // A BadWindow here means the daemon died between the two calls; the
213
+ // XFixes watch below is what recovers, so there is nothing to do.
214
+ });
215
+ session._set(await readSettings(session));
216
+ }
217
+
218
+ async function readSettings(session) {
219
+ const X = session.app.X;
220
+ const prop = await getProperty(X, session.owner, session._propertyAtom);
221
+ if (!prop || !prop.type || !prop.data?.length) return null;
222
+ return parseXSettings(prop.data);
223
+ }
224
+
225
+ /**
226
+ * Two watches, for the two ways this changes.
227
+ *
228
+ * A **setting** changing rewrites the property on the same window, which is a
229
+ * PropertyNotify. The **daemon** restarting takes the selection to a new
230
+ * window, which is an XFixes SetSelectionOwner.
231
+ *
232
+ * The spec's own answer to the second one is the `MANAGER` ClientMessage on
233
+ * the root window. XFixes is the same information delivered directly — no
234
+ * root-window event mask, no filtering other managers' announcements out of a
235
+ * message that is broadcast for all of them — and `compositing.js` already
236
+ * proves the path on this connection.
237
+ */
238
+ async function watchOwner(session) {
239
+ const app = session.app;
240
+ const X = app.X;
241
+
242
+ X.on('event', (ev) => {
243
+ if (session.stopped) return;
244
+ if (
245
+ ev.type === PROPERTY_NOTIFY &&
246
+ ev.wid === session.owner &&
247
+ ev.atom === session._propertyAtom
248
+ ) {
249
+ readSettings(session).then(
250
+ (values) => !session.stopped && session._set(values),
251
+ () => {},
252
+ );
253
+ }
254
+ });
255
+
256
+ const fixes = await requireExtension(app, 'fixes');
257
+ if (!fixes || session.stopped) return;
258
+ // A 1x1 InputOnly window, never mapped: XFixes addresses notifications to a
259
+ // window and this one exists only to be that address.
260
+ const id = X.AllocID();
261
+ X.CreateWindow(id, app.display.screen[0].root, -10, -10, 1, 1, 0, 0, 2, 0, {
262
+ eventMask: 0,
263
+ });
264
+ X.on('event', (ev) => {
265
+ if (session.stopped) return;
266
+ if (ev.type !== fixes.firstEvent) return;
267
+ if (ev.selection !== session._selectionAtom) return;
268
+ latch(session).catch(() => {});
269
+ });
270
+ fixes.SelectSelectionInput(
271
+ id,
272
+ session._selectionAtom,
273
+ fixes.SelectionEventMask.SetSelectionOwner |
274
+ fixes.SelectionEventMask.SelectionWindowDestroy |
275
+ fixes.SelectionEventMask.SelectionClientClose,
276
+ );
277
+ }
278
+
279
+ function internAtom(X, name) {
280
+ return new Promise((resolve, reject) =>
281
+ X.InternAtom(false, name, (err, atom) =>
282
+ err ? reject(err) : resolve(atom),
283
+ ),
284
+ );
285
+ }
286
+
287
+ function selectionOwner(X, atom) {
288
+ return new Promise((resolve, reject) =>
289
+ X.GetSelectionOwner(atom, (err, owner) =>
290
+ err ? reject(err) : resolve(owner),
291
+ ),
292
+ );
293
+ }
294
+
295
+ function getProperty(X, wid, atom) {
296
+ return new Promise((resolve) =>
297
+ X.GetProperty(0, wid, atom, 0, 0, 0x1fffffff, (err, prop) =>
298
+ resolve(err ? null : prop),
299
+ ),
300
+ );
301
+ }
302
+
303
+ /** The settings this connection last read, or `null`. */
304
+ export function xsettings(app) {
305
+ return sessions.get(app)?.values ?? null;
306
+ }
307
+
308
+ /** Subscribe to the settings changing. Returns an unsubscribe function. */
309
+ export function watchXSettings(app, fn) {
310
+ const session = sessions.get(app);
311
+ if (!session) return () => {};
312
+ return session.subscribe(fn);
313
+ }
314
+
315
+ /** Tear down with the root that started it. */
316
+ export function endXSettings(app) {
317
+ const session = sessions.get(app);
318
+ if (!session) return;
319
+ session.stop();
320
+ sessions.delete(app);
321
+ }
322
+
323
+ /**
324
+ * Test seam: publish settings without an X server, the way
325
+ * `setCompositingForTests` does. Also what a headless mock uses to model a
326
+ * display with or without a settings daemon.
327
+ */
328
+ export function setXSettingsForTests(app, values) {
329
+ let session = sessions.get(app);
330
+ if (!session) {
331
+ session = new XSettingsSession(app);
332
+ sessions.set(app, session);
333
+ }
334
+ session._set(values);
335
+ return session;
336
+ }
package/src/yoga.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `react-x11/yoga` — the layout engine the renderer lays every box out with.
3
+ *
4
+ * **Most extensions never need this.** An element measures through
5
+ * `measureContent`, which is handed its constraints in words (`'exactly'`,
6
+ * `'at-most'`, `'unconstrained'`) rather than yoga's integers, precisely so
7
+ * that yoga's ABI does not become part of the extension seam — see
8
+ * [extending.md](../docs/extending.md).
9
+ *
10
+ * What this entry point is for is the other case: a package implementing a
11
+ * **layout algorithm of its own** and wanting to delegate part of it. The
12
+ * worked example is `@react-x11/components`'s `<Html>`, whose `display: flex`
13
+ * builds a small yoga tree, asks it, and reads the answer back rather than
14
+ * re-deriving flexbox by hand.
15
+ *
16
+ * Such a package must use **this** engine rather than its own `yoga-layout`
17
+ * dependency. Two instances mean two WebAssembly modules, and a node created
18
+ * by one cannot be inserted into a tree owned by the other — a failure that
19
+ * surfaces as a crash inside the engine, naming nothing the author wrote.
20
+ *
21
+ * The engine is loaded, not imported: `createRoot()` awaits `loadLayout()`
22
+ * before it builds anything, which is what keeps a top-level await out of
23
+ * every bundle containing react-x11 (docs/packaging.md). The enum constants
24
+ * are readable from the first tick regardless; `Node` and `Config` throw
25
+ * until the assembly is in place.
26
+ */
27
+ import type { Yoga as YogaAssembly } from 'yoga-layout/load';
28
+
29
+ export type { Config, MeasureFunction, Node } from 'yoga-layout/load';
30
+
31
+ /**
32
+ * yoga's assembly — `Node.create()`, `Config`, and the rest — plus the flat
33
+ * `SCREAMING_CASE` enum constants (`EDGE_LEFT`, `FLEX_DIRECTION_ROW`,
34
+ * `MEASURE_MODE_AT_MOST`, …) that `styles.js` builds its lookup tables from.
35
+ *
36
+ * The constants are typed loosely because they are generated from yoga's
37
+ * typed enums at import time rather than declared; for a checked alternative,
38
+ * import the enums themselves from `yoga-layout/load` — they are plain
39
+ * JavaScript and carry no WebAssembly with them.
40
+ */
41
+ export type LayoutEngine = YogaAssembly & Record<string, number>;
42
+
43
+ export const Yoga: LayoutEngine;
44
+ export default Yoga;
45
+
46
+ /**
47
+ * Load the engine's WebAssembly. Idempotent, and resolves with the same
48
+ * `Yoga` object this module exports. `createRoot()` awaits it, so an
49
+ * application rarely calls it — code that builds nodes outside a root (a test
50
+ * harness over the mock app) needs it first.
51
+ */
52
+ export function loadLayout(): Promise<LayoutEngine>;
53
+
54
+ /** Whether the assembly is in place — `Yoga.Node` will not throw. */
55
+ export function layoutLoaded(): boolean;
package/src/yoga.js ADDED
@@ -0,0 +1,135 @@
1
+ // The layout engine, without a top-level await.
2
+ //
3
+ // Every drawn node owns one yoga node (`nodes.js`), and `styles.js` is the
4
+ // translation from style props to yoga setters. This module is where the
5
+ // engine itself comes from.
6
+ //
7
+ // It used to come from ntk, because ntk's own `HtmlView` laid out with
8
+ // flexbox and a second WASM instance would have meant nodes from one engine
9
+ // being mixed with nodes from the other. ntk's document widgets are gone, so
10
+ // the renderer is the only layout consumer left and owns the dependency
11
+ // directly.
12
+ //
13
+ // **Why not `yoga-layout`'s default entry.** It is
14
+ // `const Yoga = wrapAssembly(await loadYoga())` — and that one `await` is
15
+ // contagious: every bundle containing react-x11 inherits it, esbuild then
16
+ // refuses to emit CommonJS ("Top-level await is currently not supported with
17
+ // the cjs output format"), and Node's single-executable format runs its
18
+ // embedded main as CommonJS. One import would cost every app the ability to
19
+ // ship as a single binary (docs/packaging.md).
20
+ //
21
+ // So this imports the half of the package with no WASM in it —
22
+ // `yoga-layout/load` exports the enums as plain JavaScript and the assembly
23
+ // behind an async function — and `createRoot()` loads the assembly, which is
24
+ // already asynchronous.
25
+ //
26
+ // The object exported here keeps yoga's own shape, and that is the point:
27
+ // the flat SCREAMING_CASE constants are present from the first tick, so
28
+ // `styles.js` builds its lookup tables at module scope without awaiting
29
+ // anything. `Node`, `Config` and the rest of the assembly appear when
30
+ // `loadLayout()` resolves.
31
+ import {
32
+ loadYoga,
33
+ Align,
34
+ BoxSizing,
35
+ Dimension,
36
+ Direction,
37
+ Display,
38
+ Edge,
39
+ Errata,
40
+ ExperimentalFeature,
41
+ FlexDirection,
42
+ Gutter,
43
+ Justify,
44
+ LogLevel,
45
+ MeasureMode,
46
+ NodeType,
47
+ Overflow,
48
+ PositionType,
49
+ Unit,
50
+ Wrap,
51
+ } from 'yoga-layout/load';
52
+
53
+ const ENUMS = {
54
+ Align,
55
+ BoxSizing,
56
+ Dimension,
57
+ Direction,
58
+ Display,
59
+ Edge,
60
+ Errata,
61
+ ExperimentalFeature,
62
+ FlexDirection,
63
+ Gutter,
64
+ Justify,
65
+ LogLevel,
66
+ MeasureMode,
67
+ NodeType,
68
+ Overflow,
69
+ PositionType,
70
+ Unit,
71
+ Wrap,
72
+ };
73
+
74
+ // yoga's generator names each constant <ENUM>_<MEMBER>, both snake-cased from
75
+ // PascalCase: FlexDirection.ColumnReverse -> FLEX_DIRECTION_COLUMN_REVERSE.
76
+ // test/yoga.test.js pins every name against the real assembly, so a rename
77
+ // upstream fails loudly instead of yielding an undefined constant.
78
+ const screamingSnake = (name) =>
79
+ name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toUpperCase();
80
+
81
+ /** The layout engine: enums now, assembly after `loadLayout()`. */
82
+ const Yoga = {};
83
+ for (const [enumName, members] of Object.entries(ENUMS)) {
84
+ // a bundler may stub yoga out entirely, and importing react-x11 must still
85
+ // work — a documentation build that only reads the module graph, say
86
+ if (!members || typeof members !== 'object') continue;
87
+ const prefix = screamingSnake(enumName);
88
+ for (const [member, value] of Object.entries(members)) {
89
+ if (typeof value === 'number')
90
+ Yoga[`${prefix}_${screamingSnake(member)}`] = value;
91
+ }
92
+ }
93
+
94
+ const notLoaded = (what) => () => {
95
+ throw new Error(
96
+ `react-x11: the layout engine is not loaded, so Yoga.${what} is not ` +
97
+ 'available yet. createRoot() loads it; a node built outside one — a ' +
98
+ 'test harness with a mock app — needs `await loadLayout()` first.',
99
+ );
100
+ };
101
+
102
+ // A useful message instead of "Cannot read properties of undefined"
103
+ for (const name of ['Node', 'Config']) {
104
+ Object.defineProperty(Yoga, name, {
105
+ configurable: true,
106
+ get: notLoaded(name),
107
+ });
108
+ }
109
+
110
+ let loading = null;
111
+
112
+ /**
113
+ * Load the layout engine's WebAssembly. Idempotent, and resolves with the
114
+ * same `Yoga` object this module exports — `createRoot()` awaits it, so
115
+ * applications rarely call it themselves. A tree built without one (the
116
+ * headless mock app in `react-x11/test`) needs it.
117
+ */
118
+ export function loadLayout() {
119
+ if (!loading) {
120
+ loading = loadYoga().then((assembly) => {
121
+ for (const name of ['Node', 'Config']) delete Yoga[name]; // drop the throwing getters
122
+ Object.assign(Yoga, assembly);
123
+ return Yoga;
124
+ });
125
+ }
126
+ return loading;
127
+ }
128
+
129
+ /** Whether the assembly is in place — layout will not throw. */
130
+ export function layoutLoaded() {
131
+ return Object.getOwnPropertyDescriptor(Yoga, 'Node')?.value !== undefined;
132
+ }
133
+
134
+ export { Yoga };
135
+ export default Yoga;
@@ -1,28 +0,0 @@
1
- import { createElement as h } from 'react';
2
-
3
- /**
4
- * `<Canvas3D>` — the react-three-fiber-shaped entry point to the 3D scene.
5
- * A thin wrapper over the `<glarea>` host element: it is the surface that
6
- * owns the GL context, and the scene lives in its children.
7
- *
8
- * ```jsx
9
- * <Canvas3D flexGrow={1} camera={{ position: [3, 3, 6], fov: 50 }}>
10
- * <mesh rotation={[0.4, 0.8, 0]}>
11
- * <boxGeometry args={[1, 1, 1]} />
12
- * <meshBasicMaterial color="#2980b9" />
13
- * </mesh>
14
- * </Canvas3D>
15
- * ```
16
- *
17
- * Props are `<glarea>`'s (layout props, `clearColor`, `frameLoop`, `glx`,
18
- * `onCreated`, `onDraw`, `onError`) plus `camera`:
19
- * `{ position, target, up, fov, near, far, orthographic, zoom }`.
20
- *
21
- * The name is `Canvas3D`, not r3f's `Canvas`, because react-x11 already has
22
- * a `<canvas>` host element — the 2D `onDraw` escape hatch.
23
- */
24
- export function Canvas3D({ children, ...props }) {
25
- return h('glarea', props, children);
26
- }
27
-
28
- export default Canvas3D;