react-x11 2.16.0 → 2.17.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 (62) hide show
  1. package/README.md +38 -23
  2. package/package.json +3 -1
  3. package/src/Reconciler.js +82 -23
  4. package/src/a11y.js +18 -1
  5. package/src/acceleratorhooks.js +40 -6
  6. package/src/anchor.js +20 -2
  7. package/src/appcontext.js +8 -0
  8. package/src/appearance.js +36 -0
  9. package/src/{cocoa → backend}/context2d.js +27 -7
  10. package/src/capabilities.js +99 -1
  11. package/src/cocoa/app.js +204 -6
  12. package/src/cocoa/fonts.js +1 -1
  13. package/src/cocoa/overlay.js +2 -2
  14. package/src/cocoa/panewindow.js +2 -2
  15. package/src/cocoa/presenter.js +2 -2
  16. package/src/cocoa/surface.js +3 -3
  17. package/src/cocoa/window.js +2 -2
  18. package/src/events.js +21 -0
  19. package/src/foreignnodes.js +8 -3
  20. package/src/frame/index.js +30 -4
  21. package/src/glnodes.js +12 -1
  22. package/src/idle.js +59 -1
  23. package/src/index.d.ts +51 -1
  24. package/src/index.js +30 -3
  25. package/src/keysymchars.js +47 -0
  26. package/src/keysyms.d.ts +19 -1
  27. package/src/keysyms.js +107 -8
  28. package/src/launcher.js +17 -8
  29. package/src/launcherhooks.js +24 -10
  30. package/src/node.d.ts +1 -1
  31. package/src/nodes/cascade.js +9 -0
  32. package/src/nodes/node.js +6 -1
  33. package/src/nodes/window/hints.js +21 -2
  34. package/src/nodes/window/window.js +2 -2
  35. package/src/notifications.js +39 -14
  36. package/src/screens.js +159 -24
  37. package/src/taskbarhooks.js +164 -0
  38. package/src/transfer.js +20 -1
  39. package/src/trayhooks.js +1 -1
  40. package/src/types/capabilities.d.ts +32 -3
  41. package/src/types/elements.d.ts +23 -1
  42. package/src/types/events.d.ts +21 -0
  43. package/src/types/filedialog.d.ts +3 -1
  44. package/src/types/launcher.d.ts +20 -6
  45. package/src/types/taskbar.d.ts +79 -0
  46. package/src/wayland/context2d.js +1 -1
  47. package/src/wayland/xkb.js +170 -59
  48. package/src/win32/a11y.js +604 -0
  49. package/src/win32/app.js +768 -0
  50. package/src/win32/bezels.js +158 -0
  51. package/src/win32/dnd.js +283 -0
  52. package/src/win32/fonts.js +497 -0
  53. package/src/win32/glarea.js +548 -0
  54. package/src/win32/ime.js +267 -0
  55. package/src/win32/keymap.js +116 -0
  56. package/src/win32/native.js +54 -0
  57. package/src/win32/panehost.js +106 -0
  58. package/src/win32/panewindow.js +343 -0
  59. package/src/win32/shell.js +426 -0
  60. package/src/win32/surface.js +192 -0
  61. package/src/win32/window.js +659 -0
  62. package/src/windowid.js +128 -20
package/src/windowid.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import { useCallback, useMemo } from 'react';
9
9
 
10
10
  import { useAppOrNull } from './appcontext.js';
11
+ import { canEmbed } from './embedding.js';
11
12
 
12
13
  /**
13
14
  * The XID of the X11 window a ref points at, or `null` if there is not one
@@ -62,6 +63,71 @@ export function windowOf(target) {
62
63
  return null;
63
64
  }
64
65
 
66
+ /**
67
+ * The handle **another process** embeds to show this window, or `null` if
68
+ * this backend cannot hand one out.
69
+ *
70
+ * `<window embeddable>` is how a window says "created, not shown; somebody
71
+ * else will place me" — the guest half of embedding, on every backend. This
72
+ * is the number that goes with it, and it is deliberately not `windowIdOf`:
73
+ * on X11 the two are the same, and everywhere else they are not.
74
+ *
75
+ * - **X11** — the window's XID. The same number in every process on the
76
+ * display, which is why one function answered both questions there and
77
+ * why nobody noticed they were two questions.
78
+ * - **Windows** — a **composition surface handle**, already valid in the
79
+ * host process. A window cannot be embedded here at all: a composition
80
+ * target stops presenting the moment its window becomes a child, measured
81
+ * in docs/windows-embedding.md. So what crosses is the buffer, and the
82
+ * host binds it to a visual of its own instead of reparenting. The host is
83
+ * the parent process by default — `createRoot({ win32: { paneHostPid } })`
84
+ * when it is not.
85
+ * - **Anything else** — `null`. Not an error: it is the capability, and the
86
+ * honest answer for a backend with no way to be a guest.
87
+ *
88
+ * The handle is a number to be passed out of band, exactly as an XID is —
89
+ * argv, an environment variable, a message on a pipe. What the host does
90
+ * with it is the host's business and differs per platform; what an app
91
+ * writes to *get* it does not.
92
+ *
93
+ * ```jsx
94
+ * const guest = useRef(null);
95
+ * useEffect(() => {
96
+ * const handle = windowHandleOf(guest);
97
+ * if (handle !== null) process.send?.({ type: 'embed-me', handle });
98
+ * }, []);
99
+ * return <window ref={guest} embeddable />;
100
+ * ```
101
+ */
102
+ export function windowHandleOf(target) {
103
+ const wnd = windowOf(target);
104
+ if (!wnd) return null;
105
+ // A backend that publishes something other than its window says so with a
106
+ // method, because only it knows what that something is.
107
+ if (typeof wnd.embedHandle === 'function') return wnd.embedHandle();
108
+ // …and where windows themselves are embeddable, the window id is it. The
109
+ // same question `<foreign>` asks before it builds a socket, so the two
110
+ // halves of embedding cannot disagree about whether this one works.
111
+ if (canEmbed(wnd.app ?? null)) return windowIdOf(wnd);
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * `windowHandleOf` bound to a ref: a **getter**, stable across renders — the
117
+ * shape `useWindowId` has, and for the same reason. A window is not realized
118
+ * on the render that declares it, so a value read then would be null on the
119
+ * render that matters.
120
+ *
121
+ * ```js
122
+ * const handle = useWindowHandle(guestRef);
123
+ * // …later, in an effect, once the tree is mounted:
124
+ * const forTheHost = handle();
125
+ * ```
126
+ */
127
+ export function useWindowHandle(ref) {
128
+ return useCallback(() => windowHandleOf(ref), [ref]);
129
+ }
130
+
65
131
  /**
66
132
  * `windowIdOf` bound to a ref: returns a **getter**, stable across renders,
67
133
  * the same shape `useAnchor` has. It is a getter rather than the id itself
@@ -105,8 +171,54 @@ export function topLevelWindows(app) {
105
171
  );
106
172
  }
107
173
 
174
+ /**
175
+ * The root-level `<popup>`s this connection is currently rendering — the
176
+ * half `topLevelWindows()` leaves out, in the order they were added.
177
+ *
178
+ * Only interesting when there are no top-level windows at all: a menu-bar
179
+ * app whose whole UI is a popover a tray click opens has no `<window>` for
180
+ * anything to belong to, and the popup *is* the top of that tree
181
+ * (`useTopLevelWindow`). With a window in the tree these stay out of it, for
182
+ * the reason they are out of `topLevelWindows()` — override-redirect is
183
+ * never what a dialog should be transient for.
184
+ */
185
+ function rootLevelPopups(app) {
186
+ if (!app) return [];
187
+ return (app._rootChildren ?? []).filter(
188
+ (node) => node?.isWindow && node.isPopup && node.window?.id,
189
+ );
190
+ }
191
+
108
192
  let warnedAboutAmbiguity = false;
109
193
 
194
+ /**
195
+ * One of several candidates, on the inference `useTopLevelWindow` documents:
196
+ * one is exact, uniquely focused wins, and anything else is the most
197
+ * recently opened plus a development warning that says so.
198
+ */
199
+ function pickOwner(candidates, what) {
200
+ if (candidates.length <= 1) return candidates[0] ?? null;
201
+
202
+ const focused = candidates.filter((w) => w.events?.windowFocused);
203
+ if (focused.length === 1) return focused[0];
204
+
205
+ // Nothing separates them. `windowFocused` also defaults to true on an
206
+ // ntk too old to report focus changes, so "all of them" is the same
207
+ // answer as "none of them" and both land here.
208
+ if (process.env.NODE_ENV !== 'production' && !warnedAboutAmbiguity) {
209
+ warnedAboutAmbiguity = true;
210
+ console.warn(
211
+ `react-x11: this tree has ${candidates.length} ${what}, none of them ` +
212
+ 'uniquely focused, so the owner window is a guess (the most ' +
213
+ 'recently opened). Pass the window explicitly to be exact:\n' +
214
+ ' const win = useRef(null);\n' +
215
+ ' const { openFile } = useFileDialog({ parentWindow: win });\n' +
216
+ ' return <window ref={win}>…</window>;',
217
+ );
218
+ }
219
+ return candidates[candidates.length - 1];
220
+ }
221
+
110
222
  /**
111
223
  * The window a component belongs to, resolved when it is read.
112
224
  *
@@ -134,6 +246,13 @@ let warnedAboutAmbiguity = false;
134
246
  * - several with nothing to separate them: the most recently opened, and a
135
247
  * development warning naming `parentWindow` as the way to be exact. A
136
248
  * guess that says it is guessing beats a guess that does not.
249
+ * - **no top-level window at all: the root-level `<popup>` that has the
250
+ * keyboard.** A menu-bar app is a tray item and a popover, and nothing
251
+ * else — there is no `<window>` for a shortcut, a file dialog or the
252
+ * global menu to belong to, and answering `null` made every one of them
253
+ * quietly do nothing (issue #616). A `grabKeyboard` popup is where the
254
+ * keys are by construction, so it is preferred over one that is merely
255
+ * up; among equals the same focus/most-recent inference applies.
137
256
  *
138
257
  * Returns a **ref-like object** rather than a number: the window is not
139
258
  * realized on the first render, so a value read then would be `null` on the
@@ -146,27 +265,16 @@ export function useTopLevelWindow() {
146
265
  () => ({
147
266
  get current() {
148
267
  const windows = topLevelWindows(app);
149
- if (windows.length <= 1) return windows[0] ?? null;
150
-
151
- const focused = windows.filter((w) => w.events?.windowFocused);
152
- if (focused.length === 1) return focused[0];
268
+ if (windows.length) return pickOwner(windows, 'top-level windows');
153
269
 
154
- // Nothing separates them. `windowFocused` also defaults to true on an
155
- // ntk too old to report focus changes, so "all of them" is the same
156
- // answer as "none of them" and both land here.
157
- if (process.env.NODE_ENV !== 'production' && !warnedAboutAmbiguity) {
158
- warnedAboutAmbiguity = true;
159
- console.warn(
160
- `react-x11: this tree has ${windows.length} top-level windows and ` +
161
- 'none of them is uniquely focused, so the owner window is a ' +
162
- 'guess (the most recently opened). Pass the window explicitly ' +
163
- 'to be exact:\n' +
164
- ' const win = useRef(null);\n' +
165
- ' const { openFile } = useFileDialog({ parentWindow: win });\n' +
166
- ' return <window ref={win}>…</window>;',
167
- );
168
- }
169
- return windows[windows.length - 1];
270
+ // A popup-only tree the tray popover. Not reached while the app
271
+ // has a window, so nothing that already worked changes shape.
272
+ const popups = rootLevelPopups(app);
273
+ const keyboard = popups.filter((node) => node.props?.grabKeyboard);
274
+ return pickOwner(
275
+ keyboard.length ? keyboard : popups,
276
+ 'root-level popups and no window at all',
277
+ );
170
278
  },
171
279
  }),
172
280
  [app],