react-x11 2.1.4 → 2.2.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/package.json +1 -1
- package/src/Reconciler.js +12 -0
- package/src/a11y.js +8 -0
- package/src/appearance.js +11 -0
- package/src/atspi.js +6 -3
- package/src/bus.js +100 -13
- package/src/desktopintegration.js +98 -0
- package/src/globalmenu.js +5 -0
- package/src/index.d.ts +37 -0
- package/src/nodes.js +44 -5
package/package.json
CHANGED
package/src/Reconciler.js
CHANGED
|
@@ -53,6 +53,7 @@ import { endIdle } from './idle.js';
|
|
|
53
53
|
import { endKeyboardState } from './keyboardstate.js';
|
|
54
54
|
import { endXSettings } from './xsettings.js';
|
|
55
55
|
import { watchAppearance } from './appearance.js';
|
|
56
|
+
import { setDesktopIntegration } from './desktopintegration.js';
|
|
56
57
|
import { ForeignNode } from './foreignnodes.js';
|
|
57
58
|
import { GlAreaNode } from './glnodes.js';
|
|
58
59
|
import { createRegisteredNode, registeredElements } from './registry.js';
|
|
@@ -605,6 +606,12 @@ function watchConnection(app, onDisconnect, deliberate) {
|
|
|
605
606
|
*
|
|
606
607
|
* `display`, `fontSource`, `glxVisual` and `onXError` go straight to ntk.
|
|
607
608
|
* Anything else ntk understands, build the client yourself and pass `app`.
|
|
609
|
+
*
|
|
610
|
+
* `desktop: false` turns off the three things this turns on for you that talk
|
|
611
|
+
* to the session bus — the appearance ladder, the accessibility bridge and
|
|
612
|
+
* the global menu — for an embedder that owns them, or a process that must
|
|
613
|
+
* not fork. `desktop: { appearance: false }` names one. See
|
|
614
|
+
* src/desktopintegration.js and docs/desktop.md.
|
|
608
615
|
*/
|
|
609
616
|
export async function createRoot(options = {}) {
|
|
610
617
|
// Before anything builds a node: every drawn node creates a yoga node in
|
|
@@ -627,6 +634,11 @@ export async function createRoot(options = {}) {
|
|
|
627
634
|
}
|
|
628
635
|
const { app: borrowed, onDisconnect, ...rest } = options;
|
|
629
636
|
const owned = borrowed === undefined;
|
|
637
|
+
// Before anything starts, for two reasons: a bad `desktop` shape must throw
|
|
638
|
+
// with nothing in flight, like the check above it — and `startA11y()` below
|
|
639
|
+
// reads this policy, so it has to be settled before the first await, not
|
|
640
|
+
// after (src/desktopintegration.js).
|
|
641
|
+
setDesktopIntegration(rest.desktop);
|
|
630
642
|
// The connection is started first, and the order is the point rather than a
|
|
631
643
|
// detail. `loadLayout()` is only nominally asynchronous: instantiating the
|
|
632
644
|
// engine blocks the event loop for 15-50 ms before it returns its promise
|
package/src/a11y.js
CHANGED
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
// from gi.repository import Atspi; \
|
|
41
41
|
// print({k: int(getattr(Atspi.Role, k)) for k in dir(Atspi.Role) if not k.startswith('_')})"
|
|
42
42
|
|
|
43
|
+
// The one import, and it keeps this file's promise: a `Set` and two functions
|
|
44
|
+
// over it, with no D-Bus, no node builtins and nothing at module scope.
|
|
45
|
+
import { desktopIntegrationEnabled } from './desktopintegration.js';
|
|
46
|
+
|
|
43
47
|
/** AT-SPI role numbers (AtspiRole). */
|
|
44
48
|
export const ATSPI_ROLE = Object.freeze({
|
|
45
49
|
INVALID: 0,
|
|
@@ -1245,6 +1249,10 @@ let startPromise = null;
|
|
|
1245
1249
|
* tests are exactly this case.
|
|
1246
1250
|
*/
|
|
1247
1251
|
function a11yEnabled() {
|
|
1252
|
+
// `createRoot({ desktop: false })` outranks every environment variable,
|
|
1253
|
+
// including the one that forces the climb: it is the embedder saying this
|
|
1254
|
+
// process does not talk to the desktop (src/desktopintegration.js, #417).
|
|
1255
|
+
if (!desktopIntegrationEnabled('a11y')) return false;
|
|
1248
1256
|
const own = process.env.REACT_X11_A11Y;
|
|
1249
1257
|
if (own === '0') return false;
|
|
1250
1258
|
if (own) return true;
|
package/src/appearance.js
CHANGED
|
@@ -57,6 +57,7 @@ import os from 'node:os';
|
|
|
57
57
|
import path from 'node:path';
|
|
58
58
|
|
|
59
59
|
import { sessionBus } from './bus.js';
|
|
60
|
+
import { desktopIntegrationEnabled } from './desktopintegration.js';
|
|
60
61
|
import { PORTAL_NAME, PORTAL_PATH } from './portal.js';
|
|
61
62
|
import { beginXSettings, watchXSettings, xsettings } from './xsettings.js';
|
|
62
63
|
|
|
@@ -238,6 +239,12 @@ function sanitize(saved) {
|
|
|
238
239
|
function load() {
|
|
239
240
|
if (cacheChecked) return;
|
|
240
241
|
cacheChecked = true;
|
|
242
|
+
// `createRoot({ desktop: false })` means this process does not follow the
|
|
243
|
+
// desktop, and that has to include the remembered answer: seeding from the
|
|
244
|
+
// cache would leave an app that opted out drawing in whatever colours this
|
|
245
|
+
// machine happened to be in last time, which is the opposite of the
|
|
246
|
+
// determinism the switch is asked for (#417).
|
|
247
|
+
if (!desktopIntegrationEnabled('appearance')) return;
|
|
241
248
|
const file = cacheFile();
|
|
242
249
|
if (!file) return;
|
|
243
250
|
try {
|
|
@@ -702,6 +709,10 @@ async function runLadder(app) {
|
|
|
702
709
|
*/
|
|
703
710
|
export function systemAppearance(options = {}) {
|
|
704
711
|
if (owner) return Promise.resolve(snapshot);
|
|
712
|
+
// Turned off, so there is nothing to climb and nothing to remember: the
|
|
713
|
+
// defaults, which is a real answer — `'no-preference'` means *use your own*
|
|
714
|
+
// (src/desktopintegration.js).
|
|
715
|
+
if (!desktopIntegrationEnabled('appearance')) return Promise.resolve(NOTHING);
|
|
705
716
|
load();
|
|
706
717
|
if (!probe) {
|
|
707
718
|
// **Failure is not cached**, for the same reason `bus.js` does not cache
|
package/src/atspi.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// resurrected: the slots are cleared and the app simply stops being
|
|
33
33
|
// accessible until restarted, the same contract bus.js documents.
|
|
34
34
|
|
|
35
|
-
import {
|
|
35
|
+
import { loadTransport, resolveAddress } from './bus.js';
|
|
36
36
|
import {
|
|
37
37
|
hooks,
|
|
38
38
|
ATSPI_ROLE,
|
|
@@ -1818,8 +1818,11 @@ async function connected(bus) {
|
|
|
1818
1818
|
*/
|
|
1819
1819
|
async function accessibilityBusAddress(dbus) {
|
|
1820
1820
|
if (process.env.AT_SPI_BUS_ADDRESS) return process.env.AT_SPI_BUS_ADDRESS;
|
|
1821
|
-
|
|
1822
|
-
|
|
1821
|
+
// Resolved, not read: on macOS this is where the session bus address comes
|
|
1822
|
+
// from, and letting `dbus-native` find it for itself would be a blocking
|
|
1823
|
+
// `spawnSync` on the way to the first frame (#417).
|
|
1824
|
+
const busAddress = await resolveAddress('session');
|
|
1825
|
+
if (!busAddress) return null;
|
|
1823
1826
|
let sbus = null;
|
|
1824
1827
|
try {
|
|
1825
1828
|
sbus = dbus.createClient({ busAddress });
|
package/src/bus.js
CHANGED
|
@@ -135,9 +135,8 @@ export async function loadTransport() {
|
|
|
135
135
|
* address is incoherent under sharing. Two callers, two addresses, one
|
|
136
136
|
* socket — which would win? Tests use this same seam.
|
|
137
137
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* the note in `accessibilityBusAddress`.
|
|
138
|
+
* Synchronous, and so it cannot answer on macOS — see `resolveAddress`,
|
|
139
|
+
* which is what every dial goes through.
|
|
141
140
|
*/
|
|
142
141
|
export function addressFor(kind) {
|
|
143
142
|
if (kind === 'system') {
|
|
@@ -149,21 +148,102 @@ export function addressFor(kind) {
|
|
|
149
148
|
if (process.env.DBUS_SESSION_BUS_ADDRESS) {
|
|
150
149
|
return process.env.DBUS_SESSION_BUS_ADDRESS;
|
|
151
150
|
}
|
|
152
|
-
// macOS advertises the session bus through launchd
|
|
153
|
-
//
|
|
151
|
+
// macOS advertises the session bus through launchd rather than through the
|
|
152
|
+
// environment, and asking launchd is a subprocess — see `launchdAddress`.
|
|
154
153
|
if (process.platform === 'darwin') return undefined;
|
|
155
154
|
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
156
155
|
return runtimeDir ? `unix:path=${runtimeDir}/bus` : undefined;
|
|
157
156
|
}
|
|
158
157
|
|
|
158
|
+
/**
|
|
159
|
+
* The in-flight or settled lookup, shared by every dial in the process.
|
|
160
|
+
*
|
|
161
|
+
* **Failure is cached here**, which is the one place in this module where it
|
|
162
|
+
* is — and the exception is deliberate. A session bus can genuinely appear
|
|
163
|
+
* under `$XDG_RUNTIME_DIR` mid-run, so that answer is never remembered; what
|
|
164
|
+
* launchd exports is a *login session* fact, set when the bus is installed
|
|
165
|
+
* and started, which does not happen under a running app. Re-asking would be
|
|
166
|
+
* a fork per feature probe to learn the same "no".
|
|
167
|
+
*/
|
|
168
|
+
let launchd = null;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Ask launchd where the session bus is, **without blocking the event loop**.
|
|
172
|
+
*
|
|
173
|
+
* This is the whole reason `resolveAddress` exists (#417). `dbus-native` will
|
|
174
|
+
* do this lookup itself, from `createStream`, with `spawnSync` — it has to,
|
|
175
|
+
* because its own entry point is synchronous — and a fork+exec of `launchctl`
|
|
176
|
+
* on a cold page cache costs 120–150 ms of *blocked loop*, measured. Landing
|
|
177
|
+
* that inside `createRoot()` stalls the X handshake, the yoga instantiate and
|
|
178
|
+
* the first paint behind a question about the colour scheme.
|
|
179
|
+
*
|
|
180
|
+
* Every caller here is already asynchronous, so the same lookup done with
|
|
181
|
+
* `execFile` costs the same wall clock and none of the loop: the handshake
|
|
182
|
+
* and the layout engine run through it. Handing `dbus-native` the resolved
|
|
183
|
+
* `unix:path=…` is then what keeps it off its own synchronous path.
|
|
184
|
+
*
|
|
185
|
+
* The variable name is D-Bus's, and the fallback to our own environment is
|
|
186
|
+
* `launchdSocketPath`'s: a process launched from a shell that has it already
|
|
187
|
+
* knows the answer.
|
|
188
|
+
*/
|
|
189
|
+
function launchdAddress() {
|
|
190
|
+
if (launchd) return launchd;
|
|
191
|
+
launchd = (async () => {
|
|
192
|
+
const VAR = 'DBUS_LAUNCHD_SESSION_BUS_SOCKET';
|
|
193
|
+
let fromLaunchd = '';
|
|
194
|
+
try {
|
|
195
|
+
const { execFile } = await import('node:child_process');
|
|
196
|
+
fromLaunchd = await new Promise((resolve) => {
|
|
197
|
+
execFile(
|
|
198
|
+
'launchctl',
|
|
199
|
+
['getenv', VAR],
|
|
200
|
+
{ encoding: 'utf8' },
|
|
201
|
+
(err, stdout) => resolve(err ? '' : String(stdout).trim()),
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
} catch {
|
|
205
|
+
// no `launchctl` on $PATH, or no child processes to be had at all
|
|
206
|
+
}
|
|
207
|
+
const socket = fromLaunchd || process.env[VAR] || '';
|
|
208
|
+
return socket ? `unix:path=${socket}` : undefined;
|
|
209
|
+
})();
|
|
210
|
+
return launchd;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Where to dial, resolved — the synchronous sources first, and launchd only
|
|
215
|
+
* where they have nothing to say.
|
|
216
|
+
*
|
|
217
|
+
* Not public, but exported for atspi.js, whose one-shot discovery probe
|
|
218
|
+
* dials its own short-lived connection rather than the shared one — see the
|
|
219
|
+
* note in `accessibilityBusAddress`. It matters that it goes through here
|
|
220
|
+
* too: two dials that each let `dbus-native` ask launchd are two blocking
|
|
221
|
+
* forks, and the answer is the same both times.
|
|
222
|
+
*
|
|
223
|
+
* @param {BusKind} kind
|
|
224
|
+
* @returns {Promise<string | undefined>}
|
|
225
|
+
*/
|
|
226
|
+
export async function resolveAddress(kind) {
|
|
227
|
+
const direct = addressFor(kind);
|
|
228
|
+
if (direct !== undefined) return direct;
|
|
229
|
+
if (kind !== 'session' || process.platform !== 'darwin') return undefined;
|
|
230
|
+
return await launchdAddress();
|
|
231
|
+
}
|
|
232
|
+
|
|
159
233
|
function noAddressError(kind) {
|
|
234
|
+
if (kind !== 'session') {
|
|
235
|
+
return new Error(
|
|
236
|
+
'react-x11: no system bus address. $DBUS_SYSTEM_BUS_ADDRESS is unset ' +
|
|
237
|
+
'and there is no default.',
|
|
238
|
+
);
|
|
239
|
+
}
|
|
160
240
|
return new Error(
|
|
161
|
-
|
|
162
|
-
(
|
|
163
|
-
? '
|
|
164
|
-
'
|
|
165
|
-
|
|
166
|
-
|
|
241
|
+
'react-x11: no session bus address. $DBUS_SESSION_BUS_ADDRESS is unset ' +
|
|
242
|
+
(process.platform === 'darwin'
|
|
243
|
+
? 'and launchd has no $DBUS_LAUNCHD_SESSION_BUS_SOCKET either, which ' +
|
|
244
|
+
'is normal on a Mac with no D-Bus installed.'
|
|
245
|
+
: 'and $XDG_RUNTIME_DIR is not set either, which is normal over ssh, ' +
|
|
246
|
+
'under a bare startx and in most containers.'),
|
|
167
247
|
);
|
|
168
248
|
}
|
|
169
249
|
|
|
@@ -216,8 +296,11 @@ async function connect(kind, generation) {
|
|
|
216
296
|
return fail(noTransportError(cause));
|
|
217
297
|
}
|
|
218
298
|
|
|
219
|
-
|
|
220
|
-
|
|
299
|
+
// Resolved rather than read, so `dbus-native` never reaches its own
|
|
300
|
+
// `spawnSync` fallback on macOS (#417) — and so "this Mac has no D-Bus"
|
|
301
|
+
// fails here, before a socket is dialled, instead of inside a constructor.
|
|
302
|
+
const busAddress = await resolveAddress(kind);
|
|
303
|
+
if (!busAddress) {
|
|
221
304
|
return fail(noAddressError(kind));
|
|
222
305
|
}
|
|
223
306
|
|
|
@@ -538,6 +621,10 @@ export function busRefs(kind) {
|
|
|
538
621
|
* exported object survives into the next test.
|
|
539
622
|
*/
|
|
540
623
|
export function _resetBusState() {
|
|
624
|
+
// Including what launchd said: a suite that pins `process.platform` would
|
|
625
|
+
// otherwise inherit the answer — or the absence of one — from a case that
|
|
626
|
+
// ran under a different one.
|
|
627
|
+
launchd = null;
|
|
541
628
|
for (const kind of KINDS) {
|
|
542
629
|
state[kind] = newState(kind, state[kind].generation + 1);
|
|
543
630
|
notify(kind);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Whether this process talks to the desktop at all — the `desktop` option on
|
|
2
|
+
// `createRoot()`, and the one place that decides what it covers.
|
|
3
|
+
//
|
|
4
|
+
// ## What is in the group and why
|
|
5
|
+
//
|
|
6
|
+
// Three things react-x11 turns on for you without being asked, and all three
|
|
7
|
+
// reach the session bus:
|
|
8
|
+
//
|
|
9
|
+
// `appearance` the light/dark, accent, contrast and reduced-motion ladder
|
|
10
|
+
// (src/appearance.js) — the settings portal, then macOS, then
|
|
11
|
+
// XSETTINGS
|
|
12
|
+
// `a11y` the AT-SPI bridge, started from `createRoot()`
|
|
13
|
+
// (src/a11y.js, docs/accessibility.md)
|
|
14
|
+
// `globalMenu` a `MenuBar` handing its menu to the panel instead of drawing
|
|
15
|
+
// it (src/globalmenu.js, docs/globalmenu.md)
|
|
16
|
+
//
|
|
17
|
+
// Each already had an off switch and each of those was an **environment
|
|
18
|
+
// variable** — `REACT_X11_A11Y=0`, `NO_AT_BRIDGE=1`,
|
|
19
|
+
// `REACT_X11_NO_GLOBAL_MENU=1`, or unsetting `DBUS_SESSION_BUS_ADDRESS` for
|
|
20
|
+
// the appearance ladder. That is a seam an app cannot reach for itself: the
|
|
21
|
+
// environment is inherited, so a process that sets one before `createRoot()`
|
|
22
|
+
// has also set it for every child it spawns, and the D-Bus one turns off the
|
|
23
|
+
// portals and the app's own services along with the follower. AGENTS.md asks
|
|
24
|
+
// for the off switch to be somewhere the embedder can actually stand.
|
|
25
|
+
//
|
|
26
|
+
// ## Why the policy is process-wide when the option is per-root
|
|
27
|
+
//
|
|
28
|
+
// Because so is the thing it describes. There is one desktop, one D-Bus
|
|
29
|
+
// identity, and one AT-SPI bridge per process — `startA11y()` says so in its
|
|
30
|
+
// name, and appearance.js says so in its header. A per-root flag over
|
|
31
|
+
// process-wide state would be a seam that reads as finer than it is.
|
|
32
|
+
//
|
|
33
|
+
// So **off wins, and off latches.** A root that says `desktop: false` is a
|
|
34
|
+
// root with a constraint — an embedder that owns the toplevel, a test, a
|
|
35
|
+
// daemon that must not fork — and a second root quietly turning the feature
|
|
36
|
+
// back on for it would be the bug. Turning something back *on* is a thing
|
|
37
|
+
// this module deliberately cannot do.
|
|
38
|
+
//
|
|
39
|
+
// The one honest limit: a feature already started stays started. `startA11y()`
|
|
40
|
+
// is memoised per process, so a second root's `desktop: false` stops the next
|
|
41
|
+
// climb and not the bridge that is already up. Pass it on the first root.
|
|
42
|
+
|
|
43
|
+
/** @typedef {'appearance'|'a11y'|'globalMenu'} DesktopFeature */
|
|
44
|
+
|
|
45
|
+
/** @type {DesktopFeature[]} */
|
|
46
|
+
const FEATURES = ['appearance', 'a11y', 'globalMenu'];
|
|
47
|
+
|
|
48
|
+
/** What has been turned off, for the life of the process. */
|
|
49
|
+
const off = new Set();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Apply a root's `desktop` option. Not public — `createRoot({ desktop })` is
|
|
53
|
+
* the public shape.
|
|
54
|
+
*
|
|
55
|
+
* `undefined` is the default and means every feature stays on; `false` turns
|
|
56
|
+
* all of them off; an object names them one at a time, and a key left out is
|
|
57
|
+
* left alone.
|
|
58
|
+
*
|
|
59
|
+
* @param {boolean | Partial<Record<DesktopFeature, boolean>> | undefined} desktop
|
|
60
|
+
*/
|
|
61
|
+
export function setDesktopIntegration(desktop) {
|
|
62
|
+
if (desktop === undefined || desktop === true) return;
|
|
63
|
+
if (desktop === false) {
|
|
64
|
+
for (const feature of FEATURES) off.add(feature);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (typeof desktop !== 'object') {
|
|
68
|
+
throw new TypeError(
|
|
69
|
+
'react-x11: createRoot({ desktop }) takes false or an object of ' +
|
|
70
|
+
`${FEATURES.map((f) => `${f}: false`).join(', ')} — got ` +
|
|
71
|
+
`${JSON.stringify(desktop)}.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
for (const [feature, on] of Object.entries(desktop)) {
|
|
75
|
+
if (!FEATURES.includes(/** @type {DesktopFeature} */ (feature))) {
|
|
76
|
+
throw new TypeError(
|
|
77
|
+
`react-x11: createRoot({ desktop: { ${feature} } }) — no such ` +
|
|
78
|
+
`desktop integration. Expected ${FEATURES.join(', ')}.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (on === false) off.add(feature);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether a feature may run. Every one of the three asks this first, ahead of
|
|
87
|
+
* its own environment variable, so `desktop: false` is the outermost answer.
|
|
88
|
+
*
|
|
89
|
+
* @param {DesktopFeature} feature
|
|
90
|
+
*/
|
|
91
|
+
export function desktopIntegrationEnabled(feature) {
|
|
92
|
+
return !off.has(feature);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Test seam, not public: forget the latch. */
|
|
96
|
+
export function _resetDesktopIntegration() {
|
|
97
|
+
off.clear();
|
|
98
|
+
}
|
package/src/globalmenu.js
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
import { useEffect, useRef, useState } from 'react';
|
|
58
58
|
|
|
59
59
|
import { loadTransport, sessionBus } from './bus.js';
|
|
60
|
+
import { desktopIntegrationEnabled } from './desktopintegration.js';
|
|
60
61
|
import {
|
|
61
62
|
DBUSMENU_IFACE,
|
|
62
63
|
PROPERTY_TYPES,
|
|
@@ -126,6 +127,10 @@ const menuPathFor = (xid) => `/com/react_x11/menus/${xid}`;
|
|
|
126
127
|
* it is the one that works without touching application code.
|
|
127
128
|
*/
|
|
128
129
|
function globalMenuEnabled() {
|
|
130
|
+
// `createRoot({ desktop: false })` first: handing the menu to a panel is
|
|
131
|
+
// one of the three things core turns on for you, and that is the switch
|
|
132
|
+
// that turns the group off (src/desktopintegration.js, #417).
|
|
133
|
+
if (!desktopIntegrationEnabled('globalMenu')) return false;
|
|
129
134
|
const flag = process.env.REACT_X11_NO_GLOBAL_MENU;
|
|
130
135
|
return !flag || flag === '0';
|
|
131
136
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -298,6 +298,43 @@ export interface RootOptions {
|
|
|
298
298
|
* and coming back is the user's own Tab.
|
|
299
299
|
*/
|
|
300
300
|
restoreFocusOnReveal?: boolean;
|
|
301
|
+
/**
|
|
302
|
+
* Whether this process talks to the desktop over D-Bus. On by default, and
|
|
303
|
+
* `false` turns off all three things react-x11 starts for you (#417,
|
|
304
|
+
* docs/desktop.md):
|
|
305
|
+
*
|
|
306
|
+
* | | |
|
|
307
|
+
* | --- | --- |
|
|
308
|
+
* | `appearance` | following the desktop's light/dark, accent, contrast and reduced motion (docs/appearance.md) |
|
|
309
|
+
* | `a11y` | the AT-SPI bridge that makes the app reachable by a screen reader (docs/accessibility.md) |
|
|
310
|
+
* | `globalMenu` | a `MenuBar` handing its menu to the panel instead of drawing it (docs/globalmenu.md) |
|
|
311
|
+
*
|
|
312
|
+
* ```js
|
|
313
|
+
* await createRoot({ desktop: false }); // none of it
|
|
314
|
+
* await createRoot({ desktop: { appearance: false } }); // just that one
|
|
315
|
+
* ```
|
|
316
|
+
*
|
|
317
|
+
* For an embedder that owns those integrations itself, a kiosk or daemon
|
|
318
|
+
* that must not fork a subprocess to find the bus, and a test that wants
|
|
319
|
+
* one answer on every machine. With all three off nothing dials the
|
|
320
|
+
* session bus at startup.
|
|
321
|
+
*
|
|
322
|
+
* **Off is process-wide and it latches.** There is one desktop and one
|
|
323
|
+
* D-Bus identity per process, so a second root cannot turn back on what
|
|
324
|
+
* another turned off — and a feature already started stays started, so
|
|
325
|
+
* pass this on the first root.
|
|
326
|
+
*/
|
|
327
|
+
desktop?: boolean | DesktopIntegrationOptions;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Which desktop integrations run. See {@link CreateRootOptions.desktop}. */
|
|
331
|
+
export interface DesktopIntegrationOptions {
|
|
332
|
+
/** Follow the desktop's light/dark, accent, contrast and reduced motion. */
|
|
333
|
+
appearance?: boolean;
|
|
334
|
+
/** The AT-SPI bridge — whether a screen reader can see this app. */
|
|
335
|
+
a11y?: boolean;
|
|
336
|
+
/** Whether a `MenuBar` hands its menu to the panel. */
|
|
337
|
+
globalMenu?: boolean;
|
|
301
338
|
}
|
|
302
339
|
|
|
303
340
|
export interface ComposeOptions {
|
package/src/nodes.js
CHANGED
|
@@ -1218,6 +1218,19 @@ function contentSpan(node, axis, intrinsic, out) {
|
|
|
1218
1218
|
* one pass by `contentSpan`. `floored` collects what was written so the next
|
|
1219
1219
|
* measurement can take it back off — a floor left in place would be read
|
|
1220
1220
|
* back as content that cannot give, and could then only ratchet upwards.
|
|
1221
|
+
*
|
|
1222
|
+
* A floor is written **unrounded**, and the measurement it came from ran
|
|
1223
|
+
* with the pixel grid off (`measuringExactly`) for the reason given there:
|
|
1224
|
+
* rounding a floor grows the tree a pixel per nesting level. What that
|
|
1225
|
+
* leaves is a sharp edge in yoga worth knowing about before writing a
|
|
1226
|
+
* measure function. A line whose items are all held at their floors is one
|
|
1227
|
+
* yoga freezes item by item, subtracting each item's shrink factor from the
|
|
1228
|
+
* line's total as it goes; the total only cancels to zero if the sizes add
|
|
1229
|
+
* up exactly in binary. Three items of, say, 239.28 in a column that
|
|
1230
|
+
* overflows do not, and yoga divides the overflow by the rounding residue
|
|
1231
|
+
* instead of skipping the division — the items come back a billion pixels
|
|
1232
|
+
* tall (issue #411). Whole pixels cancel exactly, which is why the text
|
|
1233
|
+
* measures here answer in them (`TextNode._trim`).
|
|
1221
1234
|
*/
|
|
1222
1235
|
function writeContentFloors(node, axis, mins, floored) {
|
|
1223
1236
|
const axisIsMain = mainAxisOf(node) === axis;
|
|
@@ -4587,7 +4600,12 @@ export class TextNode extends Node {
|
|
|
4587
4600
|
|
|
4588
4601
|
/** Height for a width: the paragraph shaped into whatever is on offer.
|
|
4589
4602
|
* The offer is `Infinity` when nothing bounds it, which is also what
|
|
4590
|
-
* `textWrap: 'nowrap'` asks for, so neither needs a mode.
|
|
4603
|
+
* `textWrap: 'nowrap'` asks for, so neither needs a mode.
|
|
4604
|
+
*
|
|
4605
|
+
* Both answers are **whole pixels**, the trimmed one included — see
|
|
4606
|
+
* `_trim` for why the rounding is not cosmetic. The glyphs are placed
|
|
4607
|
+
* from the unrounded trim (`_placedLayout`), so what the rounding moves
|
|
4608
|
+
* is the bottom edge of the box, by less than half a pixel. */
|
|
4591
4609
|
measureContent({ width }) {
|
|
4592
4610
|
const layout = this._layoutFor(this._wrapWidth(width));
|
|
4593
4611
|
if (!layout) return { width: 0, height: 0 };
|
|
@@ -4596,7 +4614,9 @@ export class TextNode extends Node {
|
|
|
4596
4614
|
width: Math.ceil(layout.width),
|
|
4597
4615
|
height: Math.max(
|
|
4598
4616
|
0,
|
|
4599
|
-
|
|
4617
|
+
trim
|
|
4618
|
+
? Math.round(Math.ceil(layout.height) - (trim.top + trim.bottom))
|
|
4619
|
+
: Math.ceil(layout.height),
|
|
4600
4620
|
),
|
|
4601
4621
|
};
|
|
4602
4622
|
}
|
|
@@ -4852,6 +4872,21 @@ export class TextNode extends Node {
|
|
|
4852
4872
|
* Measured in the coordinates the layout is **drawn** in, not the ones it
|
|
4853
4873
|
* reports: `halfLeading` shifts it, and deriving the baseline from the
|
|
4854
4874
|
* metrics again would silently disagree the day that shift changes.
|
|
4875
|
+
*
|
|
4876
|
+
* The amounts are fractions of a pixel and stay that way — the glyphs are
|
|
4877
|
+
* placed from them (`_placedLayout`). What must not stay fractional is the
|
|
4878
|
+
* **box** they leave behind, which is why `measureContent` rounds the
|
|
4879
|
+
* height it reports and this does not (issue #411).
|
|
4880
|
+
*
|
|
4881
|
+
* A trimmed label measures to the cap band, and a cap height is a fraction
|
|
4882
|
+
* of the em — so before the rounding, a column of trimmed titles handed
|
|
4883
|
+
* yoga three or four flex items whose main size had a fraction in it and
|
|
4884
|
+
* whose content floors (#249) were that same fraction. Yoga freezes a line
|
|
4885
|
+
* like that item by item and divides the overflow by a total shrink factor
|
|
4886
|
+
* that should have cancelled to zero; a fraction that is not exact in
|
|
4887
|
+
* binary leaves a rounding residue there instead, and dividing by it laid
|
|
4888
|
+
* the section titles of `examples/configurator` out 5.6 billion pixels
|
|
4889
|
+
* tall. See `writeContentFloors`, which is the other end of it.
|
|
4855
4890
|
*/
|
|
4856
4891
|
_trim(layout) {
|
|
4857
4892
|
if (this.style.textBoxTrim !== 'cap-alphabetic') return null;
|
|
@@ -6645,8 +6680,8 @@ export class TextInputNode extends Node {
|
|
|
6645
6680
|
/** A preferred width, capped to whatever is on offer — `Infinity` when
|
|
6646
6681
|
* nothing is, which is what makes the `Math.min` the whole rule. */
|
|
6647
6682
|
measureContent({ width }) {
|
|
6648
|
-
//
|
|
6649
|
-
//
|
|
6683
|
+
// `_capBand` rounds, and a trimmed `<text>` rounds the same band the
|
|
6684
|
+
// same way — rounding one of them and not the other is a pixel of
|
|
6650
6685
|
// difference between a field and the button beside it.
|
|
6651
6686
|
return { width: Math.min(150, width), height: this._capBand() };
|
|
6652
6687
|
}
|
|
@@ -6896,12 +6931,16 @@ export class TextInputNode extends Node {
|
|
|
6896
6931
|
};
|
|
6897
6932
|
}
|
|
6898
6933
|
|
|
6934
|
+
/** Whole pixels either way: a field's height is a flex item's main size,
|
|
6935
|
+
* and a fractional one costs the tree its content floors (see
|
|
6936
|
+
* `TextNode._trim`, issue #411). The face with no `capHeight` to round is
|
|
6937
|
+
* the one that reaches the fallback. */
|
|
6899
6938
|
_capBand() {
|
|
6900
6939
|
const style = this.resolvedTextStyle();
|
|
6901
6940
|
const cap = this.app?.fonts
|
|
6902
6941
|
?.match?.(style.family, { weight: style.weight, style: style.style })
|
|
6903
6942
|
?.metrics?.(style.size)?.capHeight;
|
|
6904
|
-
return
|
|
6943
|
+
return Math.round(cap || this._lineHeight());
|
|
6905
6944
|
}
|
|
6906
6945
|
|
|
6907
6946
|
/** Shaped layout of the current value, cached per (value, style,
|