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,137 @@
1
+ // `useScreens()` — the monitor layout as something a component re-renders on.
2
+ //
3
+ // The store lives in `screens.js`, keyed by connection rather than shared
4
+ // across the process the way `appearance.js` is: the screen layout is a fact
5
+ // about one X display, and a process driving two of them is exactly what the
6
+ // test suite does routinely.
7
+
8
+ import { useCallback, useSyncExternalStore } from 'react';
9
+
10
+ import { useApp } from './appcontext.js';
11
+ import { screensSnapshot, watchScreens } from './screens.js';
12
+ import { scaleOf, monitorScalesOf } from './scale.js';
13
+
14
+ /**
15
+ * The raw snapshot is device pixels — `screens.js` clamps CreateWindow
16
+ * geometry with it and must stay that way — and the hook's callers are
17
+ * application code, which thinks in logical pixels like every prop it
18
+ * writes. Divided here, per snapshot object so referential stability
19
+ * survives (useSyncExternalStore re-renders on identity).
20
+ *
21
+ * Each screen also carries its own `scale`: on a desktop where the ladder
22
+ * read the hardware (src/scale.js), a retina lid and an office monitor
23
+ * really do answer differently, and an app that places windows can honour
24
+ * that. Where the desktop configured one factor, every entry carries it.
25
+ */
26
+ const logicalSnapshots = new WeakMap();
27
+
28
+ function logicalScreens(app, raw) {
29
+ if (!raw) return raw;
30
+ const cached = logicalSnapshots.get(raw);
31
+ if (cached) return cached;
32
+ const s = scaleOf(app);
33
+ const perMonitor = monitorScalesOf(app);
34
+ const rect = (r) =>
35
+ r == null
36
+ ? r
37
+ : Object.freeze({
38
+ ...r,
39
+ x: r.x / s,
40
+ y: r.y / s,
41
+ width: r.width / s,
42
+ height: r.height / s,
43
+ });
44
+ const out = Object.freeze({
45
+ ...raw,
46
+ screens: Object.freeze(
47
+ raw.screens.map((screen) =>
48
+ Object.freeze({
49
+ ...rect(screen),
50
+ available: rect(screen.available),
51
+ scale:
52
+ (screen.name && perMonitor.get(screen.name)?.scale) ??
53
+ perMonitor.get(screen.outputs?.[0])?.scale ??
54
+ s,
55
+ }),
56
+ ),
57
+ ),
58
+ primary: null, // reattached below so it stays an identity into `screens`
59
+ workArea: rect(raw.workArea),
60
+ virtual: rect(raw.virtual),
61
+ });
62
+ const primary =
63
+ out.screens.find((screen) => screen.primary) ??
64
+ (out.screens.length === 1 ? out.screens[0] : null);
65
+ const finished = Object.freeze({ ...out, primary });
66
+ logicalSnapshots.set(raw, finished);
67
+ return finished;
68
+ }
69
+
70
+ /**
71
+ * The monitors this display has, live.
72
+ *
73
+ * ```jsx
74
+ * const { screens, primary } = useScreens();
75
+ *
76
+ * <Select
77
+ * value={monitor}
78
+ * onChange={setMonitor}
79
+ * options={screens.map((s) => ({
80
+ * value: s.name,
81
+ * label: `${s.name} — ${s.width}×${s.height}`,
82
+ * }))}
83
+ * />
84
+ * ```
85
+ *
86
+ * Each entry is:
87
+ *
88
+ * | | |
89
+ * | --- | --- |
90
+ * | `name` | `'HDMI-1'`, `'eDP-1'` — **null** where the server has no RandR |
91
+ * | `x` `y` `width` `height` | the monitor's rect in virtual-screen coordinates |
92
+ * | `available` | that rect minus the panels — where a window can go |
93
+ * | `primary` | the desktop's main monitor, where panels and new windows land |
94
+ * | `widthMM` `heightMM` | physical size, or null |
95
+ * | `refreshRate` | Hz to two decimals (`59.99`), or null |
96
+ * | `rotation` | `0`, `90`, `180` or `270` |
97
+ * | `outputs` | every output on this monitor — two names means it is mirrored |
98
+ *
99
+ * and the object around them carries `primary` (the entry, or null), the
100
+ * desktop-wide `workArea`, the whole `virtual` screen, and `source`.
101
+ *
102
+ * **`name` is null before RandR answers, and on a server without it.** The
103
+ * geometry resolves during `createRoot()` from Xinerama, which is one round
104
+ * trip; the names, the primary flag and the physical sizes take a ten-round-
105
+ * trip RandR walk that deliberately does not hold startup up, so they appear
106
+ * a moment later. The rects do not move when they land — Xinerama on a
107
+ * modern server *is* RandR's emulation of it — so a component that rendered
108
+ * against the early answer sees fields fill in, not values change. Where a
109
+ * name is what gets persisted, treat null as "not known yet" and keep the
110
+ * last one, rather than writing it.
111
+ *
112
+ * `source` says which tier answered: `'randr'`, `'xinerama'`, `'screen'`
113
+ * (one entry covering the whole display, for a server with neither
114
+ * extension), `'test'`, or null on a headless mock with no display at all.
115
+ *
116
+ * **`available` is an approximation and the only one here.** `_NET_WORKAREA`
117
+ * is published for the whole virtual desktop rather than per monitor, so it
118
+ * is applied as a per-axis bound: exact on one head, and on several it still
119
+ * takes a top or bottom panel off the height. Deriving a true per-monitor
120
+ * work area means reading `_NET_WM_STRUT_PARTIAL` off every window on the
121
+ * screen — see the note in `screens.js`.
122
+ *
123
+ * Re-renders when a monitor is plugged in or unplugged, when the arrangement
124
+ * changes, and when a panel appears, moves or auto-hides.
125
+ */
126
+ export function useScreens() {
127
+ const app = useApp();
128
+ const subscribe = useCallback(
129
+ (onChange) => watchScreens(app, onChange),
130
+ [app],
131
+ );
132
+ const snapshot = useCallback(
133
+ () => logicalScreens(app, screensSnapshot(app)),
134
+ [app],
135
+ );
136
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
137
+ }
package/src/startup.js ADDED
@@ -0,0 +1,302 @@
1
+ // Startup notification (freedesktop): tell the desktop the app has finished
2
+ // starting, and tell the window manager which user action started it.
3
+ //
4
+ // Two visible defects without it, for anyone who launches from a launcher
5
+ // rather than a terminal. The launcher opens a startup sequence when it
6
+ // spawns us and closes it when we say we are up; say nothing and it runs to
7
+ // mutter's STARTUP_TIMEOUT_MS, which is 15 seconds of busy cursor over a
8
+ // window the user is already clicking. And `_NET_WM_USER_TIME` is the
9
+ // evidence focus-stealing prevention weighs when deciding whether a new
10
+ // window may come forward; with none, a strict desktop opens us behind
11
+ // whatever the user was doing.
12
+ //
13
+ // This is core rather than an integration package: it is X11 over the
14
+ // connection the renderer already has, using calls already in ntk and
15
+ // node-x11 — no dependency, no engines floor, nothing to opt into. It sits
16
+ // next to `WM_DELETE_WINDOW` and `_NET_WM_PID` in kind.
17
+ //
18
+ // See docs/desktop.md. Issue #174.
19
+ import { eventMask } from 'x11/lib/eventmask.js';
20
+
21
+ /** mutter gives up at 15s. This is the backstop for an app that never
22
+ * paints at all — early enough to beat that by a margin, late enough that
23
+ * no honest first frame trips it. */
24
+ const BACKSTOP_MS = 10_000;
25
+
26
+ const BEGIN = '_NET_STARTUP_INFO_BEGIN';
27
+ const CONTINUE = '_NET_STARTUP_INFO';
28
+ /** The protocol's chunk size: format 8, 20 bytes per ClientMessage. */
29
+ const CHUNK = 20;
30
+
31
+ /**
32
+ * Take `DESKTOP_STARTUP_ID` out of the environment.
33
+ *
34
+ * **Deleted, not merely read.** The variable names exactly one launch, and
35
+ * a child process that inherits it claims a sequence that is not its own
36
+ * and ends it early — the parent's busy cursor stops when the child starts.
37
+ * Every toolkit that gets this wrong produces that same bug, which is why
38
+ * the read is destructive rather than tidy.
39
+ *
40
+ * Deliberately not memoized: the deletion is the memo, and a cache here
41
+ * would outlive the launch it belongs to.
42
+ */
43
+ function consumeEnv() {
44
+ const found = process.env?.DESKTOP_STARTUP_ID || null;
45
+ if (process.env) delete process.env.DESKTOP_STARTUP_ID;
46
+ return found;
47
+ }
48
+
49
+ /** The id in force: whatever a root was given, else the environment's.
50
+ * `undefined` until something has looked. */
51
+ let currentId;
52
+
53
+ /** The live session, if any. One per process, because one launch is. */
54
+ let current = null;
55
+
56
+ /**
57
+ * The X server timestamp of the user action that launched this app, from
58
+ * the `_TIME` suffix of the startup id, or `null`.
59
+ *
60
+ * `null` is a real answer rather than a failure: an app started from a
61
+ * shell has no launch timestamp and never will. Callers that want to raise
62
+ * a window use it as the "when", and `0` is not a substitute — EWMH gives
63
+ * zero its own meaning ("do not focus this on map").
64
+ */
65
+ export function launchTimestamp() {
66
+ if (currentId === undefined) currentId = consumeEnv();
67
+ return parseLaunchTime(currentId);
68
+ }
69
+
70
+ /** `foo_TIME12345` → `12345`. Anything else, including a `_TIME` that is
71
+ * not a number, is `null`. Never throws. */
72
+ export function parseLaunchTime(id) {
73
+ if (typeof id !== 'string') return null;
74
+ const at = id.lastIndexOf('_TIME');
75
+ if (at < 0) return null;
76
+ const digits = id.slice(at + 5);
77
+ if (!/^\d+$/.test(digits)) return null;
78
+ const value = Number(digits);
79
+ return Number.isSafeInteger(value) ? value : null;
80
+ }
81
+
82
+ /**
83
+ * A value as the protocol's parser wants it. Always quoted, which is always
84
+ * legal and saves deciding; inside the quotes only `\` and `"` are escaped.
85
+ *
86
+ * Note what this is *not*: C escaping. The spec is explicit that `\n` here
87
+ * means the letter n, so a newline passes through as itself and must not be
88
+ * turned into a backslash and an n.
89
+ */
90
+ function quote(value) {
91
+ return `"${String(value).replace(/[\\"]/g, (c) => `\\${c}`)}"`;
92
+ }
93
+
94
+ /** `remove: ID="foo"` — a message type and its key/value pairs. */
95
+ export function encodeStartupMessage(type, fields) {
96
+ const pairs = Object.entries(fields)
97
+ .filter(([, v]) => v != null)
98
+ .map(([k, v]) => `${k}=${quote(v)}`);
99
+ return `${type}: ${pairs.join(' ')}`;
100
+ }
101
+
102
+ /**
103
+ * The message split into the ClientMessages that carry it: 20 bytes each,
104
+ * nul-terminated, zero-padded.
105
+ *
106
+ * The trailing nul is part of the message rather than padding, so a message
107
+ * whose bytes land on an exact multiple of 20 still needs the chunk after
108
+ * it — otherwise the last byte used is not a nul and a strict reader waits
109
+ * forever for the rest.
110
+ */
111
+ export function messageChunks(text) {
112
+ const body = Buffer.from(text, 'utf8');
113
+ const bytes = Buffer.concat([body, Buffer.from([0])]);
114
+ const chunks = [];
115
+ for (let at = 0; at < bytes.length; at += CHUNK) {
116
+ const chunk = new Array(CHUNK).fill(0);
117
+ for (let i = 0; i < CHUNK && at + i < bytes.length; i++) {
118
+ chunk[i] = bytes[at + i];
119
+ }
120
+ chunks.push(chunk);
121
+ }
122
+ return chunks;
123
+ }
124
+
125
+ /**
126
+ * Resolve the `startupNotification` option into `{ id, completeOn }`, or
127
+ * `null` for opted out.
128
+ *
129
+ * `completeOn` defaults to `'paint'` — see `StartupSession.painted`.
130
+ */
131
+ function settings(option) {
132
+ if (option === false) return null;
133
+ // Consumed either way, even when an explicit id wins: leaving it set
134
+ // would hand this launch's id to the next child process spawned.
135
+ const fromEnv = consumeEnv();
136
+ const given =
137
+ typeof option === 'string'
138
+ ? { id: option }
139
+ : option && typeof option === 'object'
140
+ ? option
141
+ : {};
142
+ return { id: given.id ?? fromEnv, completeOn: given.completeOn ?? 'paint' };
143
+ }
144
+
145
+ class StartupSession {
146
+ constructor(app, { id, completeOn }) {
147
+ this.app = app;
148
+ this.id = id;
149
+ this.completeOn = completeOn;
150
+ this.time = parseLaunchTime(id);
151
+ this.done = false;
152
+ this.timer = null;
153
+ }
154
+
155
+ /**
156
+ * `_NET_STARTUP_ID` and `_NET_WM_USER_TIME` on the first toplevel, and
157
+ * **before it maps**: EWMH's guarantee is about the state of the window at
158
+ * the moment it is mapped, so setting them afterwards is too late and
159
+ * looks identical in a log.
160
+ */
161
+ decorate(wnd) {
162
+ if (this.claimed || !this.id) return;
163
+ this.claimed = wnd;
164
+ wnd.setProperty?.('_NET_STARTUP_ID', this.id);
165
+ if (this.time !== null) {
166
+ wnd.setProperty?.('_NET_WM_USER_TIME', [this.time], {
167
+ type: 'CARDINAL',
168
+ format: 32,
169
+ });
170
+ }
171
+ // Intern now rather than at the moment of completion. Both are round
172
+ // trips, and the whole point of this feature is that the second one
173
+ // happens the instant the app is up — spending it here, while the first
174
+ // frame is still being built, costs nothing anybody can see.
175
+ this._primeAtoms(wnd);
176
+ }
177
+
178
+ _primeAtoms(wnd) {
179
+ const X = wnd.X;
180
+ if (!X?.InternAtom) return;
181
+ const atom = (name) =>
182
+ new Promise((resolve, reject) =>
183
+ X.InternAtom(false, name, (err, id) =>
184
+ err ? reject(err) : resolve(id),
185
+ ),
186
+ );
187
+ this.atomsReady = Promise.all([atom(BEGIN), atom(CONTINUE)])
188
+ .then((ids) => {
189
+ this.atoms = ids;
190
+ })
191
+ .catch(() => {
192
+ // A display that cannot intern an atom has worse problems than a
193
+ // busy cursor, and the launcher's own timeout still covers this.
194
+ });
195
+ }
196
+
197
+ /** The first toplevel is up. Arms the backstop; completes now if that is
198
+ * what this app asked for. */
199
+ mapped(wnd) {
200
+ if (this.done || !this.id || this.claimed !== wnd) return;
201
+ if (this.completeOn === 'map') return this.complete();
202
+ // Whichever comes first. An app that never paints — headless, a throw in
203
+ // the first render, a window mounted hidden — must still end the
204
+ // sequence, or this reproduces the bug it exists to fix.
205
+ this.timer ??= setTimeout(() => this.complete(), BACKSTOP_MS);
206
+ this.timer.unref?.();
207
+ }
208
+
209
+ /**
210
+ * A flush actually painted — the default moment, and the one this
211
+ * renderer can name where a toolkit that paints on map cannot.
212
+ *
213
+ * Not the map: `invalidate()` schedules through the frame clock and the
214
+ * drawing happens in `flush()` a frame later, so a mapped window is an
215
+ * empty one. Ending the sequence there stops the busy cursor over a blank
216
+ * rectangle — compliant, and a worse answer than the timeout.
217
+ *
218
+ * Not "real" content either. If the first frame is a spinner because the
219
+ * tree is suspended, that is exactly the right moment to stop the
220
+ * *system's* spinner: the app is up and is telling the user what it is
221
+ * doing. There is no signal for "finished loading" and guessing at one is
222
+ * how this ends up back at fifteen seconds.
223
+ */
224
+ painted() {
225
+ if (this.completeOn === 'paint') this.complete();
226
+ }
227
+
228
+ /**
229
+ * Send `remove:` and stand down. Idempotent, because three things race to
230
+ * call it and the protocol should see exactly one.
231
+ */
232
+ complete() {
233
+ if (this.done) return;
234
+ this.done = true;
235
+ if (this.timer) clearTimeout(this.timer);
236
+ this.timer = null;
237
+ if (this.app._reactX11Startup === this) this.app._reactX11Startup = null;
238
+ const wnd = this.claimed;
239
+ if (!this.id || !wnd) return;
240
+ const text = encodeStartupMessage('remove', { ID: this.id });
241
+ // Normally the atoms landed while the first frame was being built, and
242
+ // this goes out in the same turn as the paint that triggered it. The
243
+ // fallback is for completing before they arrive, which is what
244
+ // `completeOn: 'map'` does on a cold connection.
245
+ if (this.atoms) send(wnd, this.atoms, text);
246
+ else this.atomsReady?.then(() => this.atoms && send(wnd, this.atoms, text));
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Broadcast a startup message to the root window.
252
+ *
253
+ * Two things the transport gets wrong by default. The event mask has to be
254
+ * `PropertyChange` — `SendClientMessage` defaults to the substructure pair
255
+ * EWMH wants for root messages, and this protocol is not that. And the
256
+ * `window` field names a window the *sender* owns, while the destination is
257
+ * the root, which is why both are arguments.
258
+ */
259
+ function send(wnd, [begin, cont], text) {
260
+ const X = wnd.X;
261
+ const root = X?.display?.screen?.[0]?.root;
262
+ if (!X?.SendClientMessage || !root) return;
263
+ messageChunks(text).forEach((chunk, i) => {
264
+ X.SendClientMessage(
265
+ root,
266
+ wnd.id,
267
+ i === 0 ? begin : cont,
268
+ 8,
269
+ chunk,
270
+ eventMask.PropertyChange,
271
+ );
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Begin a session for a root, or `null` when there is nothing to do — no
277
+ * id in the environment (a terminal, CI, XQuartz) or the app opted out.
278
+ * Nothing is sent and nothing is set in that case.
279
+ */
280
+ export function beginStartup(app, option) {
281
+ const resolved = settings(option);
282
+ currentId = resolved?.id ?? null;
283
+ if (!resolved?.id) return null;
284
+ const session = new StartupSession(app, resolved);
285
+ app._reactX11Startup = session;
286
+ current = session;
287
+ return session;
288
+ }
289
+
290
+ /**
291
+ * End the startup sequence now. Idempotent, and a no-op when there is none,
292
+ * so an app may call it unconditionally.
293
+ *
294
+ * This is the seam behind `completeOn: 'manual'`, for an app that knows
295
+ * better than "the first frame" — one whose first frame is a splash it does
296
+ * not want to be judged by, or which is up only once a session is restored.
297
+ * No arguments, because a process has one launch however many roots it
298
+ * builds.
299
+ */
300
+ export function notifyStartupComplete() {
301
+ current?.complete();
302
+ }
package/src/style.d.ts ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `react-x11/style` — the style vocabulary, for code outside the package
3
+ * that has to speak it: a registered element asking whether a prop name is
4
+ * style, resolving `$token` references against a theme, or flattening the
5
+ * array/object `style` shape the built-ins accept.
6
+ */
7
+ import type {
8
+ Animation,
9
+ AnimationSpec,
10
+ Easing,
11
+ Style,
12
+ StyleProperties,
13
+ } from './types/style.js';
14
+
15
+ export type { Animation, AnimationSpec, Easing, Style, StyleProperties };
16
+
17
+ /** Freeze a stylesheet object, the `StyleSheet.create` of this renderer. */
18
+ export function createStyles<T extends Record<string, Style>>(sheet: T): T;
19
+
20
+ /** Collapse an array/nested `style` prop into one object. */
21
+ export function flattenStyle(
22
+ style: Style | Style[] | null | undefined,
23
+ ): StyleProperties;
24
+
25
+ /** Is this prop name part of the style vocabulary? The question a
26
+ * registered element asks before treating a prop as its own semantics. */
27
+ export function isStyleProp(name: string): boolean;
28
+ export function isLayoutProp(name: string): boolean;
29
+ export function isPaintProp(name: string): boolean;
30
+ export function isAnimatableProp(name: string): boolean;
31
+
32
+ /** Overlay the `:hover` / `:focus` / `:active` / `:disabled` blocks that
33
+ * the given states select. */
34
+ export function resolveStyleStates(
35
+ style: StyleProperties,
36
+ states: Record<string, boolean>,
37
+ ): StyleProperties;
38
+ export function hasStateStyles(style: StyleProperties): boolean;
39
+
40
+ /** Does this style reference any `$token`? */
41
+ export function styleUsesTokens(style: StyleProperties): boolean;
42
+ export function tokenNames(
43
+ style: StyleProperties,
44
+ out?: Set<string>,
45
+ ): Set<string>;
46
+ /** Replace `$token` references with values from the theme. */
47
+ export function resolveTokens(
48
+ style: StyleProperties,
49
+ theme: Record<string, unknown> | null | undefined,
50
+ where?: string,
51
+ strict?: boolean,
52
+ ): StyleProperties;
53
+
54
+ export function styleHasSizeQueries(style: StyleProperties): boolean;
55
+ /** Does the style carry a `'@supports …'` block? Those are re-resolved when
56
+ * the server's answer changes, not when the window is laid out. */
57
+ export function styleHasSupportsQueries(style: StyleProperties): boolean;
58
+ /** `resolveQueries` with only the size half — the shape this had before
59
+ * capability blocks existed. */
60
+ export function resolveSizeQueries(
61
+ style: StyleProperties,
62
+ size: { width: number; height: number },
63
+ ): StyleProperties;
64
+ /** Merge every matching `@` block — size and capability alike — in
65
+ * declaration order. `supports` maps feature name to whether the window can
66
+ * actually do it; a missing map matches nothing, which is the safe way
67
+ * round. */
68
+ export function resolveQueries(
69
+ style: StyleProperties,
70
+ context?: {
71
+ size?: { width: number; height: number } | null;
72
+ supports?: Record<string, boolean> | null;
73
+ },
74
+ ): StyleProperties;
75
+
76
+ export function interpolate(from: unknown, to: unknown, t: number): unknown;
77
+ /**
78
+ * A colour at a given opacity — the way to build a status *panel* out of a
79
+ * status colour: `tint(theme.danger, 0.12)` is a wash of it that the ink on
80
+ * top keeps its own contrast against, where an opaque tint would have to
81
+ * have been chosen against an ink it does not own.
82
+ */
83
+ export function tint(color: string, alpha: number): string;
84
+ /**
85
+ * Which of `inks` can be read on `fill` — the highest WCAG contrast ratio.
86
+ * The palette derives `accentText` and the status inks with this; an app
87
+ * choosing ink for a fill of its own can use the same rule.
88
+ */
89
+ export function readableInk(fill: string, inks: string[]): string;
90
+ export function transitionFor(
91
+ style: StyleProperties,
92
+ prop: string,
93
+ ): { duration: number; delay?: number } | null;
94
+ export function ease(t: number): number;
95
+
96
+ /** A resolved loop declaration: `from` filled in from the style, both ends
97
+ * checked for a midpoint, the easing looked up. */
98
+ export interface ResolvedAnimation extends Required<
99
+ Omit<AnimationSpec, 'easing' | 'alternate'>
100
+ > {
101
+ prop: keyof StyleProperties;
102
+ easing: Easing;
103
+ alternate: boolean;
104
+ ease(t: number): number;
105
+ }
106
+ /** The loops a style declares, or null. Throws on a declaration that could
107
+ * never run — see `style.animation` in docs/styling.md. */
108
+ export function animationsOf(
109
+ style: StyleProperties,
110
+ where?: string,
111
+ ): ResolvedAnimation[] | null;
112
+ /** Whether two resolved loops describe the same motion — what decides
113
+ * between keeping a running loop's phase and starting it over. */
114
+ export function sameAnimation(
115
+ a: ResolvedAnimation,
116
+ b: ResolvedAnimation,
117
+ ): boolean;
118
+ /** Where a loop is `elapsed` ms after it started. */
119
+ export function animationValueAt(
120
+ spec: ResolvedAnimation,
121
+ elapsed: number,
122
+ ): unknown;
123
+ export const EASING_NAMES: readonly Easing[];
124
+
125
+ export const EMPTY_STYLE: Readonly<StyleProperties>;
126
+ export const STATE_KEYS: readonly string[];
package/src/style.js ADDED
@@ -0,0 +1,33 @@
1
+ // `react-x11/style` — the style vocabulary, for code that has to speak it
2
+ // from outside the package: a registered element deciding whether a prop
3
+ // name is style (`isStyleProp`), resolving `$token` references against a
4
+ // theme, or flattening the same array/object `style` prop shape the
5
+ // built-ins take.
6
+ export {
7
+ createStyles,
8
+ flattenStyle,
9
+ isStyleProp,
10
+ isLayoutProp,
11
+ isPaintProp,
12
+ isAnimatableProp,
13
+ resolveStyleStates,
14
+ hasStateStyles,
15
+ styleUsesTokens,
16
+ tokenNames,
17
+ resolveTokens,
18
+ styleHasSizeQueries,
19
+ styleHasSupportsQueries,
20
+ resolveSizeQueries,
21
+ resolveQueries,
22
+ interpolate,
23
+ tint,
24
+ readableInk,
25
+ transitionFor,
26
+ animationsOf,
27
+ sameAnimation,
28
+ animationValueAt,
29
+ ease,
30
+ EASING_NAMES,
31
+ EMPTY_STYLE,
32
+ STATE_KEYS,
33
+ } from './styles.js';