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.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1096 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/imagedecode-worker.js +32 -0
- package/src/imagedecode.js +140 -2
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/ntk.d.ts +12 -0
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- 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.
|
|
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.
|
|
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;
|
|
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
|
-
//
|
|
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 (
|
|
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()
|
package/src/backend/context2d.js
CHANGED
|
@@ -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 —
|
|
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
|
|
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
|
-
// (
|
|
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
|
-
// (
|
|
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 (
|
|
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
|
-
// (
|
|
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,
|
|
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
|
|
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 —
|
|
394
|
+
// is the follow-up — design/macos.md §Menus)
|
|
395
395
|
this._globalMenus = [];
|
|
396
396
|
this._activeGlobalMenu = null;
|
|
397
397
|
|
package/src/cocoa/bezels.js
CHANGED
|
@@ -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 (
|
|
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
|
package/src/cocoa/fonts.js
CHANGED
|
@@ -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 (
|
|
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 })
|
package/src/cocoa/globalmenu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The macOS menu bar as a global-menu export (
|
|
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
|
package/src/cocoa/keymap.js
CHANGED
|
@@ -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
|
-
// (
|
|
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
|
package/src/cocoa/panewindow.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The pane process's "window" on the Cocoa backend —
|
|
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
|
package/src/cocoa/player.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `<video src>` on macOS (
|
|
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.
|
package/src/cocoa/presenter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The retained layer presenter — Tier L of
|
|
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 (
|
|
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`
|
package/src/cocoa/promotion.js
CHANGED
|
@@ -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.
|
|
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';
|
package/src/cocoa/relaunch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Threaded mode by default (
|
|
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
|
package/src/cocoa/threaded.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Threaded mode's worker half (
|
|
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
|
package/src/cocoa/video.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A video's frames on a layer of their own (
|
|
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
|
//
|
package/src/cocoa/window.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
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`
|
|
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,
|
package/src/components/Menu.js
CHANGED
|
@@ -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
|
|
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
|
},
|
package/src/components/Switch.js
CHANGED
|
@@ -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 (
|
|
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) {
|
package/src/components/native.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The native half of the widget set (
|
|
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:
|
|
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 (
|
|
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
|
-
'
|
|
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
|
-
//
|
|
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
|
+
});
|
package/src/imagedecode.js
CHANGED
|
@@ -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
|
-
//
|
|
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.
|
package/src/nodes/animation.js
CHANGED
|
@@ -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 (
|
|
313
|
+
// byte-identical (design/architecture/animation.md §4).
|
|
314
314
|
|
|
315
315
|
_offload(prop, entry) {
|
|
316
316
|
const wnd = this.root?.window;
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -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
|
-
// (
|
|
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
|
package/src/nodes/layouthost.js
CHANGED
|
@@ -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
|
-
//
|
|
5
|
+
// design/architecture/custom-layout.md is the design record.
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
createLayoutNode,
|