react-x11 2.15.2 → 2.16.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 +37 -0
- package/package.json +4 -3
- package/src/Reconciler.js +85 -22
- package/src/anchor.js +60 -18
- package/src/application.js +25 -1
- package/src/capabilities.js +349 -0
- package/src/cocoa/app.js +28 -9
- package/src/cocoa/context2d.js +139 -6
- package/src/cocoa/fonts.js +78 -0
- package/src/cocoa/presenter.js +17 -0
- package/src/cocoa/promotion.js +20 -0
- package/src/cocoa/relaunch.js +8 -3
- package/src/cocoa/symbols.js +64 -0
- package/src/cocoa/threaded.js +24 -4
- package/src/cocoa/window.js +362 -139
- package/src/components/ProgressBar.js +1 -1
- package/src/components/Slider.js +72 -39
- package/src/components/anchor.js +7 -2
- package/src/components/index.js +1 -0
- package/src/components/theme.js +32 -28
- package/src/dbusmenuexport.js +243 -0
- package/src/desktopcapabilityhooks.js +160 -0
- package/src/filedialoghooks.js +3 -5
- package/src/frame/childmain.js +8 -20
- package/src/frame/env.js +2 -10
- package/src/globalmenu.js +3 -205
- package/src/icontheme.js +240 -0
- package/src/imagesource.js +98 -3
- package/src/index.d.ts +1 -0
- package/src/index.js +11 -2
- package/src/launcher.js +235 -32
- package/src/launcherhooks.js +47 -28
- package/src/node.d.ts +7 -0
- package/src/nodes/animation.js +17 -47
- package/src/nodes/cascade.js +17 -2
- package/src/nodes/image.js +65 -2
- package/src/nodes/kinds.js +12 -0
- package/src/nodes/layout.js +5 -1
- package/src/nodes/node.js +17 -3
- package/src/nodes/paint.js +117 -0
- package/src/nodes/scope.js +259 -0
- package/src/nodes/scrollable.js +53 -6
- package/src/nodes/text.js +2 -0
- package/src/nodes/textarea.js +1 -1
- package/src/nodes/textinput.js +1 -1
- package/src/nodes/window/anchoring.js +45 -18
- package/src/nodes/window/flush.js +6 -5
- package/src/nodes/window/popup.js +10 -0
- package/src/nodes/window/size.js +40 -2
- package/src/nodes/window/window.js +41 -14
- package/src/registry.js +2 -1
- package/src/settings.js +332 -0
- package/src/statusnotifier.js +752 -0
- package/src/styles.js +212 -8
- package/src/symbols.js +200 -0
- package/src/testing/mock-app.js +10 -0
- package/src/trayhooks.js +193 -29
- package/src/types/capabilities.d.ts +139 -0
- package/src/types/components.d.ts +33 -0
- package/src/types/elements.d.ts +57 -6
- package/src/types/launcher.d.ts +50 -4
- package/src/types/style.d.ts +57 -0
- package/src/types/system.d.ts +104 -0
- package/src/types/tray.d.ts +64 -6
package/src/launcher.js
CHANGED
|
@@ -1,35 +1,55 @@
|
|
|
1
|
-
// The launcher's view of the app: the badge on its icon
|
|
1
|
+
// The launcher's view of the app: the badge on its icon, a progress bar
|
|
2
|
+
// across it, an attention flag, and the menu behind a right-click.
|
|
2
3
|
//
|
|
3
4
|
// A count on the Dock tile, a dot on the taskbar entry — the one thing an
|
|
4
5
|
// app says to the desktop that the user reads without opening it. Two
|
|
5
|
-
// desktops, two mechanisms, one call:
|
|
6
|
+
// desktops, two mechanisms, one call each:
|
|
6
7
|
//
|
|
7
|
-
// 1. **the app's own tile** — the cocoa backend's `NSDockTile
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// 1. **the app's own tile** — the cocoa backend's `NSDockTile`, reached
|
|
9
|
+
// through the app object (`setDockBadge`, `setDockMenu`, src/cocoa/
|
|
10
|
+
// app.js).
|
|
10
11
|
// 2. **`com.canonical.Unity.LauncherEntry`** over the session bus — the
|
|
11
|
-
// protocol Unity defined and the KDE, elementary
|
|
12
|
-
// launchers still listen for. One signal,
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// protocol Unity defined and the KDE, elementary, Cairo-Dock and
|
|
13
|
+
// Dash-to-Dock launchers still listen for. One signal,
|
|
14
|
+
// `Update(app_uri, a{sv})`, attributed to the app by
|
|
15
|
+
// `application://<id>.desktop` — which is why it needs the identity
|
|
15
16
|
// `registerApplication({ appId })` established: with no app id there is
|
|
16
|
-
// nothing for a launcher to pin
|
|
17
|
+
// nothing for a launcher to pin anything to, and the calls resolve
|
|
17
18
|
// false rather than guessing.
|
|
18
19
|
//
|
|
19
|
-
// The
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
20
|
+
// The badge takes the stricter of the two shapes: a **count**. A number shows
|
|
21
|
+
// on both; a string shows on macOS and is a visible count of nothing on Linux
|
|
22
|
+
// (the protocol has no text field), so pass a string only where the Mac is
|
|
23
|
+
// the audience. `0`, `null` and `''` all clear it, because a badge of zero is
|
|
24
|
+
// the badge nobody wanted.
|
|
24
25
|
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
26
|
+
// ## The quicklist is not "desktop actions"
|
|
27
|
+
//
|
|
28
|
+
// This module used to say the freedesktop counterpart of the Dock menu was
|
|
29
|
+
// `Actions=` in the `.desktop` file — an install step, not runtime code — and
|
|
30
|
+
// that the hook was therefore cocoa-only. That was half the story and the
|
|
31
|
+
// wrong half. `LauncherEntry` carries a **`quicklist`** property: an object
|
|
32
|
+
// path to a `com.canonical.dbusmenu` tree, which is the same menu protocol
|
|
33
|
+
// the global menu and the tray already speak, built from the same items. It
|
|
34
|
+
// is runtime, it follows state, and Dash-to-Dock/ubuntu-dock, Plank and the
|
|
35
|
+
// Unity heritage launchers all render it.
|
|
36
|
+
//
|
|
37
|
+
// So `useDockMenu()` has a Linux rung after all, and the three menus an app
|
|
38
|
+
// puts on the desktop — panel, tray, launcher — are now one authoring model
|
|
39
|
+
// on both backends. `.desktop` actions are still the right place for entries
|
|
40
|
+
// that must work *while the app is not running*; they are a different feature
|
|
41
|
+
// wearing a similar hat.
|
|
42
|
+
//
|
|
43
|
+
// Nothing here imports react: `useBadge`/`useDockMenu` (launcherhooks.js) are
|
|
44
|
+
// the hooks, and these are the functions under them, callable from host-side
|
|
45
|
+
// code with no tree. The entry on the bus is held for as long as anything is
|
|
46
|
+
// shown and released when the last of it is cleared — a held bus ref is a
|
|
47
|
+
// ref()'d socket, and an app that cleared its badge on the way out should be
|
|
48
|
+
// free to exit.
|
|
30
49
|
|
|
31
50
|
import { currentRegistration } from './application.js';
|
|
32
51
|
import { loadTransport, sessionBus } from './bus.js';
|
|
52
|
+
import { DbusMenuExport } from './dbusmenuexport.js';
|
|
33
53
|
import { liveApps } from './trace-registry.js';
|
|
34
54
|
|
|
35
55
|
export const LAUNCHER_ENTRY_IFACE = 'com.canonical.Unity.LauncherEntry';
|
|
@@ -78,6 +98,29 @@ function soleApp() {
|
|
|
78
98
|
/** The one exported entry per process — an app has one icon. */
|
|
79
99
|
let entry = null;
|
|
80
100
|
|
|
101
|
+
/**
|
|
102
|
+
* What the launcher currently believes, so an `Update` can carry the whole of
|
|
103
|
+
* it. The protocol's dict is a *patch* and every launcher merges it, but
|
|
104
|
+
* sending the full state makes the call idempotent and means a launcher that
|
|
105
|
+
* restarted and missed a patch is corrected by the next one of any kind.
|
|
106
|
+
*/
|
|
107
|
+
function blankState() {
|
|
108
|
+
return {
|
|
109
|
+
count: 0,
|
|
110
|
+
countVisible: false,
|
|
111
|
+
progress: 0,
|
|
112
|
+
progressVisible: false,
|
|
113
|
+
urgent: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Is anything still being shown? When not, the entry can go. */
|
|
118
|
+
function stateIsEmpty(state, hasMenu) {
|
|
119
|
+
return (
|
|
120
|
+
!hasMenu && !state.countVisible && !state.progressVisible && !state.urgent
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
81
124
|
async function launcherEntry(appUri) {
|
|
82
125
|
if (entry && entry.appUri === appUri) return entry;
|
|
83
126
|
if (entry) await releaseEntry();
|
|
@@ -104,7 +147,17 @@ async function launcherEntry(appUri) {
|
|
|
104
147
|
await ref.release();
|
|
105
148
|
return null;
|
|
106
149
|
}
|
|
107
|
-
entry = {
|
|
150
|
+
entry = {
|
|
151
|
+
appUri,
|
|
152
|
+
ref,
|
|
153
|
+
dbus,
|
|
154
|
+
iface,
|
|
155
|
+
registration,
|
|
156
|
+
state: blankState(),
|
|
157
|
+
menu: null,
|
|
158
|
+
menuRegistration: null,
|
|
159
|
+
menuPath: `${launcherEntryPath(appUri)}/Menu`,
|
|
160
|
+
};
|
|
108
161
|
return entry;
|
|
109
162
|
}
|
|
110
163
|
|
|
@@ -112,10 +165,53 @@ async function releaseEntry() {
|
|
|
112
165
|
const held = entry;
|
|
113
166
|
entry = null;
|
|
114
167
|
if (!held) return;
|
|
168
|
+
await held.menuRegistration?.remove?.().catch(() => {});
|
|
115
169
|
await held.registration?.remove?.().catch(() => {});
|
|
116
170
|
await held.ref.release();
|
|
117
171
|
}
|
|
118
172
|
|
|
173
|
+
/** Emit the whole of the current state under the app's URI. */
|
|
174
|
+
function emit(held) {
|
|
175
|
+
const V = held.dbus.Variant;
|
|
176
|
+
const props = {
|
|
177
|
+
count: new V('x', held.state.count),
|
|
178
|
+
'count-visible': new V('b', held.state.countVisible),
|
|
179
|
+
progress: new V('d', held.state.progress),
|
|
180
|
+
'progress-visible': new V('b', held.state.progressVisible),
|
|
181
|
+
urgent: new V('b', held.state.urgent),
|
|
182
|
+
};
|
|
183
|
+
// Only advertised when there is one: a path to an object that is not
|
|
184
|
+
// exported makes a launcher build a menu client against nothing, and
|
|
185
|
+
// Dash-to-Dock keeps the stale client until the path *changes*.
|
|
186
|
+
if (held.menuRegistration) props.quicklist = new V('o', held.menuPath);
|
|
187
|
+
held.iface.emit.Update(held.appUri, props);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The shared half of every setter: find the identity, take or make the entry,
|
|
192
|
+
* let `mutate` move the state, emit, and drop the entry if nothing is left.
|
|
193
|
+
*
|
|
194
|
+
* `mutate` returns false to mean "nothing changed and nothing was being
|
|
195
|
+
* shown" — the case where clearing something that was already clear must not
|
|
196
|
+
* dial the bus.
|
|
197
|
+
*/
|
|
198
|
+
async function withEntry(mutate, { clearing }) {
|
|
199
|
+
const appId = currentRegistration()?.appId;
|
|
200
|
+
if (!appId) {
|
|
201
|
+
if (clearing && entry) await releaseEntry();
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
if (clearing && !entry) return false; // nothing shown, nothing to clear
|
|
205
|
+
const held = await launcherEntry(launcherAppUri(appId));
|
|
206
|
+
if (!held) return false;
|
|
207
|
+
mutate(held.state);
|
|
208
|
+
emit(held);
|
|
209
|
+
if (stateIsEmpty(held.state, Boolean(held.menuRegistration))) {
|
|
210
|
+
await releaseEntry();
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
119
215
|
/**
|
|
120
216
|
* Show `value` on the app's icon, or clear it.
|
|
121
217
|
*
|
|
@@ -144,23 +240,130 @@ export async function setBadge(value, { app } = {}) {
|
|
|
144
240
|
}
|
|
145
241
|
|
|
146
242
|
// Rung 2: the launcher protocol, which needs an identity to badge.
|
|
243
|
+
const count =
|
|
244
|
+
typeof value === 'number' && Number.isFinite(value) ? Math.trunc(value) : 0;
|
|
245
|
+
return withEntry(
|
|
246
|
+
(state) => {
|
|
247
|
+
state.count = label === null ? 0 : count;
|
|
248
|
+
state.countVisible = label !== null;
|
|
249
|
+
},
|
|
250
|
+
{ clearing: label === null },
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* A progress bar across the app's icon: `0`…`1`, or `null` to clear it.
|
|
256
|
+
*
|
|
257
|
+
* ```js
|
|
258
|
+
* await setProgress(downloaded / total);
|
|
259
|
+
* await setProgress(null); // done
|
|
260
|
+
* ```
|
|
261
|
+
*
|
|
262
|
+
* The launcher protocol's `progress`, which ubuntu-dock, Plank and the
|
|
263
|
+
* Unity-heritage launchers draw across the tile. **Inert on the cocoa
|
|
264
|
+
* backend**: `NSDockTile` has no progress of its own — an app that wants one
|
|
265
|
+
* on a Mac draws it into a custom tile view, which is app-side art rather
|
|
266
|
+
* than a call. Resolves to whether a launcher was told.
|
|
267
|
+
*/
|
|
268
|
+
export async function setProgress(value, { app } = {}) {
|
|
269
|
+
const target = app ?? soleApp();
|
|
270
|
+
// No cocoa rung to try: the Dock has no progress. Left deliberately without
|
|
271
|
+
// a `setDockProgress` probe so that adding one to the bridge later is the
|
|
272
|
+
// only change needed here.
|
|
273
|
+
void target;
|
|
274
|
+
const clearing =
|
|
275
|
+
value == null || value === false || !Number.isFinite(Number(value));
|
|
276
|
+
const clamped = clearing ? 0 : Math.min(1, Math.max(0, Number(value)));
|
|
277
|
+
return withEntry(
|
|
278
|
+
(state) => {
|
|
279
|
+
state.progress = clamped;
|
|
280
|
+
state.progressVisible = !clearing;
|
|
281
|
+
},
|
|
282
|
+
{ clearing },
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Ask the launcher for the user's attention, or stop asking.
|
|
288
|
+
*
|
|
289
|
+
* The launcher protocol's `urgent`, which is the tile bouncing or glowing.
|
|
290
|
+
* Distinct from `<window states={['demands_attention']}>`, which is the
|
|
291
|
+
* *window's* urgency hint and what a taskbar blinks: this one marks the app's
|
|
292
|
+
* icon in the launcher whether or not any window is open. On the cocoa
|
|
293
|
+
* backend the window state is the mechanism and this is inert.
|
|
294
|
+
*/
|
|
295
|
+
export async function setUrgent(urgent, { app } = {}) {
|
|
296
|
+
void (app ?? soleApp());
|
|
297
|
+
return withEntry((state) => void (state.urgent = Boolean(urgent)), {
|
|
298
|
+
clearing: !urgent,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* The menu behind a right-click on the app's launcher icon — the quicklist.
|
|
304
|
+
*
|
|
305
|
+
* Takes `MenuBar`'s item vocabulary and exports it as a `com.canonical.dbusmenu`
|
|
306
|
+
* tree, exactly as the tray and the global menu do. `null` takes it down.
|
|
307
|
+
* Resolves to whether a launcher was told.
|
|
308
|
+
*
|
|
309
|
+
* The menu is exported **before** the property naming it is emitted, so a
|
|
310
|
+
* launcher that builds its client the instant it sees `quicklist` finds an
|
|
311
|
+
* object there.
|
|
312
|
+
*/
|
|
313
|
+
export async function setQuicklist(items, { app } = {}) {
|
|
314
|
+
const target = app ?? soleApp();
|
|
315
|
+
|
|
316
|
+
// Rung 1: the app's own tile menu.
|
|
317
|
+
if (typeof target?.setDockMenu === 'function') {
|
|
318
|
+
target.setDockMenu(items ?? null);
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
147
322
|
const appId = currentRegistration()?.appId;
|
|
148
323
|
if (!appId) {
|
|
149
|
-
if (
|
|
324
|
+
if (!items && entry) await releaseEntry();
|
|
150
325
|
return false;
|
|
151
326
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
327
|
+
if (!items) {
|
|
328
|
+
// Taking the menu down: drop the export, then say so.
|
|
329
|
+
if (!entry?.menuRegistration) return false;
|
|
330
|
+
const held = entry;
|
|
331
|
+
const registration = held.menuRegistration;
|
|
332
|
+
held.menuRegistration = null;
|
|
333
|
+
held.menu = null;
|
|
334
|
+
await registration?.remove?.().catch(() => {});
|
|
335
|
+
// `quicklist` is omitted rather than set empty — there is no "no menu"
|
|
336
|
+
// value in the protocol, and an object path to nothing is worse than a
|
|
337
|
+
// property a launcher never saw.
|
|
338
|
+
emit(held);
|
|
339
|
+
if (stateIsEmpty(held.state, false)) await releaseEntry();
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const held = await launcherEntry(launcherAppUri(appId));
|
|
155
344
|
if (!held) return false;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
345
|
+
|
|
346
|
+
if (held.menu) {
|
|
347
|
+
// Already exported: this is a re-render, and the tree diffs itself.
|
|
348
|
+
held.menu.update(items);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const menu = new DbusMenuExport({
|
|
353
|
+
getMenus: () => items,
|
|
354
|
+
onSelect: (item) => item.onSelect?.(),
|
|
355
|
+
onAboutToShow: (item) => item.onAboutToShow?.(),
|
|
162
356
|
});
|
|
163
|
-
|
|
357
|
+
const iface = menu.defineMenu(held.dbus);
|
|
358
|
+
try {
|
|
359
|
+
held.menuRegistration = await held.ref.bus.export(held.menuPath, iface);
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
menu.iface = iface;
|
|
364
|
+
menu.exported = true;
|
|
365
|
+
held.menu = menu;
|
|
366
|
+
emit(held);
|
|
164
367
|
return true;
|
|
165
368
|
}
|
|
166
369
|
|
package/src/launcherhooks.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
// `useBadge()` and `useDockMenu()` — the launcher's view of
|
|
2
|
-
// things a component declares rather than manages.
|
|
1
|
+
// `useBadge()`, `useProgress()` and `useDockMenu()` — the launcher's view of
|
|
2
|
+
// the app, as things a component declares rather than manages.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// All three now have a rung on both backends except progress, which has one
|
|
5
|
+
// only on Linux (`NSDockTile` has no progress bar; see `setProgress`). Where
|
|
6
|
+
// a hook has no mechanism it is inert and silent: a mark on an icon is not a
|
|
7
|
+
// feature an app should branch on, and a development warning for every
|
|
8
|
+
// desktop that lacks one is a warning nobody can act on.
|
|
9
|
+
//
|
|
10
|
+
// The one that changed shape is `useDockMenu`. It used to be cocoa-only on
|
|
11
|
+
// the grounds that the freedesktop counterpart was an install step — see
|
|
12
|
+
// `launcher.js`, where that reasoning is corrected: the launcher protocol
|
|
13
|
+
// carries a `quicklist` dbusmenu, so the Dock menu is runtime code on both.
|
|
10
14
|
|
|
11
15
|
import { useEffect, useRef } from 'react';
|
|
12
16
|
|
|
13
17
|
import { useAppOrNull } from './appcontext.js';
|
|
14
|
-
import { setBadge } from './launcher.js';
|
|
18
|
+
import { setBadge, setProgress, setQuicklist } from './launcher.js';
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* Show `value` on the app's icon while this component is mounted, and clear
|
|
@@ -37,12 +41,31 @@ export function useBadge(value) {
|
|
|
37
41
|
}, [app]);
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
/**
|
|
45
|
+
* A progress bar across the app's icon, `0`…`1`, cleared on unmount.
|
|
46
|
+
*
|
|
47
|
+
* ```jsx
|
|
48
|
+
* useProgress(done / total); // null or undefined clears it
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Linux launchers only — the Dock has no progress bar of its own.
|
|
52
|
+
*/
|
|
53
|
+
export function useProgress(value) {
|
|
54
|
+
const app = useAppOrNull();
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
setProgress(value, { app }).catch(() => {});
|
|
57
|
+
}, [value, app]);
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
return () => {
|
|
60
|
+
setProgress(null, { app }).catch(() => {});
|
|
61
|
+
};
|
|
62
|
+
}, [app]);
|
|
63
|
+
}
|
|
41
64
|
|
|
42
65
|
/**
|
|
43
|
-
* The menu behind a right-click on the
|
|
44
|
-
* vocabulary `MenuBar` and `ContextMenu` take — an
|
|
45
|
-
* when the user picks it.
|
|
66
|
+
* The menu behind a right-click on the app's icon in the Dock or launcher,
|
|
67
|
+
* from the same `items` vocabulary `MenuBar` and `ContextMenu` take — an
|
|
68
|
+
* item's `onSelect` fires when the user picks it.
|
|
46
69
|
*
|
|
47
70
|
* ```jsx
|
|
48
71
|
* useDockMenu([
|
|
@@ -53,8 +76,10 @@ let warnedInert = false;
|
|
|
53
76
|
* ```
|
|
54
77
|
*
|
|
55
78
|
* Installed for as long as the component is mounted, replaced when `items`
|
|
56
|
-
* changes, and taken down on unmount.
|
|
57
|
-
*
|
|
79
|
+
* changes, and taken down on unmount. `NSDockTile`'s menu on the cocoa
|
|
80
|
+
* backend; the launcher protocol's `quicklist` on Linux, which needs the
|
|
81
|
+
* identity `registerApplication({ appId })` establishes and a `.desktop` file
|
|
82
|
+
* of that name for a launcher to attach it to.
|
|
58
83
|
*/
|
|
59
84
|
export function useDockMenu(items) {
|
|
60
85
|
const app = useAppOrNull();
|
|
@@ -64,18 +89,12 @@ export function useDockMenu(items) {
|
|
|
64
89
|
live.current = items;
|
|
65
90
|
|
|
66
91
|
useEffect(() => {
|
|
67
|
-
|
|
68
|
-
if (process.env.NODE_ENV !== 'production' && !warnedInert && app) {
|
|
69
|
-
warnedInert = true;
|
|
70
|
-
console.warn(
|
|
71
|
-
'react-x11: useDockMenu() is inert on this backend — only the ' +
|
|
72
|
-
'cocoa backend has a Dock. On a Linux desktop the counterpart ' +
|
|
73
|
-
'is desktop actions in the .desktop file (docs/desktop.md).',
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
app.setDockMenu(items ?? null);
|
|
79
|
-
return () => app.setDockMenu(null);
|
|
92
|
+
setQuicklist(items ?? null, { app }).catch(() => {});
|
|
80
93
|
}, [items, app]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
return () => {
|
|
97
|
+
setQuicklist(null, { app }).catch(() => {});
|
|
98
|
+
};
|
|
99
|
+
}, [app]);
|
|
81
100
|
}
|
package/src/node.d.ts
CHANGED
|
@@ -236,6 +236,13 @@ export interface TextStyle {
|
|
|
236
236
|
variations: Record<string, number> | undefined;
|
|
237
237
|
textRendering: TextRendering | undefined;
|
|
238
238
|
color: string;
|
|
239
|
+
/** `letterSpacing`, in device pixels like `size` — undefined unless a
|
|
240
|
+
* style or the cascade above it named one. */
|
|
241
|
+
letterSpacing: number | undefined;
|
|
242
|
+
/** The OpenType features `fontVariantNumeric` and `fontFeatureSettings`
|
|
243
|
+
* resolve to, tag → 1 on, 0 off or an alternate — undefined unless either
|
|
244
|
+
* was named. The same object for the same pair of values. */
|
|
245
|
+
features: Readonly<Record<string, number>> | undefined;
|
|
239
246
|
}
|
|
240
247
|
|
|
241
248
|
/**
|
package/src/nodes/animation.js
CHANGED
|
@@ -40,36 +40,6 @@ const gridContainerMoved = (was, now) =>
|
|
|
40
40
|
const gridItemMoved = (was, now) =>
|
|
41
41
|
GRID_ITEM_PROPS.some((prop) => was[prop] !== now[prop]);
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* The node whose bounds cover where an animating node will be next frame, or
|
|
45
|
-
* `null` when that cannot be known and the frame has to repaint everything.
|
|
46
|
-
*
|
|
47
|
-
* Three cases, and the middle one is the interesting one:
|
|
48
|
-
*
|
|
49
|
-
* - **paint-only** (a colour, an opacity): the node stays put, so its own
|
|
50
|
-
* bounds are the damage.
|
|
51
|
-
* - **a layout property on an out-of-flow node** (`position: absolute`, the
|
|
52
|
-
* arrangement a sliding thumb uses): the node moves, so its own bounds
|
|
53
|
-
* cover where it is going but not where it has been. Its *parent* covers
|
|
54
|
-
* both — an absolute child is laid out inside its parent and, being out of
|
|
55
|
-
* flow, moves nothing else when it shifts. This is what keeps a `Switch`
|
|
56
|
-
* from repainting the window on every frame of its 120ms slide.
|
|
57
|
-
* - **a layout property in flow**: a reflow can move any node in the tree,
|
|
58
|
-
* including ones that leave stale pixels outside every bound we could name
|
|
59
|
-
* here. Nothing to do but repaint in full.
|
|
60
|
-
*/
|
|
61
|
-
function damageForAnimation(node) {
|
|
62
|
-
let movesInLayout = false;
|
|
63
|
-
for (const prop of node._anim?.keys() ?? []) {
|
|
64
|
-
if (isLayoutProp(prop)) movesInLayout = true;
|
|
65
|
-
}
|
|
66
|
-
if (!movesInLayout) return node;
|
|
67
|
-
if (node.style?.position !== 'absolute') return null;
|
|
68
|
-
// A window parent bounds nothing useful — its own rect is the whole surface.
|
|
69
|
-
const parent = node.parent;
|
|
70
|
-
return parent && !parent.isWindow ? parent : null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
43
|
// Frame timestamps for transitions. Indirected so tests can drive the clock
|
|
74
44
|
// instead of sleeping through real animations.
|
|
75
45
|
export let now = () => Date.now();
|
|
@@ -129,7 +99,7 @@ export class NodeAnimation {
|
|
|
129
99
|
// such presenter, the window's frame clock runs it as it always has.
|
|
130
100
|
if (this._offload(prop, entry)) {
|
|
131
101
|
entry.offloaded = true;
|
|
132
|
-
this.root?.invalidate(false,
|
|
102
|
+
this.root?.invalidate(false, this, 'animation');
|
|
133
103
|
} else {
|
|
134
104
|
// …and one the presenter had must not keep running underneath the
|
|
135
105
|
// values the clock is about to write
|
|
@@ -342,7 +312,7 @@ export class NodeAnimation {
|
|
|
342
312
|
}
|
|
343
313
|
}
|
|
344
314
|
this.root?._animating.delete(this);
|
|
345
|
-
this.root?.invalidate(false,
|
|
315
|
+
this.root?.invalidate(false, this, 'animation');
|
|
346
316
|
}
|
|
347
317
|
|
|
348
318
|
/**
|
|
@@ -608,7 +578,7 @@ export class WindowAnimation {
|
|
|
608
578
|
// invalidate anyway, but a React prop change does not — and a transition
|
|
609
579
|
// no one schedules only runs when something else dirties the window,
|
|
610
580
|
// by which time its start is stale and it snaps to the end.
|
|
611
|
-
this.invalidate(false,
|
|
581
|
+
this.invalidate(false, node, 'animation');
|
|
612
582
|
}
|
|
613
583
|
|
|
614
584
|
/**
|
|
@@ -618,27 +588,27 @@ export class WindowAnimation {
|
|
|
618
588
|
*/
|
|
619
589
|
_advanceAnimations(now) {
|
|
620
590
|
if (this._animating.size === 0) return;
|
|
621
|
-
const claims = [];
|
|
622
591
|
for (const node of [...this._animating]) {
|
|
623
592
|
if (node.destroyed) {
|
|
624
593
|
this._animating.delete(node);
|
|
625
594
|
continue;
|
|
626
595
|
}
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
// layout
|
|
630
|
-
//
|
|
631
|
-
|
|
596
|
+
// The node where it stands, which is where it *was*: nothing has been
|
|
597
|
+
// laid out yet, so its rect is the one the last pass gave it. Where it
|
|
598
|
+
// goes is the layout pass's to claim. A tick on a layout property asks
|
|
599
|
+
// for one, and that pass claims the old and new rect of every node it
|
|
600
|
+
// moves (`_assignAbs`) — this one, and whatever this one pushed, in
|
|
601
|
+
// flow or out of it. A paint-only property moves nothing, and this
|
|
602
|
+
// claim is the whole frame.
|
|
603
|
+
//
|
|
604
|
+
// Claimed rather than left unbounded because an animation repaints
|
|
605
|
+
// every frame for as long as it runs: eight height loops in a menu-bar
|
|
606
|
+
// popover were a full-window repaint per frame at 120Hz (#603). A node
|
|
607
|
+
// that *finishes* on this tick is claimed too — it just landed on its
|
|
608
|
+
// final value, and this last frame still has to paint it there.
|
|
609
|
+
this.invalidate(false, node, 'animation');
|
|
632
610
|
if (!node._tickAnimations(now)) this._animating.delete(node);
|
|
633
611
|
}
|
|
634
612
|
this.needsPaint = true;
|
|
635
|
-
// Claim a region rather than leaving the frame unbounded: an animation is
|
|
636
|
-
// a repaint every frame for its whole duration, so this is the difference
|
|
637
|
-
// between a 120ms transition costing eight full-window repaints and eight
|
|
638
|
-
// repaints of the thing that moved. Nodes that *finished* on this tick are
|
|
639
|
-
// claimed too — one just landed on its final value and that last frame
|
|
640
|
-
// still has to paint it, which is why every transition used to end with a
|
|
641
|
-
// full-window repaint.
|
|
642
|
-
for (const claim of claims) this.invalidate(false, claim, 'animation');
|
|
643
613
|
}
|
|
644
614
|
}
|
package/src/nodes/cascade.js
CHANGED
|
@@ -190,10 +190,20 @@ export class NodeCascade {
|
|
|
190
190
|
* A detached node has no ancestors yet and so cannot see a provider two
|
|
191
191
|
* levels up; it still resolves, against the base, and `_themeChanged()` on
|
|
192
192
|
* attach re-resolves it against the real one.
|
|
193
|
+
*
|
|
194
|
+
* A window can take its palette from somewhere other than its parent: a
|
|
195
|
+
* `<ThemeProvider>` above it at the root, which is not its parent because a
|
|
196
|
+
* top-level window has none, or one directly inside the window it is
|
|
197
|
+
* nested in, which passed it on to that window. Either is `_scope`, and it
|
|
198
|
+
* comes first (nodes/scope.js).
|
|
193
199
|
*/
|
|
194
200
|
get theme() {
|
|
195
201
|
if (this._theme !== undefined) return this._theme;
|
|
196
|
-
const inherited = this.
|
|
202
|
+
const inherited = this._scope
|
|
203
|
+
? this._scope.theme
|
|
204
|
+
: this.parent
|
|
205
|
+
? this.parent.theme
|
|
206
|
+
: baseTheme();
|
|
197
207
|
const own = this.props.theme;
|
|
198
208
|
this._theme = own ? { ...inherited, ...own } : inherited;
|
|
199
209
|
return this._theme;
|
|
@@ -312,7 +322,12 @@ export class NodeCascade {
|
|
|
312
322
|
const size = (inherited.size * to) / from;
|
|
313
323
|
const cached = this._textScaled;
|
|
314
324
|
if (cached?.from !== inherited || cached.style.size !== size) {
|
|
315
|
-
|
|
325
|
+
const style = { ...inherited, size };
|
|
326
|
+
// the other length that travels, re-expressed the same way
|
|
327
|
+
if (inherited.letterSpacing) {
|
|
328
|
+
style.letterSpacing = (inherited.letterSpacing * to) / from;
|
|
329
|
+
}
|
|
330
|
+
this._textScaled = { from: inherited, style };
|
|
316
331
|
}
|
|
317
332
|
return this._textScaled.style;
|
|
318
333
|
}
|