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/glnodes.js CHANGED
@@ -58,9 +58,20 @@ export function glxConfig(app, spec) {
58
58
  .chooseGLXConfig(spec)
59
59
  .then((config) => ({ backend: 'indirect', ...config }));
60
60
  } else {
61
+ // Which backend is asking decides what the honest answer is. Telling a
62
+ // Windows user to upgrade ntk sends them after a package that backend
63
+ // does not use, and an error a developer cannot act on is worse than
64
+ // the feature simply being absent (AGENTS.md, "An error you hit is an
65
+ // error an app developer will hit").
61
66
  promise = Promise.reject(
62
67
  new Error(
63
- 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLConfig)',
68
+ process.platform === 'win32'
69
+ ? 'react-x11: <glarea> is not built on the win32 backend yet — it ' +
70
+ 'needs ANGLE (EGL and GLES over Direct3D 11), which ' +
71
+ 'docs/windows.md plans as an optional dependency the way ' +
72
+ 'x11-dri is on X11. Everything else on this backend works ' +
73
+ 'without it; for GL content today, use the X11 backend.'
74
+ : 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLConfig)',
64
75
  ),
65
76
  );
66
77
  }
package/src/idle.js CHANGED
@@ -186,6 +186,17 @@ class IdleWatcher {
186
186
  */
187
187
  async function armIdle(watcher) {
188
188
  const session = watcher.session;
189
+
190
+ // The Windows rung, first because it is the only one there: everything
191
+ // below this reaches into `app.X`, and a tree that is not on an X server
192
+ // has none. `GetLastInputInfo` answers for the whole session, which is what
193
+ // an idle timeout means, so the shape is the polling rung's — with the
194
+ // answer arriving synchronously instead of over a connection.
195
+ if (typeof session.app?.lastInputMs === 'function') {
196
+ pollLastInput(watcher);
197
+ return;
198
+ }
199
+
189
200
  const counter = await session.counter();
190
201
  if (watcher.stopped) return;
191
202
 
@@ -281,6 +292,31 @@ function poll(watcher, saver) {
281
292
  });
282
293
  }
283
294
 
295
+ /**
296
+ * The same adaptive wait as {@link poll}, over a counter this process can read
297
+ * without asking anybody: `GetLastInputInfo` is a call, not a round trip.
298
+ *
299
+ * The two directions are still asymmetric for the same reason. Not idle yet:
300
+ * sleep for exactly the remainder. Idle: nothing says when the user will come
301
+ * back, and no input reaches this process while they are typing in another
302
+ * window, so poll on an interval scaled to the timeout.
303
+ */
304
+ function pollLastInput(watcher) {
305
+ if (watcher.stopped) return;
306
+ const elapsed = watcher.session.app.lastInputMs?.();
307
+ if (typeof elapsed !== 'number') return;
308
+ const idle = elapsed >= watcher.timeout;
309
+ watcher.set(idle);
310
+ clearTimeout(watcher._timer);
311
+ watcher._timer = setTimeout(
312
+ () => pollLastInput(watcher),
313
+ idle
314
+ ? Math.min(30_000, Math.max(1_000, watcher.timeout / 4))
315
+ : Math.max(250, watcher.timeout - elapsed),
316
+ );
317
+ watcher._timer.unref?.();
318
+ }
319
+
284
320
  function schedule(watcher, saver, delay) {
285
321
  clearTimeout(watcher._timer);
286
322
  watcher._timer = setTimeout(() => poll(watcher, saver), delay);
@@ -364,7 +400,12 @@ export function setIdleForTests(app, timeout, idle) {
364
400
  * code and for tests.
365
401
  */
366
402
  export async function keepAwake({ reason = 'Busy', app = null } = {}) {
367
- for (const rung of [portalInhibit, screenSaverInhibit, xInhibit]) {
403
+ for (const rung of [
404
+ windowsInhibit,
405
+ portalInhibit,
406
+ screenSaverInhibit,
407
+ xInhibit,
408
+ ]) {
368
409
  try {
369
410
  const release = await rung(reason, app);
370
411
  if (release) return once(release);
@@ -375,6 +416,23 @@ export async function keepAwake({ reason = 'Busy', app = null } = {}) {
375
416
  return () => {};
376
417
  }
377
418
 
419
+ /**
420
+ * Rung 0: `SetThreadExecutionState`, on Windows.
421
+ *
422
+ * Above the portal rungs because it is the only one a Windows session has,
423
+ * and below nothing: on a desktop with a portal this returns null on the
424
+ * first line and costs a property read.
425
+ *
426
+ * `reason` is dropped rather than passed. Windows takes no string with the
427
+ * call — `powercfg /requests` names the process, not a reason — and inventing
428
+ * somewhere to put it would be pretending the system shows it.
429
+ */
430
+ async function windowsInhibit(reason, app) {
431
+ const hold = app?.keepAwake;
432
+ if (typeof hold !== 'function') return null;
433
+ return hold.call(app, true);
434
+ }
435
+
378
436
  /** A release that runs once however many times it is called — a double
379
437
  * release would drop somebody else's inhibition on the counted X rung. */
380
438
  function once(fn) {
package/src/index.d.ts CHANGED
@@ -27,6 +27,7 @@ export * from './types/fonts.js';
27
27
  export * from './types/system.js';
28
28
  export * from './types/capabilities.js';
29
29
  export * from './types/launcher.js';
30
+ export * from './types/taskbar.js';
30
31
  export * from './types/tray.js';
31
32
  export * from './types/permissions.js';
32
33
  export * from './types/notifications.js';
@@ -62,6 +63,46 @@ export function useWindowId(
62
63
  ref: RefObject<NtkWindow | DrawnNode | null>,
63
64
  ): () => number | null;
64
65
 
66
+ /**
67
+ * The handle **another process** embeds to show this window, or `null` where
68
+ * this backend cannot hand one out.
69
+ *
70
+ * The companion to `<window embeddable>`, and deliberately not
71
+ * {@link windowIdOf}: on X11 the two are the same number, and everywhere
72
+ * else they are not.
73
+ *
74
+ * - **X11** — the window's XID, which means the same thing in every process
75
+ * on the display.
76
+ * - **Windows** — a composition surface handle, already valid in the host
77
+ * process. A window cannot be embedded here (a composition target stops
78
+ * presenting once its window is a child), so the *buffer* crosses instead
79
+ * and the host binds it to a visual of its own. The host is the parent
80
+ * process unless `createRoot({ win32: { paneHostPid } })` says otherwise.
81
+ * - **Anything else** — `null`, which is the capability rather than a
82
+ * failure.
83
+ *
84
+ * Pass it to the host out of band, as an XID is passed: argv, an environment
85
+ * variable, a message. What the host does with it differs per platform; what
86
+ * an app writes to get it does not.
87
+ */
88
+ export function windowHandleOf(
89
+ target:
90
+ | NtkWindow
91
+ | DrawnNode
92
+ | RefObject<NtkWindow | DrawnNode | null>
93
+ | null
94
+ | undefined,
95
+ ): number | null;
96
+
97
+ /**
98
+ * `windowHandleOf` bound to a ref. A **getter**, stable across renders, for
99
+ * {@link useWindowId}'s reason: the window is not realized on the render
100
+ * that declares it.
101
+ */
102
+ export function useWindowHandle(
103
+ ref: RefObject<NtkWindow | DrawnNode | null>,
104
+ ): () => number | null;
105
+
65
106
  /**
66
107
  * Parse a `text/uri-list` payload (RFC 2483): CRLF-separated,
67
108
  * percent-encoded, `#` lines are comments. What `DropEvent.files` is made
@@ -233,7 +274,15 @@ export interface RootOptions {
233
274
  * `resizeWait` is how long, in ms, AppKit may hold a live-resize tick for
234
275
  * the app's frame at the new size under `react-x11/cocoa-main`, where
235
276
  * the frame is painted on another thread (50 by default; 0 lets the edge
236
- * move without waiting). `appName` is what the Dock, ⌘-Tab and the app menu print for
277
+ * move without waiting).
278
+ * `screenPoll` is how often the screen layout is re-read while a
279
+ * {@link useScreens} subscriber is mounted, in ms — 500 by default, 0 for
280
+ * never. macOS has no event for a display plugged in, unplugged or
281
+ * rearranged that reaches a client, so a component watching the layout is
282
+ * kept current by asking; an app that never calls `useScreens` never
283
+ * polls, and the paths where a stale layout would misplace a window ask
284
+ * for themselves whatever this says.
285
+ * `appName` is what the Dock, ⌘-Tab and the app menu print for
237
286
  * an unbundled process (a bundle's Info.plist wins); `activationPolicy`
238
287
  * is `'regular'` (a Dock tile, a ⌘-Tab entry — the default),
239
288
  * `'accessory'` (a menu-bar app: windows but no tile) or `'prohibited'`,
@@ -253,6 +302,7 @@ export interface RootOptions {
253
302
  frameInterval?: number;
254
303
  pumpInterval?: number;
255
304
  resizeWait?: number;
305
+ screenPoll?: number;
256
306
  appName?: string;
257
307
  activationPolicy?: 'regular' | 'accessory' | 'prohibited';
258
308
  exitOnQuit?: boolean;
package/src/index.js CHANGED
@@ -4,7 +4,13 @@ import './bootstrap.js';
4
4
 
5
5
  export { createRoot, Renderer } from './Reconciler.js';
6
6
  export { createStyles, flattenStyle } from './styles.js';
7
- export { windowIdOf, useWindowId, useTopLevelWindow } from './windowid.js';
7
+ export {
8
+ windowIdOf,
9
+ useWindowId,
10
+ windowHandleOf,
11
+ useWindowHandle,
12
+ useTopLevelWindow,
13
+ } from './windowid.js';
8
14
  export { launchTimestamp, notifyStartupComplete } from './startup.js';
9
15
  export { activateWindow } from './activate.js';
10
16
  export { lastInputTime, serverTime } from './inputtime.js';
@@ -14,9 +20,30 @@ export {
14
20
  registerApplication,
15
21
  } from './application.js';
16
22
  export { useAppActivate, useAppOpen } from './apphooks.js';
17
- export { setBadge, setProgress, setQuicklist, setUrgent } from './launcher.js';
18
- export { useBadge, useDockMenu, useProgress } from './launcherhooks.js';
23
+ export {
24
+ setBadge,
25
+ setLauncherMenu,
26
+ setProgress,
27
+ setQuicklist,
28
+ setUrgent,
29
+ } from './launcher.js';
30
+ export {
31
+ useBadge,
32
+ useDockMenu,
33
+ useLauncherMenu,
34
+ useProgress,
35
+ } from './launcherhooks.js';
19
36
  export { useTray } from './trayhooks.js';
37
+ // The Windows taskbar's own surfaces. Exported unconditionally and inert
38
+ // where the backend has none — `useDesktopCapability('launcher').features`
39
+ // carries `tasks`, `thumbnailToolbar` and `recentDocuments`, so an app
40
+ // branches on what this desktop has rather than on the platform.
41
+ export {
42
+ noteRecentDocument,
43
+ useJumpList,
44
+ useRecentDocument,
45
+ useThumbnailToolbar,
46
+ } from './taskbarhooks.js';
20
47
  export {
21
48
  CAPABILITIES,
22
49
  NO_CAPABILITY,
@@ -0,0 +1,47 @@
1
+ // Generated by scripts/keysym-chars.mjs from X11's keysymdef.h — do not edit.
2
+ //
3
+ // The character each legacy keysym produces, run-length encoded: a run is
4
+ // `<keysym>:<codePoint>` in hex, or `<keysym>+<n>:<codePoint>` where the
5
+ // next n keysyms carry the next n code points. `charOf` (src/keysyms.js)
6
+ // expands it once, on first use. One line per keysymdef.h block, because that
7
+ // is how the header is organised and how a regeneration reads as a diff.
8
+ //
9
+ // Latin-1 and the Unicode keysym form are rules rather than entries and are
10
+ // not in here; neither is any keysym whose character is a control character,
11
+ // which is a key that types nothing.
12
+ export const KEYSYM_CHAR_RUNS = [
13
+ // Latin-2 (0x01xx), 57 keysyms
14
+ '1a1:104 1a2:2d8 1a3:141 1a5:13d 1a6:15a 1a9:160 1aa:15e 1ab:164 1ac:179 1ae:17d 1af:17b 1b1:105 1b2:2db 1b3:142 1b5:13e 1b6:15b 1b7:2c7 1b9:161 1ba:15f 1bb:165 1bc:17a 1bd:2dd 1be:17e 1bf:17c 1c0:154 1c3:102 1c5:139 1c6:106 1c8:10c 1ca:118 1cc:11a 1cf:10e 1d0:110 1d1:143 1d2:147 1d5:150 1d8:158 1d9:16e 1db:170 1de:162 1e0:155 1e3:103 1e5:13a 1e6:107 1e8:10d 1ea:119 1ec:11b 1ef:10f 1f0:111 1f1:144 1f2:148 1f5:151 1f8:159 1f9:16f 1fb:171 1fe:163 1ff:2d9',
15
+ // Latin-3 (0x02xx), 22 keysyms
16
+ '2a1:126 2a6:124 2a9:130 2ab:11e 2ac:134 2b1:127 2b6:125 2b9:131 2bb:11f 2bc:135 2c5:10a 2c6:108 2d5:120 2d8:11c 2dd:16c 2de:15c 2e5:10b 2e6:109 2f5:121 2f8:11d 2fd:16d 2fe:15d',
17
+ // Latin-4 (0x03xx), 35 keysyms
18
+ '3a2:138 3a3:156 3a5:128 3a6:13b 3aa:112 3ab:122 3ac:166 3b3:157 3b5:129 3b6:13c 3ba:113 3bb:123 3bc:167 3bd:14a 3bf:14b 3c0:100 3c7:12e 3cc:116 3cf:12a 3d1:145 3d2:14c 3d3:136 3d9:172 3dd:168 3de:16a 3e0:101 3e7:12f 3ec:117 3ef:12b 3f1:146 3f2:14d 3f3:137 3f9:173 3fd:169 3fe:16b',
19
+ // Katakana (0x04xx), 64 keysyms
20
+ '47e:203e 4a1:3002 4a2+1:300c 4a4:3001 4a5:30fb 4a6:30f2 4a7:30a1 4a8:30a3 4a9:30a5 4aa:30a7 4ab:30a9 4ac:30e3 4ad:30e5 4ae:30e7 4af:30c3 4b0:30fc 4b1:30a2 4b2:30a4 4b3:30a6 4b4:30a8 4b5+1:30aa 4b7:30ad 4b8:30af 4b9:30b1 4ba:30b3 4bb:30b5 4bc:30b7 4bd:30b9 4be:30bb 4bf:30bd 4c0:30bf 4c1:30c1 4c2:30c4 4c3:30c6 4c4:30c8 4c5+5:30ca 4cb:30d2 4cc:30d5 4cd:30d8 4ce:30db 4cf+4:30de 4d4:30e4 4d5:30e6 4d6+5:30e8 4dc:30ef 4dd:30f3 4de+1:309b',
21
+ // Arabic (0x05xx), 48 keysyms
22
+ '5ac:60c 5bb:61b 5bf:61f 5c1+25:621 5e0+18:640',
23
+ // Cyrillic (0x06xx), 95 keysyms
24
+ '6a1+1:452 6a3:451 6a4+8:454 6ad:491 6ae+1:45e 6b0:2116 6b1+1:402 6b3:401 6b4+8:404 6bd:490 6be+1:40e 6c0:44e 6c1+1:430 6c3:446 6c4+1:434 6c6:444 6c7:433 6c8:445 6c9+7:438 6d1:44f 6d2+3:440 6d6:436 6d7:432 6d8:44c 6d9:44b 6da:437 6db:448 6dc:44d 6dd:449 6de:447 6df:44a 6e0:42e 6e1+1:410 6e3:426 6e4+1:414 6e6:424 6e7:413 6e8:425 6e9+7:418 6f1:42f 6f2+3:420 6f6:416 6f7:412 6f8:42c 6f9:42b 6fa:417 6fb:428 6fc:42d 6fd:429 6fe:427 6ff:42a',
25
+ // Greek (0x07xx), 71 keysyms
26
+ '7a1:386 7a2+2:388 7a5:3aa 7a7:38c 7a8:38e 7a9:3ab 7ab:38f 7ae:385 7af:2015 7b1+3:3ac 7b5:3ca 7b6:390 7b7+1:3cc 7b9:3cb 7ba:3b0 7bb:3ce 7c1+16:391 7d2:3a3 7d4+5:3a4 7e1+16:3b1 7f2:3c3 7f3:3c2 7f4+5:3c4',
27
+ // Technical (0x08xx), 42 keysyms
28
+ '8a1:23b7 8a2:250c 8a3:2500 8a4+1:2320 8a6:2502 8a7:23a1 8a8+1:23a3 8aa:23a6 8ab:239b 8ac+1:239d 8ae:23a0 8af:23a8 8b0:23ac 8bc:2264 8bd:2260 8be:2265 8bf:222b 8c0:2234 8c1+1:221d 8c5:2207 8c8:223c 8c9:2243 8cd:21d4 8ce:21d2 8cf:2261 8d6:221a 8da+1:2282 8dc+1:2229 8de+1:2227 8ef:2202 8f6:192 8fb+3:2190',
29
+ // Special (0x09xx), 23 keysyms
30
+ '9e0:25c6 9e1:2592 9e2:2409 9e3+1:240c 9e5:240a 9e8:2424 9e9:240b 9ea:2518 9eb:2510 9ec:250c 9ed:2514 9ee:253c 9ef+1:23ba 9f1:2500 9f2+1:23bc 9f4:251c 9f5:2524 9f6:2534 9f7:252c 9f8:2502',
31
+ // Publishing (0x0axx), 80 keysyms
32
+ 'aa1:2003 aa2:2002 aa3+1:2004 aa5+3:2007 aa9:2014 aaa:2013 aac:2423 aae:2026 aaf:2025 ab0+7:2153 ab8:2105 abb:2012 abc:27e8 abd:2e abe:27e9 ac3+3:215b ac9:2122 aca:2613 acc:25c1 acd:25b7 ace:25cb acf:25af ad0+1:2018 ad2+1:201c ad4:211e ad5:2030 ad6+1:2032 ad9:271d adb:25ac adc:25c0 add:25b6 ade:25cf adf:25ae ae0:25e6 ae1:25ab ae2:25ad ae3:25b3 ae4:25bd ae5:2606 ae6:2022 ae7:25aa ae8:25b2 ae9:25bc aea:261c aeb:261e aec:2663 aed:2666 aee:2665 af0:2720 af1+1:2020 af3:2713 af4:2717 af5:266f af6:266d af7:2642 af8:2640 af9:260e afa:2315 afb:2117 afc:2038 afd:201a afe:201e',
33
+ // APL (0x0bxx), 19 keysyms
34
+ 'ba3:3c ba6:3e ba8:2228 ba9:2227 bc0:af bc2:22a4 bc3:2229 bc4:230a bc6:5f bca:2218 bcc:2395 bce:22a5 bcf:25cb bd3:2308 bd6:222a bd8:2283 bda:2282 bdc:22a3 bfc:22a2',
35
+ // Hebrew (0x0cxx), 28 keysyms
36
+ 'cdf:2017 ce0+26:5d0',
37
+ // Thai (0x0dxx), 84 keysyms
38
+ 'da1+57:e01 dde+15:e3e df0+9:e50',
39
+ // Korean (0x0exx), 91 keysyms
40
+ 'ea1+50:3131 ed4+26:11a8 eef:316d ef0:3171 ef1:3178 ef2:317f ef3:3181 ef4:3184 ef5:3186 ef6+1:318d ef8:11eb ef9:11f0 efa:11f9 eff:20a9',
41
+ // Latin-8 and Latin-9 (0x13xx), 3 keysyms
42
+ '13bc+1:152 13be:178',
43
+ // Currency (0x20xx), 1 keysyms
44
+ '20ac:20ac',
45
+ // the keypad (0xffxx), 18 keysyms
46
+ 'ff80:20 ffaa+15:2a ffbd:3d',
47
+ ];
package/src/keysyms.d.ts CHANGED
@@ -9,9 +9,27 @@
9
9
  */
10
10
  export function keysymOf(char: string): number;
11
11
 
12
- /** The character a keysym produces, or `''` for a non-printing key. */
12
+ /**
13
+ * The character a keysym produces, or `''` for a key that types nothing — a
14
+ * modifier, a function key, an arrow, or a dead key waiting for the letter it
15
+ * decorates. Latin-1 and the Unicode form are rules; the legacy blocks a real
16
+ * keymap is written in — Cyrillic, Greek, Latin-2/3/4, Arabic, Hebrew, Thai,
17
+ * the keypad, `EuroSign` — come out of a generated table.
18
+ */
13
19
  export function charOf(keysym: number): string;
14
20
 
21
+ /**
22
+ * The uppercase of a keysym, answered in the spelling the keysym was written
23
+ * in: `й` (`0x6ca`) uppercases to `Й` (`0x6ea`) and not to the Unicode-form
24
+ * spelling of the same letter. A keysym with no case comes back unchanged.
25
+ * This is how Caps Lock capitalises.
26
+ *
27
+ * `'ß'.toUpperCase()` is `'SS'` — two characters, where a key has one to give
28
+ * — so the first code point is what comes back: `S`. Same for `fi` and the
29
+ * polytonic Greek letters whose uppercase is a sequence.
30
+ */
31
+ export function keysymToUpper(keysym: number): number;
32
+
15
33
  /**
16
34
  * The letter of a Ctrl chord, independent of Shift — the keysym for its
17
35
  * lowercase form, so `keysymOf('z')` matches both Ctrl+Z and Ctrl+Shift+Z.
package/src/keysyms.js CHANGED
@@ -5,13 +5,28 @@
5
5
  // `ev.keysym`. The full X11 set is several thousand names; this is the part
6
6
  // a GUI actually handles, plus the rule for everything else.
7
7
  //
8
- // Two facts make the long tail unnecessary:
8
+ // Two rules cover most of it:
9
9
  //
10
10
  // - **Latin-1 is identity.** For U+0020 to U+00FF the keysym *is* the code
11
11
  // point, so `'a'` is `0x61` and `'é'` is `0xe9`. That is the whole ASCII
12
12
  // and Latin-1 range, no table needed.
13
- // - **Everything else is `0x01000000 + codePoint`.** That is the Unicode
14
- // keysym rule, and `keysymOf` below applies both.
13
+ // - **The Unicode form is `0x01000000 + codePoint`.** That is what `keysymOf`
14
+ // below produces for everything outside Latin-1.
15
+ //
16
+ // What the two rules do *not* cover is the **legacy keysym blocks**, and a
17
+ // real keymap is written in them: Cyrillic is `0x6xx`, Greek `0x7xx`,
18
+ // Latin-2/3/4 `0x1xx`–`0x3xx`, Hebrew `0x8xx`, Arabic `0x5xx`, `EuroSign` is
19
+ // `0x20ac` and the keypad digits are `0xffbx`. Those are a table, and it is
20
+ // `src/keysymchars.js` — generated from X11's `keysymdef.h`, which is where
21
+ // libxkbcommon's own table comes from (`scripts/keysym-chars.mjs`).
22
+ //
23
+ // It is only `charOf` that needs it. On X11 ntk supplies the code point and
24
+ // this function is never reached; the Wayland backend decodes the keymap
25
+ // itself (`src/wayland/xkb.js`) and is the first caller that needs `charOf`
26
+ // to be complete, which is why a Russian, Greek or Czech layout typed
27
+ // nothing at all and AltGr+E produced no Euro sign.
28
+
29
+ import { KEYSYM_CHAR_RUNS } from './keysymchars.js';
15
30
 
16
31
  /** The keysym for a single character, by the two rules above. */
17
32
  export function keysymOf(char) {
@@ -21,13 +36,97 @@ export function keysymOf(char) {
21
36
  return 0x01000000 + code;
22
37
  }
23
38
 
24
- /** The character a keysym produces, or `''` for a non-printing key. */
39
+ /**
40
+ * The legacy blocks, expanded once on first use — keysym -> code point, and
41
+ * the way back. The reverse direction is what keeps a case map inside the
42
+ * block it started in: `keysymToUpper(Cyrillic_shorti)` is `Cyrillic_SHORTI`
43
+ * and not the Unicode-form spelling of the same letter, which is the answer
44
+ * a keymap's own keysyms can be compared against.
45
+ */
46
+ let legacyChars;
47
+ let legacyKeysyms;
48
+ function expandLegacy() {
49
+ if (legacyChars) return;
50
+ legacyChars = new Map();
51
+ legacyKeysyms = new Map();
52
+ for (const line of KEYSYM_CHAR_RUNS)
53
+ for (const run of line.split(' ')) {
54
+ const [keysyms, cp] = run.split(':');
55
+ const [first, span] = keysyms.split('+');
56
+ const from = parseInt(first, 16);
57
+ const to = parseInt(cp, 16);
58
+ for (let i = 0; i <= (span ? +span : 0); i++) {
59
+ legacyChars.set(from + i, to + i);
60
+ // The eleven code points two blocks both spell — box drawing, a few
61
+ // set operators, `.` — keep the first, so the answer is stable.
62
+ if (!legacyKeysyms.has(to + i)) legacyKeysyms.set(to + i, from + i);
63
+ }
64
+ }
65
+ }
66
+ function legacyChar(keysym) {
67
+ expandLegacy();
68
+ return legacyChars.get(keysym);
69
+ }
70
+
71
+ /**
72
+ * The character a keysym produces, or `''` for a key that types nothing —
73
+ * a modifier, a function key, an arrow, or a dead key waiting for the letter
74
+ * it decorates.
75
+ *
76
+ * "Types nothing" includes the keys whose code point is a control character:
77
+ * `keysymdef.h` gives BackSpace U+0008 and Delete U+007F, and a text field
78
+ * that inserted those would be inserting a control byte rather than deleting
79
+ * anything. The same goes for the unassigned 0x7f–0x9f stretch of the
80
+ * Latin-1 range, which is not a keysym at all.
81
+ */
25
82
  export function charOf(keysym) {
26
- if (keysym >= 0x20 && keysym <= 0xff) return String.fromCodePoint(keysym);
27
- if (keysym >= 0x01000100 && keysym <= 0x0110ffff) {
28
- return String.fromCodePoint(keysym - 0x01000000);
83
+ if (keysym >= 0x20 && keysym <= 0x7e) return String.fromCodePoint(keysym);
84
+ if (keysym >= 0xa0 && keysym <= 0xff) return String.fromCodePoint(keysym);
85
+ if (keysym >= 0x01000000 && keysym <= 0x0110ffff) {
86
+ const cp = keysym - 0x01000000;
87
+ return cp >= 0x20 && cp !== 0x7f ? String.fromCodePoint(cp) : '';
88
+ }
89
+ const cp = legacyChar(keysym);
90
+ return cp === undefined ? '' : String.fromCodePoint(cp);
91
+ }
92
+
93
+ /**
94
+ * The uppercase of a keysym, staying in the block it came from — `й`
95
+ * (`0x6ca`) uppercases to `Й` (`0x6ea`) and not to the Unicode-form spelling
96
+ * of the same letter. A keysym with no case comes back unchanged.
97
+ *
98
+ * This is how Caps Lock capitalises. It is not a lookup of an uppercase
99
+ * sibling level on the same key: French AZERTY's `é` key is `[é, 2, ~, ˘]`,
100
+ * where level 2 is a digit, and German's AltGr `ſ` has no sibling at all —
101
+ * there is no key anywhere with `ſ` and `S` next to each other.
102
+ *
103
+ * `'ß'.toUpperCase()` is **`'SS'`**, two characters, and a keyboard has one
104
+ * key's worth of character to answer with, so the first code point is what
105
+ * comes back: `S`. libxkbcommon's narrower table says `ẞ` (U+1E9E) there, and
106
+ * the same goes for `ΐ` and `ΰ`, which it leaves alone. Those three are the
107
+ * whole of the disagreement, and `S` is the more useful of the two answers
108
+ * for a key that is about to insert a character.
109
+ */
110
+ export function keysymToUpper(keysym) {
111
+ const ch = charOf(keysym);
112
+ if (!ch) return keysym;
113
+ const upper = ch.toUpperCase();
114
+ if (upper === ch) return keysym;
115
+ const cp = upper.codePointAt(0);
116
+ if (cp === ch.codePointAt(0)) return keysym;
117
+ expandLegacy();
118
+ // Answer in the spelling the keysym was written in. A keymap that uses the
119
+ // legacy blocks gets a legacy keysym back — `й` is `Й` (`0x6ea`), not the
120
+ // Unicode-form spelling of the same letter — and so does Latin-1, where
121
+ // AltGr's `µ` uppercases to `Greek_MU`. A keysym already written in the
122
+ // Unicode form keeps it, because that is the block *it* chose. The two
123
+ // spell the same character either way; what differs is whether the answer
124
+ // can be compared against the keysyms the keymap itself carries.
125
+ if (keysym < 0x01000000) {
126
+ const legacy = legacyKeysyms.get(cp);
127
+ if (legacy !== undefined) return legacy;
29
128
  }
30
- return '';
129
+ return keysymOf(String.fromCodePoint(cp));
31
130
  }
32
131
 
33
132
  // --- editing and navigation ------------------------------------------------
package/src/launcher.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // desktops, two mechanisms, one call each:
7
7
  //
8
8
  // 1. **the app's own tile** — the cocoa backend's `NSDockTile`, reached
9
- // through the app object (`setDockBadge`, `setDockMenu`, src/cocoa/
9
+ // through the app object (`setDockBadge`, `setLauncherMenu`, src/cocoa/
10
10
  // app.js).
11
11
  // 2. **`com.canonical.Unity.LauncherEntry`** over the session bus — the
12
12
  // protocol Unity defined and the KDE, elementary, Cairo-Dock and
@@ -34,13 +34,13 @@
34
34
  // is runtime, it follows state, and Dash-to-Dock/ubuntu-dock, Plank and the
35
35
  // Unity heritage launchers all render it.
36
36
  //
37
- // So `useDockMenu()` has a Linux rung after all, and the three menus an app
37
+ // So `useLauncherMenu()` has a Linux rung after all, and the three menus an app
38
38
  // puts on the desktop — panel, tray, launcher — are now one authoring model
39
39
  // on both backends. `.desktop` actions are still the right place for entries
40
40
  // that must work *while the app is not running*; they are a different feature
41
41
  // wearing a similar hat.
42
42
  //
43
- // Nothing here imports react: `useBadge`/`useDockMenu` (launcherhooks.js) are
43
+ // Nothing here imports react: `useBadge`/`useLauncherMenu` (launcherhooks.js) are
44
44
  // the hooks, and these are the functions under them, callable from host-side
45
45
  // code with no tree. The entry on the bus is held for as long as anything is
46
46
  // shown and released when the last of it is cleared — a held bus ref is a
@@ -300,7 +300,8 @@ export async function setUrgent(urgent, { app } = {}) {
300
300
  }
301
301
 
302
302
  /**
303
- * The menu behind a right-click on the app's launcher icon — the quicklist.
303
+ * The menu behind a right-click on the app's launcher icon — the Dock menu on
304
+ * macOS, the quicklist on Linux.
304
305
  *
305
306
  * Takes `MenuBar`'s item vocabulary and exports it as a `com.canonical.dbusmenu`
306
307
  * tree, exactly as the tray and the global menu do. `null` takes it down.
@@ -310,12 +311,12 @@ export async function setUrgent(urgent, { app } = {}) {
310
311
  * launcher that builds its client the instant it sees `quicklist` finds an
311
312
  * object there.
312
313
  */
313
- export async function setQuicklist(items, { app } = {}) {
314
+ export async function setLauncherMenu(items, { app } = {}) {
314
315
  const target = app ?? soleApp();
315
316
 
316
- // Rung 1: the app's own tile menu.
317
- if (typeof target?.setDockMenu === 'function') {
318
- target.setDockMenu(items ?? null);
317
+ // Rung 1: the app's own icon menu.
318
+ if (typeof target?.setLauncherMenu === 'function') {
319
+ target.setLauncherMenu(items ?? null);
319
320
  return true;
320
321
  }
321
322
 
@@ -367,6 +368,14 @@ export async function setQuicklist(items, { app } = {}) {
367
368
  return true;
368
369
  }
369
370
 
371
+ /**
372
+ * @deprecated Renamed to {@link setLauncherMenu}. "Quicklist" is the Unity
373
+ * launcher's word for the menu macOS calls the Dock menu; this function
374
+ * always drove both, and the name only ever named one of them. Kept working
375
+ * and kept quiet.
376
+ */
377
+ export const setQuicklist = setLauncherMenu;
378
+
370
379
  /** Test seam, not public: drop the exported entry without emitting. */
371
380
  export async function _resetLauncher() {
372
381
  await releaseEntry();
@@ -1,4 +1,4 @@
1
- // `useBadge()`, `useProgress()` and `useDockMenu()` — the launcher's view of
1
+ // `useBadge()`, `useProgress()` and `useLauncherMenu()` — the launcher's view of
2
2
  // the app, as things a component declares rather than manages.
3
3
  //
4
4
  // All three now have a rung on both backends except progress, which has one
@@ -7,7 +7,7 @@
7
7
  // feature an app should branch on, and a development warning for every
8
8
  // desktop that lacks one is a warning nobody can act on.
9
9
  //
10
- // The one that changed shape is `useDockMenu`. It used to be cocoa-only on
10
+ // The one that changed shape is `useLauncherMenu`. It used to be cocoa-only on
11
11
  // the grounds that the freedesktop counterpart was an install step — see
12
12
  // `launcher.js`, where that reasoning is corrected: the launcher protocol
13
13
  // carries a `quicklist` dbusmenu, so the Dock menu is runtime code on both.
@@ -15,7 +15,7 @@
15
15
  import { useEffect, useRef } from 'react';
16
16
 
17
17
  import { useAppOrNull } from './appcontext.js';
18
- import { setBadge, setProgress, setQuicklist } from './launcher.js';
18
+ import { setBadge, setLauncherMenu, setProgress } from './launcher.js';
19
19
 
20
20
  /**
21
21
  * Show `value` on the app's icon while this component is mounted, and clear
@@ -63,12 +63,12 @@ export function useProgress(value) {
63
63
  }
64
64
 
65
65
  /**
66
- * The menu behind a right-click on the app's icon in the Dock or launcher,
67
- * from the same `items` vocabulary `MenuBar` and `ContextMenu` take — an
68
- * item's `onSelect` fires when the user picks it.
66
+ * The menu behind a right-click on the app's **launcher icon** the Dock on
67
+ * macOS, the launcher on Linux — from the same `items` vocabulary `MenuBar`
68
+ * and `ContextMenu` take. An item's `onSelect` fires when the user picks it.
69
69
  *
70
70
  * ```jsx
71
- * useDockMenu([
71
+ * useLauncherMenu([
72
72
  * { label: 'New Window', onSelect: openWindow },
73
73
  * { type: 'separator' },
74
74
  * { label: 'Recent', items: recent.map(toItem) },
@@ -80,8 +80,14 @@ export function useProgress(value) {
80
80
  * backend; the launcher protocol's `quicklist` on Linux, which needs the
81
81
  * identity `registerApplication({ appId })` establishes and a `.desktop` file
82
82
  * of that name for a launcher to attach it to.
83
+ *
84
+ * It reads `useDesktopCapability('launcher').features.menu`, which is where
85
+ * the name comes from: every desktop has one icon standing for this
86
+ * application and calls it something different — Dock, taskbar, panel, dash —
87
+ * and `launcher` is the one word that is none of their words and all of their
88
+ * meanings (AGENTS.md, "Vocabulary").
83
89
  */
84
- export function useDockMenu(items) {
90
+ export function useLauncherMenu(items) {
85
91
  const app = useAppOrNull();
86
92
  // read at activation time, so a pick three minutes from now runs the
87
93
  // handler from the current render rather than the mounting one
@@ -89,12 +95,20 @@ export function useDockMenu(items) {
89
95
  live.current = items;
90
96
 
91
97
  useEffect(() => {
92
- setQuicklist(items ?? null, { app }).catch(() => {});
98
+ setLauncherMenu(items ?? null, { app }).catch(() => {});
93
99
  }, [items, app]);
94
100
 
95
101
  useEffect(() => {
96
102
  return () => {
97
- setQuicklist(null, { app }).catch(() => {});
103
+ setLauncherMenu(null, { app }).catch(() => {});
98
104
  };
99
105
  }, [app]);
100
106
  }
107
+
108
+ /**
109
+ * @deprecated Renamed to {@link useLauncherMenu}. "Dock" is one desktop's
110
+ * word for the icon every desktop has; this hook drove the Linux launcher's
111
+ * quicklist long before the name caught up. Kept working, and kept quiet —
112
+ * an alias that warned would punish an app for code that is still correct.
113
+ */
114
+ export const useDockMenu = useLauncherMenu;
package/src/node.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface ImageDataLike {
40
40
  /**
41
41
  * The 2d context a node paints into — the canvas-shaped subset **both**
42
42
  * backends implement: ntk's `RenderingContext2D` over XRender on X11, and
43
- * `CocoaContext2D` over CoreGraphics on macOS. Declared as the contract an
43
+ * `BackendContext2D` over CoreGraphics on macOS. Declared as the contract an
44
44
  * element may rely on rather than as either class: what is here is on
45
45
  * both, and a member one backend has and the other does not is optional
46
46
  * here or absent. Coordinates are device pixels in the owning window's
@@ -422,6 +422,15 @@ export class NodeCascade {
422
422
  get placed() {
423
423
  const owner = this.isWindow ? this : this.root;
424
424
  if (!owner) return false;
425
+ // A window written under a root `<ThemeProvider>` is handed that palette
426
+ // when the scope inserts it, which is after it and its subtree were built
427
+ // and first resolved (nodes/scope.js `insertBefore`). Until then its
428
+ // ancestry is as incomplete as a popup's without its parent, and every
429
+ // token the provider defines would be reported as unknown — a warning for
430
+ // a style that then resolves correctly, and under
431
+ // `REACT_X11_STRICT_TOKENS=1` a throw that killed an app whose palette was
432
+ // fine.
433
+ if (owner._awaitsRootScope && owner._scope == null) return false;
425
434
  return owner.isPopup ? owner.parent != null : true;
426
435
  }
427
436
 
package/src/nodes/node.js CHANGED
@@ -70,7 +70,12 @@ export class Node {
70
70
  return DEVTOOLS_FAKE_DOCUMENT;
71
71
  }
72
72
 
73
- constructor(kind, props, app, { yoga = true } = {}) {
73
+ constructor(kind, props, app, { yoga = true, awaitsRootScope = false } = {}) {
74
+ // Set before anything resolves a style: a `<window>`'s own style is
75
+ // resolved by this constructor, and whether its ancestry is complete
76
+ // decides whether an unresolved `$token` is reported (cascade.js
77
+ // `placed`). A window under a root `<ThemeProvider>` has no palette yet.
78
+ this._awaitsRootScope = awaitsRootScope;
74
79
  this.kind = kind;
75
80
  this.props = props;
76
81
  this.app = app;