react-x11 2.51.1 → 2.53.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 (121) hide show
  1. package/README.md +94 -61
  2. package/docs/README.md +256 -0
  3. package/docs/accessibility.md +575 -0
  4. package/docs/appearance.md +317 -0
  5. package/docs/click-to-component.md +92 -0
  6. package/docs/clipboard.md +262 -0
  7. package/docs/components.md +1231 -0
  8. package/docs/dbus.md +205 -0
  9. package/docs/debugging.md +371 -0
  10. package/docs/desktop-calendar.md +247 -0
  11. package/docs/desktop.md +523 -0
  12. package/docs/devtools.md +133 -0
  13. package/docs/drag-and-drop.md +585 -0
  14. package/docs/ecosystem/animation.md +360 -0
  15. package/docs/ecosystem/data-fetching.md +186 -0
  16. package/docs/ecosystem/dev-tooling.md +393 -0
  17. package/docs/ecosystem/forms.md +277 -0
  18. package/docs/ecosystem/headless.md +194 -0
  19. package/docs/ecosystem/i18n.md +86 -0
  20. package/docs/ecosystem/icons.md +377 -0
  21. package/docs/ecosystem/layout.md +206 -0
  22. package/docs/ecosystem/routing.md +135 -0
  23. package/docs/ecosystem/state.md +344 -0
  24. package/docs/ecosystem/testing.md +474 -0
  25. package/docs/ecosystem/theming.md +494 -0
  26. package/docs/ecosystem.md +439 -0
  27. package/docs/elements.md +2079 -0
  28. package/docs/embedding.md +239 -0
  29. package/docs/events.md +742 -0
  30. package/docs/extending.md +1700 -0
  31. package/docs/eyedropper.md +164 -0
  32. package/docs/filedialog.md +222 -0
  33. package/docs/frame.md +436 -0
  34. package/docs/gl.md +173 -0
  35. package/docs/globalmenu.md +195 -0
  36. package/docs/macos.md +592 -0
  37. package/docs/notifications.md +150 -0
  38. package/docs/packaging.md +468 -0
  39. package/docs/permissions.md +143 -0
  40. package/docs/react-features.md +405 -0
  41. package/docs/remote.md +179 -0
  42. package/docs/scale.md +225 -0
  43. package/docs/security.md +132 -0
  44. package/docs/styling.md +1710 -0
  45. package/docs/system.md +519 -0
  46. package/docs/testing.md +494 -0
  47. package/docs/typescript.md +75 -0
  48. package/docs/uri-schemes.md +340 -0
  49. package/docs/wayland-backend.md +332 -0
  50. package/docs/windows-integrations.md +177 -0
  51. package/docs/windows.md +380 -0
  52. package/package.json +11 -3
  53. package/src/Reconciler.js +2 -2
  54. package/src/a11y.js +1 -1
  55. package/src/backend/context2d.js +3 -3
  56. package/src/bootstrap.js +1 -1
  57. package/src/bus.js +39 -26
  58. package/src/cli/create.d.ts +43 -0
  59. package/src/cli/create.js +305 -0
  60. package/src/cli/env.js +484 -0
  61. package/src/cli/index.js +29 -2
  62. package/src/cli/templates/AGENTS.template.md +195 -0
  63. package/src/cli/templates/DESIGN.template.md +269 -0
  64. package/src/cocoa/app.js +6 -6
  65. package/src/cocoa/bezels.js +1 -1
  66. package/src/cocoa/fonts.js +1 -1
  67. package/src/cocoa/globalmenu.js +1 -1
  68. package/src/cocoa/keymap.js +1 -1
  69. package/src/cocoa/panewindow.js +1 -1
  70. package/src/cocoa/player.js +1 -1
  71. package/src/cocoa/presenter.js +2 -2
  72. package/src/cocoa/promotion.js +1 -1
  73. package/src/cocoa/relaunch.js +1 -1
  74. package/src/cocoa/threaded.js +1 -1
  75. package/src/cocoa/video.js +1 -1
  76. package/src/cocoa/window.js +4 -4
  77. package/src/components/Menu.js +7 -2
  78. package/src/components/Switch.js +1 -1
  79. package/src/components/native.js +1 -1
  80. package/src/desktopintegration.js +2 -1
  81. package/src/desktopsettings.js +193 -24
  82. package/src/dnd.js +2 -2
  83. package/src/filedialog.js +3 -3
  84. package/src/glnodes.js +1 -1
  85. package/src/grid.js +1 -1
  86. package/src/keyboardstate.js +28 -0
  87. package/src/layouts.js +1 -1
  88. package/src/node.d.ts +10 -10
  89. package/src/nodes/animation.js +1 -1
  90. package/src/nodes/invalidate.js +1 -1
  91. package/src/nodes/layouthost.js +1 -1
  92. package/src/nodes/video.js +19 -8
  93. package/src/nodes/window/droptarget.js +1 -1
  94. package/src/pacing.js +1 -1
  95. package/src/portal.js +31 -0
  96. package/src/refresh/index.js +2 -1
  97. package/src/screencolor.js +5 -10
  98. package/src/types/system.d.ts +8 -6
  99. package/src/types/video.d.ts +5 -2
  100. package/src/videobackend.js +28 -0
  101. package/src/videoframes.js +29 -16
  102. package/src/wayland/app.js +86 -0
  103. package/src/wayland/clipboard.js +14 -7
  104. package/src/wayland/connection.js +3 -0
  105. package/src/wayland/context2d.js +129 -10
  106. package/src/wayland/protocols/index.json +6 -0
  107. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  108. package/src/wayland/seat.js +28 -1
  109. package/src/wayland/swapchain.js +23 -0
  110. package/src/wayland/video.js +376 -0
  111. package/src/wayland/xkb.js +137 -0
  112. package/src/win32/a11y.js +1 -1
  113. package/src/win32/app.js +2 -2
  114. package/src/win32/fonts.js +1 -1
  115. package/src/win32/glarea.js +4 -4
  116. package/src/win32/overlay.js +1 -1
  117. package/src/win32/panewindow.js +3 -3
  118. package/src/win32/shell.js +1 -1
  119. package/src/win32/window.js +2 -2
  120. package/src/windowid.js +1 -1
  121. package/src/xvideo.js +189 -0
package/docs/macos.md ADDED
@@ -0,0 +1,592 @@
1
+ # A native macOS backend
2
+
3
+ On macOS `createRoot()` renders through **Cocoa** by default: real
4
+ `NSWindow`s, Core Animation compositing, the system menu bar and native
5
+ control bezels, with no X server anywhere. The tree, components, styles and
6
+ hooks are the same; this page is what an app sees differently here, and what
7
+ it does not have yet. The design record — presenters, threads, the bridge
8
+ contract, measurements, the plan — is [design/macos.md](../design/macos.md).
9
+
10
+ ## Choosing the backend
11
+
12
+ `createRoot({ backend })`:
13
+
14
+ - `'auto'` (the default) — Cocoa on macOS where the bridge is installed, X11
15
+ through `$DISPLAY` (XQuartz) where it is not.
16
+ - `'cocoa'` — Cocoa, with the bridge **required**.
17
+ - `'x11'` — X11 through XQuartz, still the way to run X-only features
18
+ (`<foreign>`, `examples/wm.jsx`) on a Mac.
19
+
20
+ `REACT_X11_BACKEND=x11` or `=cocoa` overrides both. `createRoot({ app })`
21
+ ignores `backend`: a connection already is one.
22
+
23
+ The bridge is **[`@windowkit/appkit`](https://www.npmjs.com/package/@windowkit/appkit)**,
24
+ an `optionalDependency` with N-API prebuilds for arm64 and x64, so it works
25
+ under Node and Bun. An install that skips optional dependencies has no
26
+ bridge, and `'auto'` falls back to X11. The range react-x11 asks for has
27
+ everything on this page; where an older bridge lacks a verb, the feature is
28
+ detected and degrades as described below.
29
+
30
+ ## Root options and environment
31
+
32
+ `createRoot({ cocoa: { … } })`, ignored off macOS and when `app` is passed:
33
+
34
+ - `presenter` — `'surface'` (the default: one bitmap per window) or
35
+ `'layers'` (one CALayer per drawn node, opt-in while it is measured).
36
+ `REACT_X11_COCOA_PRESENTER` says the same.
37
+ - `promote` — [layer promotion](#layer-promotion-the-animated-few-on-their-own-layers-above-the-surface-presenter)
38
+ of animated boxes; `false` keeps every animation on the frame clock.
39
+ `REACT_X11_COCOA_PROMOTE=1`/`=0` says the same.
40
+ - `frameInterval` — ms between scheduled frames. By default each window
41
+ paces itself on its own display (8.3 ms at 120 Hz, 16.7 at 60); a number
42
+ applies to every window.
43
+ - `pumpInterval` — ms between AppKit event pumps (8) when the app's JS runs
44
+ on the main thread: the floor under input latency there. A
45
+ [worker](#js-on-a-worker-a-ui-thread-of-the-bridges-own) has no pump.
46
+ - `resizeWait` — ms AppKit may hold a live-resize tick for the app's frame at
47
+ the new size, on a worker (50; `0` lets the edge move without waiting).
48
+ - `paneStretch` — when a `<Frame>` pane's last frame is stretched while its
49
+ next one comes: `'behind'` (the default: once it is later than
50
+ `resizeWait`), `'never'` or `'always'`. `REACT_X11_COCOA_PANE_STRETCH` says
51
+ the same.
52
+ - `screenPoll` — ms between re-reads of the screen layout while a
53
+ `useScreens()` subscriber is mounted (500; `0` for never).
54
+ - `appName` — what the Dock, ⌘-Tab and the app menu print for an unbundled
55
+ process; a bundle's `Info.plist` wins.
56
+ - `activationPolicy` — `'regular'` (the default: a Dock tile and a ⌘-Tab
57
+ entry), `'accessory'` (a menu-bar app: windows, no tile) or `'prohibited'`.
58
+ On a worker, set it [at launch](#js-on-a-worker-a-ui-thread-of-the-bridges-own).
59
+ - `exitOnQuit` — end the process once a quit request (the Dock's Quit, ⌘Q, a
60
+ logout) has closed the app (`true`); `false` is for an embedder that owns
61
+ the process's lifetime.
62
+
63
+ Other variables: `REACT_X11_THREADED=0` (keep the JS on the main thread),
64
+ `APPKIT_ACTIVATION_POLICY` (the policy AppKit launches with),
65
+ `REACT_X11_CALAYERS_PATH` (where the bridge's addon is, for a
66
+ [compiled binary](#running-as-an-app-bundle)), and
67
+ [`REACT_X11_NO_STROKE_CHUNKING=1`](#stroking-a-path-with-many-subpaths) and
68
+ [`REACT_X11_NO_SHADOW_TILES=1`](#a-shadow-from-a-tile).
69
+
70
+ The `desktop` option defaults to `{ a11y: false, globalMenu: false }` here:
71
+ AT-SPI and the D-Bus menu export have nothing to register against on a Mac.
72
+ The menu bar is [the system's own](#menus-the-global-menu-finally-at-home)
73
+ regardless.
74
+
75
+ ## Windowing semantics: what maps, what bends, what breaks
76
+
77
+ macOS has no separate window manager, so what one does is where the backends
78
+ part company. The per-prop list is
79
+ [elements.md](elements.md#on-the-cocoa-backend); in short:
80
+
81
+ - **Maps**: `<window>` is an `NSWindow` that places itself (`x`/`y` work),
82
+ min/max sizes are its own, and its size is a whole number of points,
83
+ rounded up. `resizable` and `decorations` are read once, at creation.
84
+ `fullscreen`, `maximized`, `hidden` and `demands_attention` are the
85
+ full-screen Space, the zoom, the Dock and the Dock bounce.
86
+ - **Better**: `transparent` and a window's `borderRadius` always work —
87
+ every window composites, so `useSupports('transparency')` is always true.
88
+ - **Inert**: the `sticky`, `below`, `skip_taskbar` and `shaded` states;
89
+ `appId`, `windowType`, `gravity` and the increment and aspect hints (bundle
90
+ identity lives in `Info.plist`); `transientFor`, for now — a managed
91
+ `<Dialog>` floats on its own window anyway. `onClientMessage` never fires.
92
+ - **`<popup>`** is a borderless panel at the popup level, placed by the same
93
+ anchor math. `grab` bends: there are no pointer grabs, so a press in
94
+ another of the app's windows reaches the popup as a press outside it,
95
+ which dismisses it. `grabKeyboard` makes a key-capable window, as
96
+ `NSPopover` does, which closes on `onBlur` ([elements.md](elements.md)).
97
+ - **`<glarea>`** is a CGL context rendering into IOSurfaces on a sublayer
98
+ ([gl.md](gl.md)); **`<Frame>`** works through shared surfaces rather than
99
+ reparenting ([frame.md](frame.md)).
100
+ - **Screens** (`useScreens`) map, but macOS sends a client no event for a
101
+ display change, so the layout is re-read on a clock while watched
102
+ ([system.md](system.md#on-macos-the-layout-is-asked-for-not-announced)).
103
+ `app.scale` is the screen's `backingScaleFactor`, read once, at startup.
104
+ - **Not yet**: nested `<window>` elements throw.
105
+ - **Gone by design**: `<foreign>` and XEmbed, a window manager
106
+ (`examples/wm.jsx`), `ssh -X`. A `<foreign>` reports one `onError` and
107
+ draws an empty box; `useSupports('embedding')` is false.
108
+
109
+ **Inert means inert, not fatal, and silent**: a prop with no meaning here is
110
+ accepted and does nothing, so the same JSX runs on every backend and shared
111
+ application code stays branch-free.
112
+
113
+ ## Desktop integrations: what an app does outside its own windows
114
+
115
+ Most of these are [filedialog.md](filedialog.md)'s ladder: the platform's
116
+ mechanism, a shell-out beneath it, and a typed "cannot do this here" at the
117
+ floor.
118
+
119
+ - **File open/save**: `NSOpenPanel`/`NSSavePanel`, a sheet on the window that
120
+ asked ([filedialog.md](filedialog.md)).
121
+ - **Raise**: `activateWindow()` activates the app and orders the window front.
122
+ - **Deep links, documents, reopen**: Apple Events into
123
+ `useAppOpen`/`useAppActivate`; Quit is the primary window's close request;
124
+ single instance is the platform default
125
+ ([uri-schemes.md](uri-schemes.md#on-macos)).
126
+ - **Drag and drop with other apps**: AppKit's dragging behind the same
127
+ `dropAccept`/`onDrag*`/`dragData` props
128
+ ([drag-and-drop.md](drag-and-drop.md#interoperating-with-other-applications)).
129
+ - **Clipboard**: `NSPasteboard.general` as `CLIPBOARD`; `watch()` polls.
130
+ No `PRIMARY`: selecting copies nothing ([clipboard.md](clipboard.md)).
131
+ - **Tray**: `useTray()` over `NSStatusItem`, icon names as SF Symbols
132
+ ([desktop.md](desktop.md#the-tray)).
133
+ - **Dock**: `setBadge`/`useBadge`, the `demands_attention` bounce,
134
+ `useLauncherMenu`; the name and activation policy are the `cocoa` options
135
+ ([desktop.md](desktop.md#the-launcher-a-badge-attention-and-the-dock-menu)).
136
+ - **Permissions**: `usePermission()`/`requestPermission()` over TCC, and
137
+ `openPrivacySettings`
138
+ ([permissions.md](permissions.md#what-each-kind-actually-does-on-macos)).
139
+ - **Notifications**: `notify()` over `UNUserNotificationCenter`, only in a
140
+ **code-signed app bundle**; `osascript` banners otherwise
141
+ ([notifications.md](notifications.md)).
142
+ - **Calendar**: `useDesktopCalendarEvents()` over EventKit
143
+ ([desktop-calendar.md](desktop-calendar.md)).
144
+ - **Eyedropper**: `pickScreenColor()` over `NSColorSampler`; the system draws
145
+ the loupe, so no Screen Recording grant is needed. A bridge older than 0.9
146
+ has no rung, and `useEyedropper().supported` is false
147
+ ([eyedropper.md](eyedropper.md)).
148
+ - **Idle time**: `useIdle()`, from the bridge or `ioreg`
149
+ ([system.md](system.md#useidle-and-usekeepawake--is-anyone-there)).
150
+ - **Appearance**: dark or light, the accent, and the desktop's whole palette
151
+ ([appearance.md](appearance.md#the-desktops-palette)).
152
+ - **Keep-awake**: no macOS rung — `useKeepAwake()` holds nothing here.
153
+ - **Accessibility**: none yet ([below](#not-supported-yet)).
154
+
155
+ ## Text
156
+
157
+ Text goes through CoreText behind the same API: the same `<text>` measures,
158
+ wraps, elides, selects and hit-tests on both backends, with the platform's
159
+ metrics, which differ from X11's. Layout can wrap differently across
160
+ backends; compare structure and behaviour across them, not pixels.
161
+
162
+ - **Fonts are the system's.** The CSS generic families map to system faces
163
+ (`sans-serif` is San Francisco), `opsz` is CoreText's own optical sizing,
164
+ and `-apple-system` and `BlinkMacSystemFont` read as `system-ui`, so a font
165
+ stack written for the web sets in SF. Labels and emoji measure as AppKit
166
+ sets them.
167
+ - **OpenType features** (`fontVariantNumeric`, `fontFeatureSettings`) and
168
+ `letterSpacing` work; `letterSpacing` turns optional ligatures off, as on
169
+ X11. A bridge too old for one draws the plain face and says so once in
170
+ development.
171
+ - **Lines break** at the same UAX#14 opportunities as on X11, so a `<text>`
172
+ in a flex item shrinks to the same min-content width; one that elides
173
+ (`textOverflow: 'ellipsis'`) floors at its `…`. An empty paragraph is one
174
+ empty line, so an empty field's caret has a line's height.
175
+ - **Antialiasing is greyscale**, as macOS has drawn it since Mojave.
176
+ - **Glyph-level drawing** (`drawGlyphs`, `positioned`, a face's
177
+ `glyphIdFor`/`advanceOf`/`shape`/`metrics`, `fallbackFor`) answers ntk's
178
+ contract, so a renderer written against ntk's context runs unchanged.
179
+ Three differences: `shape()` substitutes fallback faces, so an emoji in
180
+ Menlo comes out as the emoji where ntk shapes `.notdef`; every op draws as
181
+ `Over`; and a transform scales glyphs as well as their origins.
182
+ - **`coverage()`** is the signed-area fill X11 uses, so both give the same
183
+ bytes for the same outlines; colour emoji come out as silhouettes. A bridge
184
+ without the verb answers `null`.
185
+ - **No input method yet**: dead keys, Press-and-Hold accents and CJK input
186
+ wait on `NSTextInputClient` ([events.md](events.md#input-methods) says what
187
+ a dead key types meanwhile).
188
+
189
+ ## Input and the run loop
190
+
191
+ - **Pointer**: top-left coordinates, `clickCount` as `detail`, and a
192
+ trackpad's precise deltas as the wheel's `smooth` flag.
193
+ - **Hover belongs to the active window**: a background window gets no
194
+ `:hover`, enter, leave or motion until it is active, the platform's
195
+ convention ([events.md](events.md#when-the-response-is-painted)). A first
196
+ click on a background window still acts, and the wheel still scrolls it.
197
+ - **Modifiers**: ⌘ is `metaKey`, ⌥ is `altKey`. ⌘ is the app's primary
198
+ modifier (`app.primaryModifier` is `'Super'`), so the text controls answer
199
+ ⌘Z/⌘C/⌘V, their edit menu prints ⌘C, and a chord written with `Control`
200
+ means ⌘ ([events.md](events.md#primary-modifier)).
201
+ - **Keys** arrive as the same `XK_*` keysyms as on X11, and shortcuts keep
202
+ their Latin letters under a non-Latin layout. The lock state is readable
203
+ before the first key.
204
+ - **Frames** are paced per window on its own display. A window that is
205
+ ordered out, miniaturized or entirely covered by another app's window
206
+ paints nothing — its frames and `requestAnimationFrame` callbacks wait —
207
+ and gets one catch-up frame when it is back. A window fed off a stream (a
208
+ terminal under `cat`) paints every refresh unless it sets
209
+ `frameRate: 'adaptive'`
210
+ ([elements.md](elements.md#framerate--pacing-the-frames-under-a-flood)).
211
+
212
+ ## JS on a worker: a UI thread of the bridge's own
213
+
214
+ AppKit owns the process main thread and runs its menus, drags, live resizes
215
+ and modal panels there. So on macOS react-x11 **moves the app's JS onto a
216
+ `worker_threads` Worker** by default, and the main thread sits in a real
217
+ `[NSApp run]`: the JS keeps running through every menu and resize, and input
218
+ reaches it as it happens rather than at the next pump. It needs a bridge with
219
+ `runMain()` (`@windowkit/appkit` 0.10).
220
+
221
+ `node app.js` and `bun app.jsx` move with nothing asked for, and so does
222
+ `tsx app.jsx` on Node 24 and later. The launcher moves the app before its
223
+ entry is loaded at all, which is cheaper:
224
+
225
+ ```sh
226
+ node --import react-x11/cocoa-main app.js
227
+ bun --preload react-x11/cocoa-main app.jsx
228
+ ```
229
+
230
+ The app **stays on the main thread**, with the pump, off macOS; with
231
+ `REACT_X11_THREADED=0` or `REACT_X11_BACKEND=x11`; in a REPL or under `-e`;
232
+ in a single executable (a Node SEA, `bun build --compile`); in a `<Frame>`
233
+ pane; under a test runner; when react-x11 is imported after the app started
234
+ running (an `import()` the entry awaits, or one from a timer); and on Node 20
235
+ and 22 under an ES module loader (`--import`, `--loader`, tsx). There a menu,
236
+ a drag, a live resize or a modal panel stops the JS while it is open. The
237
+ startup cost of each route and the matrix by Node version are in
238
+ [packaging.md](packaging.md#on-macos-the-app-moves-onto-a-worker).
239
+
240
+ **A menu-bar app sets its policy at launch.** On a worker AppKit is up before
241
+ the app's code runs, so `cocoa: { activationPolicy }` can only switch it
242
+ afterwards, when a Regular launch has already shown a Dock tile. Launch with
243
+ the variable, or put `LSUIElement` in the bundle's `Info.plist`; an app whose
244
+ option disagrees with its launch is told so in development.
245
+
246
+ ```sh
247
+ APPKIT_ACTIVATION_POLICY=accessory node --import react-x11/cocoa-main app.js
248
+ ```
249
+
250
+ ### What changes for an app
251
+
252
+ - About **16 MB** of RSS for the worker's isolate, and **26–31 ms** before
253
+ the worker runs its first line.
254
+ - The idle app stops waking 125 times a second: the pump is an 8 ms interval
255
+ whether anything happens or not.
256
+ - Inside a Worker, `process.chdir()` throws, `process.on('SIGINT')` is never
257
+ called (the launcher forwards signals: a signal is re-emitted on the
258
+ worker's `process`, and one nobody listens for exits with 128 + n),
259
+ `process.env` is a copy unless it is shared, and `process.stdin` forwards
260
+ through the main thread the way stdout does.
261
+ - **What the entry imports ahead of react-x11 runs twice**: on the main
262
+ thread, then on the worker. A module that does work as it loads — opens a
263
+ port, writes a file, logs — does it twice. A call only a main thread may
264
+ make — `process.chdir()`, `process.umask(mask)` — goes through the first
265
+ time and throws the second, and the app stops with that error and a word
266
+ on why. Make it on the main thread only (`isMainThread`, from
267
+ `node:worker_threads`): what it changes is the process's, so the worker
268
+ starts with it. One made after the import runs on the worker alone and
269
+ throws there. Import react-x11 first in the entry, or set
270
+ `REACT_X11_THREADED=0` to keep the whole app on the main thread.
271
+ - `process.exit()` and an uncaught error end the app as they would a process
272
+ of its own: exit handlers run, and the error is printed with exit code 1.
273
+ - `--inspect` attaches to the main thread; the app is the worker target
274
+ beside it.
275
+ - A single executable does not move, and runs on the pump
276
+ ([packaging.md](packaging.md)).
277
+
278
+ Not done yet on a worker:
279
+
280
+ - A dropped image, or any binary type, reads as nothing: the drop carries its
281
+ text and URL forms.
282
+ - The layer presenter's colour transitions start from the declared value.
283
+ - A transparent window's shadow is recomputed a frame interval after a flip,
284
+ not once the flip has committed.
285
+ - A worker that cannot load the entry stops the app: under tsx before 4.21.1
286
+ on Node 24 and later, or tsx on Node 20 and 22 under the launcher, it stops
287
+ as it starts on `ERR_UNKNOWN_FILE_EXTENSION`. Update tsx, or set
288
+ `REACT_X11_THREADED=0`.
289
+ - Under Bun, react-x11 required from a callback before the entry has
290
+ finished — a `nextTick` or microtask a CommonJS entry queues, a timer while
291
+ an ES module entry awaits at top level — reads as the entry's first run,
292
+ and that code runs again on the worker.
293
+ - Not yet tried on the worker: `<glarea>`'s GL context and
294
+ `react-x11/refresh`'s module hooks.
295
+
296
+ ## Native controls
297
+
298
+ `Button`, `Checkbox`, `Radio`, `Switch`, `Slider` and `Select` wear
299
+ **AppKit's own bezels** here by default; interaction, focus, keyboard and
300
+ accessibility stay the shared implementation. A bezel is an image drawn
301
+ through the ordinary paint path, not an embedded `NSView`
302
+ ([components.md](components.md#native-bezels)).
303
+
304
+ - `ThemeProvider`'s `controls`: `'auto'` (the default) takes bezels wherever
305
+ the backend has them — Cocoa, and win32 while the desktop is light — and
306
+ draws elsewhere; `'native'` asks outright, and on a backend without them
307
+ warns once and draws; `'drawn'` is this library's rendering everywhere.
308
+ `native={false}` on one control is the escape hatch.
309
+ - `useSupports('nativeControls')` says whether the backend has them.
310
+ - A native control is **two boxes**: the footprint `style` sizes, and the
311
+ control at AppKit's metrics, centred in it. Role, handlers, focus ring and
312
+ ref are on the control, so a press in the slack around it does nothing.
313
+ - A `<Button>` label **wraps** when its row is too narrow, and the control
314
+ grows with it; past one line it is AppKit's flexible push button, whose
315
+ default-button accent is flat. A bridge too old for that bezel keeps the
316
+ push and says so once in development.
317
+ - Text controls stay drawn: a native `NSTextField` would fork the shared
318
+ editing model.
319
+ - Drawn content sits beside the bezels through the desktop's palette
320
+ ([appearance.md](appearance.md#the-desktops-palette)).
321
+
322
+ ## Menus: the global menu, finally at home
323
+
324
+ **The menu bar is the system's, always.** A window's `menus` go to the macOS
325
+ menu bar as soon as they are installed, the drawn bar never renders, and
326
+ `onGlobalMenuChange(true)` reports it ([globalmenu.md](globalmenu.md)). Item
327
+ icon names are SF Symbols.
328
+
329
+ - An **application menu** goes in front of the app's, carrying Quit (⌘Q).
330
+ Quit is a quit request: the primary window's close request, which
331
+ `onCloseRequest` can intercept, then, with `exitOnQuit`, the end of the
332
+ process. Nothing else is synthesized — no About, Hide, Edit or Window
333
+ menu — so an app that wants them names them in `menus`.
334
+ - **Accelerators**: `Control` in a chord is ⌘, so `[['Control', 'S']]` is ⌘S;
335
+ there is no separate `Primary` token. The menu bar answers the chords it
336
+ can spell — one chord, one character key — and reserves them so no window
337
+ binding fires them twice; the rest (F5, `plus`, a second alternative) are
338
+ matched by the window ([events.md](events.md#the-macos-menu-bar)).
339
+ - Where native controls render, `<ContextMenu>`, a `<MenuBar>`'s dropdowns
340
+ and a `<Select>`'s drawn list are **sized as NSMenu is**: 22pt rows, 5pt
341
+ padding, 11pt separators, the 13pt menu font.
342
+ - **A `<Select>`'s list is the real NSMenu**, dropped from the trigger as a
343
+ browser drops a `<select>`'s; `nativeMenu` chooses
344
+ ([components.md](components.md)).
345
+ - The Dock menu and a tray item's menu take the same item vocabulary
346
+ ([desktop.md](desktop.md)).
347
+
348
+ ## Custom drawing on a layer tree
349
+
350
+ `<canvas onDraw>`, `<svg>` and a registered element's `paint(ctx)` run
351
+ unchanged, through a CoreGraphics context that speaks the whole dialect:
352
+ canvas2d plus `fillRects`, `drawGlyphs`, `positioned`, `layoutSubtree` and
353
+ `vw`/`vh`/`em`.
354
+
355
+ **Offscreen surfaces.** `react-x11/ntk`'s `Surface` is a CoreGraphics bitmap
356
+ here, in device pixels. Unlike ntk:
357
+
358
+ - A surface has **one context for its life**: `getContext('2d')` answers the
359
+ same object every time, its `destroy()` is a no-op, and `render()` brackets
360
+ its callback in `save`/`restore` from the identity.
361
+ - `format: 'a8'` **throws**: allocate `argb32` and tint through
362
+ `fillStyle`/`globalAlpha`.
363
+ - `Picture`, `<image picture>` and `<image drawable>` are X server
364
+ resources: they draw nothing here and warn once in development. An `Image`
365
+ works.
366
+
367
+ **Context properties** that differ or matter here:
368
+
369
+ - **`globalCompositeOperation`** is canvas's whole set, including blend
370
+ modes (`multiply` and the rest) that X11 lacks. An op applies inside what
371
+ the draw covers, as on X11 and unlike a browser's `copy`. A bridge without
372
+ the verb refuses all but `source-over`: assign, then read it back. A
373
+ `drawGlyphs` call's `op` is ignored.
374
+ - **`'copy'` plus `drawImage(surface, x, y)`** at the surface's own size, a
375
+ whole-pixel translate, no alpha, no shadow and at most a rectangular clip
376
+ is a row memcpy, not a blend — the cheap way to present a surface every
377
+ frame (a 125×45 terminal grid in 0.42 ms rather than 1.37). Anything else
378
+ is drawn, with the same result.
379
+ - **`globalAlpha`** below 1 on a surface draw is cheap where
380
+ `ctx.fadesSurfacesCheaply` is true.
381
+ - **`imageSmoothingEnabled`/`imageSmoothingQuality`** are honoured; a context
382
+ starts at `'medium'`, not canvas's `'low'`. `'low'` makes a surface drawn
383
+ tile by tile through a matrix far cheaper (784 tiles: 200 ms → 27 ms).
384
+ - **`filter`** applies the colour functions (`grayscale()`, `sepia()`,
385
+ `saturate()`, `hue-rotate()`, `invert()`, `brightness()`, `contrast()`),
386
+ `opacity()`, and `blur()` on drawn images only; `drop-shadow()` and `url()`
387
+ do not stick. Read `filter` back to know.
388
+ - **`clip(rule)`** honours `'evenodd'`; **`roundRect`** takes canvas's radii,
389
+ `{ x, y }` corners included; **`createPattern`** is not supported.
390
+
391
+ An element can hand parts of its drawing to the render server as `sprites()`
392
+ ([extending.md](extending.md#parts-of-your-drawing-on-layers-of-their-own)).
393
+ One that composites its own surface over its box every frame — a terminal, a
394
+ chart on a socket — pays that composite and a copy behind it; a layer of the
395
+ element's own is [designed, not built](../design/architecture/element-layer-contents.md).
396
+
397
+ ### Stroking a path with many subpaths
398
+
399
+ CoreGraphics strokes in time quadratic in a path's subpaths, so the context
400
+ splits a large stroke at subpath boundaries: a path of 4,000 rings strokes
401
+ in 113 ms rather than 986. Left whole: a hairline (1 device pixel or less)
402
+ and anything that composites — a translucent `strokeStyle`, `globalAlpha`
403
+ below 1, a shadow. The one visible difference: where two subpaths' strokes
404
+ overlap, the antialiased fringe reads a little lighter.
405
+ `ctx.strokeChunking = false` turns it off for one context,
406
+ `REACT_X11_NO_STROKE_CHUNKING=1` for the process. `fill` is never split.
407
+
408
+ ### A shadow from a tile
409
+
410
+ On macOS and Windows the context draws the shadow of a rect, a `roundRect`,
411
+ a `fillRect` or an inset frame of two from a **tile** made once and
412
+ stretched, shared by every card with the same shadow — a page header's inset
413
+ shadow went from 130–380 ms to 4 ms.
414
+
415
+ - A tiled shadow is the canvas spec's gaussian (σ half the blur, three σ
416
+ wide), as Chrome and X11 draw it. Other shapes, rotations and composite
417
+ ops get CoreGraphics' own, which is a little narrower.
418
+ - `<box boxShadow>` draws on macOS and Windows.
419
+
420
+ `REACT_X11_NO_SHADOW_TILES=1`, or `ctx._shadowTiles = false` on one context,
421
+ puts every shadow back on CoreGraphics.
422
+
423
+ ## Animations and transforms
424
+
425
+ `transition` and `animation` styles work as everywhere. Where a layer can
426
+ express the property — a plain `<box>`'s `backgroundColor`, `borderColor`,
427
+ `borderWidth`, `borderRadius` and `opacity` — the **render server runs the
428
+ animation**: no JS frames, and it keeps moving while the JS thread is blocked
429
+ (`examples/animation.jsx` blocks it for two seconds). Everything else — text
430
+ colour, layout properties, a node that paints as a raster — runs on the frame
431
+ clock exactly as on X11.
432
+
433
+ - **Timing** matches the frame clock's easings.
434
+ - **Interruption**: a length retargets smoothly from where it is; a colour
435
+ restarts from the value on screen.
436
+ - **Not offloaded**: `boxShadow`. `transform` is not a style property on any
437
+ backend yet.
438
+
439
+ On the default presenter the offload is
440
+ [layer promotion](#layer-promotion-the-animated-few-on-their-own-layers-above-the-surface-presenter).
441
+ `cocoa: { presenter: 'layers' }` gives every drawn node a CALayer instead; it
442
+ is opt-in while it is measured, and a pan there re-rasters the whole scene.
443
+
444
+ ## Layer promotion: the animated few on their own layers above the surface presenter
445
+
446
+ The default presenter keeps one bitmap per window and gives the few nodes
447
+ that animate a CALayer of their own above it while they do. Hit testing never
448
+ moves: promotion changes pixels and nothing else. With no prop to set, a node
449
+ is promoted when it is
450
+
451
+ - a plain `<box>` with a transition or a loop on `backgroundColor`,
452
+ `borderColor`, `borderWidth`, `borderRadius` or `opacity` — for as long as
453
+ it has one and a second after; or
454
+ - a box out of the flow that no pointer lands on (`position: 'absolute'`,
455
+ `pointerEvents: 'none'`) with a loop on an inset, which moves it and
456
+ nothing else — `<ProgressBar indeterminate>`'s block costs no frames;
457
+ - and **only where nothing paints over it**: no later sibling's ink in its
458
+ bounds, no ancestor's border or focus ring, no ancestor that fades. A
459
+ clipping ancestor, round corners included, clips the layer. A node in a
460
+ pane that scrolled in the last 250 ms waits until the pane stops.
461
+
462
+ A node that stops qualifying returns to the bitmap in the frame that finds
463
+ it, and its animation carries on on the frame clock from where it was. A
464
+ scrollbar thumb over promoted rows goes on a layer of its own.
465
+
466
+ It costs a frame to start, one to come back a second after the last
467
+ animation ends, and meanwhile a CALayer, plus for a node with children a
468
+ bitmap the size of their reach. `cocoa: { promote: false }` or
469
+ `REACT_X11_COCOA_PROMOTE=0` keeps every animation on the frame clock; `true`
470
+ or `=1` forces it on whatever the bridge (it is on by default where the
471
+ bridge reports sRGB colours, 0.5.1 and later).
472
+
473
+ **On a wide-gamut display** (a MacBook's built-in panel, a monitor with its
474
+ own profile) every window is colour-managed, as a browser manages a page, so
475
+ it looks less saturated than an unmanaged one. Opaque colours agree to the
476
+ pixel between bitmap and layer; translucent ones differ slightly, because
477
+ Core Animation blends in the display's space. `window.snapshot()` reads the
478
+ display's numbers and carries its profile, so a script comparing raw
479
+ snapshot pixels with CSS colours sees a shift.
480
+
481
+ Design notes and measurements:
482
+ [design/macos.md](../design/macos.md#layer-promotion-the-animated-few-on-their-own-layers-above-the-surface-presenter).
483
+
484
+ ### Video on a layer
485
+
486
+ A [`<video>`](elements.md#video) goes on a layer of its own under the same
487
+ rule: nothing painted over it, no fade, no rounded clip that cannot hold it.
488
+
489
+ - **`<video frames>`**: lifted, each pushed frame goes straight to a surface
490
+ the render server shows, at no cost to the window; declined, the element
491
+ draws the newest frame into the window. The colours are `AVPlayerLayer`'s.
492
+ Over a bridge without the video-surface verbs the frame is converted in
493
+ JavaScript, and `app.videoFormats()` lists `BGRA` first.
494
+ - **`<video src>`** plays through `AVPlayer`: lifted, its player layer shows
495
+ every frame at no cost to the window; covered, each new frame is copied
496
+ into the window. Over a bridge without the player verbs
497
+ `useSupports('mediaPlayback')` is false and `src` refuses, as on X11. In a
498
+ `<Frame>` pane the host plays the video and sends the frames down.
499
+
500
+ ## Running as an app bundle
501
+
502
+ AppKit names, icons and identifies an app by the **bundle its executable
503
+ lives in**. Run from the shell, the menu bar says "node", the Dock shows the
504
+ generic icon, the defaults domain (window frames, the tab bar) is shared with
505
+ every script that runtime ever ran, and the notification centre refuses the
506
+ app. `cocoa: { appName }` fixes the name alone; a bundle fixes all of it.
507
+ `examples/form/` is the example:
508
+
509
+ ```sh
510
+ npm run examples:form:app # builds examples/form/build/Guestbook.app and opens it
511
+ ```
512
+
513
+ [`make-app.sh`](../examples/form/make-app.sh) makes the executable with
514
+ `bun build --compile` at `Contents/MacOS/Guestbook`: AppKit takes the main
515
+ bundle from the running executable's path, so it must be a real binary, not
516
+ a script.
517
+
518
+ - **The bridge's addon cannot be compiled in.** The bundle carries it as
519
+ `Contents/Resources/calayers.node`, and the entry,
520
+ [`main.js`](../examples/form/main.js), points `REACT_X11_CALAYERS_PATH` at
521
+ it relative to `process.execPath` before importing the app, so the bundle
522
+ can be moved.
523
+ - **`Info.plist`** carries the name, the identifier, the icon,
524
+ `NSHighResolutionCapable` (without it AppKit renders at 1×) and
525
+ `LSEnvironment` naming the Cocoa backend. `lsappinfo` then reports the
526
+ process under the app's name and identifier.
527
+
528
+ **The tab bar under bun.** Unbundled, a `bun` app's windows live in the
529
+ `bun` defaults domain, and once anything turns the tab bar on there (View →
530
+ Show Tab Bar, a stray shortcut) every window the bridge opens wears a 28pt
531
+ tab bar and every popup lands that much off. Clear it, or run as a bundle:
532
+
533
+ ```sh
534
+ defaults delete bun "NSWindowTabbingShoudShowTabBarKey-NSWindow-CALBackendDelegate-(null)-VT-FS"
535
+ ```
536
+
537
+ ### Distributing the bundle
538
+
539
+ `make-app.sh` builds a developer's bundle, which runs where it was built;
540
+ [packaging.md](packaging.md#tier-5--a-macos-app) has the recipes. What
541
+ differs on macOS:
542
+
543
+ - **Signing.** A downloaded app needs a Developer ID signature with the
544
+ hardened runtime, then notarization. Sign inside-out, not with `--deep`:
545
+ `calayers.node` first, without entitlements, then the app with Bun's five
546
+ entitlements ([packaging.md](packaging.md#developer-id-or-the-app-store)).
547
+ For a colleague, ad-hoc signing (`--sign -`) and
548
+ `xattr -dr com.apple.quarantine` on their end are the short path.
549
+ - **The Mac App Store** requires App Sandbox, under which a
550
+ `bun build --compile` binary does not start. A Node single executable in
551
+ the same bundle shape does, so that is the store build; Bun is fine for
552
+ Developer ID.
553
+ - **Architecture.** A compiled binary and the bridge's prebuild are each for
554
+ one CPU; a universal bundle is two builds and `lipo`, for the executable
555
+ and `calayers.node` alike.
556
+ - **Size.** About 70 MB with bun, nearly all of it the runtime; 142 MB as a
557
+ Node single executable.
558
+
559
+ ## Testing
560
+
561
+ `react-x11/test` drives the **X11 backend on every platform, macOS
562
+ included**; there is no `'cocoa'` backend for `renderX11()`. Test the tree,
563
+ queries and behaviour there, which are shared. Pixels are per-backend
564
+ (CoreText is not ntk's rasterizer), so native bezels, the `NSMenu` and
565
+ pasteboard flavours are checked by running on a Mac
566
+ ([testing.md](testing.md)). A window's `snapshot()` resolves to its
567
+ composited pixels, in the display's colour profile.
568
+
569
+ ## Not supported yet
570
+
571
+ - **VoiceOver**: no `NSAccessibility` bridge, and AT-SPI is off here, so a
572
+ Cocoa app is not reachable by assistive technology yet. Write roles and
573
+ labels anyway; the bridge will read them ([accessibility.md](accessibility.md)).
574
+ - **Input methods**: no `NSTextInputClient` — no marked text, Press-and-Hold
575
+ accents or CJK input ([events.md](events.md#input-methods)).
576
+ - **Nested `<window>` elements** throw; a `<glarea>` is the only child
577
+ surface.
578
+ - **`Surface` `format: 'a8'`** throws — allocate `argb32` and tint through
579
+ `fillStyle`/`globalAlpha`.
580
+ - **`createPattern`**, and the X-only `Picture`, `<image picture>` and
581
+ `<image drawable>`.
582
+ - **The eyedropper over a bridge older than 0.9**: `useEyedropper().supported`
583
+ is false — leave the eyedropper button undrawn.
584
+ - **`useKeepAwake()`**: no macOS rung.
585
+ - **`transform`** as a style property (on any backend).
586
+ - **`transientFor`** and the `sticky`, `below`, `skip_taskbar` and `shaded`
587
+ states: inert.
588
+ - **A `'cocoa'` backend for `react-x11/test`.**
589
+ - **Dropped images** while the app runs on a worker.
590
+ - **Never, by design**: `<foreign>` and XEmbed, a window manager
591
+ (`examples/wm.jsx`), remoting over `ssh -X` — run the X11 backend through
592
+ XQuartz for those.