react-x11 2.10.2 → 2.12.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 (100) hide show
  1. package/README.md +278 -129
  2. package/package.json +10 -3
  3. package/src/Reconciler.js +15 -17
  4. package/src/a11y.js +2 -2
  5. package/src/anchor.js +7 -5
  6. package/src/bootstrap.js +14 -0
  7. package/src/clientmessage.js +1 -1
  8. package/src/cocoa/app.js +304 -49
  9. package/src/cocoa/bezels.js +175 -30
  10. package/src/cocoa/dnd.js +27 -13
  11. package/src/cocoa/fonts.js +3 -3
  12. package/src/cocoa/glarea.js +20 -3
  13. package/src/cocoa/main.d.ts +8 -0
  14. package/src/cocoa/main.js +43 -0
  15. package/src/cocoa/panehost.js +15 -5
  16. package/src/cocoa/presenter.js +13 -9
  17. package/src/cocoa/promotion.js +4 -7
  18. package/src/cocoa/relaunch.js +207 -0
  19. package/src/cocoa/screencolor.js +62 -0
  20. package/src/cocoa/threaded.js +246 -0
  21. package/src/cocoa/window.js +256 -42
  22. package/src/components/Select.js +2 -2
  23. package/src/components/anchor.js +3 -3
  24. package/src/components/native.js +12 -7
  25. package/src/components/theme.js +2 -2
  26. package/src/debug.js +1 -1
  27. package/src/decorations.js +1 -1
  28. package/src/editmenu.js +2 -2
  29. package/src/errors.js +46 -0
  30. package/src/events.js +6 -6
  31. package/src/foreignnodes.js +3 -2
  32. package/src/frames.js +2 -2
  33. package/src/glnodes.js +1 -1
  34. package/src/grid.js +1653 -0
  35. package/src/host.d.ts +230 -0
  36. package/src/host.js +11 -3
  37. package/src/imagesource.js +1 -1
  38. package/src/index.d.ts +21 -4
  39. package/src/index.js +9 -1
  40. package/src/layouts.js +721 -0
  41. package/src/node.d.ts +4 -2
  42. package/src/node.js +19 -21
  43. package/src/nodes/animation.js +644 -0
  44. package/src/nodes/box.js +21 -0
  45. package/src/nodes/boxpaint.js +473 -0
  46. package/src/nodes/canvas.js +269 -0
  47. package/src/nodes/cascade.js +600 -0
  48. package/src/nodes/damage.js +183 -0
  49. package/src/nodes/edithistory.js +124 -0
  50. package/src/nodes/editmenupopup.js +260 -0
  51. package/src/nodes/hittest.js +185 -0
  52. package/src/nodes/image.js +266 -0
  53. package/src/nodes/install.js +75 -0
  54. package/src/nodes/invalidate.js +465 -0
  55. package/src/nodes/kinds.js +31 -0
  56. package/src/nodes/layout.js +439 -0
  57. package/src/nodes/layouthost.js +949 -0
  58. package/src/nodes/node.js +868 -0
  59. package/src/nodes/paint.js +466 -0
  60. package/src/nodes/position.js +366 -0
  61. package/src/nodes/preedit.js +127 -0
  62. package/src/nodes/queries.js +330 -0
  63. package/src/nodes/rects.js +102 -0
  64. package/src/nodes/scrollable.js +891 -0
  65. package/src/nodes/scrollbars.js +138 -0
  66. package/src/nodes/scrollblit.js +1034 -0
  67. package/src/nodes/selectable.js +142 -0
  68. package/src/nodes/styling.js +225 -0
  69. package/src/nodes/text.js +649 -0
  70. package/src/nodes/textarea.js +391 -0
  71. package/src/nodes/textinput.js +1146 -0
  72. package/src/nodes/util.js +17 -0
  73. package/src/nodes/window/anchoring.js +161 -0
  74. package/src/nodes/window/capabilities.js +190 -0
  75. package/src/nodes/window/debugpaint.js +83 -0
  76. package/src/nodes/window/droptarget.js +145 -0
  77. package/src/nodes/window/floors.js +577 -0
  78. package/src/nodes/window/flush.js +334 -0
  79. package/src/nodes/window/hints.js +482 -0
  80. package/src/nodes/window/listeners.js +222 -0
  81. package/src/nodes/window/popup.js +71 -0
  82. package/src/nodes/window/size.js +591 -0
  83. package/src/nodes/window/window.js +945 -0
  84. package/src/palette.js +1 -1
  85. package/src/registry.js +7 -3
  86. package/src/screencolor.js +212 -38
  87. package/src/screencolorhooks.js +6 -2
  88. package/src/styles.js +137 -15
  89. package/src/svgnodes.js +2 -1
  90. package/src/testing/harness.js +2 -2
  91. package/src/textselection.js +5 -3
  92. package/src/trace-registry.js +1 -1
  93. package/src/types/components.d.ts +38 -6
  94. package/src/types/elements.d.ts +11 -1
  95. package/src/types/nodes.d.ts +33 -5
  96. package/src/types/screencolor.d.ts +20 -14
  97. package/src/types/style.d.ts +94 -3
  98. package/src/windowstate.js +1 -1
  99. package/src/yoga.js +1 -1
  100. package/src/nodes.js +0 -13120
package/src/palette.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Separate from `components/theme.js` because the palette is not a React
4
4
  // concern. There are two routes into the tree — `useTheme()` through React
5
5
  // context, and a `$token` resolved against the nearest `theme` **prop** by
6
- // walking the node tree — and the second one lives in `nodes.js`, below the
6
+ // walking the node tree — and the second one lives in `nodes/cascade.js`, below the
7
7
  // widget layer. Both have to agree on what "no theme was given" means, so the
8
8
  // answer belongs underneath both of them.
9
9
  //
package/src/registry.js CHANGED
@@ -28,8 +28,9 @@ import {
28
28
  DRAWN_KINDS,
29
29
  CUSTOM_SEMANTIC_NAMES,
30
30
  CUSTOM_SELF_DAMAGED,
31
- Node,
32
- } from './nodes.js';
31
+ } from './nodes/kinds.js';
32
+ import { Node } from './nodes/node.js';
33
+ import { markLayoutsHotReloadSession } from './layouts.js';
33
34
 
34
35
  /** kind -> definition. Insertion-ordered, which is the order errors list. */
35
36
  const registry = new Map();
@@ -54,6 +55,9 @@ let hotReloadSession = false;
54
55
  /** @internal — called by react-x11/refresh; not part of the public API. */
55
56
  export function markHotReloadSession(active = true) {
56
57
  hotReloadSession = active;
58
+ // the layout and placement registries follow the same rule, for the same
59
+ // module-scope reason
60
+ markLayoutsHotReloadSession(active);
57
61
  }
58
62
 
59
63
  function assertNode(node, type) {
@@ -225,7 +229,7 @@ export function createRegisteredNode(type, props, app, hostContext) {
225
229
  const definition = registry.get(type);
226
230
  if (!definition) return undefined;
227
231
  const node = assertNode(definition.create(props, app, hostContext), type);
228
- // read by Node.insertBefore — carried on the instance so nodes.js needs
232
+ // read by Node.insertBefore — carried on the instance so src/nodes/ needs
229
233
  // no import from here
230
234
  if (!definition.childrenAllowed) node._childrenAllowed = false;
231
235
  return node;
@@ -1,24 +1,44 @@
1
1
  // Sample one pixel from the screen — the eyedropper, through whatever this
2
2
  // machine actually has.
3
3
  //
4
- // The file dialog's ladder again (docs/filedialog.md), two rungs this time:
4
+ // The file dialog's ladder again (docs/filedialog.md), three rungs:
5
5
  //
6
- // 1. **the portal** — `org.freedesktop.portal.Screenshot.PickColor`. The
6
+ // 1. **the system sampler** — `NSColorSampler` on the cocoa backend
7
+ // (src/cocoa/screencolor.js). macOS draws the loupe out of process, so
8
+ // the app needs no Screen Recording grant of its own and the user gets
9
+ // the magnifier every other Mac colour picker shows them. Found by the
10
+ // app carrying `colorSampler`, never by naming a backend here.
11
+ // 2. **the portal** — `org.freedesktop.portal.Screenshot.PickColor`. The
7
12
  // desktop draws its own magnifier and hands back the colour, which is
8
13
  // also the only route that works under a compositor that would refuse a
9
14
  // root read, and the only route Wayland has at all. Needs version 2 of
10
15
  // the Screenshot interface — XFCE ships none, GNOME and KDE ship 2 —
11
16
  // so the gate is the interface's `version` property, not `hasService()`.
12
- // 2. **X11** — grab the pointer with a crosshair, wait for the click,
17
+ // 3. **X11** — grab the pointer with a crosshair, wait for the click,
13
18
  // `GetImage` a 1×1 at it, decode by the server's own pixel layout.
14
19
  // Reached under a bare WM, over ssh, on XQuartz: everywhere there is a
15
20
  // display and nothing else, which is the case react-x11 exists for.
16
21
  //
17
- // There is no third rung to draw, because the thing being read the whole
22
+ // Rungs 1 and 2 are the same shape — ask the system, it draws the picker,
23
+ // it hands back an sRGB triple — which is why the sampler goes on top of the
24
+ // portal rather than under the crosshair: where the OS will do this for us,
25
+ // it does it better, and `hexFromPortalColor()` converts for both.
26
+ //
27
+ // There is no rung to *draw*, because the thing being read — the whole
18
28
  // screen — is precisely what an application cannot draw itself. So unlike
19
29
  // `useFileDialog()`, `useEyedropper()` adds no rung; it adds the binding a
20
- // component wants (`picking`, `supported`, the owner window) over the same
21
- // two.
30
+ // component wants (`picking`, `supported`, the owner window) over these
31
+ // three.
32
+ //
33
+ // Which means the ladder really can run out, and where it does the floor has
34
+ // to be the typed rejection rather than a crash. A cocoa app on a bridge
35
+ // older than 0.9 is that place: its `app.X` is a stub with just enough on it
36
+ // for the modules that keep an X escape hatch to no-op (src/cocoa/app.js),
37
+ // so an app object is not by itself a connection that can grab a pointer and
38
+ // read a root window. Rung 3 is gated on the requests it is built out of,
39
+ // not on there being an app — the feature-detection rule `requireExtension()`
40
+ // already follows, and the rule rung 1 follows too, one bridge verb instead
41
+ // of three X requests.
22
42
  //
23
43
  // ## The grab is the dangerous part
24
44
  //
@@ -42,6 +62,7 @@ import {
42
62
  portalRequest,
43
63
  portalVersion,
44
64
  } from './portal.js';
65
+ import { liveApps } from './trace-registry.js';
45
66
  import { windowIdOf } from './windowid.js';
46
67
 
47
68
  export const SCREENSHOT_IFACE = 'org.freedesktop.portal.Screenshot';
@@ -60,10 +81,11 @@ export class NoScreenColorError extends Error {
60
81
  super(
61
82
  `react-x11: ${
62
83
  message ??
63
- 'no way to sample a colour from the screen — there is no ' +
64
- 'Screenshot portal with PickColor (interface version 2) on the ' +
65
- 'session bus, and no X connection was given for the fallback. ' +
66
- 'Pass `app` (from createRoot() or useApp()), or use useEyedropper().'
84
+ 'no way to sample a colour from the screen — no cocoa app with a ' +
85
+ 'system sampler, no Screenshot portal with PickColor (interface ' +
86
+ 'version 2) on the session bus, and no X connection was given for ' +
87
+ 'the fallback. Pass `app` (from createRoot() or useApp()), or use ' +
88
+ 'useEyedropper().'
67
89
  }`,
68
90
  { cause },
69
91
  );
@@ -72,7 +94,91 @@ export class NoScreenColorError extends Error {
72
94
  }
73
95
 
74
96
  // --------------------------------------------------------------------------
75
- // Rung 1: the portal
97
+ // Rung 1: the system sampler, on the cocoa backend
98
+ // --------------------------------------------------------------------------
99
+
100
+ /**
101
+ * The app whose system sampler a pick should use, or null.
102
+ *
103
+ * Never a backend check: an app that can sample the screen says so by
104
+ * carrying `colorSampler` (src/cocoa/screencolor.js), and this asks the app
105
+ * the caller named — `app`, or a `parentWindow` that points at a mounted
106
+ * node — before asking the connections the renderer is drawing through, the
107
+ * rule `filePanels` and `calendars` follow. Several of those with only one
108
+ * showing a window is the next case (a borrowed connection stays registered
109
+ * after its root unmounts); genuinely several is a real null, since the
110
+ * sampler belongs to one process's NSApplication.
111
+ */
112
+ function samplerApp(opts) {
113
+ const named = appFor(opts);
114
+ if (named) return named.colorSampler ? named : null;
115
+ const apps = liveApps().filter((one) => one.colorSampler);
116
+ if (apps.length <= 1) return apps[0] ?? null;
117
+ const showing = apps.filter((one) => (one._rootChildren ?? []).length > 0);
118
+ return showing.length === 1 ? showing[0] : null;
119
+ }
120
+
121
+ /**
122
+ * Show `NSColorSampler` and wait for it.
123
+ *
124
+ * Resolves `'#rrggbb'` on a pick and `null` on a dismissal — the two
125
+ * outcomes every rung answers with — through the *portal's* conversion,
126
+ * because the bridge answers in the portal's units: sRGB in 0–1, gamut
127
+ * mapped from whatever space the display is in. Rejects on an abort, and on
128
+ * a colour with no sRGB form at all (a pattern colour), which is the
129
+ * bridge's one error.
130
+ *
131
+ * **An abort ends our wait, not the sampler.** AppKit has no verb to
132
+ * dismiss it — the session ends when the user picks or presses Escape — so
133
+ * where the portal rung Closes its request and the X11 rung releases its
134
+ * grab, this one can only stop listening: the loupe stays up, the colour
135
+ * that arrives afterwards is dropped, and until then the pending sample
136
+ * holds the event loop open the way pending I/O does. Nothing is left
137
+ * grabbed, which is what the abort exists to guarantee.
138
+ */
139
+ function cocoaPick(opts, app) {
140
+ const signal = opts.signal;
141
+ if (signal?.aborted) {
142
+ return Promise.reject(signal.reason ?? new PortalCancelledError());
143
+ }
144
+ return new Promise((resolve, reject) => {
145
+ const onAbort = () => reject(signal.reason ?? new PortalCancelledError());
146
+ signal?.addEventListener('abort', onAbort, { once: true });
147
+ // No `settle()` gate, unlike the X11 rung: that one exists because a
148
+ // grab must be released exactly once on every path out, and an aborted
149
+ // pick here holds nothing — the loupe is the user's, not ours. The
150
+ // colour that lands after an abort reaches an already-rejected promise
151
+ // and is dropped by the promise itself.
152
+ const done = () => signal?.removeEventListener('abort', onAbort);
153
+
154
+ app.colorSampler.sample().then(
155
+ (color) => {
156
+ done();
157
+ // A dismissal is an ordinary outcome, not a throw — Escape on the
158
+ // X11 rung, the dialog's own cancel on the portal, this here.
159
+ if (color == null) return resolve(null);
160
+ const hex = hexFromPortalColor([color.r, color.g, color.b]);
161
+ if (!hex) {
162
+ return reject(
163
+ new Error(
164
+ 'react-x11: the system colour sampler answered without a ' +
165
+ 'colour — expected sRGB { r, g, b } in 0–1, got ' +
166
+ `${JSON.stringify(color)}.`,
167
+ ),
168
+ );
169
+ }
170
+ resolve(hex);
171
+ },
172
+ (err) => {
173
+ done();
174
+ reject(err);
175
+ },
176
+ );
177
+ });
178
+ }
179
+
180
+ // --------------------------------------------------------------------------
181
+ // Rung 2: the portal
76
182
  // --------------------------------------------------------------------------
77
183
 
78
184
  /**
@@ -131,7 +237,7 @@ async function portalCanPick(ref) {
131
237
  }
132
238
 
133
239
  // --------------------------------------------------------------------------
134
- // Rung 2: X11
240
+ // Rung 3: X11
135
241
  // --------------------------------------------------------------------------
136
242
 
137
243
  // x11.eventMask bits, spelled out the way xsettings.js spells its one. No
@@ -539,28 +645,87 @@ function appOf(target) {
539
645
  return target.app ?? target.window?.app ?? target.root?.window?.app ?? null;
540
646
  }
541
647
 
542
- /** The connection a pick would use, or null. */
648
+ /** The app a pick would use, or null. Not yet: a *connection*. */
543
649
  function appFor(opts) {
544
650
  return opts.app ?? appOf(opts.parentWindow);
545
651
  }
546
652
 
547
653
  /**
548
- * Which rung this machine lands on, without grabbing anything.
654
+ * Can this app run the X11 rung is its `X` a connection at all?
549
655
  *
550
- * `'portal'` needs the Screenshot interface at version 2 the probe reads
551
- * the interface's `version` property, because `hasService()` cannot see
552
- * which interfaces a portal's backends actually provide (XFCE's provides no
553
- * Screenshot at all). `'x11'` needs a connection to answer with, so pass
554
- * `app` (or a `parentWindow` that resolves to one); without either the
555
- * fallback is unreachable and the honest answer is `null`.
656
+ * The cocoa backend hands the renderer an app whose `X` is a stub
657
+ * (src/cocoa/app.js): `InternAtom` and `on` so the modules with an X escape
658
+ * hatch no-op cleanly, and nothing else. Reaching the rung through it used
659
+ * to throw `X.AllocID is not a function` out of the promise — a crash where
660
+ * the ladder's whole contract is a typed "not here", and one an app cannot
661
+ * hide a button on.
662
+ *
663
+ * So the gate is the three requests the rung is actually built out of, asked
664
+ * of the object rather than of `process.platform` or the backend's name:
665
+ * `requireExtension()`'s rule, and the one that keeps the next backend from
666
+ * landing here by default too.
667
+ */
668
+ function canGrabOn(app) {
669
+ const X = app?.X;
670
+ return (
671
+ typeof X?.AllocID === 'function' &&
672
+ typeof X?.GrabPointer === 'function' &&
673
+ typeof X?.GetImage === 'function'
674
+ );
675
+ }
676
+
677
+ /**
678
+ * Why the X11 rung is out of reach, for the typed rejection — an app that
679
+ * cannot grab and no app at all are different mistakes with different
680
+ * fixes, and only one of them is the caller's.
681
+ */
682
+ function noX11Reason(app, backend) {
683
+ if (app) {
684
+ return (
685
+ 'this tree does not render through an X connection — a cocoa-backend ' +
686
+ 'app, for instance, whose `X` cannot grab the pointer or read a root ' +
687
+ "window. macOS's own sampler is the rung above, and this app does not " +
688
+ 'carry it: `@windowkit/appkit` is older than 0.9, which has no ' +
689
+ '`sampleScreenColor`. Until it is updated there is no rung here — ' +
690
+ '`useEyedropper().supported` is false, which is the signal to leave ' +
691
+ 'the eyedropper button undrawn (docs/macos.md).'
692
+ );
693
+ }
694
+ if (backend === 'x11') {
695
+ return (
696
+ "backend: 'x11' needs a connection to grab on. Pass `app` (from " +
697
+ 'createRoot() or useApp()), or a `parentWindow` that points at a ' +
698
+ 'mounted window.'
699
+ );
700
+ }
701
+ return undefined;
702
+ }
703
+
704
+ /**
705
+ * Which rung this machine lands on, without showing or grabbing anything.
706
+ *
707
+ * `'cocoa'` is the app carrying `colorSampler` — a cocoa-backend tree on
708
+ * `@windowkit/appkit` >= 0.9 — and it is asked first, so a Mac never falls
709
+ * through to a rung that would draw a worse picker. `'portal'` needs the
710
+ * Screenshot interface at version 2 — the probe reads the interface's
711
+ * `version` property, because `hasService()` cannot see which interfaces a
712
+ * portal's backends actually provide (XFCE's provides no Screenshot at
713
+ * all). `'x11'` needs a connection to answer with, so pass `app` (or a
714
+ * `parentWindow` that resolves to one) — and one that can actually grab,
715
+ * which a cocoa-backend app cannot. With none of the three the honest
716
+ * answer is `null`.
556
717
  *
557
718
  * Acquires a bus reference and releases it, so it is cheap but not free —
558
719
  * `useEyedropper().supported` caches it for you.
559
720
  *
560
- * @returns {Promise<'portal'|'x11'|null>}
721
+ * @returns {Promise<'cocoa'|'portal'|'x11'|null>}
561
722
  */
562
723
  export async function screenColorBackend(options = {}) {
563
724
  const backend = options.backend;
725
+ if (!backend || backend === 'cocoa') {
726
+ if (samplerApp(options)) return 'cocoa';
727
+ if (backend === 'cocoa') return null;
728
+ }
564
729
  if (!backend || backend === 'portal') {
565
730
  const ref = await sessionBus();
566
731
  if (ref) {
@@ -572,10 +737,23 @@ export async function screenColorBackend(options = {}) {
572
737
  }
573
738
  if (backend === 'portal') return null;
574
739
  }
575
- return appFor(options) ? 'x11' : null;
740
+ return canGrabOn(appFor(options)) ? 'x11' : null;
576
741
  }
577
742
 
578
743
  async function runPick(opts) {
744
+ const wantCocoa = !opts.backend || opts.backend === 'cocoa';
745
+ if (wantCocoa) {
746
+ const app = samplerApp(opts);
747
+ if (app) return await cocoaPick(opts, app);
748
+ if (opts.backend === 'cocoa') {
749
+ throw new NoScreenColorError(
750
+ "backend: 'cocoa' — no system colour sampler here: this tree does " +
751
+ 'not render through the cocoa backend, or its `@windowkit/appkit` ' +
752
+ 'is older than 0.9.',
753
+ );
754
+ }
755
+ }
756
+
579
757
  const wantPortal = !opts.backend || opts.backend === 'portal';
580
758
  if (wantPortal) {
581
759
  const ref = await sessionBus();
@@ -598,31 +776,27 @@ async function runPick(opts) {
598
776
  }
599
777
 
600
778
  const app = appFor(opts);
601
- if (app) return x11Pick(opts, app);
602
- throw new NoScreenColorError(
603
- opts.backend === 'x11'
604
- ? "backend: 'x11' needs a connection to grab on. Pass `app` (from " +
605
- 'createRoot() or useApp()), or a `parentWindow` that points at a ' +
606
- 'mounted window.'
607
- : undefined,
608
- );
779
+ if (canGrabOn(app)) return x11Pick(opts, app);
780
+ throw new NoScreenColorError(noX11Reason(app, opts.backend));
609
781
  }
610
782
 
611
783
  /**
612
- * Sample one pixel from the screen: the desktop's own picker where there is
613
- * one, a crosshair grab on plain X11 everywhere else.
784
+ * Sample one pixel from the screen: the system's own picker where there is
785
+ * one — `NSColorSampler` on macOS, the Screenshot portal on a desktop that
786
+ * has it — and a crosshair grab on plain X11 everywhere else.
614
787
  *
615
788
  * ```js
616
789
  * const hex = await pickScreenColor({ app });
617
790
  * if (hex) setFill(hex); // '#rrggbb'; null means cancelled
618
791
  * ```
619
792
  *
620
- * Resolves to **`'#rrggbb'`**, or `null` when the user cancelled (Escape, or
621
- * the portal dialog's own cancel) — cancelling is an ordinary outcome and
622
- * should not need a `try`. Rejects with {@link NoScreenColorError} when
623
- * neither rung is reachable, which is the signal to hide the button;
624
- * `signal` aborts the pick and releases the grab before the rejection is
625
- * reported.
793
+ * Resolves to **`'#rrggbb'`**, or `null` when the user cancelled (Escape,
794
+ * the portal dialog's own cancel, dismissing the sampler) — cancelling is an
795
+ * ordinary outcome and should not need a `try`. Rejects with
796
+ * {@link NoScreenColorError} when no rung is reachable, which is the signal
797
+ * to hide the button; `signal` aborts the pick, releasing the X11 grab or
798
+ * closing the portal request before the rejection is reported (the system
799
+ * sampler cannot be dismissed from code — see docs/eyedropper.md).
626
800
  *
627
801
  * In a component, reach for {@link useEyedropper} instead — it binds the
628
802
  * connection and the owner window, and exposes `picking`/`supported` as
@@ -33,12 +33,16 @@ import { useTopLevelWindow } from './windowid.js';
33
33
  *
34
34
  * `pick()` resolves to `'#rrggbb'`, or `null` when the user cancelled. It
35
35
  * never rejects for lack of a backend on an X11 tree — the connection this
36
- * tree renders through *is* the fallback rung — so `supported` is about the
37
- * forced-backend and future-platform cases, not a check most apps must make.
36
+ * tree renders through *is* the fallback rung — nor on a cocoa tree whose
37
+ * bridge has the system sampler (`@windowkit/appkit` >= 0.9). Where neither
38
+ * is true the flag is the answer: `supported` is false, and a picker that
39
+ * gates its eyedropper button on it simply does not draw one.
38
40
  *
39
41
  * The portal dialog is parented to the window this component is in, the
40
42
  * `useFileDialog()` way: resolved at the moment the pick starts, with
41
43
  * `parentWindow` as the override for a tree with several top-level windows.
44
+ * On the cocoa rung that window is what names the app whose sampler runs;
45
+ * the system draws the loupe over the whole screen, owned by no window.
42
46
  *
43
47
  * While a pick is in flight, `picking` is true and another `pick()` returns
44
48
  * **the same promise** — a double-clicked button must not queue a second