react-x11 2.15.1 → 2.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/application.js +25 -1
- package/src/capabilities.js +324 -0
- package/src/cocoa/app.js +13 -0
- package/src/cocoa/context2d.js +116 -6
- package/src/dbusmenuexport.js +243 -0
- package/src/desktopcapabilityhooks.js +137 -0
- package/src/globalmenu.js +3 -205
- package/src/imagesource.js +15 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +8 -2
- package/src/launcher.js +235 -32
- package/src/launcherhooks.js +47 -28
- package/src/nodes/image.js +2 -1
- package/src/statusnotifier.js +605 -0
- package/src/trayhooks.js +177 -29
- package/src/types/capabilities.d.ts +127 -0
- package/src/types/launcher.d.ts +50 -4
- package/src/types/tray.d.ts +52 -6
- package/src/wayland/context2d.js +28 -2
- package/src/wayland/device.js +23 -0
- package/src/wayland/target.js +9 -1
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,52 @@ 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
|
-
*
|
|
77
|
+
* `onClick` is called with the button and, where the backend knows it, the
|
|
78
|
+
* item's screen rect. `null` means no item. Every field follows its value
|
|
79
|
+
* while mounted; the item is removed on unmount.
|
|
33
80
|
*
|
|
34
|
-
* `available` is whether this backend has a tray at all
|
|
35
|
-
*
|
|
81
|
+
* `available` is whether this backend has a tray at all, and it **settles**:
|
|
82
|
+
* false on the first frame, true once a tray has been found. Branch on it for
|
|
83
|
+
* a "show tray icon" setting; do not branch on it to decide whether to call
|
|
84
|
+
* the hook, which would break the rules of hooks the moment it changed.
|
|
36
85
|
*/
|
|
37
86
|
export function useTray(options) {
|
|
38
87
|
const app = useAppOrNull();
|
|
39
|
-
const
|
|
88
|
+
const native = typeof app?.createStatusItem === 'function';
|
|
40
89
|
const itemRef = useRef(null);
|
|
41
90
|
const [rect] = useState(null);
|
|
91
|
+
const [remote, setRemote] = useState(false);
|
|
92
|
+
const [error, setError] = useState(null);
|
|
93
|
+
// The feature vocabulary for whichever rung answered. Probed once per
|
|
94
|
+
// backend rather than per render — it describes the mechanism, not the
|
|
95
|
+
// item — and for *both* rungs: the cocoa one is synchronous about whether
|
|
96
|
+
// it has a tray, but not about what that tray can do, and an app reading
|
|
97
|
+
// `features.clickModifiers` to dim a shift-click affordance would
|
|
98
|
+
// otherwise be told `undefined` on the one backend that has modifiers.
|
|
99
|
+
const [caps, setCaps] = useState(NO_CAPABILITY);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!options) return undefined;
|
|
103
|
+
let cancelled = false;
|
|
104
|
+
// `app` explicitly rather than letting the probe find the sole one: a
|
|
105
|
+
// process with several connections has several trays, and this hook
|
|
106
|
+
// belongs to one of them.
|
|
107
|
+
void desktopCapability('tray', { app }).then((c) => {
|
|
108
|
+
if (!cancelled) setCaps(c);
|
|
109
|
+
});
|
|
110
|
+
return () => {
|
|
111
|
+
cancelled = true;
|
|
112
|
+
};
|
|
113
|
+
}, [app, native, options == null]);
|
|
42
114
|
|
|
43
115
|
// the options a click or a pick reads are the current render's, not the
|
|
44
116
|
// ones the item was created with three minutes ago
|
|
45
117
|
const live = useRef(options);
|
|
46
118
|
live.current = options;
|
|
47
119
|
|
|
120
|
+
// ------------------------------------------------------- rung 1: the app's
|
|
48
121
|
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;
|
|
122
|
+
if (!native || !options) return undefined;
|
|
61
123
|
const item = app.createStatusItem({
|
|
62
124
|
...options,
|
|
63
125
|
onClick: (ev) => live.current?.onClick?.(ev),
|
|
@@ -70,7 +132,7 @@ export function useTray(options) {
|
|
|
70
132
|
// Recreated only when the item comes or goes: the fields patch in
|
|
71
133
|
// place below, and an `options` object rebuilt every render must not
|
|
72
134
|
// rebuild the item every render.
|
|
73
|
-
}, [app,
|
|
135
|
+
}, [app, native, options == null]);
|
|
74
136
|
|
|
75
137
|
useEffect(() => {
|
|
76
138
|
const item = itemRef.current;
|
|
@@ -86,5 +148,91 @@ export function useTray(options) {
|
|
|
86
148
|
options?.menu,
|
|
87
149
|
]);
|
|
88
150
|
|
|
89
|
-
|
|
151
|
+
// ------------------------------------------- rung 2: the freedesktop tray
|
|
152
|
+
const sniRef = useRef(null);
|
|
153
|
+
const previous = useRef(options ?? {});
|
|
154
|
+
// One slot for the life of this hook, **not** per item object. Passing
|
|
155
|
+
// `null` and then options again is the same tray icon going away and coming
|
|
156
|
+
// back; on a fresh path the host has no way to know that and draws a second
|
|
157
|
+
// one beside the first. See `StatusNotifierItem.announcePassive`.
|
|
158
|
+
const slotRef = useRef(null);
|
|
159
|
+
slotRef.current ??= allocateItemSlot();
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (native || !options) return undefined;
|
|
163
|
+
let cancelled = false;
|
|
164
|
+
// The id hosts key their hidden-icons setting on. The app's registered id
|
|
165
|
+
// when it has one, so the choice survives a restart.
|
|
166
|
+
const appId = currentRegistration()?.appId ?? 'react-x11';
|
|
167
|
+
const item = new StatusNotifierItem({
|
|
168
|
+
getOptions: () => live.current,
|
|
169
|
+
appId,
|
|
170
|
+
slot: slotRef.current,
|
|
171
|
+
decodeIcon: decodeIconBytes,
|
|
172
|
+
// A host that answered the bus and then refused the registration is a
|
|
173
|
+
// fact worth reporting; a desktop with no tray at all is not, and does
|
|
174
|
+
// not come through here.
|
|
175
|
+
onError: (err) => {
|
|
176
|
+
if (!cancelled)
|
|
177
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
sniRef.current = item;
|
|
181
|
+
previous.current = { ...live.current };
|
|
182
|
+
void item.start().then((ok) => {
|
|
183
|
+
if (!cancelled) setRemote(ok);
|
|
184
|
+
});
|
|
185
|
+
return () => {
|
|
186
|
+
cancelled = true;
|
|
187
|
+
sniRef.current = null;
|
|
188
|
+
setRemote(false);
|
|
189
|
+
setError(null);
|
|
190
|
+
void item.stop();
|
|
191
|
+
};
|
|
192
|
+
}, [native, options == null]);
|
|
193
|
+
|
|
194
|
+
// The item reads its fields through `getOptions`, so a render only has to
|
|
195
|
+
// say *which* of them moved — see `StatusNotifierItem.update`.
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
const item = sniRef.current;
|
|
198
|
+
if (!item) return;
|
|
199
|
+
item.update(previous.current);
|
|
200
|
+
previous.current = { ...live.current };
|
|
201
|
+
}, [
|
|
202
|
+
options?.icon,
|
|
203
|
+
options?.overlayIcon,
|
|
204
|
+
options?.attentionIcon,
|
|
205
|
+
options?.title,
|
|
206
|
+
options?.tooltip,
|
|
207
|
+
options?.visible,
|
|
208
|
+
options?.attention,
|
|
209
|
+
options?.menu,
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
// The cocoa rung is synchronous and always itself; the freedesktop one
|
|
213
|
+
// reports whatever the probe found. `features` is empty until one settles,
|
|
214
|
+
// which is the same "render the fallback first" order `available` has.
|
|
215
|
+
const backend = native ? 'cocoa' : remote ? caps.backend : null;
|
|
216
|
+
return {
|
|
217
|
+
available: native || remote,
|
|
218
|
+
backend,
|
|
219
|
+
features: native || remote ? caps.features : NO_CAPABILITY.features,
|
|
220
|
+
error,
|
|
221
|
+
rect,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* PNG/JPEG bytes → raw RGBA, through ntk's decoder.
|
|
227
|
+
*
|
|
228
|
+
* Defensive: a corrupt or unsupported image should cost the tray its *icon*,
|
|
229
|
+
* not its tray. The pixmap ends up empty and the item still registers with
|
|
230
|
+
* its name, tooltip and menu intact.
|
|
231
|
+
*/
|
|
232
|
+
function decodeIconBytes(bytes) {
|
|
233
|
+
try {
|
|
234
|
+
return decodeImage(bytes);
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
90
238
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
): DesktopCapabilityFor<N>;
|
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>;
|
package/src/types/tray.d.ts
CHANGED
|
@@ -3,16 +3,22 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { MenuItem } from './components.js';
|
|
6
|
+
import type { DesktopBackend, TrayFeatures } from './capabilities.js';
|
|
6
7
|
|
|
7
8
|
export interface TrayClickEvent {
|
|
8
9
|
button: 'left' | 'right' | 'middle';
|
|
9
|
-
/**
|
|
10
|
-
*
|
|
10
|
+
/** Where the click was, in global top-left screen coordinates — the anchor
|
|
11
|
+
* for a popup of your own. */
|
|
11
12
|
x: number;
|
|
12
13
|
y: number;
|
|
14
|
+
/** The item's rect. `0` on the freedesktop rung, whose protocol has none —
|
|
15
|
+
* read `features.clickRect` rather than testing for zero. */
|
|
13
16
|
width: number;
|
|
14
17
|
height: number;
|
|
18
|
+
/** `1` on the freedesktop rung, which does not count clicks. */
|
|
15
19
|
clickCount: number;
|
|
20
|
+
/** All `false` on the freedesktop rung, which carries no modifier state.
|
|
21
|
+
* `features.clickModifiers` is the honest answer. */
|
|
16
22
|
shift: boolean;
|
|
17
23
|
control: boolean;
|
|
18
24
|
option: boolean;
|
|
@@ -20,9 +26,27 @@ export interface TrayClickEvent {
|
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface TrayOptions {
|
|
23
|
-
/**
|
|
24
|
-
*
|
|
29
|
+
/** A themed icon name — an SF Symbol on the cocoa rung (`'bell.badge'`),
|
|
30
|
+
* an icon-theme name on a freedesktop one (`'mail-unread'`) — or the bytes
|
|
31
|
+
* of a PNG. On the cocoa rung bytes are drawn as a template image so they
|
|
32
|
+
* follow the bar's light and dark. */
|
|
25
33
|
icon?: string | Uint8Array | null;
|
|
34
|
+
/** Shown instead of `icon` while `attention` is set. Freedesktop only. */
|
|
35
|
+
attentionIcon?: string | Uint8Array | null;
|
|
36
|
+
/** A small badge drawn over the icon. Freedesktop only. */
|
|
37
|
+
overlayIcon?: string | Uint8Array | null;
|
|
38
|
+
/** Ask the panel to mark the item — `Status = NeedsAttention`.
|
|
39
|
+
* Freedesktop only; `features.attention` says so. */
|
|
40
|
+
attention?: boolean;
|
|
41
|
+
/** The spec's item category: `'ApplicationStatus'` (default),
|
|
42
|
+
* `'Communications'`, `'SystemServices'` or `'Hardware'`. Freedesktop
|
|
43
|
+
* only, and mostly affects where a panel sorts the icon. */
|
|
44
|
+
category?: string;
|
|
45
|
+
/** A directory to look `icon` up in, for icons shipped beside the app
|
|
46
|
+
* rather than installed in a theme. Freedesktop only. */
|
|
47
|
+
iconThemePath?: string;
|
|
48
|
+
/** A scroll over the icon. Freedesktop only. */
|
|
49
|
+
onScroll?: (event: { delta: number; orientation: string }) => void;
|
|
26
50
|
/** Text beside the icon, or alone. */
|
|
27
51
|
title?: string | null;
|
|
28
52
|
tooltip?: string | null;
|
|
@@ -40,8 +64,25 @@ export interface TrayOptions {
|
|
|
40
64
|
}
|
|
41
65
|
|
|
42
66
|
export interface TrayState {
|
|
43
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Whether **this item** was taken by a tray.
|
|
69
|
+
*
|
|
70
|
+
* A measurement, not a prediction: the hook tried. It **settles** — false
|
|
71
|
+
* on the first frame, true a tick later if a host is there — so render the
|
|
72
|
+
* fallback first and upgrade. It follows the host, so a panel that exits
|
|
73
|
+
* flips it back.
|
|
74
|
+
*/
|
|
44
75
|
available: boolean;
|
|
76
|
+
/** Which mechanism took it, or null. */
|
|
77
|
+
backend: DesktopBackend | null;
|
|
78
|
+
/** What that mechanism can do. Empty until `available` settles. */
|
|
79
|
+
features: Partial<TrayFeatures>;
|
|
80
|
+
/**
|
|
81
|
+
* A tray that answered and then **refused**, which is a different fact from
|
|
82
|
+
* a desktop with no tray — and the only one of the two with a fix. Null
|
|
83
|
+
* when there is simply no tray.
|
|
84
|
+
*/
|
|
85
|
+
error: Error | null;
|
|
45
86
|
/** Reserved. */
|
|
46
87
|
rect: null;
|
|
47
88
|
}
|
|
@@ -49,6 +90,11 @@ export interface TrayState {
|
|
|
49
90
|
/**
|
|
50
91
|
* An icon in the system tray while this component is mounted; every field
|
|
51
92
|
* follows its value, and the item is removed on unmount. `null` means no
|
|
52
|
-
* item.
|
|
93
|
+
* item.
|
|
94
|
+
*
|
|
95
|
+
* `NSStatusItem` on the cocoa backend; `org.kde.StatusNotifierItem` over
|
|
96
|
+
* D-Bus on a freedesktop session, which needs something hosting a tray —
|
|
97
|
+
* Plasma and most panels do, GNOME needs an AppIndicator extension. Where
|
|
98
|
+
* neither answers, `available` stays false and nothing is logged.
|
|
53
99
|
*/
|
|
54
100
|
export declare function useTray(options: TrayOptions | null): TrayState;
|
package/src/wayland/context2d.js
CHANGED
|
@@ -532,7 +532,7 @@ export class WaylandContext2D {
|
|
|
532
532
|
*/
|
|
533
533
|
begin(width, height, timestamp = 0) {
|
|
534
534
|
this.init();
|
|
535
|
-
this.
|
|
535
|
+
this._take();
|
|
536
536
|
const t = this._target;
|
|
537
537
|
if (t) {
|
|
538
538
|
t.bind();
|
|
@@ -612,7 +612,7 @@ export class WaylandContext2D {
|
|
|
612
612
|
// Mid stencil-then-cover the state is set up for the pass, not for a
|
|
613
613
|
// plain batch, and the device cannot have changed hands under it.
|
|
614
614
|
if (this._device.owner === this || this._stencilling) return;
|
|
615
|
-
this.
|
|
615
|
+
this._take();
|
|
616
616
|
this._makeCurrent?.();
|
|
617
617
|
this.init();
|
|
618
618
|
// As `begin()` has it: a target's size *is* the projection.
|
|
@@ -624,6 +624,26 @@ export class WaylandContext2D {
|
|
|
624
624
|
this._applyState();
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Take the device, drawing whatever the last owner still had buffered.
|
|
629
|
+
*
|
|
630
|
+
* A context batches: its quads sit in a vertex buffer until the mode
|
|
631
|
+
* changes, the buffer fills, or someone flushes. So the handover is where
|
|
632
|
+
* the outgoing owner's batch is drawn — otherwise a frame's worth of work
|
|
633
|
+
* can sit there while the pixels it was meant to produce are read by
|
|
634
|
+
* whoever comes next, and stay there until that context happens to draw
|
|
635
|
+
* something of a different kind (#578). The state is still the outgoing
|
|
636
|
+
* owner's at this point, which is what the flush needs.
|
|
637
|
+
*
|
|
638
|
+
* It is also what device.js's invariant rests on: the owner is the only
|
|
639
|
+
* context that can be holding anything.
|
|
640
|
+
*/
|
|
641
|
+
_take() {
|
|
642
|
+
const previous = this._device.owner;
|
|
643
|
+
if (previous && previous !== this) previous._flush();
|
|
644
|
+
this._device.owner = this;
|
|
645
|
+
}
|
|
646
|
+
|
|
627
647
|
/** Give the device up, so the next draw through this context re-takes it. */
|
|
628
648
|
_release() {
|
|
629
649
|
if (this._device.owner === this) this._device.owner = null;
|
|
@@ -1950,6 +1970,11 @@ export class WaylandContext2D {
|
|
|
1950
1970
|
|
|
1951
1971
|
/** Two triangles from four corners, in order TL, TR, BR, BL. */
|
|
1952
1972
|
_quad(pos, uv, color, params) {
|
|
1973
|
+
// Buffering is taking the device, not a step before it: the quad is GL
|
|
1974
|
+
// work that has been decided on and not yet issued, and the handover in
|
|
1975
|
+
// `_take` is the only thing that can get it issued before someone reads
|
|
1976
|
+
// the pixels it makes. A property compare in the common case.
|
|
1977
|
+
this._claim();
|
|
1953
1978
|
if (this._n + 6 > MAX_VERTS) this._flush();
|
|
1954
1979
|
const v = this._verts;
|
|
1955
1980
|
let o = this._n * STRIDE;
|
|
@@ -1974,6 +1999,7 @@ export class WaylandContext2D {
|
|
|
1974
1999
|
|
|
1975
2000
|
/** One triangle, for the stencil pass. Colour and params are irrelevant. */
|
|
1976
2001
|
_tri(x0, y0, x1, y1, x2, y2) {
|
|
2002
|
+
this._claim();
|
|
1977
2003
|
if (this._n + 3 > MAX_VERTS) this._flush();
|
|
1978
2004
|
const v = this._verts;
|
|
1979
2005
|
let o = this._n * STRIDE;
|
package/src/wayland/device.js
CHANGED
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
// back. Whoever drew in between does not have to know who comes next, and a
|
|
17
17
|
// caller does not have to know there is a device at all.
|
|
18
18
|
//
|
|
19
|
+
// Claiming is also a *batch boundary*: a 2d context buffers its quads and
|
|
20
|
+
// draws them in one call later, so the outgoing owner is flushed on the way
|
|
21
|
+
// out. That is what makes the invariant this module offers true — **only the
|
|
22
|
+
// owner can have work still buffered** — and the invariant is what lets
|
|
23
|
+
// anyone about to read a target's pixels get them by flushing one context
|
|
24
|
+
// (`flushDevice`) rather than by knowing which one drew them. Without it a
|
|
25
|
+
// held surface context's whole frame could sit in a vertex buffer while the
|
|
26
|
+
// `drawImage` that should show it sampled the texture behind it (#578).
|
|
27
|
+
//
|
|
19
28
|
// One record per `gl`, keyed weakly: a GPU that goes away takes its record
|
|
20
29
|
// with it.
|
|
21
30
|
const devices = new WeakMap();
|
|
@@ -53,3 +62,17 @@ export function releaseDevice(gl) {
|
|
|
53
62
|
const device = shared(gl) ? devices.get(gl) : null;
|
|
54
63
|
if (device) device.owner = null;
|
|
55
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Draw what is still buffered, so that a target's texture holds everything
|
|
68
|
+
* that has been drawn into it.
|
|
69
|
+
*
|
|
70
|
+
* The owner is the only context that can be holding anything (see above), so
|
|
71
|
+
* flushing it is flushing the device. Call before *reading* a target's pixels
|
|
72
|
+
* behind the contexts' backs — a framebuffer blit, a scroll copy — the way
|
|
73
|
+
* `releaseDevice` is called after writing them.
|
|
74
|
+
*/
|
|
75
|
+
export function flushDevice(gl) {
|
|
76
|
+
const device = shared(gl) ? devices.get(gl) : null;
|
|
77
|
+
device?.owner?.flush();
|
|
78
|
+
}
|
package/src/wayland/target.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// asking EGL for it means the window's own config no longer has to carry
|
|
22
22
|
// stencil bits at all; the request in glcontext.js stays as belt and braces.
|
|
23
23
|
|
|
24
|
-
import { releaseDevice } from './device.js';
|
|
24
|
+
import { flushDevice, releaseDevice } from './device.js';
|
|
25
25
|
|
|
26
26
|
export class GLTarget {
|
|
27
27
|
/**
|
|
@@ -133,6 +133,9 @@ export class GLTarget {
|
|
|
133
133
|
*/
|
|
134
134
|
blitTo(drawFbo, rects, dstWidth = this.width, dstHeight = this.height) {
|
|
135
135
|
const gl = this.gl;
|
|
136
|
+
// Reading these pixels, so everything drawn into them has to be in the
|
|
137
|
+
// texture and not still in a context's vertex buffer (device.js).
|
|
138
|
+
flushDevice(gl);
|
|
136
139
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fbo);
|
|
137
140
|
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, drawFbo);
|
|
138
141
|
gl.disable(gl.SCISSOR_TEST);
|
|
@@ -201,6 +204,11 @@ export class GLTarget {
|
|
|
201
204
|
const sy = dstY0 - dy;
|
|
202
205
|
|
|
203
206
|
const gl = this.gl;
|
|
207
|
+
// A scroll reads this target before it writes it, so anything a context
|
|
208
|
+
// still has buffered has to land first — or the band that moves is the
|
|
209
|
+
// frame before last's, and the buffered quads arrive afterwards at the
|
|
210
|
+
// position the scroll just left (device.js).
|
|
211
|
+
flushDevice(gl);
|
|
204
212
|
const scratch = scratchFor(gl, bw, bh);
|
|
205
213
|
// out: this[sx,sy,bw,bh] -> scratch[0,0]
|
|
206
214
|
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.fbo);
|