react-x11 2.51.1 → 2.52.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 +87 -54
- package/docs/README.md +410 -0
- package/docs/accessibility.md +628 -0
- package/docs/appearance.md +416 -0
- package/docs/architecture/accessibility.md +470 -0
- package/docs/architecture/animation.md +707 -0
- package/docs/architecture/container-queries.md +525 -0
- package/docs/architecture/custom-layout.md +334 -0
- package/docs/architecture/drag-and-drop.md +941 -0
- package/docs/architecture/element-layer-contents.md +622 -0
- package/docs/architecture/frame-pacing.md +255 -0
- package/docs/architecture/grid-layout.md +358 -0
- package/docs/architecture/protocol-efficiency.md +357 -0
- package/docs/architecture/video.md +1149 -0
- package/docs/architecture/windowed-regions.md +854 -0
- package/docs/click-to-component.md +125 -0
- package/docs/clipboard.md +281 -0
- package/docs/components.md +1749 -0
- package/docs/dbus.md +277 -0
- package/docs/debugging.md +464 -0
- package/docs/desktop-calendar.md +284 -0
- package/docs/desktop.md +648 -0
- package/docs/devtools.md +250 -0
- package/docs/drag-and-drop.md +663 -0
- package/docs/ecosystem/animation.md +394 -0
- package/docs/ecosystem/data-fetching.md +201 -0
- package/docs/ecosystem/dev-tooling.md +436 -0
- package/docs/ecosystem/forms.md +313 -0
- package/docs/ecosystem/headless.md +219 -0
- package/docs/ecosystem/i18n.md +89 -0
- package/docs/ecosystem/icons.md +410 -0
- package/docs/ecosystem/layout.md +216 -0
- package/docs/ecosystem/routing.md +146 -0
- package/docs/ecosystem/state.md +361 -0
- package/docs/ecosystem/testing.md +500 -0
- package/docs/ecosystem/theming.md +540 -0
- package/docs/ecosystem.md +473 -0
- package/docs/elements.md +2762 -0
- package/docs/embedding.md +288 -0
- package/docs/events.md +1025 -0
- package/docs/extending.md +2242 -0
- package/docs/eyedropper.md +223 -0
- package/docs/filedialog.md +258 -0
- package/docs/frame.md +580 -0
- package/docs/gl.md +170 -0
- package/docs/globalmenu.md +278 -0
- package/docs/glx.md +137 -0
- package/docs/macos.md +3295 -0
- package/docs/notifications.md +164 -0
- package/docs/packaging.md +611 -0
- package/docs/permissions.md +148 -0
- package/docs/react-features.md +430 -0
- package/docs/remote.md +223 -0
- package/docs/scale.md +339 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1928 -0
- package/docs/system.md +599 -0
- package/docs/testing.md +514 -0
- package/docs/typescript.md +117 -0
- package/docs/uri-schemes.md +380 -0
- package/docs/wayland-backend.md +591 -0
- package/docs/wayland.md +545 -0
- package/docs/windows-embedding.md +271 -0
- package/docs/windows-gl.md +221 -0
- package/docs/windows-integrations.md +233 -0
- package/docs/windows.md +1404 -0
- package/package.json +9 -2
- package/src/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- package/src/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/filedialog.js +3 -3
- package/src/keyboardstate.js +28 -0
- package/src/node.d.ts +10 -10
- package/src/portal.js +31 -0
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- package/src/wayland/app.js +59 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/xkb.js +137 -0
package/src/desktopsettings.js
CHANGED
|
@@ -11,17 +11,30 @@
|
|
|
11
11
|
//
|
|
12
12
|
// `appearance.js` climbs three sources for colour scheme and accent, because
|
|
13
13
|
// those are answered by three different things and the portal is the modern
|
|
14
|
-
// one. Nothing here has a portal equivalent:
|
|
15
|
-
// four keys and none of them is a timing.
|
|
16
|
-
//
|
|
17
|
-
// XQuartz — the defaults
|
|
14
|
+
// one. Nothing here has a *standard* portal equivalent:
|
|
15
|
+
// `org.freedesktop.appearance` has four keys and none of them is a timing.
|
|
16
|
+
// On X11, XSETTINGS is the whole story, and where there is no settings
|
|
17
|
+
// daemon — a bare `startx`, most window managers, XQuartz — the defaults
|
|
18
|
+
// below stand.
|
|
18
19
|
//
|
|
19
|
-
// So this reads the map `xsettings.js` already maintains, rather than
|
|
20
|
-
// standing up a source of its own.
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
20
|
+
// So on X11 this reads the map `xsettings.js` already maintains, rather than
|
|
21
|
+
// standing up a source of its own. There are two exceptions, one per backend
|
|
22
|
+
// that has no X server to ask:
|
|
23
|
+
//
|
|
24
|
+
// - **macOS** answers exactly one of these — "reduce motion", through
|
|
25
|
+
// `NSWorkspace` — so the Cocoa app exposes that behind a two-method seam
|
|
26
|
+
// (`accessibilityDisplayOptions`, `watchAccessibilityDisplay`) and this
|
|
27
|
+
// module reads it the way it reads the map: synchronously, cached, live.
|
|
28
|
+
//
|
|
29
|
+
// - **The Wayland backend** has no XSETTINGS either, but the settings portal
|
|
30
|
+
// is not limited to `org.freedesktop.appearance`: the GNOME and GTK portal
|
|
31
|
+
// backends pass whole GSettings schemas through it, and
|
|
32
|
+
// `org.gnome.desktop.interface` and `org.gnome.desktop.peripherals.mouse`
|
|
33
|
+
// are exactly what gsd-xsettings translates into the XSETTINGS keys this
|
|
34
|
+
// reads on X11. Same numbers, one hop closer to the source. Read once at
|
|
35
|
+
// startup and again on `SettingChanged`; a desktop whose portal does not
|
|
36
|
+
// carry them (KDE, a bare compositor) keeps the defaults, as a bare
|
|
37
|
+
// `startx` does.
|
|
25
38
|
//
|
|
26
39
|
// ## Two of these are read synchronously, which is what shapes the module
|
|
27
40
|
//
|
|
@@ -34,8 +47,26 @@
|
|
|
34
47
|
// desktop's cadence from the next focus onward; that is the one seam, and it
|
|
35
48
|
// is worth more than the round trips on every app's startup path.
|
|
36
49
|
|
|
50
|
+
import { sessionBus } from './bus.js';
|
|
51
|
+
import { desktopIntegrationEnabled } from './desktopintegration.js';
|
|
52
|
+
import { PORTAL_NAME, PORTAL_PATH } from './portal.js';
|
|
37
53
|
import { beginXSettings, watchXSettings, xsettings } from './xsettings.js';
|
|
38
54
|
|
|
55
|
+
const SETTINGS_IFACE = 'org.freedesktop.portal.Settings';
|
|
56
|
+
const INTERFACE_NS = 'org.gnome.desktop.interface';
|
|
57
|
+
const MOUSE_NS = 'org.gnome.desktop.peripherals.mouse';
|
|
58
|
+
|
|
59
|
+
/** The keys a `SettingChanged` has to name for a re-read to be worth it —
|
|
60
|
+
* `org.gnome.desktop.interface` also carries the clock format, the font
|
|
61
|
+
* and the icon theme, and none of those is a reason to re-render. */
|
|
62
|
+
const WATCHED = new Set([
|
|
63
|
+
`${INTERFACE_NS}/cursor-blink`,
|
|
64
|
+
`${INTERFACE_NS}/cursor-blink-time`,
|
|
65
|
+
`${INTERFACE_NS}/enable-animations`,
|
|
66
|
+
`${MOUSE_NS}/double-click`,
|
|
67
|
+
`${MOUSE_NS}/drag-threshold`,
|
|
68
|
+
]);
|
|
69
|
+
|
|
39
70
|
/**
|
|
40
71
|
* What is true with no settings daemon to ask.
|
|
41
72
|
*
|
|
@@ -71,24 +102,15 @@ const sessions = new WeakMap();
|
|
|
71
102
|
* export a key with a string value or a nonsense one, and a caret with a
|
|
72
103
|
* blink period of `'fast'` would never come back on. */
|
|
73
104
|
function positive(map, key) {
|
|
74
|
-
|
|
105
|
+
return positiveValue(map?.get(key));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function positiveValue(value) {
|
|
75
109
|
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
76
110
|
? value
|
|
77
111
|
: undefined;
|
|
78
112
|
}
|
|
79
113
|
|
|
80
|
-
/**
|
|
81
|
-
* An XSETTINGS map → the settings. Pure, and exported for the test: the
|
|
82
|
-
* in-process X server has no settings daemon, so this is the part of the
|
|
83
|
-
* path worth pinning without one.
|
|
84
|
-
*
|
|
85
|
-
* **`Net/CursorBlinkTime` is a full cycle**, on and off together — that is
|
|
86
|
-
* what GTK means by `gtk-cursor-blink-time` and what every daemon writes —
|
|
87
|
-
* and `caretBlinkMs` is how long the caret stays in *each* state, because
|
|
88
|
-
* that is the number that goes into a timer. Halving it is the whole
|
|
89
|
-
* conversion, and forgetting to is a caret that blinks at half speed and
|
|
90
|
-
* looks broken rather than wrong.
|
|
91
|
-
*/
|
|
92
114
|
/**
|
|
93
115
|
* macOS's accessibility display options → the settings. Only `animations`
|
|
94
116
|
* comes from the system there: it is the one of these macOS publishes at all
|
|
@@ -105,6 +127,18 @@ export function fromMacOS(options) {
|
|
|
105
127
|
});
|
|
106
128
|
}
|
|
107
129
|
|
|
130
|
+
/**
|
|
131
|
+
* An XSETTINGS map → the settings. Pure, and exported for the test: the
|
|
132
|
+
* in-process X server has no settings daemon, so this is the part of the
|
|
133
|
+
* path worth pinning without one.
|
|
134
|
+
*
|
|
135
|
+
* **`Net/CursorBlinkTime` is a full cycle**, on and off together — that is
|
|
136
|
+
* what GTK means by `gtk-cursor-blink-time` and what every daemon writes —
|
|
137
|
+
* and `caretBlinkMs` is how long the caret stays in *each* state, because
|
|
138
|
+
* that is the number that goes into a timer. Halving it is the whole
|
|
139
|
+
* conversion, and forgetting to is a caret that blinks at half speed and
|
|
140
|
+
* looks broken rather than wrong.
|
|
141
|
+
*/
|
|
108
142
|
export function fromXSettings(map) {
|
|
109
143
|
if (!map) return DEFAULTS;
|
|
110
144
|
const cycle = positive(map, 'Net/CursorBlinkTime');
|
|
@@ -134,6 +168,39 @@ export function fromXSettings(map) {
|
|
|
134
168
|
});
|
|
135
169
|
}
|
|
136
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The settings portal's GNOME namespaces → the settings. Pure, and exported
|
|
173
|
+
* for the test. `all` is a `ReadAll` reply, namespace → key → value.
|
|
174
|
+
*
|
|
175
|
+
* The same keys gsd-xsettings turns into the XSETTINGS ones above, so the
|
|
176
|
+
* same rules: `cursor-blink-time` is a full cycle and is halved, only an
|
|
177
|
+
* explicit `false` turns blinking or animation off, and a value of the wrong
|
|
178
|
+
* type is no answer. GNOME has no double-click *distance*; GTK's is a
|
|
179
|
+
* constant there, so the default stands.
|
|
180
|
+
*
|
|
181
|
+
* `null` when neither namespace came back — a portal that does not pass
|
|
182
|
+
* GSettings through has not answered, and the source stays null.
|
|
183
|
+
*/
|
|
184
|
+
export function fromGnomePortal(all) {
|
|
185
|
+
const iface = all?.[INTERFACE_NS];
|
|
186
|
+
const mouse = all?.[MOUSE_NS];
|
|
187
|
+
if (!iface && !mouse) return null;
|
|
188
|
+
const cycle = positiveValue(iface?.['cursor-blink-time']);
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
...DEFAULTS,
|
|
191
|
+
caretBlink: iface?.['cursor-blink'] !== false,
|
|
192
|
+
caretBlinkMs: cycle
|
|
193
|
+
? Math.max(1, Math.round(cycle / 2))
|
|
194
|
+
: DEFAULTS.caretBlinkMs,
|
|
195
|
+
doubleClickMs:
|
|
196
|
+
positiveValue(mouse?.['double-click']) ?? DEFAULTS.doubleClickMs,
|
|
197
|
+
dragThreshold:
|
|
198
|
+
positiveValue(mouse?.['drag-threshold']) ?? DEFAULTS.dragThreshold,
|
|
199
|
+
animations: iface?.['enable-animations'] !== false,
|
|
200
|
+
source: 'portal',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
137
204
|
/**
|
|
138
205
|
* The settings for a connection, **synchronously**. Always a complete answer:
|
|
139
206
|
* the defaults before the daemon has been read, and on a display with none.
|
|
@@ -148,7 +215,9 @@ export function desktopSettings(app) {
|
|
|
148
215
|
if (!session.snapshot) {
|
|
149
216
|
session.snapshot = session.macos
|
|
150
217
|
? fromMacOS(app.accessibilityDisplayOptions())
|
|
151
|
-
:
|
|
218
|
+
: session.wayland
|
|
219
|
+
? (fromGnomePortal(session.portal) ?? DEFAULTS)
|
|
220
|
+
: fromXSettings(xsettings(app));
|
|
152
221
|
}
|
|
153
222
|
return session.snapshot;
|
|
154
223
|
}
|
|
@@ -173,6 +242,20 @@ export function beginDesktopSettings(app) {
|
|
|
173
242
|
});
|
|
174
243
|
return;
|
|
175
244
|
}
|
|
245
|
+
// The Wayland backend: no X server, so no XSETTINGS — the portal instead,
|
|
246
|
+
// behind the same switch as the appearance ladder that reads the portal
|
|
247
|
+
// too. `createRoot({ desktop: false })` keeps the defaults, which is what a
|
|
248
|
+
// test or an embedder that asked for determinism wants.
|
|
249
|
+
if (app?.backend === 'wayland') {
|
|
250
|
+
session.wayland = true;
|
|
251
|
+
if (desktopIntegrationEnabled('appearance')) {
|
|
252
|
+
portalSettings(app, session).catch(() => {
|
|
253
|
+
// no bus, no portal, a portal without the GNOME namespaces: the
|
|
254
|
+
// defaults stand, which is what they are for
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
176
259
|
beginXSettings(app).then(
|
|
177
260
|
() => {
|
|
178
261
|
if (!sessions.has(app)) return;
|
|
@@ -190,6 +273,92 @@ export function beginDesktopSettings(app) {
|
|
|
190
273
|
);
|
|
191
274
|
}
|
|
192
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Read the GNOME namespaces off the settings portal, and again whenever one
|
|
278
|
+
* of the keys this cares about changes.
|
|
279
|
+
*
|
|
280
|
+
* The order is `appearance.js`'s portal rung, for the same reasons: the
|
|
281
|
+
* match rule and the handler go in **before** the first read, so a change
|
|
282
|
+
* that lands while it is in flight is not lost; and reads are sequenced, so
|
|
283
|
+
* a slow reply never publishes over a newer one. The bus ref is released as
|
|
284
|
+
* soon as the first read settles — the subscription outlives it, and holding
|
|
285
|
+
* it would keep the process alive for as long as the app cared how fast its
|
|
286
|
+
* caret blinks.
|
|
287
|
+
*/
|
|
288
|
+
async function portalSettings(app, session) {
|
|
289
|
+
const ref = await sessionBus();
|
|
290
|
+
if (!ref) return;
|
|
291
|
+
const { bus } = ref;
|
|
292
|
+
let sub = null;
|
|
293
|
+
let handler = null;
|
|
294
|
+
const key = bus.mangle(PORTAL_PATH, SETTINGS_IFACE, 'SettingChanged');
|
|
295
|
+
const stop = () => {
|
|
296
|
+
if (handler) bus.signals.removeListener(key, handler);
|
|
297
|
+
handler = null;
|
|
298
|
+
sub?.remove().catch(() => {});
|
|
299
|
+
sub = null;
|
|
300
|
+
};
|
|
301
|
+
try {
|
|
302
|
+
sub = await bus.watch(
|
|
303
|
+
`type='signal',sender='${PORTAL_NAME}',` +
|
|
304
|
+
`interface='${SETTINGS_IFACE}',member='SettingChanged'`,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
let started = 0;
|
|
308
|
+
let published = 0;
|
|
309
|
+
const refresh = async () => {
|
|
310
|
+
const mine = ++started;
|
|
311
|
+
const all = await bus.invoke(
|
|
312
|
+
{
|
|
313
|
+
destination: PORTAL_NAME,
|
|
314
|
+
path: PORTAL_PATH,
|
|
315
|
+
interface: SETTINGS_IFACE,
|
|
316
|
+
member: 'ReadAll',
|
|
317
|
+
signature: 'as',
|
|
318
|
+
body: [[INTERFACE_NS, MOUSE_NS]],
|
|
319
|
+
},
|
|
320
|
+
{ timeout: 5_000 },
|
|
321
|
+
);
|
|
322
|
+
if (mine <= published || sessions.get(app) !== session) return;
|
|
323
|
+
published = mine;
|
|
324
|
+
const next = fromGnomePortal(all);
|
|
325
|
+
if (!next) return;
|
|
326
|
+
const before = desktopSettings(app);
|
|
327
|
+
session.portal = all;
|
|
328
|
+
session.snapshot = null;
|
|
329
|
+
// A re-read that changed nothing this reads notifies nobody: the hook
|
|
330
|
+
// re-renders on identity, and so would every subscriber.
|
|
331
|
+
if (!sameSettings(before, desktopSettings(app))) notify(session);
|
|
332
|
+
else session.snapshot = before;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
handler = ([namespace, name]) => {
|
|
336
|
+
if (!WATCHED.has(`${namespace}/${name}`)) return;
|
|
337
|
+
refresh().catch(() => {
|
|
338
|
+
// the portal went away mid-session; the last answer stands
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
bus.signals.on(key, handler);
|
|
342
|
+
|
|
343
|
+
// Torn down while the bus was connecting: undo, and publish nothing.
|
|
344
|
+
if (sessions.get(app) !== session) return stop();
|
|
345
|
+
session.stop = stop;
|
|
346
|
+
await refresh();
|
|
347
|
+
} catch (err) {
|
|
348
|
+
stop();
|
|
349
|
+
throw err;
|
|
350
|
+
} finally {
|
|
351
|
+
await ref.release();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function sameSettings(a, b) {
|
|
356
|
+
for (const key of Object.keys(DEFAULTS)) {
|
|
357
|
+
if (a[key] !== b[key]) return false;
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
193
362
|
function notify(session) {
|
|
194
363
|
for (const fn of [...session.listeners]) {
|
|
195
364
|
try {
|
package/src/filedialog.js
CHANGED
|
@@ -46,14 +46,14 @@ import {
|
|
|
46
46
|
RESPONSE_CANCELLED,
|
|
47
47
|
RESPONSE_OK,
|
|
48
48
|
hasService,
|
|
49
|
-
parentWindowHandle,
|
|
50
49
|
pathBytes,
|
|
50
|
+
portalParentFor,
|
|
51
51
|
portalRequest,
|
|
52
52
|
variant,
|
|
53
53
|
} from './portal.js';
|
|
54
54
|
import { sessionBus } from './bus.js';
|
|
55
55
|
import { liveApps } from './trace-registry.js';
|
|
56
|
-
import {
|
|
56
|
+
import { windowOf } from './windowid.js';
|
|
57
57
|
|
|
58
58
|
const FILE_CHOOSER = 'org.freedesktop.portal.FileChooser';
|
|
59
59
|
|
|
@@ -149,7 +149,7 @@ async function portalDialog(kind, opts, ref) {
|
|
|
149
149
|
const { response, results } = await portalRequest(ref, {
|
|
150
150
|
iface: FILE_CHOOSER,
|
|
151
151
|
member,
|
|
152
|
-
parentWindow:
|
|
152
|
+
parentWindow: await portalParentFor(opts.parentWindow),
|
|
153
153
|
title: opts.title ?? defaultTitle(kind),
|
|
154
154
|
options: await portalOptions(opts, {
|
|
155
155
|
save: kind === 'save',
|
package/src/keyboardstate.js
CHANGED
|
@@ -32,6 +32,18 @@
|
|
|
32
32
|
// it answers atoms that have to be interned back one round trip each, and
|
|
33
33
|
// what it returns is a description like `English (US)` where an indicator in
|
|
34
34
|
// a status bar wants `us`.
|
|
35
|
+
//
|
|
36
|
+
// ## A backend with no X server
|
|
37
|
+
//
|
|
38
|
+
// The native Wayland backend has no XKB extension to ask, but its seat holds
|
|
39
|
+
// the same three answers: the locked modifiers and the group from
|
|
40
|
+
// `wl_keyboard.modifiers`, and the layout codes from the name of the keymap
|
|
41
|
+
// the compositor sent (src/wayland/xkb.js, `layoutsFromKeymap`). It hands
|
|
42
|
+
// them over through a two-method seam — `keyboardState()` and
|
|
43
|
+
// `watchKeyboardState(fn)` — and this module publishes what it says the way
|
|
44
|
+
// it publishes a StateNotify. The one difference is the protocol's:
|
|
45
|
+
// a compositor tells only the focused client, so the locks are as of the last
|
|
46
|
+
// time this app had the keyboard.
|
|
35
47
|
|
|
36
48
|
import { requireExtension } from './extensions.js';
|
|
37
49
|
|
|
@@ -71,6 +83,7 @@ class KeyboardSession {
|
|
|
71
83
|
this.armed = false;
|
|
72
84
|
this.stopped = false;
|
|
73
85
|
this._handler = null;
|
|
86
|
+
this._unwatch = null;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
publish(values) {
|
|
@@ -106,6 +119,8 @@ class KeyboardSession {
|
|
|
106
119
|
|
|
107
120
|
stop() {
|
|
108
121
|
this.stopped = true;
|
|
122
|
+
this._unwatch?.();
|
|
123
|
+
this._unwatch = null;
|
|
109
124
|
if (this._handler && this.app?.X?.off) {
|
|
110
125
|
try {
|
|
111
126
|
this.app.X.off('event', this._handler);
|
|
@@ -157,6 +172,19 @@ async function arm(session) {
|
|
|
157
172
|
if (session.armed) return;
|
|
158
173
|
session.armed = true;
|
|
159
174
|
const app = session.app;
|
|
175
|
+
|
|
176
|
+
// A backend that answers for itself (the Wayland seat): subscribe, then
|
|
177
|
+
// read, for the same reason the XKB path below does.
|
|
178
|
+
if (typeof app?.watchKeyboardState === 'function') {
|
|
179
|
+
const read = () => {
|
|
180
|
+
const state = !session.stopped && app.keyboardState?.();
|
|
181
|
+
if (state) session.publish(state);
|
|
182
|
+
};
|
|
183
|
+
session._unwatch = app.watchKeyboardState(read);
|
|
184
|
+
read();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
160
188
|
const xkb = await requireExtension(app, 'xkb');
|
|
161
189
|
if (!xkb || session.stopped) return;
|
|
162
190
|
|
package/src/node.d.ts
CHANGED
|
@@ -39,12 +39,13 @@ export interface ImageDataLike {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* The 2d context a node paints into — the canvas-shaped subset **
|
|
43
|
-
*
|
|
44
|
-
* `BackendContext2D` over CoreGraphics
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
42
|
+
* The 2d context a node paints into — the canvas-shaped subset **every**
|
|
43
|
+
* backend implements: ntk's `RenderingContext2D` over XRender on X11, the
|
|
44
|
+
* Wayland backend's GLES rasterizer, and `BackendContext2D` over CoreGraphics
|
|
45
|
+
* on macOS and Direct2D on Windows. Declared as the contract an element may
|
|
46
|
+
* rely on rather than as any one class: what is here is on all of them, and
|
|
47
|
+
* a member some backends have and others do not is optional here or
|
|
48
|
+
* absent. Coordinates are device pixels in the owning window's
|
|
48
49
|
* space — `abs`, `contentBox()` — unless a transform says otherwise.
|
|
49
50
|
* Anything ntk documents beyond this is reachable at runtime and is
|
|
50
51
|
* ntk's to change.
|
|
@@ -67,10 +68,9 @@ export interface Context2D {
|
|
|
67
68
|
shadowOffsetY: number;
|
|
68
69
|
shadowColor: string;
|
|
69
70
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* `'globalCompositeOperation' in ctx`.
|
|
71
|
+
* The compositing operator — `'source-over'` (the default), `'copy'`,
|
|
72
|
+
* and the rest ntk documents on X11. A native context (macOS, Windows)
|
|
73
|
+
* takes only the modes its bridge draws: assign and read back to know.
|
|
74
74
|
*/
|
|
75
75
|
globalCompositeOperation?: string;
|
|
76
76
|
/**
|
package/src/portal.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
// re-solve.
|
|
30
30
|
|
|
31
31
|
import { loadTransport, sessionBus } from './bus.js';
|
|
32
|
+
import { windowIdOf, windowOf } from './windowid.js';
|
|
32
33
|
|
|
33
34
|
export const PORTAL_NAME = 'org.freedesktop.portal.Desktop';
|
|
34
35
|
export const PORTAL_PATH = '/org/freedesktop/portal/desktop';
|
|
@@ -439,6 +440,36 @@ export function parentWindowHandle(xid) {
|
|
|
439
440
|
return typeof xid === 'number' && xid > 0 ? `x11:${xid.toString(16)}` : '';
|
|
440
441
|
}
|
|
441
442
|
|
|
443
|
+
/**
|
|
444
|
+
* The `parent_window` handle for whatever names a window — a ref, a node, a
|
|
445
|
+
* backend window, an XID — on whichever backend it belongs to.
|
|
446
|
+
*
|
|
447
|
+
* - **X11**: `x11:<xid>`, as `parentWindowHandle` spells it.
|
|
448
|
+
* - **Wayland**: `wayland:<handle>`, from an xdg-foreign export of the
|
|
449
|
+
* toplevel (src/wayland/app.js, `exportHandle`) — what the portal needs to
|
|
450
|
+
* make the dialog modal to the window and stack it there. `''` where the
|
|
451
|
+
* compositor has no xdg-foreign, or the window is a popup; never an `x11:`
|
|
452
|
+
* handle, because a Wayland window's id names no X window, and on a desktop
|
|
453
|
+
* running Xwayland it could name somebody else's.
|
|
454
|
+
*
|
|
455
|
+
* `app` is the fallback for a target that resolves to no window object — a
|
|
456
|
+
* bare number on Wayland is that app's id for one of its own windows.
|
|
457
|
+
*
|
|
458
|
+
* @param {any} target
|
|
459
|
+
* @param {any} [app]
|
|
460
|
+
* @returns {Promise<string>}
|
|
461
|
+
*/
|
|
462
|
+
export async function portalParentFor(target, app = null) {
|
|
463
|
+
let win = windowOf(target);
|
|
464
|
+
const owner = win?.app ?? app;
|
|
465
|
+
if (owner?.backend !== 'wayland') {
|
|
466
|
+
return parentWindowHandle(windowIdOf(target));
|
|
467
|
+
}
|
|
468
|
+
if (!win && typeof target === 'number') win = owner.windowById?.(target);
|
|
469
|
+
const handle = win ? await owner.exportHandle?.(win) : null;
|
|
470
|
+
return handle ? `wayland:${handle}` : '';
|
|
471
|
+
}
|
|
472
|
+
|
|
442
473
|
/**
|
|
443
474
|
* `Variant`, from the one copy of the transport the bus layer loaded.
|
|
444
475
|
*
|
package/src/screencolor.js
CHANGED
|
@@ -65,12 +65,11 @@ import {
|
|
|
65
65
|
PortalCancelledError,
|
|
66
66
|
RESPONSE_OK,
|
|
67
67
|
hasService,
|
|
68
|
-
|
|
68
|
+
portalParentFor,
|
|
69
69
|
portalRequest,
|
|
70
70
|
portalVersion,
|
|
71
71
|
} from './portal.js';
|
|
72
72
|
import { liveApps } from './trace-registry.js';
|
|
73
|
-
import { windowIdOf } from './windowid.js';
|
|
74
73
|
|
|
75
74
|
export const SCREENSHOT_IFACE = 'org.freedesktop.portal.Screenshot';
|
|
76
75
|
/** `PickColor` arrived in version 2 of the Screenshot interface. */
|
|
@@ -220,14 +219,10 @@ async function portalPick(opts, ref) {
|
|
|
220
219
|
// options)` — no title — which is the whole reason portalRequest takes a
|
|
221
220
|
// signature now.
|
|
222
221
|
signature: 'sa{sv}',
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
args: [
|
|
227
|
-
appFor(opts)?.backend === 'wayland'
|
|
228
|
-
? ''
|
|
229
|
-
: parentWindowHandle(windowIdOf(opts.parentWindow)),
|
|
230
|
-
],
|
|
222
|
+
// An X window's handle on X11, an xdg-foreign export on Wayland, and
|
|
223
|
+
// nothing — a floating dialog, which the portal allows — where neither
|
|
224
|
+
// can be had.
|
|
225
|
+
args: [await portalParentFor(opts.parentWindow, appFor(opts))],
|
|
231
226
|
options: {},
|
|
232
227
|
signal: opts.signal,
|
|
233
228
|
});
|
package/src/types/system.d.ts
CHANGED
|
@@ -253,16 +253,18 @@ export interface DesktopSettings {
|
|
|
253
253
|
/** How far a press moves before it is a drag rather than a click. */
|
|
254
254
|
readonly dragThreshold: number;
|
|
255
255
|
/** **False means hold still** — the desktop asked for less motion:
|
|
256
|
-
* `Gtk/EnableAnimations` off over XSETTINGS,
|
|
256
|
+
* `Gtk/EnableAnimations` off over XSETTINGS, `enable-animations` off on
|
|
257
|
+
* the settings portal (Wayland), reduce motion on macOS.
|
|
257
258
|
* Core's loops do not start then (docs/styling.md, "Loops"); an element
|
|
258
259
|
* that animates on a timer of its own holds still too, and draws its
|
|
259
260
|
* resting frame. True wherever the desktop said nothing. */
|
|
260
261
|
readonly animations: boolean;
|
|
261
|
-
/** `'xsettings'`; `'
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* defaults
|
|
265
|
-
|
|
262
|
+
/** `'xsettings'`; `'portal'` on the Wayland backend, from the settings
|
|
263
|
+
* portal's GNOME namespaces; `'macos'` on the Cocoa backend, where only
|
|
264
|
+
* `animations` comes from the system (reduce motion) and the rest are the
|
|
265
|
+
* defaults; or null where nothing answered and these are the renderer's
|
|
266
|
+
* own defaults. */
|
|
267
|
+
readonly source: 'xsettings' | 'portal' | 'macos' | 'test' | null;
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
/**
|
package/src/wayland/app.js
CHANGED
|
@@ -104,6 +104,9 @@ export class WaylandApp extends EventEmitter {
|
|
|
104
104
|
this.viewporter = null;
|
|
105
105
|
this.fractionalScale = null;
|
|
106
106
|
this.activation = null;
|
|
107
|
+
/** xdg-foreign's exporter: a toplevel's handle for another client to
|
|
108
|
+
* parent to (`exportHandle`); null where the compositor has none */
|
|
109
|
+
this.exporter = null;
|
|
107
110
|
/** the monitors, published into src/screens.js (outputs.js) */
|
|
108
111
|
this.outputs = null;
|
|
109
112
|
/** xdg-decoration, layer-shell, shm: null where the compositor has none */
|
|
@@ -153,6 +156,7 @@ export class WaylandApp extends EventEmitter {
|
|
|
153
156
|
app.viewporter = await conn.bind('wp_viewporter');
|
|
154
157
|
app.fractionalScale = await conn.bind('wp_fractional_scale_manager_v1');
|
|
155
158
|
app.activation = await conn.bind('xdg_activation_v1');
|
|
159
|
+
app.exporter = await conn.bind('zxdg_exporter_v2');
|
|
156
160
|
// The scale session first, so the outputs have a per-monitor map to
|
|
157
161
|
// fill; then the outputs, awaited like the X11 backend's Xinerama tier:
|
|
158
162
|
// an auto-sized window clamps to a monitor synchronously inside
|
|
@@ -304,6 +308,9 @@ export class WaylandApp extends EventEmitter {
|
|
|
304
308
|
}
|
|
305
309
|
|
|
306
310
|
_forgetWindow(win) {
|
|
311
|
+
// before the surface goes: the export is a handle *to* it
|
|
312
|
+
win._exported?.proxy.$.destroy();
|
|
313
|
+
win._exported = null;
|
|
307
314
|
if (this.windows.get(win.wl.surface.id) === win)
|
|
308
315
|
this.windows.delete(win.wl.surface.id);
|
|
309
316
|
const i = this.toplevels.indexOf(win);
|
|
@@ -510,6 +517,37 @@ export class WaylandApp extends EventEmitter {
|
|
|
510
517
|
return new WaylandOverlayPane(this, attributes);
|
|
511
518
|
}
|
|
512
519
|
|
|
520
|
+
/**
|
|
521
|
+
* A handle another client can parent a window of its own to — the
|
|
522
|
+
* portal's `parent_window`, spelled `wayland:<handle>` there — or null.
|
|
523
|
+
*
|
|
524
|
+
* xdg-foreign's `export_toplevel`, made once per window on first ask and
|
|
525
|
+
* kept for its lifetime: the handle is valid for as long as the export
|
|
526
|
+
* object lives, and a file dialog opened twice from one window should name
|
|
527
|
+
* it the same way both times. Only an xdg_toplevel can be exported — a
|
|
528
|
+
* popup or a layer surface is a protocol error, so those answer null, as
|
|
529
|
+
* does a compositor without the protocol. The dialog then floats, which the
|
|
530
|
+
* portal allows; what it must never get is an `x11:` handle naming a
|
|
531
|
+
* Wayland window, which is what it got before this existed.
|
|
532
|
+
*/
|
|
533
|
+
exportHandle(win) {
|
|
534
|
+
if (!this.exporter || !win?.wl?.surface || win.isPopup || win.isLayer) {
|
|
535
|
+
return Promise.resolve(null);
|
|
536
|
+
}
|
|
537
|
+
if (win._destroyed) return Promise.resolve(null);
|
|
538
|
+
if (!win._exported) {
|
|
539
|
+
const proxy = this.exporter.$.export_toplevel(win.wl.surface.id);
|
|
540
|
+
const handle = new Promise((resolve) => {
|
|
541
|
+
proxy.on('handle', (value) => resolve(value || null));
|
|
542
|
+
// the answer is one round trip away; a compositor that never sends
|
|
543
|
+
// it is a floating dialog, not a hung one
|
|
544
|
+
setTimeout(() => resolve(null), 1000).unref?.();
|
|
545
|
+
});
|
|
546
|
+
win._exported = { proxy, handle };
|
|
547
|
+
}
|
|
548
|
+
return win._exported.handle;
|
|
549
|
+
}
|
|
550
|
+
|
|
513
551
|
/**
|
|
514
552
|
* Ask the compositor to activate a window. Stacking is the compositor's;
|
|
515
553
|
* xdg-activation lets a client ask with a token from a recent interaction,
|
|
@@ -542,6 +580,27 @@ export class WaylandApp extends EventEmitter {
|
|
|
542
580
|
|
|
543
581
|
cancelAttention() {}
|
|
544
582
|
|
|
583
|
+
/**
|
|
584
|
+
* `useKeyboardState()`'s source on this backend: there is no XKB extension
|
|
585
|
+
* to ask, and the seat has the same answers from `wl_keyboard`
|
|
586
|
+
* (src/keyboardstate.js, `WaylandSeat.keyboardState`).
|
|
587
|
+
*/
|
|
588
|
+
keyboardState() {
|
|
589
|
+
return this.seat?.keyboardState() ?? null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Call `fn` when the locks, the group or the keymap change. */
|
|
593
|
+
watchKeyboardState(fn) {
|
|
594
|
+
const seat = this.seat;
|
|
595
|
+
if (!seat) return () => {};
|
|
596
|
+
seat.on('modifiers', fn);
|
|
597
|
+
seat.on('keymap', fn);
|
|
598
|
+
return () => {
|
|
599
|
+
seat.off('modifiers', fn);
|
|
600
|
+
seat.off('keymap', fn);
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
545
604
|
async close() {
|
|
546
605
|
for (const w of [...this.windows.values()]) w.destroy();
|
|
547
606
|
this.windows.clear();
|
package/src/wayland/clipboard.js
CHANGED
|
@@ -67,17 +67,22 @@ class Selection {
|
|
|
67
67
|
/** our own source while we own the selection */
|
|
68
68
|
this.source = null;
|
|
69
69
|
this.ownData = null;
|
|
70
|
-
this._pendingOffers = new Map(); // offer id -> types
|
|
70
|
+
this._pendingOffers = new Map(); // offer id -> { offer, types }
|
|
71
71
|
/** `watch()` handlers, told when the selection changes hands */
|
|
72
72
|
this.watchers = new Set();
|
|
73
73
|
|
|
74
74
|
device.on('data_offer', (offer) => {
|
|
75
75
|
const types = [];
|
|
76
|
-
this._pendingOffers.set(offer.id, types);
|
|
76
|
+
this._pendingOffers.set(offer.id, { offer, types });
|
|
77
77
|
offer.on('offer', (mime) => types.push(mime));
|
|
78
78
|
});
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
// The offer arrives as an id: an `object` argument is a number off the
|
|
80
|
+
// wire, where `data_offer`'s `new_id` was a proxy. The proxy is what a
|
|
81
|
+
// read calls `receive` on, so it is found by the id — keeping the number
|
|
82
|
+
// made every read throw, and nothing another application copied could
|
|
83
|
+
// be pasted here.
|
|
84
|
+
device.on('selection', (offerArg) => {
|
|
85
|
+
const id = offerArg?.id ?? offerArg ?? 0;
|
|
81
86
|
if (this.offer && this.offer.id !== id) {
|
|
82
87
|
try {
|
|
83
88
|
this.offer.$.destroy();
|
|
@@ -85,15 +90,17 @@ class Selection {
|
|
|
85
90
|
/* gone */
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
|
-
|
|
93
|
+
const pending = id ? this._pendingOffers.get(id) : null;
|
|
94
|
+
this._pendingOffers.delete(id);
|
|
95
|
+
const offer = pending?.offer ?? (offerArg?.$ ? offerArg : null);
|
|
96
|
+
if (!offer) {
|
|
89
97
|
this.offer = null;
|
|
90
98
|
this.offerTypes = [];
|
|
91
99
|
this._notify('closed');
|
|
92
100
|
return;
|
|
93
101
|
}
|
|
94
102
|
this.offer = offer;
|
|
95
|
-
this.offerTypes =
|
|
96
|
-
this._pendingOffers.delete(id);
|
|
103
|
+
this.offerTypes = pending?.types ?? [];
|
|
97
104
|
// an offer from our own source means we still own it; anyone else's
|
|
98
105
|
// means we lost it
|
|
99
106
|
if (this.source && !this._isOurs(offer)) this._dropSource();
|
|
@@ -65,6 +65,9 @@ export const PROTOCOLS = [
|
|
|
65
65
|
'cursor-shape-v1',
|
|
66
66
|
'tablet-v2',
|
|
67
67
|
'xdg-activation-v1',
|
|
68
|
+
// a handle another client can parent a dialog to — the portal's
|
|
69
|
+
// `parent_window` (src/portal.js, `portalParentFor`)
|
|
70
|
+
'xdg-foreign-unstable-v2',
|
|
68
71
|
'xdg-decoration-unstable-v1',
|
|
69
72
|
// the older KDE protocol that does the same job, for the compositors that
|
|
70
73
|
// have only that one (ssd.js picks between them)
|