react-x11 2.52.0 → 2.54.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 (115) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1096 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/imagedecode-worker.js +32 -0
  76. package/src/imagedecode.js +140 -2
  77. package/src/layouts.js +1 -1
  78. package/src/nodes/animation.js +1 -1
  79. package/src/nodes/invalidate.js +1 -1
  80. package/src/nodes/layouthost.js +1 -1
  81. package/src/nodes/video.js +19 -8
  82. package/src/nodes/window/droptarget.js +1 -1
  83. package/src/ntk.d.ts +12 -0
  84. package/src/pacing.js +1 -1
  85. package/src/refresh/index.js +2 -1
  86. package/src/types/video.d.ts +5 -2
  87. package/src/videobackend.js +28 -0
  88. package/src/videoframes.js +29 -16
  89. package/src/wayland/app.js +27 -0
  90. package/src/wayland/video.js +376 -0
  91. package/src/win32/a11y.js +1 -1
  92. package/src/win32/app.js +2 -2
  93. package/src/win32/fonts.js +1 -1
  94. package/src/win32/glarea.js +4 -4
  95. package/src/win32/overlay.js +1 -1
  96. package/src/win32/panewindow.js +3 -3
  97. package/src/win32/shell.js +1 -1
  98. package/src/win32/window.js +2 -2
  99. package/src/windowid.js +1 -1
  100. package/src/xvideo.js +189 -0
  101. package/docs/architecture/accessibility.md +0 -470
  102. package/docs/architecture/animation.md +0 -707
  103. package/docs/architecture/container-queries.md +0 -525
  104. package/docs/architecture/custom-layout.md +0 -334
  105. package/docs/architecture/drag-and-drop.md +0 -941
  106. package/docs/architecture/element-layer-contents.md +0 -622
  107. package/docs/architecture/frame-pacing.md +0 -255
  108. package/docs/architecture/grid-layout.md +0 -358
  109. package/docs/architecture/protocol-efficiency.md +0 -357
  110. package/docs/architecture/video.md +0 -1149
  111. package/docs/architecture/windowed-regions.md +0 -854
  112. package/docs/glx.md +0 -137
  113. package/docs/wayland.md +0 -545
  114. package/docs/windows-embedding.md +0 -271
  115. package/docs/windows-gl.md +0 -221
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.52.0",
3
+ "version": "2.54.0",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "bin": {
@@ -72,6 +72,7 @@
72
72
  "labs:direct-gl": "tsx examples/labs/direct-gl.jsx",
73
73
  "labs:gl-overlay": "tsx examples/labs/gl-overlay.jsx",
74
74
  "examples:viewer3d": "tsx examples/viewer3d.jsx",
75
+ "examples:video": "tsx examples/video.jsx",
75
76
  "examples:transparent": "tsx examples/transparent.jsx",
76
77
  "examples:windows": "tsx examples/windows.jsx",
77
78
  "examples:wm": "tsx examples/wm.jsx",
@@ -110,7 +111,7 @@
110
111
  "ical.js": "^2.2.1",
111
112
  "image-in-browser": "^3.6.0",
112
113
  "linebreak": "^1.1.0",
113
- "ntk": "^8.23.2",
114
+ "ntk": "^8.24.0",
114
115
  "react-reconciler": "^0.34.0",
115
116
  "yoga-layout": "^3.2.1"
116
117
  },
package/src/Reconciler.js CHANGED
@@ -678,7 +678,7 @@ const isNtkApp = (v) =>
678
678
  *
679
679
  * `'auto'` — the default — is one rule: macOS gets the native Core
680
680
  * Animation backend, everything else gets X11 via `$DISPLAY`. (More rungs —
681
- * Wayland — join this ladder, not a new one; docs/wayland.md.) On a mac
681
+ * Wayland — join this ladder, not a new one; design/wayland.md.) On a mac
682
682
  * without the `@windowkit/appkit` bridge installed, auto falls back to X11 so
683
683
  * an XQuartz setup keeps working; asking for `'cocoa'` by name means the
684
684
  * bridge is required, and its absence is an error that says how to fix it.
@@ -809,7 +809,7 @@ export async function createRoot(options = {}) {
809
809
  // The cocoa backend's desktop is not freedesktop's: the AT-SPI bridge and
810
810
  // the D-Bus global menu have nothing to register against there, so they
811
811
  // default off (their macOS replacements are their own workstream —
812
- // docs/macos.md). The appearance ladder stays on: its macOS rung is the
812
+ // design/macos.md). The appearance ladder stays on: its macOS rung is the
813
813
  // one that answers. An explicit `desktop` option still wins.
814
814
  setDesktopIntegration(
815
815
  rest.desktop ??
package/src/a11y.js CHANGED
@@ -1289,7 +1289,7 @@ export function startA11y(app) {
1289
1289
  // presence *is* the capability — the same rule the rest of the library
1290
1290
  // uses. The Windows backend installs one, because UI Automation asks
1291
1291
  // synchronously and has to be answered from a pushed copy rather than from
1292
- // the live tree (docs/windows.md §Accessibility); AT-SPI's asynchronous
1292
+ // the live tree (design/windows.md §Accessibility); AT-SPI's asynchronous
1293
1293
  // questions are answered from the tree itself, below.
1294
1294
  if (typeof app?.startAccessibility === 'function') {
1295
1295
  startPromise = Promise.resolve()
@@ -1,7 +1,7 @@
1
1
  // A canvas-shaped 2d context over a **verb table** — the drawing dialect
2
2
  // every native backend answers, and the one place it is written down.
3
3
  //
4
- // This is macos.md §"The split" step 4, taken when a second native backend
4
+ // This is design/macos.md §"The split" step 4, taken when a second native backend
5
5
  // started: the class takes its `native` as a constructor argument and calls
6
6
  // nothing else, so one wrapper drives @windowkit/appkit's CoreGraphics verbs
7
7
  // and @windowkit/win32's Direct2D verbs alike. A bridge that lacks an
@@ -16,7 +16,7 @@
16
16
  //
17
17
  // On the surface presenter this is the whole drawing path; on the layer
18
18
  // presenter it stays as the fallback every painted-code node (<canvas>,
19
- // <svg>, registered elements) rasters through — docs/macos.md §"Custom
19
+ // <svg>, registered elements) rasters through — design/macos.md §"Custom
20
20
  // drawing on a layer tree".
21
21
  //
22
22
  // The native surface holds the real graphics state (paths, CTM, clip); this
@@ -322,7 +322,7 @@ const NON_RECT = Symbol('non-rectangular clip');
322
322
  * linear in it: 14ms, 29ms, 56ms, 113ms. The driver is the subpath count,
323
323
  * not the vertex count — one 26,000-vertex subpath strokes in 4ms where
324
324
  * two thousand 13-vertex ones take 208. The full table, the two shapes
325
- * left whole and why, are in docs/macos.md
325
+ * left whole and why, are in design/macos.md
326
326
  * §"Stroking a path with many subpaths".
327
327
  *
328
328
  * Note the X11 context wants the opposite — there a stroke is an a8
package/src/bootstrap.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // an app on macOS moves onto a worker (src/cocoa/relaunch.js), and on that
5
5
  // worker the same import sets it up. A Windows backend's UI thread will be
6
6
  // the addon's own, and X11 has none, so neither needs this step
7
- // (docs/windows.md §"Three shapes on Windows"). Imported first by
7
+ // (design/windows.md §"Three shapes on Windows"). Imported first by
8
8
  // src/index.js, so that it runs before the rest of the package.
9
9
  import { isMainThread, workerData } from 'node:worker_threads';
10
10
 
package/src/cocoa/app.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // that stands in for the X socket.
6
6
  //
7
7
  // Two run loops. By default Node's loop is master and a timer pumps AppKit
8
- // (docs/macos.md §"Input and the run loop", option 1): input waits for the
8
+ // (design/macos.md §"Input and the run loop", option 1): input waits for the
9
9
  // pump, and AppKit's modal loops (live resize, menu tracking, a drag) stall
10
10
  // JS timers — but not the delegate callbacks, which is why live resize
11
11
  // still relayouts: the resize event flushes the frame synchronously on its
@@ -13,7 +13,7 @@
13
13
  // Under the launcher (`node --import react-x11/cocoa-main`, src/cocoa/
14
14
  // main.js) the app runs on a worker instead, AppKit keeps the main thread
15
15
  // in a real `[NSApp run]`, and this object is fed by the bridge's batches
16
- // (`_routeBatch`) with no pump at all (docs/macos.md §"JS on a worker").
16
+ // (`_routeBatch`) with no pump at all (design/macos.md §"JS on a worker").
17
17
  import { isMainThread } from 'node:worker_threads';
18
18
 
19
19
  import { cssColorStraight } from 'ntk/color';
@@ -64,11 +64,11 @@ const PUMP_INTERVAL_MS = 8;
64
64
  // The edge moves when the delegate returns, so the budget is how long a
65
65
  // slow frame may hold the drag back. 50 covers what a large tree costs —
66
66
  // the Cocoa `resize` cell measures 28ms a tick at 3,662 nodes
67
- // (docs/macos.md) — with room, where the bridge's own test met every tick
67
+ // (design/macos.md) — with room, where the bridge's own test met every tick
68
68
  // in 3.1ms against 3ms of layout. Past three 60Hz frames an edge that waits
69
69
  // reads as the drag stuttering, so a tree slower than that lets the edge
70
70
  // go first and its frame land after (Flutter's Windows embedder waits up
71
- // to 100, docs/windows.md §"Resize"). `createRoot({ cocoa: { resizeWait } })`
71
+ // to 100, design/windows.md §"Resize: where the threads meet"). `createRoot({ cocoa: { resizeWait } })`
72
72
  // sets it; 0 is no handshake at all.
73
73
  const RESIZE_WAIT_MS = 50;
74
74
  // What `createRoot({ cocoa: { paneStretch } })` and
@@ -272,7 +272,7 @@ export class CocoaApp {
272
272
  }
273
273
 
274
274
  // 'surface' (the measured default) or 'layers' — the retained CALayer
275
- // presenter, opt-in while docs/macos.md's measure-first gate is open.
275
+ // presenter, opt-in while design/macos.md's measure-first gate is open.
276
276
  this._presenterMode =
277
277
  options.cocoa?.presenter ??
278
278
  process.env.REACT_X11_COCOA_PRESENTER ??
@@ -391,7 +391,7 @@ export class CocoaApp {
391
391
 
392
392
  // the global-menu exports, newest active: the macOS menu bar is one per
393
393
  // app, so the most recent MenuBar owns it (per-focused-window switching
394
- // is the follow-up — docs/macos.md §Menus)
394
+ // is the follow-up — design/macos.md §Menus)
395
395
  this._globalMenus = [];
396
396
  this._activeGlobalMenu = null;
397
397
 
@@ -1,5 +1,5 @@
1
1
  // Native control bezels — AppKit's own pixels for the core controls, cached
2
- // as surfaces the ordinary 2d paint path can blit (docs/macos.md §Native
2
+ // as surfaces the ordinary 2d paint path can blit (design/macos.md §Native
3
3
  // controls). The mechanism is offscreen NSCell/NSControl rendering, the
4
4
  // WebKit/Gecko form-control technique: interaction, focus and keyboard stay
5
5
  // the shared component implementation, and only the *bezel* — the pixels of
@@ -1,6 +1,6 @@
1
1
  // The Cocoa text engine: CoreText behind the same `app.fonts` contract ntk's
2
2
  // FontManager answers on the X11 backend. The renderer touches exactly this
3
- // surface (docs/macos.md §"Text: the engine contract"):
3
+ // surface (design/macos.md §"Text: the engine contract"):
4
4
  //
5
5
  // fonts.layout(spans, base, { maxWidth, align, lineHeight, maxLines,
6
6
  // overflow, direction })
@@ -1,4 +1,4 @@
1
- // The macOS menu bar as a global-menu export (docs/macos.md §Menus).
1
+ // The macOS menu bar as a global-menu export (design/macos.md §Menus).
2
2
  //
3
3
  // The Linux global menu was built for exactly this moment: the item
4
4
  // vocabulary is data, and the pure snapshot/IdAllocator machinery in
@@ -23,7 +23,7 @@
23
23
  //
24
24
  // There is no input method behind this. The bridge's view swallows
25
25
  // `keyDown:` without `interpretKeyEvents:` and is no `NSTextInputClient`
26
- // (docs/macos.md, "IME"), so these fields are all the text there is. Two
26
+ // (design/macos.md, "IME"), so these fields are all the text there is. Two
27
27
  // consequences, both handled below:
28
28
  //
29
29
  // - **A dead key types nothing.** ⌥E on a US layout leaves `chars` empty, and
@@ -1,4 +1,4 @@
1
- // The pane process's "window" on the Cocoa backend — docs/frame.md's child
1
+ // The pane process's "window" on the Cocoa backend — design/frame.md's child
2
2
  // half, with the X server replaced by shared memory.
3
3
  //
4
4
  // On X11 a pane renders into a real window of its own and the host embeds
@@ -1,4 +1,4 @@
1
- // `<video src>` on macOS (docs/architecture/video.md §3.2, §9): a file or URL
1
+ // `<video src>` on macOS (design/architecture/video.md §3.2, §9): a file or URL
2
2
  // played by AVFoundation through @windowkit/appkit's player verbs — the
3
3
  // platform's decoder, audio, seeking and HLS — with the picture on the
4
4
  // player's own AVPlayerLayer.
@@ -1,4 +1,4 @@
1
- // The retained layer presenter — Tier L of docs/macos.md: one CALayer per
1
+ // The retained layer presenter — Tier L of design/macos.md: one CALayer per
2
2
  // drawn node, React commits landing as property sets inside a single
3
3
  // disabled-actions CATransaction per frame, the WindowServer compositing.
4
4
  //
@@ -350,7 +350,7 @@ export function stylePaintsPlain(node, style = node.style ?? {}) {
350
350
  /**
351
351
  * Style property → the key path on a PropBox's own layer, for the
352
352
  * animations the presenter takes off the frame clock and hands to the
353
- * render server (docs/architecture/animation.md §4.2). Only what the layer
353
+ * render server (design/architecture/animation.md §4.2). Only what the layer
354
354
  * expresses as a property of itself qualifies: a colour on `<text>` is a
355
355
  * re-raster per frame, a layout property moves the siblings, and both stay
356
356
  * on the JS loop. `scaled` values go out in points, as `_syncPropBox`
@@ -36,7 +36,7 @@
36
36
  // overlapped, hidden, clipped, faded or non-plain returns to the bitmap in
37
37
  // the frame that finds it, the animation handed back to the clock — and a
38
38
  // loop the clock runs is offered again once the scene would take it
39
- // (`wouldTake`), where it is in its cycle. docs/macos.md §"Layer
39
+ // (`wouldTake`), where it is in its cycle. design/macos.md §"Layer
40
40
  // promotion" is the account.
41
41
  import { now } from '../nodes/animation.js';
42
42
  import { BoxNode } from '../nodes/box.js';
@@ -1,4 +1,4 @@
1
- // Threaded mode by default (docs/macos.md §"JS on a worker"). On macOS the
1
+ // Threaded mode by default (design/macos.md §"JS on a worker"). On macOS the
2
2
  // process main thread is AppKit's — the only thread it lets own a window —
3
3
  // and while the app's JS runs there, every menu, drag, live resize and
4
4
  // modal panel stops it. So the first import of react-x11 on the main thread
@@ -1,4 +1,4 @@
1
- // Threaded mode's worker half (docs/macos.md §"JS on a worker: a UI thread
1
+ // Threaded mode's worker half (design/macos.md §"JS on a worker: a UI thread
2
2
  // of the bridge's own"): with the launcher (src/cocoa/main.js) holding the
3
3
  // process main thread in the bridge's `runMain()`, the app's JS runs on a
4
4
  // `worker_threads` Worker, and this module is what that worker needs to
@@ -1,4 +1,4 @@
1
- // A video's frames on a layer of their own (docs/architecture/video.md §7):
1
+ // A video's frames on a layer of their own (design/architecture/video.md §7):
2
2
  // what the surface presenter's sprites (src/cocoa/sprites.js) do with a part
3
3
  // whose `contents` is a `VideoFrames` sink instead of a `paint`.
4
4
  //
@@ -39,7 +39,7 @@ const FENCE_TIMEOUT_MS = 100;
39
39
  // (`_takeBack`). Three, a `CAMetalLayer`'s depth, is what an animation
40
40
  // mostly needs; an input answered between two of its frames puts two flips
41
41
  // inside one refresh, and four kept every write of a clicked 120Hz animation
42
- // off a held buffer (docs/macos.md, "Measured: the swapchain and the
42
+ // off a held buffer (design/macos.md, "Measured: the swapchain and the
43
43
  // WindowServer's hold").
44
44
  const MAX_BUFFERS = 4;
45
45
  // Past this many rects, what a buffer missed is copied whole: a buffer left
@@ -235,14 +235,14 @@ export class CocoaWindow {
235
235
  // change.
236
236
  this.damageRectCap = 16;
237
237
 
238
- // The retained layer presenter (docs/macos.md Tier L), behind
238
+ // The retained layer presenter (design/macos.md Tier L), behind
239
239
  // REACT_X11_COCOA_PRESENTER=layers while the surface path is the
240
240
  // measured default. Its hooks exist only in this mode, so the feature
241
241
  // detection in src/nodes/ keeps the surface path byte-identical; the
242
242
  // scroll blit is shadowed off because a layer frame has no backing
243
243
  // bitmap to blit. The last two are the animation seam: a transition or
244
244
  // a loop the presenter takes runs in the render server and schedules no
245
- // frames here (docs/architecture/animation.md §4).
245
+ // frames here (design/architecture/animation.md §4).
246
246
  if (app._presenterMode === 'layers') {
247
247
  this._presenter = new CocoaLayerPresenter(this);
248
248
  this.presentFrame = (windowNode) => this._presenter.frame(windowNode);
@@ -659,7 +659,7 @@ export class CocoaWindow {
659
659
  * A new size retires the chain, released on the spot (`_releaseBacking`):
660
660
  * left to the handles' finalizers, a forty-tick drag of the two-buffer
661
661
  * chain this used to be held 800MB until a collection happened to run —
662
- * the `rss +80MB` docs/macos.md measured. The layer keeps its own
662
+ * the `rss +80MB` design/macos.md measured. The layer keeps its own
663
663
  * reference to whichever IOSurface it is still showing, so the free is
664
664
  * safe while that frame is on glass. A new chain starts with the one
665
665
  * buffer its first frame paints: a live-resize tick paints its size once,
@@ -984,7 +984,9 @@ function useMenuAccelerators(
984
984
  *
985
985
  * The wrapper is focusable and takes focus when the menu opens, because the
986
986
  * popup is override-redirect and never gets focus itself — the same trick
987
- * `Select` uses to keep arrow keys working while its menu is open.
987
+ * `Select` uses to keep arrow keys working while its menu is open. It is not
988
+ * a Tab stop: it has no role or name, so Tab landing on it would announce
989
+ * nothing, and the controls inside are stops of their own.
988
990
  */
989
991
  export function ContextMenu({
990
992
  items = [],
@@ -1087,7 +1089,10 @@ export function ContextMenu({
1087
1089
  'box',
1088
1090
  {
1089
1091
  ref,
1090
- focusable: true,
1092
+ // focusable by a press and by `focus()` above, but out of the Tab
1093
+ // cycle: a stop here is a nameless "filler" around content whose own
1094
+ // controls are already reachable
1095
+ tabIndex: -1,
1091
1096
  onMouseDown: (ev) => {
1092
1097
  if (ev.button !== 3 && rect) close();
1093
1098
  },
@@ -98,7 +98,7 @@ export function Switch({
98
98
  );
99
99
 
100
100
  // NSSwitch's own pixels, on/off as two cached bezels. The knob slide is
101
- // the fidelity this loses for now (docs/macos.md flags the switch as the
101
+ // the fidelity this loses for now (design/macos.md flags the switch as the
102
102
  // one control worth revisiting if that disappoints); the press keeps its
103
103
  // answer as the wash, since NSSwitch renders no pressed state offscreen.
104
104
  if (nativeControls) {
@@ -1,4 +1,4 @@
1
- // The native half of the widget set (docs/macos.md §Native controls).
1
+ // The native half of the widget set (design/macos.md §Native controls).
2
2
  //
3
3
  // House rule: **default native, seam out.** On a backend that renders the
4
4
  // platform's own control bezels — today the Cocoa backend — the core
package/src/dnd.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // XDND drag and drop, the drop-target half: react-x11 windows accept drags
2
2
  // from other X11 applications (file managers, editors, browsers).
3
3
  //
4
- // Design: docs/architecture/drag-and-drop.md; protocol: XDND version 5.
4
+ // Design: design/architecture/drag-and-drop.md; protocol: XDND version 5.
5
5
  // The advertisement (`XdndAware`) is a 4-byte property written
6
6
  // unconditionally on every top-level window at realize time — it creates no
7
7
  // X windows, so it is orthogonal to the windowed/windowless split.
@@ -1080,7 +1080,7 @@ export class DropSession {
1080
1080
  // ---------------------------------------------------------------------------
1081
1081
  // The drag source.
1082
1082
  //
1083
- // One drag session, two transports (docs/architecture/drag-and-drop.md §6.3):
1083
+ // One drag session, two transports (design/architecture/drag-and-drop.md §6.3):
1084
1084
  // while the pointer is over this process's own windows the drag is local —
1085
1085
  // no wire, live JS payloads, the DropSession above delivering to the same
1086
1086
  // handlers with `e.source === 'internal'`. The moment it leaves them, the
package/src/glnodes.js CHANGED
@@ -95,7 +95,7 @@ export function glxConfig(app, spec) {
95
95
  process.platform === 'win32'
96
96
  ? 'react-x11: <glarea> is not built on the win32 backend yet — it ' +
97
97
  'needs ANGLE (EGL and GLES over Direct3D 11), which ' +
98
- 'docs/windows.md plans as an optional dependency the way ' +
98
+ 'design/windows.md plans as an optional dependency the way ' +
99
99
  'x11-dri is on X11. Everything else on this backend works ' +
100
100
  'without it; for GL content today, use the X11 backend.'
101
101
  : 'react-x11: <glarea> needs ntk >= 3.6.0 (app.chooseGLConfig)',
package/src/grid.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // CSS grid, built in (docs/styling.md "Grid"; the design record is
2
- // docs/architecture/grid-layout.md). A box with `display: 'grid'` — or
2
+ // design/architecture/grid-layout.md). A box with `display: 'grid'` — or
3
3
  // `layout: 'grid'`, the same request under the name every layout goes by —
4
4
  // arranges its children by CSS Grid's placement and track sizing algorithms
5
5
  // (css-grid-2 §8.5 and §12), inside the pass that lays the window out. It is
@@ -0,0 +1,32 @@
1
+ // The other end of `decodeImageBytes`'s `offThread` (imagedecode.js): the
2
+ // JavaScript rung, run where it holds no frame. A decode's pixels go back
3
+ // handed over, never copied, and its error as the message the caller's
4
+ // rejection carries.
5
+ import { parentPort } from 'node:worker_threads';
6
+
7
+ import { decodeHere } from './imagedecode.js';
8
+
9
+ parentPort.on('message', async ({ id, bytes }) => {
10
+ try {
11
+ const image = await decodeHere(bytes);
12
+ let data = image.data;
13
+ // a view of a larger buffer — a pooled Buffer — is copied out first,
14
+ // so what is handed over is the pixels and nothing beside them
15
+ if (
16
+ !(data.buffer instanceof ArrayBuffer) ||
17
+ data.byteOffset !== 0 ||
18
+ data.byteLength !== data.buffer.byteLength
19
+ ) {
20
+ data = new Uint8Array(data);
21
+ }
22
+ parentPort.postMessage(
23
+ { id, width: image.width, height: image.height, data: data.buffer },
24
+ [data.buffer],
25
+ );
26
+ } catch (err) {
27
+ parentPort.postMessage({
28
+ id,
29
+ error: err instanceof Error ? err.message : String(err),
30
+ });
31
+ }
32
+ });
@@ -13,7 +13,11 @@
13
13
  // 2. JavaScript. ntk's PNG and JPEG decoders (pngjs, jpeg-js), which are
14
14
  // synchronous, and WebP through image-in-browser's decoder — imported
15
15
  // the first time a WebP arrives, because evaluating it takes ~140ms
16
- // that an app showing no WebP should never pay at startup.
16
+ // that an app showing no WebP should never pay at startup. A caller
17
+ // that takes a promise anyway asks for `offThread`, and the same
18
+ // decoders run on a worker thread (`imagedecode-worker.js`) for bytes
19
+ // large enough to pay for the trip: a document's images, which held
20
+ // the JavaScript thread for half of the Guardian's load.
17
21
  //
18
22
  // Two things here are easy to undo:
19
23
  //
@@ -115,17 +119,41 @@ const hex = (bytes) =>
115
119
  * elsewhere). Throws — or rejects — on bytes that are not an image this
116
120
  * runtime can decode, with what to do about it.
117
121
  *
122
+ * `options.offThread` asks for the decode to run on a worker thread where
123
+ * the runtime decodes in JavaScript, which it does for bytes long enough
124
+ * (`OFF_THREAD_BYTES`): a PNG or a JPEG then lands as a promise too. A
125
+ * caller that waits for a promise anyway — a document's images — asks for
126
+ * it; `<image>` does not, so a PNG it is handed still shows in the first
127
+ * frame.
128
+ *
118
129
  * The bytes are read, never written, and under Bun they are read on another
119
130
  * thread after this returns: the caller must not change them meanwhile,
120
131
  * which `<image>`'s contract (a source is immutable content) already says.
121
132
  */
122
- export function decodeImageBytes(bytes) {
133
+ export function decodeImageBytes(bytes, options) {
123
134
  if (bytes.length === 0) {
124
135
  throw new Error('react-x11: an empty buffer is not an image.');
125
136
  }
126
137
  const decoder = runtimeDecoder();
127
138
  if (decoder) return decodeWithRuntime(decoder, bytes);
128
139
  const format = imageFormat(bytes);
140
+ if (
141
+ options?.offThread &&
142
+ bytes.length >= OFF_THREAD_BYTES &&
143
+ (format === 'png' || format === 'jpeg' || format === 'webp')
144
+ ) {
145
+ const pool = decodePool();
146
+ if (pool) return pool.decode(bytes, format);
147
+ }
148
+ return decodeHere(bytes, format);
149
+ }
150
+
151
+ /**
152
+ * The JavaScript rung, on whichever thread runs it: what `decodeImageBytes`
153
+ * does where the runtime has no decoder of its own, and what its worker
154
+ * does with the bytes it is handed.
155
+ */
156
+ export function decodeHere(bytes, format = imageFormat(bytes)) {
129
157
  if (format === 'png' || format === 'jpeg') return decodeImage(bytes);
130
158
  if (format === 'webp') return decodeWebP(bytes);
131
159
  if (format) {
@@ -142,6 +170,116 @@ export function decodeImageBytes(bytes) {
142
170
  );
143
171
  }
144
172
 
173
+ /**
174
+ * How long encoded bytes are before a decode asked to run off the
175
+ * JavaScript thread is handed to a worker: under it, the message costs
176
+ * about what the decode does.
177
+ */
178
+ const OFF_THREAD_BYTES = 8 * 1024;
179
+
180
+ /** The most decode workers a process starts. */
181
+ const MAX_WORKERS = 2;
182
+
183
+ /** The pool `offThread` decodes on: undefined until asked, null where the
184
+ * runtime has no worker threads. */
185
+ let pool;
186
+
187
+ function decodePool() {
188
+ if (pool !== undefined) return pool;
189
+ const threads = globalThis.process?.getBuiltinModule?.('node:worker_threads');
190
+ pool = threads?.Worker ? new DecodePool(threads.Worker) : null;
191
+ return pool;
192
+ }
193
+
194
+ /**
195
+ * Workers that decode with the JavaScript rung (`decodeHere`), started as
196
+ * decodes arrive, up to `MAX_WORKERS`. A worker holds the process open only
197
+ * while it has a decode to answer. One that cannot start, or dies, has its
198
+ * decodes made here, and none starts after it: a bundle that left the
199
+ * worker's file behind decodes as it always did.
200
+ */
201
+ class DecodePool {
202
+ constructor(Worker) {
203
+ this._Worker = Worker;
204
+ this._workers = [];
205
+ this._jobs = new Map();
206
+ this._next = 1;
207
+ this._broken = false;
208
+ }
209
+
210
+ decode(bytes, format) {
211
+ const worker = this._worker();
212
+ if (!worker) return decodeHere(bytes, format);
213
+ const id = this._next++;
214
+ // a copy, handed over: the caller's bytes stay where they are, and a
215
+ // Buffer's `slice` is a view of them
216
+ const copy = new Uint8Array(bytes);
217
+ try {
218
+ worker.thread.postMessage({ id, bytes: copy }, [copy.buffer]);
219
+ } catch {
220
+ return decodeHere(bytes, format);
221
+ }
222
+ if (worker.pending++ === 0) worker.thread.ref();
223
+ return new Promise((resolve, reject) => {
224
+ this._jobs.set(id, { resolve, reject, worker, bytes, format });
225
+ });
226
+ }
227
+
228
+ /** The least busy worker, or a new one while every one is busy. */
229
+ _worker() {
230
+ if (this._broken) return null;
231
+ let best = null;
232
+ for (const w of this._workers) {
233
+ if (!best || w.pending < best.pending) best = w;
234
+ }
235
+ if (best && (best.pending === 0 || this._workers.length >= MAX_WORKERS)) {
236
+ return best;
237
+ }
238
+ let thread;
239
+ try {
240
+ thread = new this._Worker(
241
+ new URL('./imagedecode-worker.js', import.meta.url),
242
+ );
243
+ } catch {
244
+ this._broken = true;
245
+ return best;
246
+ }
247
+ const w = { thread, pending: 0 };
248
+ thread.unref();
249
+ thread.on('message', (message) => this._answer(w, message));
250
+ thread.on('error', () => this._lost(w));
251
+ thread.on('exit', () => this._lost(w));
252
+ this._workers.push(w);
253
+ return w;
254
+ }
255
+
256
+ _answer(w, { id, width, height, data, error }) {
257
+ const job = this._jobs.get(id);
258
+ if (!job) return;
259
+ this._jobs.delete(id);
260
+ if (--w.pending === 0) w.thread.unref();
261
+ if (error !== undefined) job.reject(new Error(error));
262
+ else job.resolve(new Image({ width, height, data: new Uint8Array(data) }));
263
+ }
264
+
265
+ _lost(w) {
266
+ this._broken = true;
267
+ this._workers = this._workers.filter((other) => other !== w);
268
+ for (const [id, job] of this._jobs) {
269
+ if (job.worker !== w) continue;
270
+ this._jobs.delete(id);
271
+ try {
272
+ Promise.resolve(decodeHere(job.bytes, job.format)).then(
273
+ job.resolve,
274
+ job.reject,
275
+ );
276
+ } catch (err) {
277
+ job.reject(err);
278
+ }
279
+ }
280
+ }
281
+ }
282
+
145
283
  /** Read a file path or file URL and decode it. */
146
284
  export async function loadImageFile(path) {
147
285
  const { readFile } = await import('node:fs/promises');
package/src/layouts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Layout algorithms and positions from outside the renderer — the two seams
2
- // docs/architecture/custom-layout.md is the design record for.
2
+ // design/architecture/custom-layout.md is the design record for.
3
3
  //
4
4
  // A **layout** arranges a box's children. It is asked how big the box is for
5
5
  // the room on offer, and then where each child goes in the room the box got.
@@ -310,7 +310,7 @@ export class NodeAnimation {
310
310
  // loop-stop rule and `sameAnimation` all see it — but it contributes no
311
311
  // value to the style, is skipped by the tick, and keeps the node out of the
312
312
  // window's animating set. The X11 path has none of these hooks and is
313
- // byte-identical (docs/architecture/animation.md §4).
313
+ // byte-identical (design/architecture/animation.md §4).
314
314
 
315
315
  _offload(prop, entry) {
316
316
  const wnd = this.root?.window;
@@ -361,7 +361,7 @@ export class WindowInvalidate {
361
361
  }
362
362
  // A retained presenter keeps a per-node diff instead of damage rects,
363
363
  // and this is the one channel every change already announces itself on
364
- // (docs/macos.md §"One renderer, two presenters"). Feature-detected: an
364
+ // (design/macos.md §"One renderer, two presenters"). Feature-detected: an
365
365
  // ntk window has no ear here and the X11 path is byte-identical. A
366
366
  // presenter that answers `true` has taken the claim onto a layer of its
367
367
  // own (src/cocoa/promotion.js): the bitmap owes nothing for it, and the
@@ -2,7 +2,7 @@
2
2
  // in layouts.js arranges its children itself. Each child is a yoga root of
3
3
  // its own, and the host is a measured leaf in its parent — so what the
4
4
  // algorithm decides is on screen in the frame that asked.
5
- // docs/architecture/custom-layout.md is the design record.
5
+ // design/architecture/custom-layout.md is the design record.
6
6
 
7
7
  import {
8
8
  createLayoutNode,