react-x11 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +330 -81
- package/package.json +132 -14
- package/src/ClickToComponent.js +9 -2
- package/src/DevToolsIntegration.js +440 -32
- package/src/Reconciler.js +463 -165
- package/src/a11y.js +1299 -0
- package/src/acceleratorhooks.js +86 -0
- package/src/accelerators.js +128 -0
- package/src/activate.js +168 -0
- package/src/anchor.js +360 -0
- package/src/appcontext.js +161 -0
- package/src/appearance.js +748 -0
- package/src/appearancehooks.js +96 -0
- package/src/apphooks.js +73 -0
- package/src/application.js +748 -0
- package/src/atspi.js +1953 -0
- package/src/bus.js +545 -0
- package/src/bushooks.js +136 -0
- package/src/clientmessage.js +140 -0
- package/src/clipboard.js +121 -0
- package/src/components/Button.js +148 -33
- package/src/components/Checkbox.js +66 -32
- package/src/components/Dialog.js +96 -29
- package/src/components/FileDialog.js +491 -0
- package/src/components/Icon.js +329 -0
- package/src/components/Menu.js +797 -133
- package/src/components/PasswordInput.js +434 -0
- package/src/components/ProgressBar.js +91 -12
- package/src/components/Radio.js +78 -35
- package/src/components/Select.js +255 -65
- package/src/components/Slider.js +102 -41
- package/src/components/SplitPane.js +206 -0
- package/src/components/Switch.js +100 -22
- package/src/components/Table.js +529 -0
- package/src/components/Tabs.js +211 -0
- package/src/components/Tooltip.js +528 -54
- package/src/components/anchor.js +154 -107
- package/src/components/change.js +34 -0
- package/src/components/dnd.js +103 -0
- package/src/components/index.js +16 -3
- package/src/components/keys.js +17 -20
- package/src/components/scribble.js +247 -0
- package/src/components/theme.js +350 -42
- package/src/components/typeahead.js +12 -2
- package/src/compose.js +868 -0
- package/src/compositing.js +224 -0
- package/src/dbusmenu.js +384 -0
- package/src/debug.d.ts +63 -0
- package/src/debug.js +642 -0
- package/src/decorations.js +486 -0
- package/src/desktopsettings.js +209 -0
- package/src/desktopsettingshooks.js +62 -0
- package/src/dnd.js +1725 -0
- package/src/editmenu.js +272 -0
- package/src/errors.js +98 -0
- package/src/events.js +1406 -162
- package/src/extensions.js +45 -0
- package/src/filedialog.js +375 -0
- package/src/filedialoghooks.js +132 -0
- package/src/fonthooks.js +64 -0
- package/src/fonts.js +301 -0
- package/src/foreignnodes.js +519 -0
- package/src/frame/child.js +33 -0
- package/src/frame/childmain.js +274 -0
- package/src/frame/env.js +140 -0
- package/src/frame/index.js +444 -0
- package/src/frame/lifecycle.js +67 -0
- package/src/frame/protocol.js +179 -0
- package/src/frames.js +73 -0
- package/src/glbackend.js +114 -0
- package/src/glnodes.js +125 -67
- package/src/globalmenu.js +683 -0
- package/src/host.d.ts +88 -0
- package/src/host.js +42 -0
- package/src/idle.js +490 -0
- package/src/idlehooks.js +100 -0
- package/src/imagesource.js +349 -0
- package/src/index.d.ts +475 -0
- package/src/index.js +83 -9
- package/src/inputtime.js +206 -0
- package/src/jsx-dev-runtime.d.ts +21 -0
- package/src/jsx-dev-runtime.js +2 -0
- package/src/jsx-runtime.d.ts +39 -0
- package/src/jsx-runtime.js +6 -0
- package/src/keyboard.js +256 -0
- package/src/keyboardstate.js +278 -0
- package/src/keyboardstatehooks.js +58 -0
- package/src/keysyms.d.ts +149 -0
- package/src/keysyms.js +270 -0
- package/src/locale.js +170 -0
- package/src/localehooks.js +47 -0
- package/src/menuitem.js +223 -0
- package/src/node.d.ts +597 -0
- package/src/node.js +44 -0
- package/src/nodes.js +9546 -690
- package/src/ntk.d.ts +44 -0
- package/src/ntk.js +25 -0
- package/src/paintcache.js +366 -0
- package/src/palette.js +380 -0
- package/src/pastestate.js +66 -0
- package/src/portal.js +461 -0
- package/src/priority.js +26 -0
- package/src/refresh/index.d.ts +40 -0
- package/src/refresh/index.js +122 -0
- package/src/refresh/loader.d.ts +37 -0
- package/src/refresh/loader.js +401 -0
- package/src/refresh/register.d.ts +5 -0
- package/src/refresh/register.js +13 -0
- package/src/registry.js +232 -0
- package/src/scale.js +626 -0
- package/src/scalehooks.js +27 -0
- package/src/screencolor.js +640 -0
- package/src/screencolorhooks.js +101 -0
- package/src/screens.js +754 -0
- package/src/screenshooks.js +137 -0
- package/src/startup.js +302 -0
- package/src/style.d.ts +126 -0
- package/src/style.js +33 -0
- package/src/styles.js +1482 -6
- package/src/svgnodes.js +306 -0
- package/src/testing/a11y.js +484 -0
- package/src/testing/components.js +414 -0
- package/src/testing/events.js +407 -0
- package/src/testing/harness.js +455 -0
- package/src/testing/index.d.ts +558 -0
- package/src/testing/index.js +85 -0
- package/src/testing/mock-app.js +463 -0
- package/src/testing/pixels.js +152 -0
- package/src/testing/queries.js +224 -0
- package/src/textrange.js +83 -0
- package/src/textselection.js +439 -0
- package/src/trace-registry.js +63 -0
- package/src/transfer.js +93 -0
- package/src/types/appearance.d.ts +84 -0
- package/src/types/application.d.ts +173 -0
- package/src/types/components.d.ts +820 -0
- package/src/types/dbus.d.ts +177 -0
- package/src/types/elements.d.ts +897 -0
- package/src/types/events.d.ts +549 -0
- package/src/types/filedialog.d.ts +260 -0
- package/src/types/fonts.d.ts +124 -0
- package/src/types/frame.d.ts +146 -0
- package/src/types/globalmenu.d.ts +42 -0
- package/src/types/nodes.d.ts +199 -0
- package/src/types/screencolor.d.ts +84 -0
- package/src/types/style.d.ts +434 -0
- package/src/types/system.d.ts +287 -0
- package/src/windowid.js +151 -0
- package/src/windowstate.js +393 -0
- package/src/xsettings.js +336 -0
- package/src/yoga.d.ts +55 -0
- package/src/yoga.js +135 -0
- package/src/components/Canvas3D.js +0 -28
- package/src/geometry3d.js +0 -223
- package/src/pointer3d.js +0 -158
- package/src/raycast3d.js +0 -146
- package/src/richnodes.js +0 -436
- package/src/scene3d.js +0 -683
package/src/xsettings.js
ADDED
|
@@ -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;
|