react-x11 1.2.0 → 2.0.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 (158) hide show
  1. package/README.md +330 -81
  2. package/package.json +132 -14
  3. package/src/ClickToComponent.js +9 -2
  4. package/src/DevToolsIntegration.js +440 -32
  5. package/src/Reconciler.js +463 -165
  6. package/src/a11y.js +1299 -0
  7. package/src/acceleratorhooks.js +86 -0
  8. package/src/accelerators.js +128 -0
  9. package/src/activate.js +168 -0
  10. package/src/anchor.js +360 -0
  11. package/src/appcontext.js +161 -0
  12. package/src/appearance.js +748 -0
  13. package/src/appearancehooks.js +96 -0
  14. package/src/apphooks.js +73 -0
  15. package/src/application.js +748 -0
  16. package/src/atspi.js +1953 -0
  17. package/src/bus.js +545 -0
  18. package/src/bushooks.js +136 -0
  19. package/src/clientmessage.js +140 -0
  20. package/src/clipboard.js +121 -0
  21. package/src/components/Button.js +148 -33
  22. package/src/components/Checkbox.js +66 -32
  23. package/src/components/Dialog.js +96 -29
  24. package/src/components/FileDialog.js +491 -0
  25. package/src/components/Icon.js +329 -0
  26. package/src/components/Menu.js +797 -133
  27. package/src/components/PasswordInput.js +434 -0
  28. package/src/components/ProgressBar.js +91 -12
  29. package/src/components/Radio.js +78 -35
  30. package/src/components/Select.js +255 -65
  31. package/src/components/Slider.js +102 -41
  32. package/src/components/SplitPane.js +206 -0
  33. package/src/components/Switch.js +100 -22
  34. package/src/components/Table.js +529 -0
  35. package/src/components/Tabs.js +211 -0
  36. package/src/components/Tooltip.js +528 -54
  37. package/src/components/anchor.js +154 -107
  38. package/src/components/change.js +34 -0
  39. package/src/components/dnd.js +103 -0
  40. package/src/components/index.js +16 -3
  41. package/src/components/keys.js +17 -20
  42. package/src/components/scribble.js +247 -0
  43. package/src/components/theme.js +350 -42
  44. package/src/components/typeahead.js +12 -2
  45. package/src/compose.js +868 -0
  46. package/src/compositing.js +224 -0
  47. package/src/dbusmenu.js +384 -0
  48. package/src/debug.d.ts +63 -0
  49. package/src/debug.js +642 -0
  50. package/src/decorations.js +486 -0
  51. package/src/desktopsettings.js +209 -0
  52. package/src/desktopsettingshooks.js +62 -0
  53. package/src/dnd.js +1725 -0
  54. package/src/editmenu.js +272 -0
  55. package/src/errors.js +98 -0
  56. package/src/events.js +1406 -162
  57. package/src/extensions.js +45 -0
  58. package/src/filedialog.js +375 -0
  59. package/src/filedialoghooks.js +132 -0
  60. package/src/fonthooks.js +64 -0
  61. package/src/fonts.js +301 -0
  62. package/src/foreignnodes.js +519 -0
  63. package/src/frame/child.js +33 -0
  64. package/src/frame/childmain.js +274 -0
  65. package/src/frame/env.js +140 -0
  66. package/src/frame/index.js +444 -0
  67. package/src/frame/lifecycle.js +67 -0
  68. package/src/frame/protocol.js +179 -0
  69. package/src/frames.js +73 -0
  70. package/src/glbackend.js +114 -0
  71. package/src/glnodes.js +125 -67
  72. package/src/globalmenu.js +683 -0
  73. package/src/host.d.ts +88 -0
  74. package/src/host.js +42 -0
  75. package/src/idle.js +490 -0
  76. package/src/idlehooks.js +100 -0
  77. package/src/imagesource.js +349 -0
  78. package/src/index.d.ts +475 -0
  79. package/src/index.js +83 -9
  80. package/src/inputtime.js +206 -0
  81. package/src/jsx-dev-runtime.d.ts +21 -0
  82. package/src/jsx-dev-runtime.js +2 -0
  83. package/src/jsx-runtime.d.ts +39 -0
  84. package/src/jsx-runtime.js +6 -0
  85. package/src/keyboard.js +256 -0
  86. package/src/keyboardstate.js +278 -0
  87. package/src/keyboardstatehooks.js +58 -0
  88. package/src/keysyms.d.ts +149 -0
  89. package/src/keysyms.js +270 -0
  90. package/src/locale.js +170 -0
  91. package/src/localehooks.js +47 -0
  92. package/src/menuitem.js +223 -0
  93. package/src/node.d.ts +597 -0
  94. package/src/node.js +44 -0
  95. package/src/nodes.js +9546 -690
  96. package/src/ntk.d.ts +44 -0
  97. package/src/ntk.js +25 -0
  98. package/src/paintcache.js +366 -0
  99. package/src/palette.js +380 -0
  100. package/src/pastestate.js +66 -0
  101. package/src/portal.js +461 -0
  102. package/src/priority.js +26 -0
  103. package/src/refresh/index.d.ts +40 -0
  104. package/src/refresh/index.js +122 -0
  105. package/src/refresh/loader.d.ts +37 -0
  106. package/src/refresh/loader.js +401 -0
  107. package/src/refresh/register.d.ts +5 -0
  108. package/src/refresh/register.js +13 -0
  109. package/src/registry.js +232 -0
  110. package/src/scale.js +626 -0
  111. package/src/scalehooks.js +27 -0
  112. package/src/screencolor.js +640 -0
  113. package/src/screencolorhooks.js +101 -0
  114. package/src/screens.js +754 -0
  115. package/src/screenshooks.js +137 -0
  116. package/src/startup.js +302 -0
  117. package/src/style.d.ts +126 -0
  118. package/src/style.js +33 -0
  119. package/src/styles.js +1482 -6
  120. package/src/svgnodes.js +306 -0
  121. package/src/testing/a11y.js +484 -0
  122. package/src/testing/components.js +414 -0
  123. package/src/testing/events.js +407 -0
  124. package/src/testing/harness.js +455 -0
  125. package/src/testing/index.d.ts +558 -0
  126. package/src/testing/index.js +85 -0
  127. package/src/testing/mock-app.js +463 -0
  128. package/src/testing/pixels.js +152 -0
  129. package/src/testing/queries.js +224 -0
  130. package/src/textrange.js +83 -0
  131. package/src/textselection.js +439 -0
  132. package/src/trace-registry.js +63 -0
  133. package/src/transfer.js +93 -0
  134. package/src/types/appearance.d.ts +84 -0
  135. package/src/types/application.d.ts +173 -0
  136. package/src/types/components.d.ts +820 -0
  137. package/src/types/dbus.d.ts +177 -0
  138. package/src/types/elements.d.ts +897 -0
  139. package/src/types/events.d.ts +549 -0
  140. package/src/types/filedialog.d.ts +260 -0
  141. package/src/types/fonts.d.ts +124 -0
  142. package/src/types/frame.d.ts +146 -0
  143. package/src/types/globalmenu.d.ts +42 -0
  144. package/src/types/nodes.d.ts +199 -0
  145. package/src/types/screencolor.d.ts +84 -0
  146. package/src/types/style.d.ts +434 -0
  147. package/src/types/system.d.ts +287 -0
  148. package/src/windowid.js +151 -0
  149. package/src/windowstate.js +393 -0
  150. package/src/xsettings.js +336 -0
  151. package/src/yoga.d.ts +55 -0
  152. package/src/yoga.js +135 -0
  153. package/src/components/Canvas3D.js +0 -28
  154. package/src/geometry3d.js +0 -223
  155. package/src/pointer3d.js +0 -158
  156. package/src/raycast3d.js +0 -146
  157. package/src/richnodes.js +0 -436
  158. package/src/scene3d.js +0 -683
@@ -0,0 +1,287 @@
1
+ /**
2
+ * What the machine around the app is doing — the monitors, the window
3
+ * manager, the keyboard, whether anyone is at the desk, and the conventions
4
+ * the app was launched under. See docs/system.md.
5
+ */
6
+
7
+ import type { RefObject } from 'react';
8
+
9
+ import type { WindowStateName } from './elements.js';
10
+ import type { DrawnNode, NtkApp, NtkWindow, Rect } from './nodes.js';
11
+
12
+ // --------------------------------------------------------------------------
13
+ // Screens
14
+ // --------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Which tier answered.
18
+ *
19
+ * `'xinerama'` is the geometry-only answer that resolves during
20
+ * `createRoot()`; `'randr'` is the same geometry with names, the primary flag
21
+ * and physical sizes, and lands a moment later. `'screen'` is one entry
22
+ * covering the whole display, for a server with neither extension.
23
+ */
24
+ export type ScreensSource = 'randr' | 'xinerama' | 'screen' | 'test' | null;
25
+
26
+ export interface Screen {
27
+ /** `'HDMI-1'`, `'eDP-1'` — **null** until RandR answers, and on a server
28
+ * without it. Treat null as "not known yet" rather than persisting it. */
29
+ readonly name: string | null;
30
+ /** Every output driving this monitor. Two names means it is mirrored. */
31
+ readonly outputs: readonly string[];
32
+ readonly x: number;
33
+ readonly y: number;
34
+ readonly width: number;
35
+ readonly height: number;
36
+ /**
37
+ * The monitor minus the panels — where a window can actually go.
38
+ *
39
+ * An approximation: `_NET_WORKAREA` is published for the whole virtual
40
+ * desktop rather than per monitor, so it is applied as a per-axis bound.
41
+ * Exact on one head; on several it still takes a top or bottom panel off
42
+ * the height.
43
+ */
44
+ readonly available: Rect;
45
+ readonly primary: boolean;
46
+ readonly widthMM: number | null;
47
+ readonly heightMM: number | null;
48
+ /** Hz to two decimals (`59.99`), or null. */
49
+ readonly refreshRate: number | null;
50
+ readonly rotation: 0 | 90 | 180 | 270;
51
+ /**
52
+ * This monitor's own device-pixels-per-logical-pixel (docs/scale.md).
53
+ * Where the desktop configured one factor it is the root's on every
54
+ * entry; where the hardware answered, a retina lid and an office monitor
55
+ * really do differ, and an app that places windows can honour that.
56
+ * Geometry above is logical, like every rect the renderer hands out.
57
+ */
58
+ readonly scale: number;
59
+ }
60
+
61
+ export interface Screens {
62
+ readonly screens: readonly Screen[];
63
+ /** The primary monitor — or the only one, where nothing is flagged. */
64
+ readonly primary: Screen | null;
65
+ /** The desktop-wide `_NET_WORKAREA`, or null. */
66
+ readonly workArea: Rect | null;
67
+ /** The whole virtual screen every monitor sits in. */
68
+ readonly virtual: Rect | null;
69
+ readonly source: ScreensSource;
70
+ }
71
+
72
+ /**
73
+ * The monitors this display has, live — re-renders when one is plugged in or
74
+ * unplugged, when the arrangement changes, and when a panel moves.
75
+ *
76
+ * ```tsx
77
+ * const { screens, primary } = useScreens();
78
+ * ```
79
+ */
80
+ export function useScreens(): Screens;
81
+
82
+ /**
83
+ * Device pixels per logical pixel for this root — `1` on an ordinary
84
+ * display, `2` on a retina panel, fractional on desktops configured to
85
+ * 1.25/1.5. Resolved once by `createRoot` (see `RootOptions.scale`,
86
+ * docs/scale.md) and static for the life of the root, so there is nothing
87
+ * to subscribe to.
88
+ *
89
+ * Everything the renderer hands an app is already logical; reach for this
90
+ * only to talk about device pixels deliberately — sizing detail in a
91
+ * `<canvas onDraw>` (whose payload carries the same number), or showing
92
+ * the factor in a settings pane.
93
+ */
94
+ export function useScale(): number;
95
+
96
+ // --------------------------------------------------------------------------
97
+ // Window state
98
+ // --------------------------------------------------------------------------
99
+
100
+ export interface WindowState {
101
+ /** This window has the keyboard. */
102
+ readonly focused: boolean;
103
+ /** **The one to branch on**: not minimized, and not fully covered. */
104
+ readonly visible: boolean;
105
+ /**
106
+ * Fully covered by other windows — **always false when a compositing
107
+ * manager is running**, which it is on every stock GNOME and KDE session.
108
+ * A composited window is redirected offscreen and the server considers it
109
+ * entirely visible. Prefer `visible`.
110
+ */
111
+ readonly obscured: boolean;
112
+ /** `_NET_WM_STATE_HIDDEN`: iconified, or shaded away. */
113
+ readonly minimized: boolean;
114
+ /** Both axes. One axis alone shows up in `states`. */
115
+ readonly maximized: boolean;
116
+ /** What the window manager actually did, not what `<window fullscreen>`
117
+ * asked for. */
118
+ readonly fullscreen: boolean;
119
+ /** The raw `_NET_WM_STATE` names, e.g. `['maximized_vert', 'focused']` —
120
+ * what the window manager put there, where `<window states>` is what was
121
+ * asked for. */
122
+ readonly states: readonly WindowStateName[];
123
+ /** The workspace index, or null. A sticky window shows up in `states`. */
124
+ readonly desktop: number | null;
125
+ }
126
+
127
+ /**
128
+ * What the window manager has done with a window, live.
129
+ *
130
+ * ```tsx
131
+ * const { focused, visible, fullscreen } = useWindowState();
132
+ * ```
133
+ *
134
+ * With no argument it reads the window the component is in, inferred the way
135
+ * {@link useTopLevelWindow} infers it. Pass a ref to be exact.
136
+ */
137
+ export function useWindowState(
138
+ ref?: RefObject<NtkWindow | DrawnNode | null> | null,
139
+ ): WindowState;
140
+
141
+ // --------------------------------------------------------------------------
142
+ // Idle and inhibition
143
+ // --------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Whether the user has been away from the keyboard for `timeout`
147
+ * milliseconds.
148
+ *
149
+ * ```tsx
150
+ * const away = useIdle(5 * 60_000);
151
+ * ```
152
+ *
153
+ * Idleness is the whole display's, not this window's — it counts input on
154
+ * every device, whichever application it went to. For "has the user ignored
155
+ * *my* window", read `useWindowState().focused`.
156
+ *
157
+ * Costs no timer where the X server carries an `IDLETIME` counter (Xorg
158
+ * does): a SYNC alarm fires on each crossing. Falls back to polling
159
+ * MIT-SCREEN-SAVER, and stays `false` on a display with neither.
160
+ */
161
+ export function useIdle(timeout: number): boolean;
162
+
163
+ /**
164
+ * Keep the screen awake while `active` is true, releasing on unmount.
165
+ *
166
+ * ```tsx
167
+ * useKeepAwake(playing, 'Playing a video');
168
+ * ```
169
+ *
170
+ * Inhibits **screen blanking only** — nothing here stops a suspend. Silent
171
+ * where no rung is available.
172
+ */
173
+ export function useKeepAwake(active: boolean, reason?: string): void;
174
+
175
+ export interface KeepAwakeOptions {
176
+ /** Shown by desktops that list what is holding the screen on. */
177
+ reason?: string;
178
+ /** The ntk connection, so the `ScreenSaverSuspend` rung can run — the one
179
+ * that needs no session bus. */
180
+ app?: NtkApp;
181
+ }
182
+
183
+ /**
184
+ * The imperative twin of {@link useKeepAwake}: resolves to the release.
185
+ *
186
+ * Never rejects — a machine with no portal, no screensaver service and no
187
+ * MIT-SCREEN-SAVER hands back a release that does nothing.
188
+ */
189
+ export function keepAwake(options?: KeepAwakeOptions): Promise<() => void>;
190
+
191
+ // --------------------------------------------------------------------------
192
+ // Keyboard
193
+ // --------------------------------------------------------------------------
194
+
195
+ export interface KeyboardState {
196
+ /** On or off **now** — not "was on for the last key", which is what an
197
+ * event's modifier state can tell you. */
198
+ readonly capsLock: boolean;
199
+ readonly numLock: boolean;
200
+ /** The active XKB group, 0–3. */
201
+ readonly group: number;
202
+ /** That group's layout code — `'ru'` — or null. */
203
+ readonly layout: string | null;
204
+ /** Every configured layout, `['us', 'ru']`. Empty where XKB could not be
205
+ * asked, which is what distinguishes "off" from "not known". */
206
+ readonly layouts: readonly string[];
207
+ }
208
+
209
+ /**
210
+ * The keyboard's locks and layout, live.
211
+ *
212
+ * ```tsx
213
+ * const { capsLock, layout } = useKeyboardState();
214
+ * ```
215
+ *
216
+ * True of the keyboard rather than of an event, so a password field can warn
217
+ * about Caps Lock before the first character is typed. Needs XKB; the locks
218
+ * read false without it.
219
+ */
220
+ export function useKeyboardState(): KeyboardState;
221
+
222
+ // --------------------------------------------------------------------------
223
+ // Desktop interaction settings
224
+ // --------------------------------------------------------------------------
225
+
226
+ export interface DesktopSettings {
227
+ /** **False means do not blink at all** — an accessibility setting, not a
228
+ * preference. Draw the caret solid rather than not at all. */
229
+ readonly caretBlink: boolean;
230
+ /** How long a caret stays in each state. The XSETTINGS key is a full
231
+ * cycle; this is half of it, which is what goes into a timer. */
232
+ readonly caretBlinkMs: number;
233
+ readonly doubleClickMs: number;
234
+ readonly doubleClickDistance: number;
235
+ /** How far a press moves before it is a drag rather than a click. */
236
+ readonly dragThreshold: number;
237
+ /** `'xsettings'`, or null where no settings daemon answered and these are
238
+ * the renderer's own defaults. */
239
+ readonly source: 'xsettings' | 'test' | null;
240
+ }
241
+
242
+ /**
243
+ * How this desktop wants an application to feel, live.
244
+ *
245
+ * ```tsx
246
+ * const { doubleClickMs, dragThreshold } = useDesktopSettings();
247
+ * ```
248
+ *
249
+ * The built-in controls already follow these. This is for an app drawing its
250
+ * own — a `<canvas>` with a text cursor, a custom gesture — so that one
251
+ * widget on the screen does not feel unlike every other.
252
+ */
253
+ export function useDesktopSettings(): DesktopSettings;
254
+
255
+ // --------------------------------------------------------------------------
256
+ // Locale
257
+ // --------------------------------------------------------------------------
258
+
259
+ export interface SystemLocale {
260
+ /** A BCP-47 tag: `'en-GB'`, `'ru-RU'`. */
261
+ readonly locale: string;
262
+ readonly direction: 'ltr' | 'rtl';
263
+ /** `0` Sunday … `6` Saturday, from CLDR. */
264
+ readonly weekStartsOn: number;
265
+ readonly timeZone: string | null;
266
+ /** `'env'` when `LANG` and friends said, `'intl'` when ICU did. */
267
+ readonly source: 'env' | 'intl' | 'test' | null;
268
+ }
269
+
270
+ /**
271
+ * Which language and conventions this app was started in.
272
+ *
273
+ * ```tsx
274
+ * const { locale, weekStartsOn } = useLocale();
275
+ * ```
276
+ *
277
+ * `LC_ALL`/`LC_MESSAGES`/`LANG` win over `Intl`'s own answer. **This does not
278
+ * change while the app runs** and there is no subscription behind it — a
279
+ * process's environment is fixed at exec.
280
+ *
281
+ * For text direction inside a component prefer `useDirection()`, which also
282
+ * honours `<ThemeProvider direction>` and the `direction` style property.
283
+ */
284
+ export function useLocale(): SystemLocale;
285
+
286
+ /** The imperative twin of {@link useLocale}, for code outside a tree. */
287
+ export function systemLocale(): SystemLocale;
@@ -0,0 +1,151 @@
1
+ // One supported way to turn a ref into the XID of the window it belongs to.
2
+ //
3
+ // The walk itself is not new — `anchor.js` has done it for popup placement
4
+ // since the beginning — but it was private, and two features need it at
5
+ // once: `transientFor` resolves its prop through this, and the
6
+ // xdg-desktop-portal work needs it to build a `parent_window` handle.
7
+
8
+ import { useCallback, useMemo } from 'react';
9
+
10
+ import { useAppOrNull } from './appcontext.js';
11
+
12
+ /**
13
+ * The XID of the X11 window a ref points at, or `null` if there is not one
14
+ * (yet). Accepts:
15
+ *
16
+ * - a `<window>` / `<popup>` ref — `getPublicInstance` hands back the live
17
+ * ntk window, so `ref.current.id` is the XID;
18
+ * - a ref to any **drawn** node — resolved to the window that owns it;
19
+ * - a raw XID, returned unchanged;
20
+ * - the ref object itself, so `windowIdOf(ref)` works as well as
21
+ * `windowIdOf(ref.current)`.
22
+ *
23
+ * `null` is a real answer, not a failure: a ref is empty until the node
24
+ * mounts, and a `<window>` has no XID until the commit phase realizes it.
25
+ */
26
+ export function windowIdOf(target) {
27
+ if (target == null) return null;
28
+ if (typeof target === 'number') return target;
29
+ // a ref object, so callers need not remember which one this takes
30
+ if (typeof target === 'object' && 'current' in target && !target.isWindow) {
31
+ return windowIdOf(target.current);
32
+ }
33
+ // an ntk Window (what a <window>/<popup> ref holds), or a WindowNode that
34
+ // has been realized
35
+ if (typeof target.id === 'number') return target.id;
36
+ if (typeof target.window?.id === 'number') return target.window.id;
37
+ // a drawn node: `root` is the WindowNode that owns it
38
+ if (typeof target.root?.window?.id === 'number') return target.root.window.id;
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * `windowIdOf` bound to a ref: returns a **getter**, stable across renders,
44
+ * the same shape `useAnchor` has. It is a getter rather than the id itself
45
+ * because refs attach after the commit that created the window, so a value
46
+ * read during render would be null on the render that matters.
47
+ *
48
+ * ```js
49
+ * const windowId = useWindowId(anchorRef);
50
+ * // …later, in an effect or a handler:
51
+ * const parentWindow = `x11:${windowId().toString(16)}`; // xdg-desktop-portal
52
+ * ```
53
+ *
54
+ * Note the format: lowercase hex, **no** `0x`. Both shipping portal backends
55
+ * happen to tolerate a prefix, which is exactly why it is easy to get wrong
56
+ * and never notice — and Qt's parser returns 0 on failure with no error
57
+ * path, so a third backend would give an unparented, non-modal dialog rather
58
+ * than an exception.
59
+ */
60
+ export function useWindowId(ref) {
61
+ return useCallback(() => windowIdOf(ref), [ref]);
62
+ }
63
+
64
+ /**
65
+ * The top-level windows this connection is currently rendering, in the order
66
+ * they were added.
67
+ *
68
+ * `createContainer(app, …)` passes the app as the container, so
69
+ * `appendChildToContainer` records every top-level node on the app itself —
70
+ * which makes "what windows does this tree have" answerable without the app
71
+ * author wiring anything. Popups are excluded: a `<popup>` is
72
+ * override-redirect and never what a dialog should be transient for.
73
+ *
74
+ * Not public: `useTopLevelWindow()` is the shape a component wants.
75
+ * `activate.js` needs the list itself, to pick the window a raise with no
76
+ * argument goes to.
77
+ */
78
+ export function topLevelWindows(app) {
79
+ if (!app) return [];
80
+ return (app._rootChildren ?? []).filter(
81
+ (node) => node?.isWindow && !node.isPopup && node.window?.id,
82
+ );
83
+ }
84
+
85
+ let warnedAboutAmbiguity = false;
86
+
87
+ /**
88
+ * The window a component belongs to, resolved when it is read.
89
+ *
90
+ * ```js
91
+ * const owner = useTopLevelWindow();
92
+ * // …later, in a handler, when everything is mounted:
93
+ * const id = windowIdOf(owner);
94
+ * ```
95
+ *
96
+ * **Why this is inference and not a lookup.** A hook has no position in the
97
+ * host tree: React context cannot come from a host element, the host context
98
+ * `getChildHostContext` builds reaches `createInstance` and not components,
99
+ * and a `<window>` has no XID at all until the commit phase. Everything else
100
+ * in this file walks *up from a node* — which is why the alternative is
101
+ * `useRef` on the window and a line of wiring in every app that wants a
102
+ * parented dialog.
103
+ *
104
+ * So it answers from what the renderer does know, at read time, when the tree
105
+ * is mounted and realized:
106
+ *
107
+ * - **one top-level window — the overwhelmingly common case — is exact.** The
108
+ * component is in that window, because there is nowhere else to be.
109
+ * - several, and one of them holds the X input focus: that one. You clicked
110
+ * in it a moment ago, which is why a dialog is opening.
111
+ * - several with nothing to separate them: the most recently opened, and a
112
+ * development warning naming `parentWindow` as the way to be exact. A
113
+ * guess that says it is guessing beats a guess that does not.
114
+ *
115
+ * Returns a **ref-like object** rather than a number: the window is not
116
+ * realized on the first render, so a value read then would be `null` on the
117
+ * render that matters. It drops into anything that already takes a ref —
118
+ * `parentWindow`, `transientFor` — and `windowIdOf()` resolves it.
119
+ */
120
+ export function useTopLevelWindow() {
121
+ const app = useAppOrNull();
122
+ return useMemo(
123
+ () => ({
124
+ get current() {
125
+ const windows = topLevelWindows(app);
126
+ if (windows.length <= 1) return windows[0] ?? null;
127
+
128
+ const focused = windows.filter((w) => w.events?.windowFocused);
129
+ if (focused.length === 1) return focused[0];
130
+
131
+ // Nothing separates them. `windowFocused` also defaults to true on an
132
+ // ntk too old to report focus changes, so "all of them" is the same
133
+ // answer as "none of them" and both land here.
134
+ if (process.env.NODE_ENV !== 'production' && !warnedAboutAmbiguity) {
135
+ warnedAboutAmbiguity = true;
136
+ console.warn(
137
+ `react-x11: this tree has ${windows.length} top-level windows and ` +
138
+ 'none of them is uniquely focused, so the owner window is a ' +
139
+ 'guess (the most recently opened). Pass the window explicitly ' +
140
+ 'to be exact:\n' +
141
+ ' const win = useRef(null);\n' +
142
+ ' const { openFile } = useFileDialog({ parentWindow: win });\n' +
143
+ ' return <window ref={win}>…</window>;',
144
+ );
145
+ }
146
+ return windows[windows.length - 1];
147
+ },
148
+ }),
149
+ [app],
150
+ );
151
+ }