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.
Files changed (64) hide show
  1. package/README.md +37 -0
  2. package/package.json +4 -3
  3. package/src/Reconciler.js +85 -22
  4. package/src/anchor.js +60 -18
  5. package/src/application.js +25 -1
  6. package/src/capabilities.js +349 -0
  7. package/src/cocoa/app.js +28 -9
  8. package/src/cocoa/context2d.js +139 -6
  9. package/src/cocoa/fonts.js +78 -0
  10. package/src/cocoa/presenter.js +17 -0
  11. package/src/cocoa/promotion.js +20 -0
  12. package/src/cocoa/relaunch.js +8 -3
  13. package/src/cocoa/symbols.js +64 -0
  14. package/src/cocoa/threaded.js +24 -4
  15. package/src/cocoa/window.js +362 -139
  16. package/src/components/ProgressBar.js +1 -1
  17. package/src/components/Slider.js +72 -39
  18. package/src/components/anchor.js +7 -2
  19. package/src/components/index.js +1 -0
  20. package/src/components/theme.js +32 -28
  21. package/src/dbusmenuexport.js +243 -0
  22. package/src/desktopcapabilityhooks.js +160 -0
  23. package/src/filedialoghooks.js +3 -5
  24. package/src/frame/childmain.js +8 -20
  25. package/src/frame/env.js +2 -10
  26. package/src/globalmenu.js +3 -205
  27. package/src/icontheme.js +240 -0
  28. package/src/imagesource.js +98 -3
  29. package/src/index.d.ts +1 -0
  30. package/src/index.js +11 -2
  31. package/src/launcher.js +235 -32
  32. package/src/launcherhooks.js +47 -28
  33. package/src/node.d.ts +7 -0
  34. package/src/nodes/animation.js +17 -47
  35. package/src/nodes/cascade.js +17 -2
  36. package/src/nodes/image.js +65 -2
  37. package/src/nodes/kinds.js +12 -0
  38. package/src/nodes/layout.js +5 -1
  39. package/src/nodes/node.js +17 -3
  40. package/src/nodes/paint.js +117 -0
  41. package/src/nodes/scope.js +259 -0
  42. package/src/nodes/scrollable.js +53 -6
  43. package/src/nodes/text.js +2 -0
  44. package/src/nodes/textarea.js +1 -1
  45. package/src/nodes/textinput.js +1 -1
  46. package/src/nodes/window/anchoring.js +45 -18
  47. package/src/nodes/window/flush.js +6 -5
  48. package/src/nodes/window/popup.js +10 -0
  49. package/src/nodes/window/size.js +40 -2
  50. package/src/nodes/window/window.js +41 -14
  51. package/src/registry.js +2 -1
  52. package/src/settings.js +332 -0
  53. package/src/statusnotifier.js +752 -0
  54. package/src/styles.js +212 -8
  55. package/src/symbols.js +200 -0
  56. package/src/testing/mock-app.js +10 -0
  57. package/src/trayhooks.js +193 -29
  58. package/src/types/capabilities.d.ts +139 -0
  59. package/src/types/components.d.ts +33 -0
  60. package/src/types/elements.d.ts +57 -6
  61. package/src/types/launcher.d.ts +50 -4
  62. package/src/types/style.d.ts +57 -0
  63. package/src/types/system.d.ts +104 -0
  64. 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
- // Today that is the cocoa backend's menu-bar extra (`NSStatusItem`,
5
- // src/cocoa/statusitem.js). The freedesktop counterpart, StatusNotifierItem
6
- // over D-Bus, is react-x11#353's open question; until it lands the hook is
7
- // inert on X11 and says so — `available: false`, and a one-time development
8
- // note — rather than pretending, so an app can keep the feature behind the
9
- // answer. The inert-props policy of docs/macos.md, for a hook.
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
- let warnedInert = false;
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 name, or PNG bytes
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 item's screen rect, which is
31
- * where to anchor a popup of your own. `null` means no item. Every field
32
- * follows its value while mounted; the item is removed on unmount.
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: false on X11
35
- * today (#353), and the honest answer to branch on.
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 available = typeof app?.createStatusItem === 'function';
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 (!available) {
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, available, options == null]);
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
- return { available, rect };
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,
@@ -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
- export interface PopupAnchor extends Omit<AnchorOptions, 'alignTo'> {
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, or an ntk `Image`/`Surface`.
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
- string | FileUrl | Uint8Array | RawImageSource | DirectImageSource;
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
@@ -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 Dock icon, from the same item
39
- * vocabulary `MenuBar` takes; an item's `onSelect` fires when picked.
40
- * Installed while mounted, replaced when `items` changes, taken down on
41
- * unmount. Inert off the cocoa backend.
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>;