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/trayhooks.js
CHANGED
|
@@ -1,25 +1,72 @@
|
|
|
1
1
|
// `useTray()` — an icon in the system tray for as long as a component is
|
|
2
2
|
// mounted, on the backends that have one.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Two rungs, found by capability and never by naming a platform:
|
|
5
|
+
//
|
|
6
|
+
// 1. **the app's own status bar** — the cocoa backend's menu-bar extra
|
|
7
|
+
// (`NSStatusItem`, src/cocoa/statusitem.js), found by the app carrying
|
|
8
|
+
// `createStatusItem`.
|
|
9
|
+
// 2. **`org.kde.StatusNotifierItem`** over the session bus
|
|
10
|
+
// (`statusnotifier.js`) — the freedesktop tray, and the same protocol
|
|
11
|
+
// under X11, XWayland and Wayland. Used when a **live**
|
|
12
|
+
// `org.kde.StatusNotifierWatcher` is hosting one.
|
|
13
|
+
//
|
|
14
|
+
// Where neither answers there is no tray, and `available` is `false` — a
|
|
15
|
+
// stock GNOME session with no AppIndicator extension is the common case, and
|
|
16
|
+
// it is a configuration rather than a fault. An app reads `available` and
|
|
17
|
+
// keeps its tray feature behind it; nothing is logged, because "this desktop
|
|
18
|
+
// has no tray" is not news.
|
|
19
|
+
//
|
|
20
|
+
// ## What the hook returns, and why it is not just a boolean
|
|
21
|
+
//
|
|
22
|
+
// `{ available, backend, features, error }` — the same shape
|
|
23
|
+
// `useDesktopCapability('tray')` resolves to, and built from the same probe so the
|
|
24
|
+
// two can never disagree. The difference is which question they answer:
|
|
25
|
+
//
|
|
26
|
+
// - **this hook measures.** `available` is whether *this item* was taken by
|
|
27
|
+
// a host. It tried; it knows.
|
|
28
|
+
// - **`useDesktopCapability` predicts.** It answers "would a tray icon be taken if
|
|
29
|
+
// one were asked for", which is what a settings screen needs — a "Show
|
|
30
|
+
// tray icon" checkbox must render correctly *without* putting an icon in
|
|
31
|
+
// the tray to find out.
|
|
32
|
+
//
|
|
33
|
+
// Prefer this one wherever the feature is actually mounted. `features` is the
|
|
34
|
+
// portable vocabulary from `capabilities.js` — `features.clickModifiers` is
|
|
35
|
+
// false on the freedesktop rung, because the protocol carries no modifier
|
|
36
|
+
// state, and an app that dims a shift-click affordance reads it here rather
|
|
37
|
+
// than testing the platform.
|
|
38
|
+
//
|
|
39
|
+
// `error` is the fourth field because "no tray on this desktop" and "there is
|
|
40
|
+
// a tray and it refused me" are different facts with different fixes, and
|
|
41
|
+
// collapsing them into `available: false` loses the one an app could act on.
|
|
42
|
+
//
|
|
43
|
+
// ## `available` settles, it does not start true
|
|
44
|
+
//
|
|
45
|
+
// The cocoa rung can be answered synchronously — the method is either on the
|
|
46
|
+
// app object or it is not. The freedesktop one cannot: it takes a bus
|
|
47
|
+
// connection and a `NameHasOwner` round trip. So `available` is **render
|
|
48
|
+
// state that settles**, false on the first frame and true a tick later if a
|
|
49
|
+
// watcher is there, and it follows the watcher for the life of the item —
|
|
50
|
+
// a panel that exits takes the icon with it and flips this back to false.
|
|
51
|
+
//
|
|
52
|
+
// That is the shape every capability in this family should have, and
|
|
53
|
+
// docs/desktop.md says so: a synchronous "can I?" that is honest on the first
|
|
54
|
+
// frame is not available for anything that has to ask the desktop.
|
|
10
55
|
|
|
11
56
|
import { useEffect, useRef, useState } from 'react';
|
|
57
|
+
import { decodeImage } from 'ntk';
|
|
12
58
|
|
|
13
59
|
import { useAppOrNull } from './appcontext.js';
|
|
14
|
-
|
|
15
|
-
|
|
60
|
+
import { currentRegistration } from './application.js';
|
|
61
|
+
import { NO_CAPABILITY, desktopCapability } from './capabilities.js';
|
|
62
|
+
import { StatusNotifierItem, allocateItemSlot } from './statusnotifier.js';
|
|
16
63
|
|
|
17
64
|
/**
|
|
18
65
|
* An icon in the system tray while this component is mounted.
|
|
19
66
|
*
|
|
20
67
|
* ```jsx
|
|
21
68
|
* const { available } = useTray({
|
|
22
|
-
* icon: 'bell.badge', // an SF Symbol
|
|
69
|
+
* icon: 'bell.badge', // a themed icon name (an SF Symbol on macOS), or PNG bytes
|
|
23
70
|
* tooltip: 'Notifications',
|
|
24
71
|
* menu: [{ label: 'Open', onSelect: open }, { label: 'Quit', onSelect: quit }],
|
|
25
72
|
* });
|
|
@@ -27,37 +74,55 @@ let warnedInert = false;
|
|
|
27
74
|
*
|
|
28
75
|
* With `menu`, a click opens it — the same `items` vocabulary `MenuBar` and
|
|
29
76
|
* `useDockMenu` take, an item's `onSelect` firing when picked. Without one,
|
|
30
|
-
* `onClick` is called with the button and the
|
|
31
|
-
*
|
|
32
|
-
* follows its value while mounted; the item is
|
|
77
|
+
* `onClick` is called with the button and where the click was, in logical
|
|
78
|
+
* screen pixels — with the item's rect, where the backend knows it. `null`
|
|
79
|
+
* means no item. Every field follows its value while mounted; the item is
|
|
80
|
+
* removed on unmount.
|
|
33
81
|
*
|
|
34
|
-
* `available` is whether this backend has a tray at all
|
|
35
|
-
*
|
|
82
|
+
* `available` is whether this backend has a tray at all, and it **settles**:
|
|
83
|
+
* false on the first frame, true once a tray has been found. Branch on it for
|
|
84
|
+
* a "show tray icon" setting; do not branch on it to decide whether to call
|
|
85
|
+
* the hook, which would break the rules of hooks the moment it changed.
|
|
36
86
|
*/
|
|
37
87
|
export function useTray(options) {
|
|
38
88
|
const app = useAppOrNull();
|
|
39
|
-
const
|
|
89
|
+
const native = typeof app?.createStatusItem === 'function';
|
|
40
90
|
const itemRef = useRef(null);
|
|
41
91
|
const [rect] = useState(null);
|
|
92
|
+
const [remote, setRemote] = useState(false);
|
|
93
|
+
// whether the freedesktop registration has answered, either way
|
|
94
|
+
const [answered, setAnswered] = useState(false);
|
|
95
|
+
const [error, setError] = useState(null);
|
|
96
|
+
// The feature vocabulary for whichever rung answered. Probed once per
|
|
97
|
+
// backend rather than per render — it describes the mechanism, not the
|
|
98
|
+
// item — and for *both* rungs: the cocoa one is synchronous about whether
|
|
99
|
+
// it has a tray, but not about what that tray can do, and an app reading
|
|
100
|
+
// `features.clickModifiers` to dim a shift-click affordance would
|
|
101
|
+
// otherwise be told `undefined` on the one backend that has modifiers.
|
|
102
|
+
const [caps, setCaps] = useState(NO_CAPABILITY);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!options) return undefined;
|
|
106
|
+
let cancelled = false;
|
|
107
|
+
// `app` explicitly rather than letting the probe find the sole one: a
|
|
108
|
+
// process with several connections has several trays, and this hook
|
|
109
|
+
// belongs to one of them.
|
|
110
|
+
void desktopCapability('tray', { app }).then((c) => {
|
|
111
|
+
if (!cancelled) setCaps(c);
|
|
112
|
+
});
|
|
113
|
+
return () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
};
|
|
116
|
+
}, [app, native, options == null]);
|
|
42
117
|
|
|
43
118
|
// the options a click or a pick reads are the current render's, not the
|
|
44
119
|
// ones the item was created with three minutes ago
|
|
45
120
|
const live = useRef(options);
|
|
46
121
|
live.current = options;
|
|
47
122
|
|
|
123
|
+
// ------------------------------------------------------- rung 1: the app's
|
|
48
124
|
useEffect(() => {
|
|
49
|
-
if (!
|
|
50
|
-
if (process.env.NODE_ENV !== 'production' && !warnedInert && app) {
|
|
51
|
-
warnedInert = true;
|
|
52
|
-
console.warn(
|
|
53
|
-
'react-x11: useTray() is inert on this backend — the freedesktop ' +
|
|
54
|
-
'tray (StatusNotifierItem) is not implemented yet (#353). Read ' +
|
|
55
|
-
'`available` to keep the feature behind the answer.',
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
if (!options) return undefined;
|
|
125
|
+
if (!native || !options) return undefined;
|
|
61
126
|
const item = app.createStatusItem({
|
|
62
127
|
...options,
|
|
63
128
|
onClick: (ev) => live.current?.onClick?.(ev),
|
|
@@ -70,7 +135,7 @@ export function useTray(options) {
|
|
|
70
135
|
// Recreated only when the item comes or goes: the fields patch in
|
|
71
136
|
// place below, and an `options` object rebuilt every render must not
|
|
72
137
|
// rebuild the item every render.
|
|
73
|
-
}, [app,
|
|
138
|
+
}, [app, native, options == null]);
|
|
74
139
|
|
|
75
140
|
useEffect(() => {
|
|
76
141
|
const item = itemRef.current;
|
|
@@ -86,5 +151,104 @@ export function useTray(options) {
|
|
|
86
151
|
options?.menu,
|
|
87
152
|
]);
|
|
88
153
|
|
|
89
|
-
|
|
154
|
+
// ------------------------------------------- rung 2: the freedesktop tray
|
|
155
|
+
const sniRef = useRef(null);
|
|
156
|
+
const previous = useRef(options ?? {});
|
|
157
|
+
// One slot for the life of this hook, **not** per item object. Passing
|
|
158
|
+
// `null` and then options again is the same tray icon going away and coming
|
|
159
|
+
// back; on a fresh path the host has no way to know that and draws a second
|
|
160
|
+
// one beside the first. See `StatusNotifierItem.announcePassive`.
|
|
161
|
+
const slotRef = useRef(null);
|
|
162
|
+
slotRef.current ??= allocateItemSlot();
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (native || !options) return undefined;
|
|
166
|
+
let cancelled = false;
|
|
167
|
+
// The id hosts key their hidden-icons setting on. The app's registered id
|
|
168
|
+
// when it has one, so the choice survives a restart.
|
|
169
|
+
const appId = currentRegistration()?.appId ?? 'react-x11';
|
|
170
|
+
const item = new StatusNotifierItem({
|
|
171
|
+
getOptions: () => live.current,
|
|
172
|
+
// The display a click's position is read against: the host sends it in
|
|
173
|
+
// a unit of its own choosing — see "The position has no unit" in
|
|
174
|
+
// statusnotifier.js.
|
|
175
|
+
app,
|
|
176
|
+
appId,
|
|
177
|
+
slot: slotRef.current,
|
|
178
|
+
decodeIcon: decodeIconBytes,
|
|
179
|
+
// A host that answered the bus and then refused the registration is a
|
|
180
|
+
// fact worth reporting; a desktop with no tray at all is not, and does
|
|
181
|
+
// not come through here.
|
|
182
|
+
onError: (err) => {
|
|
183
|
+
if (!cancelled)
|
|
184
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
sniRef.current = item;
|
|
188
|
+
previous.current = { ...live.current };
|
|
189
|
+
void item.start().then((ok) => {
|
|
190
|
+
if (cancelled) return;
|
|
191
|
+
setRemote(ok);
|
|
192
|
+
setAnswered(true);
|
|
193
|
+
});
|
|
194
|
+
return () => {
|
|
195
|
+
cancelled = true;
|
|
196
|
+
sniRef.current = null;
|
|
197
|
+
setRemote(false);
|
|
198
|
+
setAnswered(false);
|
|
199
|
+
setError(null);
|
|
200
|
+
void item.stop();
|
|
201
|
+
};
|
|
202
|
+
}, [app, native, options == null]);
|
|
203
|
+
|
|
204
|
+
// The item reads its fields through `getOptions`, so a render only has to
|
|
205
|
+
// say *which* of them moved — see `StatusNotifierItem.update`.
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const item = sniRef.current;
|
|
208
|
+
if (!item) return;
|
|
209
|
+
item.update(previous.current);
|
|
210
|
+
previous.current = { ...live.current };
|
|
211
|
+
}, [
|
|
212
|
+
options?.icon,
|
|
213
|
+
options?.overlayIcon,
|
|
214
|
+
options?.attentionIcon,
|
|
215
|
+
options?.title,
|
|
216
|
+
options?.tooltip,
|
|
217
|
+
options?.visible,
|
|
218
|
+
options?.attention,
|
|
219
|
+
options?.menu,
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
// The cocoa rung is synchronous and always itself; the freedesktop one
|
|
223
|
+
// reports whatever the probe found. `features` is empty until one settles,
|
|
224
|
+
// which is the same "render the fallback first" order `available` has.
|
|
225
|
+
const backend = native ? 'cocoa' : remote ? caps.backend : null;
|
|
226
|
+
return {
|
|
227
|
+
available: native || remote,
|
|
228
|
+
// Whether `available` is an answer yet: at once on the Cocoa rung and for
|
|
229
|
+
// no item at all, and on the freedesktop one once the host has taken or
|
|
230
|
+
// refused the registration — so an app whose whole UI is its tray can
|
|
231
|
+
// render nothing until then, rather than a fallback window that flashes
|
|
232
|
+
// up and away on every start.
|
|
233
|
+
settled: native || !options || answered,
|
|
234
|
+
backend,
|
|
235
|
+
features: native || remote ? caps.features : NO_CAPABILITY.features,
|
|
236
|
+
error,
|
|
237
|
+
rect,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* PNG/JPEG bytes → raw RGBA, through ntk's decoder.
|
|
243
|
+
*
|
|
244
|
+
* Defensive: a corrupt or unsupported image should cost the tray its *icon*,
|
|
245
|
+
* not its tray. The pixmap ends up empty and the item still registers with
|
|
246
|
+
* its name, tooltip and menu intact.
|
|
247
|
+
*/
|
|
248
|
+
function decodeIconBytes(bytes) {
|
|
249
|
+
try {
|
|
250
|
+
return decodeImage(bytes);
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
90
254
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature discovery for the things an app does outside its own windows.
|
|
3
|
+
* See docs/desktop.md "Feature discovery".
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Which mechanism answered. Named after the mechanism, never the platform. */
|
|
7
|
+
export type DesktopBackend =
|
|
8
|
+
| 'dbus'
|
|
9
|
+
| 'cocoa'
|
|
10
|
+
| 'osascript'
|
|
11
|
+
| 'notify-send'
|
|
12
|
+
| 'statusnotifier'
|
|
13
|
+
| 'launcherentry';
|
|
14
|
+
|
|
15
|
+
/** The capabilities {@link desktopCapability} can be asked about. */
|
|
16
|
+
export type DesktopCapabilityName = 'notifications' | 'tray' | 'launcher';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What a notification can carry here.
|
|
20
|
+
*
|
|
21
|
+
* `actions`/`events` are the pair that decide whether a notification is a
|
|
22
|
+
* conversation or a sign: false on the shell-out rungs and on a freedesktop
|
|
23
|
+
* daemon that does not advertise `actions`, where buttons silently never
|
|
24
|
+
* appear and nothing is ever reported back.
|
|
25
|
+
*/
|
|
26
|
+
export interface NotificationFeatures {
|
|
27
|
+
actions: boolean;
|
|
28
|
+
/** `onAction`/`onClose` will fire. Tracks `actions` on every rung. */
|
|
29
|
+
events: boolean;
|
|
30
|
+
/** `update()` replaces the banner in place rather than posting a new one. */
|
|
31
|
+
update: boolean;
|
|
32
|
+
close: boolean;
|
|
33
|
+
body: boolean;
|
|
34
|
+
bodyMarkup: boolean;
|
|
35
|
+
bodyImage: boolean;
|
|
36
|
+
icon: boolean;
|
|
37
|
+
sound: boolean;
|
|
38
|
+
/** The banner survives in a tray or centre rather than expiring unseen. */
|
|
39
|
+
persistence: boolean;
|
|
40
|
+
urgency: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TrayFeatures {
|
|
44
|
+
menu: boolean;
|
|
45
|
+
/** An icon named in the desktop's theme (an SF Symbol on the cocoa rung). */
|
|
46
|
+
iconName: boolean;
|
|
47
|
+
iconBytes: boolean;
|
|
48
|
+
attention: boolean;
|
|
49
|
+
overlay: boolean;
|
|
50
|
+
tooltip: boolean;
|
|
51
|
+
title: boolean;
|
|
52
|
+
click: boolean;
|
|
53
|
+
clickPosition: boolean;
|
|
54
|
+
/** The item's screen rect. False on the freedesktop rung, which has none. */
|
|
55
|
+
clickRect: boolean;
|
|
56
|
+
/** Modifier state on a click. False on the freedesktop rung. */
|
|
57
|
+
clickModifiers: boolean;
|
|
58
|
+
scroll: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LauncherFeatures {
|
|
62
|
+
badge: boolean;
|
|
63
|
+
/** A string badge. macOS only — the launcher protocol carries a count. */
|
|
64
|
+
badgeText: boolean;
|
|
65
|
+
progress: boolean;
|
|
66
|
+
urgent: boolean;
|
|
67
|
+
/** The Dock menu / quicklist. */
|
|
68
|
+
menu: boolean;
|
|
69
|
+
/** The launcher needs an installed `.desktop` file to attach this to. */
|
|
70
|
+
needsDesktopFile: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DesktopCapabilityResult<F = Record<string, boolean>> {
|
|
74
|
+
/** Whether there is any mechanism at all. Branch on `features` for detail. */
|
|
75
|
+
available: boolean;
|
|
76
|
+
backend: DesktopBackend | null;
|
|
77
|
+
/** Empty when `available` is false. */
|
|
78
|
+
features: Partial<F>;
|
|
79
|
+
/**
|
|
80
|
+
* Why not, when the `false` has a cause worth naming.
|
|
81
|
+
*
|
|
82
|
+
* `'no-app-id'` — the launcher needs `registerApplication({ appId })`, and
|
|
83
|
+
* this one is a mistake in the source.
|
|
84
|
+
* `'not-primary'` — it was called, and another copy of the app owns the
|
|
85
|
+
* identity. Correct single-instance behaviour, not a bug: the first copy
|
|
86
|
+
* owns the badge and the quicklist.
|
|
87
|
+
*/
|
|
88
|
+
reason?: 'no-app-id' | 'not-primary';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type DesktopCapabilityFor<N extends DesktopCapabilityName> =
|
|
92
|
+
N extends 'notifications'
|
|
93
|
+
? DesktopCapabilityResult<NotificationFeatures>
|
|
94
|
+
: N extends 'tray'
|
|
95
|
+
? DesktopCapabilityResult<TrayFeatures>
|
|
96
|
+
: DesktopCapabilityResult<LauncherFeatures>;
|
|
97
|
+
|
|
98
|
+
export declare const CAPABILITIES: readonly DesktopCapabilityName[];
|
|
99
|
+
|
|
100
|
+
/** The "nothing here" answer: available false, no backend, no features. */
|
|
101
|
+
export declare const NO_CAPABILITY: DesktopCapabilityResult;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* What this desktop can do for one feature.
|
|
105
|
+
*
|
|
106
|
+
* Never cached — a panel restarting or an extension being enabled changes the
|
|
107
|
+
* answer, and a cached `false` would outlive the fix. Throws only for an
|
|
108
|
+
* unknown name, which is a mistake in the source.
|
|
109
|
+
*/
|
|
110
|
+
export declare function desktopCapability<N extends DesktopCapabilityName>(
|
|
111
|
+
name: N,
|
|
112
|
+
options?: { app?: unknown },
|
|
113
|
+
): Promise<DesktopCapabilityFor<N>>;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* {@link desktopCapability} as render state: {@link NO_CAPABILITY} on the
|
|
117
|
+
* first frame, settling a tick later, and re-probed whenever a name appears
|
|
118
|
+
* or vanishes on the session bus.
|
|
119
|
+
*
|
|
120
|
+
* Use it for the *pre-flight* question — a settings screen that must render a
|
|
121
|
+
* "Show tray icon" checkbox without putting an icon in the tray. Where the
|
|
122
|
+
* feature is actually mounted, the feature hook's own status is a measurement
|
|
123
|
+
* rather than a prediction and should be preferred.
|
|
124
|
+
*/
|
|
125
|
+
export declare function useDesktopCapability<N extends DesktopCapabilityName>(
|
|
126
|
+
name: N,
|
|
127
|
+
): DesktopCapabilityState<N>;
|
|
128
|
+
|
|
129
|
+
/** {@link useDesktopCapability}'s answer, and whether it is one yet. */
|
|
130
|
+
export type DesktopCapabilityState<N extends DesktopCapabilityName> =
|
|
131
|
+
DesktopCapabilityFor<N> & {
|
|
132
|
+
/**
|
|
133
|
+
* False until the first probe answers, and true from then on, re-probes
|
|
134
|
+
* included — so an app can hold its fallback back rather than flash it.
|
|
135
|
+
* True on the first frame where nothing had to be asked: the tray and
|
|
136
|
+
* the Dock tile on macOS.
|
|
137
|
+
*/
|
|
138
|
+
settled: boolean;
|
|
139
|
+
};
|
|
@@ -418,6 +418,17 @@ export interface SliderProps
|
|
|
418
418
|
disabled?: boolean;
|
|
419
419
|
height?: number;
|
|
420
420
|
style?: StyleProp;
|
|
421
|
+
/**
|
|
422
|
+
* The thumb's style, over its default: a 16px circle on `surface` with a
|
|
423
|
+
* ring. Its `width` and `height`, as numbers, are what the drag's travel
|
|
424
|
+
* and the control's height are measured with.
|
|
425
|
+
*/
|
|
426
|
+
thumbStyle?: StyleProp;
|
|
427
|
+
/** The track's style, over `height` and `track`. Its `height` is the
|
|
428
|
+
* fill's too. */
|
|
429
|
+
trackStyle?: StyleProp;
|
|
430
|
+
/** The filled part of the track, up to the value, over `accent`. */
|
|
431
|
+
fillStyle?: StyleProp;
|
|
421
432
|
}
|
|
422
433
|
export const Slider: ComponentType<SliderProps>;
|
|
423
434
|
|
|
@@ -761,6 +772,28 @@ export function anchorRect(
|
|
|
761
772
|
options?: AnchorOptions,
|
|
762
773
|
): AnchorRect | null;
|
|
763
774
|
|
|
775
|
+
/**
|
|
776
|
+
* The same placement against a rect **on the screen** rather than a node —
|
|
777
|
+
* the item a tray click reports, a point where the pointer was — in logical
|
|
778
|
+
* screen pixels on both sides. With no node to ask, `scale` is the
|
|
779
|
+
* display's (default 1) and `direction` decides `'start'` and `'end'`; the
|
|
780
|
+
* popup is kept on the monitor the rect is on. `null` for a rect with no
|
|
781
|
+
* `x`/`y`.
|
|
782
|
+
*/
|
|
783
|
+
export function anchorScreenRect(
|
|
784
|
+
app: unknown,
|
|
785
|
+
rect: ScreenAnchorRect,
|
|
786
|
+
options?: Omit<AnchorOptions, 'at' | 'alignTo'> & { scale?: number },
|
|
787
|
+
): AnchorRect | null;
|
|
788
|
+
|
|
789
|
+
/** A rect on the screen, in logical pixels; `{x, y}` alone is a point. */
|
|
790
|
+
export interface ScreenAnchorRect {
|
|
791
|
+
x: number;
|
|
792
|
+
y: number;
|
|
793
|
+
width?: number;
|
|
794
|
+
height?: number;
|
|
795
|
+
}
|
|
796
|
+
|
|
764
797
|
/** Centre a popup of this size on the node's screen. */
|
|
765
798
|
export function centerRect(
|
|
766
799
|
node: DrawnNode,
|
package/src/types/elements.d.ts
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { Ref, RefObject, ReactNode, Key } from 'react';
|
|
8
|
-
import type { Color, Cursor, StyleProp } from './style.js';
|
|
8
|
+
import type { Color, Cursor, FontWeight, StyleProp } from './style.js';
|
|
9
9
|
import type {
|
|
10
10
|
DrawnNode,
|
|
11
11
|
NtkWindow,
|
|
12
12
|
ScrollableNode,
|
|
13
13
|
TextInputNode,
|
|
14
14
|
} from './nodes.js';
|
|
15
|
-
import type { AnchorOptions } from './components.js';
|
|
15
|
+
import type { AnchorOptions, ScreenAnchorRect } from './components.js';
|
|
16
16
|
import type {
|
|
17
17
|
ChangeEvent,
|
|
18
18
|
SelectionChangeEvent,
|
|
@@ -580,6 +580,16 @@ export interface PopupProps extends WindowProps {
|
|
|
580
580
|
* this client at all. Needs ntk >= 3.7.0.
|
|
581
581
|
*/
|
|
582
582
|
grab?: boolean;
|
|
583
|
+
/**
|
|
584
|
+
* Take the keyboard while the popup is up: its keys come to it whatever
|
|
585
|
+
* holds the focus, which is what a popover a tray click opened needs — a
|
|
586
|
+
* menu-bar app has no window of its own for keys to arrive at. A keyboard
|
|
587
|
+
* grab on X, taken and dropped with the map like `grab`. On macOS a window
|
|
588
|
+
* AppKit can make key, shown activating the app, as `NSPopover`'s own
|
|
589
|
+
* window is — decided when the popup is created, so it does not change on
|
|
590
|
+
* a mounted one. A grabbing popup on Wayland has the keyboard already.
|
|
591
|
+
*/
|
|
592
|
+
grabKeyboard?: boolean;
|
|
583
593
|
/**
|
|
584
594
|
* `true` (the default) keeps the window manager out entirely, which is
|
|
585
595
|
* what makes a menu a menu — no frame, no repositioning, no taskbar entry.
|
|
@@ -597,7 +607,9 @@ export interface PopupProps extends WindowProps {
|
|
|
597
607
|
/**
|
|
598
608
|
* Hang this popup off a node — `anchorRect`'s options plus the node to
|
|
599
609
|
* measure, and the popup works out its own position from them, ignoring
|
|
600
|
-
* `x`/`y`.
|
|
610
|
+
* `x`/`y`. Or off a rect on the screen with no node behind it, `rect`
|
|
611
|
+
* instead of `to`: the item a tray click reports, placed against with the
|
|
612
|
+
* same flip and clamp.
|
|
601
613
|
*
|
|
602
614
|
* What this does that computing a rect in the application cannot: a popup
|
|
603
615
|
* with an `'auto'` size only knows how big it is *inside* `realize()`,
|
|
@@ -622,12 +634,26 @@ export interface PopupProps extends WindowProps {
|
|
|
622
634
|
* that renders them) or the node itself. */
|
|
623
635
|
export type AnchorTarget = DrawnNode | RefObject<DrawnNode | null> | null;
|
|
624
636
|
|
|
625
|
-
|
|
637
|
+
/** Where a popup hangs: a node, or a rect on the screen. */
|
|
638
|
+
export type PopupAnchor = NodeAnchor | ScreenAnchor;
|
|
639
|
+
|
|
640
|
+
export interface NodeAnchor extends Omit<AnchorOptions, 'alignTo'> {
|
|
626
641
|
/** The node this popup hangs off. */
|
|
627
642
|
to: AnchorTarget;
|
|
628
643
|
/** Takes the alignment axis from another node — see
|
|
629
644
|
* {@link AnchorOptions.alignTo}. */
|
|
630
645
|
alignTo?: AnchorTarget;
|
|
646
|
+
rect?: never;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export interface ScreenAnchor extends Omit<AnchorOptions, 'alignTo' | 'at'> {
|
|
650
|
+
/**
|
|
651
|
+
* A rect on the screen, in logical pixels — what `useTray`'s `onClick`
|
|
652
|
+
* reports where the platform knows the item's frame. `{x, y}` alone is a
|
|
653
|
+
* point. Nothing moves it but a new one, and it is never out of view.
|
|
654
|
+
*/
|
|
655
|
+
rect: ScreenAnchorRect;
|
|
656
|
+
to?: never;
|
|
631
657
|
}
|
|
632
658
|
|
|
633
659
|
// --- drawn elements --------------------------------------------------------
|
|
@@ -758,10 +784,35 @@ export interface FileUrl {
|
|
|
758
784
|
|
|
759
785
|
/**
|
|
760
786
|
* What `src` accepts: a file path or file URL (PNG/JPEG, decoded in JS),
|
|
761
|
-
* encoded PNG/JPEG bytes, raw RGBA pixels,
|
|
787
|
+
* encoded PNG/JPEG bytes, raw RGBA pixels, an ntk `Image`/`Surface`, or a
|
|
788
|
+
* symbol by name.
|
|
762
789
|
*/
|
|
763
790
|
export type ImageSource =
|
|
764
|
-
|
|
791
|
+
| string
|
|
792
|
+
| FileUrl
|
|
793
|
+
| Uint8Array
|
|
794
|
+
| RawImageSource
|
|
795
|
+
| DirectImageSource
|
|
796
|
+
| SymbolImageSource;
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* The platform's own icon by name, drawn in the text colour: an SF Symbol on
|
|
800
|
+
* macOS (`'speaker.wave.3.fill'`), and elsewhere an icon from the user's
|
|
801
|
+
* freedesktop icon theme (`'audio-volume-high'`), its `-symbolic` variant
|
|
802
|
+
* preferred. Sized like the text around it — the size its `fontSize` calls
|
|
803
|
+
* for — unless the element is styled a size of its own. A name this desktop
|
|
804
|
+
* does not have takes no room and draws nothing.
|
|
805
|
+
*/
|
|
806
|
+
export interface SymbolImageSource {
|
|
807
|
+
symbol: string;
|
|
808
|
+
/** Default: the weight of the text around it. SF Symbols only. */
|
|
809
|
+
weight?: FontWeight;
|
|
810
|
+
/** Relative to the text size, default `'medium'`. SF Symbols only. */
|
|
811
|
+
scale?: 'small' | 'medium' | 'large';
|
|
812
|
+
/** How much of a variable symbol shows, 0 to 1 — the waves of
|
|
813
|
+
* `speaker.wave.3.fill` at a volume. SF Symbols on macOS 13 and later. */
|
|
814
|
+
variableValue?: number;
|
|
815
|
+
}
|
|
765
816
|
|
|
766
817
|
/**
|
|
767
818
|
* An existing server-side Picture, named by X id. The size is stated by the
|
package/src/types/launcher.d.ts
CHANGED
|
@@ -35,9 +35,55 @@ export declare function setBadge(
|
|
|
35
35
|
export declare function useBadge(value: BadgeValue): void;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* The menu behind a right-click on the
|
|
39
|
-
* vocabulary `MenuBar` takes; an item's `onSelect` fires
|
|
40
|
-
* Installed while mounted, replaced when `items` changes, taken
|
|
41
|
-
*
|
|
38
|
+
* The menu behind a right-click on the app's icon in the Dock or launcher,
|
|
39
|
+
* from the same item vocabulary `MenuBar` takes; an item's `onSelect` fires
|
|
40
|
+
* when picked. Installed while mounted, replaced when `items` changes, taken
|
|
41
|
+
* down on unmount.
|
|
42
|
+
*
|
|
43
|
+
* `NSDockTile`'s menu on the cocoa backend; the launcher protocol's
|
|
44
|
+
* **quicklist** on Linux — a `com.canonical.dbusmenu` tree, the same menu
|
|
45
|
+
* protocol the tray and the global menu speak. Needs the identity
|
|
46
|
+
* `registerApplication({ appId })` establishes and a `.desktop` file of that
|
|
47
|
+
* name, like the badge.
|
|
42
48
|
*/
|
|
43
49
|
export declare function useDockMenu(items: MenuItem[] | null): void;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A progress bar across the app's icon, `0`…`1`. `null` clears it; values
|
|
53
|
+
* outside the range are clamped rather than refused.
|
|
54
|
+
*
|
|
55
|
+
* The launcher protocol's `progress`. **Linux launchers only** — `NSDockTile`
|
|
56
|
+
* has no progress bar, so this is inert on the cocoa backend and
|
|
57
|
+
* `useDesktopCapability('launcher').features.progress` is the honest answer.
|
|
58
|
+
*/
|
|
59
|
+
export declare function setProgress(
|
|
60
|
+
value: number | null | false | undefined,
|
|
61
|
+
options?: SetBadgeOptions,
|
|
62
|
+
): Promise<boolean>;
|
|
63
|
+
|
|
64
|
+
/** {@link setProgress} while mounted; cleared on unmount. */
|
|
65
|
+
export declare function useProgress(
|
|
66
|
+
value: number | null | false | undefined,
|
|
67
|
+
): void;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Ask the launcher for the user's attention, or stop asking.
|
|
71
|
+
*
|
|
72
|
+
* Distinct from `<window states={['demands_attention']}>`, which is the
|
|
73
|
+
* *window's* urgency hint: this marks the app's icon in the launcher whether
|
|
74
|
+
* or not any window is open. Inert on the cocoa backend, where the window
|
|
75
|
+
* state is the mechanism.
|
|
76
|
+
*/
|
|
77
|
+
export declare function setUrgent(
|
|
78
|
+
urgent: boolean,
|
|
79
|
+
options?: SetBadgeOptions,
|
|
80
|
+
): Promise<boolean>;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* {@link useDockMenu}'s imperative twin, for code with no component. `null`
|
|
84
|
+
* takes the menu down.
|
|
85
|
+
*/
|
|
86
|
+
export declare function setQuicklist(
|
|
87
|
+
items: MenuItem[] | null,
|
|
88
|
+
options?: SetBadgeOptions,
|
|
89
|
+
): Promise<boolean>;
|