react-x11 2.15.3 → 2.16.1

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 (67) hide show
  1. package/README.md +37 -0
  2. package/package.json +3 -3
  3. package/src/Reconciler.js +85 -22
  4. package/src/acceleratorhooks.js +40 -6
  5. package/src/anchor.js +79 -19
  6. package/src/capabilities.js +29 -4
  7. package/src/cocoa/app.js +211 -11
  8. package/src/cocoa/context2d.js +23 -0
  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/desktopcapabilityhooks.js +29 -6
  22. package/src/filedialoghooks.js +3 -5
  23. package/src/frame/childmain.js +8 -20
  24. package/src/frame/env.js +2 -10
  25. package/src/icontheme.js +240 -0
  26. package/src/imagesource.js +83 -1
  27. package/src/index.d.ts +10 -1
  28. package/src/index.js +3 -0
  29. package/src/keysymchars.js +47 -0
  30. package/src/keysyms.d.ts +19 -1
  31. package/src/keysyms.js +107 -8
  32. package/src/node.d.ts +7 -0
  33. package/src/nodes/animation.js +17 -47
  34. package/src/nodes/cascade.js +17 -2
  35. package/src/nodes/image.js +63 -1
  36. package/src/nodes/kinds.js +12 -0
  37. package/src/nodes/layout.js +5 -1
  38. package/src/nodes/node.js +17 -3
  39. package/src/nodes/paint.js +117 -0
  40. package/src/nodes/scope.js +259 -0
  41. package/src/nodes/scrollable.js +53 -6
  42. package/src/nodes/text.js +2 -0
  43. package/src/nodes/textarea.js +1 -1
  44. package/src/nodes/textinput.js +1 -1
  45. package/src/nodes/window/anchoring.js +45 -18
  46. package/src/nodes/window/flush.js +6 -5
  47. package/src/nodes/window/popup.js +10 -0
  48. package/src/nodes/window/size.js +40 -2
  49. package/src/nodes/window/window.js +41 -14
  50. package/src/registry.js +2 -1
  51. package/src/screens.js +159 -24
  52. package/src/settings.js +332 -0
  53. package/src/statusnotifier.js +164 -17
  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 +21 -5
  58. package/src/types/capabilities.d.ts +13 -1
  59. package/src/types/components.d.ts +33 -0
  60. package/src/types/elements.d.ts +57 -6
  61. package/src/types/events.d.ts +5 -0
  62. package/src/types/filedialog.d.ts +3 -1
  63. package/src/types/style.d.ts +57 -0
  64. package/src/types/system.d.ts +104 -0
  65. package/src/types/tray.d.ts +14 -2
  66. package/src/wayland/xkb.js +170 -59
  67. package/src/windowid.js +62 -20
@@ -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
@@ -541,6 +541,11 @@ export interface AcceleratorOptions {
541
541
  * matched against the Latin keysym so a layout switch does not turn it off,
542
542
  * and behind whatever a focused element consumed with `preventDefault()`.
543
543
  * See docs/events.md.
544
+ *
545
+ * With no `scope` the binding belongs to the tree's top-level `<window>`, or,
546
+ * in an app that has none, to the root-level `<popup>` holding the keyboard —
547
+ * a tray popover. One that can reach neither binds nothing and says so once
548
+ * in development.
544
549
  */
545
550
  export function useAccelerator(
546
551
  shortcut: MenuShortcut,
@@ -162,7 +162,9 @@ export interface FileDialogs {
162
162
  *
163
163
  * Exact when the tree has one top-level window, which is nearly every app.
164
164
  * With several it prefers the focused one and warns in development when it
165
- * has to guess — see docs/filedialog.md.
165
+ * has to guess; with none a menu-bar app, which is a tray item and a
166
+ * popover — it answers the root-level `<popup>` that took the keyboard. See
167
+ * docs/filedialog.md.
166
168
  */
167
169
  export declare function useTopLevelWindow(): {
168
170
  readonly current: NtkWindow | DrawnNode | null;
@@ -305,6 +305,14 @@ export interface PaintStyle {
305
305
  outlineColor?: Color;
306
306
  /** The gap between the border box and the ring. Default 1. */
307
307
  outlineOffset?: number;
308
+ /**
309
+ * How opaque the node is **with everything inside it**, from 0 to 1: the
310
+ * subtree is drawn once and composited at this alpha, so a card, its
311
+ * border, its icon and its text fade as one. A paint property — legal in a
312
+ * state block, and it transitions and loops. `0` draws nothing and is
313
+ * still hit; values outside 0..1 are clamped.
314
+ */
315
+ opacity?: number;
308
316
  }
309
317
 
310
318
  /**
@@ -327,6 +335,34 @@ export type TextOverflow = 'clip' | 'ellipsis';
327
335
  export type TextRendering =
328
336
  'auto' | 'optimizeSpeed' | 'optimizeLegibility' | 'geometricPrecision';
329
337
 
338
+ /** One `fontVariantNumeric` keyword, and the OpenType feature it turns on. */
339
+ export type NumericVariant =
340
+ | 'lining-nums' // lnum
341
+ | 'oldstyle-nums' // onum
342
+ | 'proportional-nums' // pnum
343
+ | 'tabular-nums' // tnum
344
+ | 'diagonal-fractions' // frac
345
+ | 'stacked-fractions' // afrc
346
+ | 'ordinal' // ordn
347
+ | 'slashed-zero'; // zero
348
+
349
+ /**
350
+ * CSS's `font-variant-numeric`: `'normal'`, or keywords separated by spaces,
351
+ * at most one of each pair that contradicts — lining or oldstyle,
352
+ * proportional or tabular, diagonal or stacked fractions. Typed as a first
353
+ * keyword and whatever follows; the rest is checked when the style is.
354
+ */
355
+ export type FontVariantNumeric =
356
+ 'normal' | NumericVariant | `${NumericVariant} ${string}`;
357
+
358
+ /**
359
+ * CSS's `font-feature-settings`, by OpenType tag: the tags to turn on,
360
+ * `['tnum', 'ss01']`, or tag → on or off, or the alternate a feature picks,
361
+ * `{ liga: false, salt: 2 }`.
362
+ */
363
+ export type FontFeatureSettings =
364
+ readonly string[] | Readonly<Record<string, boolean | number>>;
365
+
330
366
  /** Text properties. All affect measurement except `color`. */
331
367
  export interface TextStyle {
332
368
  color?: Color;
@@ -346,6 +382,19 @@ export interface TextStyle {
346
382
  * at any size. `'auto'` (default) lets size decide. Changing it repaints
347
383
  * without reflowing: it cannot move anything. */
348
384
  textRendering?: TextRendering;
385
+ /** CSS's `letter-spacing`, in pixels: added after every character, the
386
+ * last on a line included, and negative to tighten. Spaced text drops the
387
+ * optional ligatures (`liga`, `clig`, `dlig`, `hlig`) unless
388
+ * `fontFeatureSettings` names them. Inherits. */
389
+ letterSpacing?: number;
390
+ /** Which figures: `'tabular-nums'` gives every digit one width, so a
391
+ * number that changes holds its width while it does. The friendly names
392
+ * for features `fontFeatureSettings` can also set by tag, and it wins
393
+ * where the two meet. Inherits, apart from `fontFeatureSettings`. */
394
+ fontVariantNumeric?: FontVariantNumeric;
395
+ /** Any OpenType feature, by tag. A feature the face does not have is
396
+ * ignored. Compared by value, so an object literal is fine. Inherits. */
397
+ fontFeatureSettings?: FontFeatureSettings;
349
398
  textAlign?: TextAlign;
350
399
  lineHeight?: number;
351
400
  /** CSS's `text-wrap`. `'nowrap'` measures the text at unbounded width, so
@@ -436,6 +485,14 @@ export interface AnimationSpec {
436
485
  easing?: Easing;
437
486
  /** Turn around at each end instead of wrapping back to `from`. */
438
487
  alternate?: boolean;
488
+ /**
489
+ * When the loop's own time starts, in ms — CSS's `animation-delay`. A
490
+ * positive delay holds `from` that long before the first crossing; a
491
+ * negative one starts the loop that far in. Default `0`. What staggers
492
+ * loops of one duration: three dots at `0`, `-150` and `-300` never move
493
+ * together.
494
+ */
495
+ delay?: number;
439
496
  }
440
497
 
441
498
  /**
@@ -267,6 +267,110 @@ export interface DesktopSettings {
267
267
  */
268
268
  export function useDesktopSettings(): DesktopSettings;
269
269
 
270
+ // --------------------------------------------------------------------------
271
+ // The app's own settings
272
+ // --------------------------------------------------------------------------
273
+
274
+ /** A value a settings store can keep: what JSON can. */
275
+ export type SettingValue =
276
+ | string
277
+ | number
278
+ | boolean
279
+ | null
280
+ | readonly SettingValue[]
281
+ | { readonly [key: string]: SettingValue };
282
+
283
+ export interface SettingsOptions<T extends Record<string, SettingValue>> {
284
+ /**
285
+ * A reverse-DNS name no other app uses, like `'com.example.myapp'`: the
286
+ * name of the app's settings directory. Nothing is registered with it.
287
+ */
288
+ appId: string;
289
+ /** Each setting's value when nothing was saved. */
290
+ defaults?: T;
291
+ /** Where `settings.json` goes instead of the per-user directory for the
292
+ * app — for a portable install, or a test. */
293
+ directory?: string;
294
+ }
295
+
296
+ /** A value's kind: `false` as `boolean`, `'brown'` as `string`, so a
297
+ * default does not narrow what the setting may be set to. */
298
+ export type SettingKind<V> = V extends boolean
299
+ ? boolean
300
+ : V extends string
301
+ ? string
302
+ : V extends number
303
+ ? number
304
+ : V;
305
+
306
+ /** A setting's type: its default's kind, for a key the defaults name, else
307
+ * the fallback's. */
308
+ export type SettingOf<T, K, V> = SettingKind<K extends keyof T ? T[K] : V>;
309
+
310
+ /**
311
+ * What an app remembers between launches, kept in `settings.json` in its
312
+ * per-user directory, written atomically and coalesced.
313
+ */
314
+ export interface Settings<T extends Record<string, SettingValue>> {
315
+ /** The file the values are kept in. */
316
+ readonly path: string;
317
+ /** `[value, setValue]` for one setting, like `useState`; every component
318
+ * using the key sees the same value, and a change is saved. A key with no
319
+ * default takes a fallback. */
320
+ use<K extends string, V extends SettingValue = never>(
321
+ key: K,
322
+ fallback?: V,
323
+ ): [
324
+ SettingOf<T, K, V>,
325
+ (
326
+ value:
327
+ | SettingOf<T, K, V>
328
+ | ((previous: SettingOf<T, K, V>) => SettingOf<T, K, V>),
329
+ ) => void,
330
+ ];
331
+ get<K extends string, V extends SettingValue = never>(
332
+ key: K,
333
+ fallback?: V,
334
+ ): K extends keyof T ? SettingOf<T, K, V> : SettingOf<T, K, V> | undefined;
335
+ /** Change a setting now, and save it a moment later — a quarter second
336
+ * after the last change, and at least once a second while they keep
337
+ * coming. Throws a `TypeError` for a value JSON cannot keep. */
338
+ set<K extends string>(
339
+ key: K,
340
+ value: K extends keyof T ? SettingOf<T, K, never> : SettingValue,
341
+ ): void;
342
+ /** Forget what was saved for a setting: its default again. */
343
+ reset(key: string): void;
344
+ /** Save what is waiting now. Resolves when it is on disk. */
345
+ flush(): Promise<void>;
346
+ /** Hear every change; returns the unsubscribe. */
347
+ subscribe(listener: () => void): () => void;
348
+ }
349
+
350
+ /**
351
+ * The settings store for an app — one per file in the process, so every call
352
+ * with the same `appId` shares values.
353
+ *
354
+ * ```tsx
355
+ * const settings = createSettings({
356
+ * appId: 'com.example.Hush',
357
+ * defaults: { volume: 0.5, dark: false },
358
+ * });
359
+ * function Volume() {
360
+ * const [volume, setVolume] = settings.use('volume');
361
+ * return <Slider value={volume} onChange={setVolume} />;
362
+ * }
363
+ * ```
364
+ *
365
+ * It lives in `~/Library/Application Support/<appId>/settings.json` on macOS
366
+ * and `$XDG_CONFIG_HOME/<appId>/settings.json` elsewhere, and is read the
367
+ * first time a value is asked for. What is still unsaved when the process
368
+ * exits is written then.
369
+ */
370
+ export function createSettings<T extends Record<string, SettingValue>>(
371
+ options: SettingsOptions<T>,
372
+ ): Settings<T>;
373
+
270
374
  // --------------------------------------------------------------------------
271
375
  // Locale
272
376
  // --------------------------------------------------------------------------
@@ -7,8 +7,12 @@ import type { DesktopBackend, TrayFeatures } from './capabilities.js';
7
7
 
8
8
  export interface TrayClickEvent {
9
9
  button: 'left' | 'right' | 'middle';
10
- /** Where the click was, in global top-left screen coordinates the anchor
11
- * for a popup of your own. */
10
+ /** Where the click was: global top-left screen coordinates in **logical
11
+ * pixels**, the unit a `<popup>`'s `x`/`y` and `anchor={{ rect }}` take —
12
+ * the anchor for a popup of your own. The freedesktop protocol names no
13
+ * unit for its point and hosts differ, so there it is read against the
14
+ * monitors and the pointer; docs/desktop.md "The tray" says where that
15
+ * cannot tell. */
12
16
  x: number;
13
17
  y: number;
14
18
  /** The item's rect. `0` on the freedesktop rung, whose protocol has none —
@@ -73,6 +77,14 @@ export interface TrayState {
73
77
  * flips it back.
74
78
  */
75
79
  available: boolean;
80
+ /**
81
+ * Whether `available` is an answer yet: true on the first frame on macOS,
82
+ * where the status item is made there and then, and for `null` options;
83
+ * on the freedesktop tray, once the host has taken or refused the item. An
84
+ * app whose whole UI is its tray renders nothing until this, instead of a
85
+ * fallback window that flashes on every start.
86
+ */
87
+ settled: boolean;
76
88
  /** Which mechanism took it, or null. */
77
89
  backend: DesktopBackend | null;
78
90
  /** What that mechanism can do. Empty until `available` settles. */