react-x11 2.51.0 → 2.52.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 (91) hide show
  1. package/README.md +87 -54
  2. package/docs/README.md +410 -0
  3. package/docs/accessibility.md +628 -0
  4. package/docs/appearance.md +416 -0
  5. package/docs/architecture/accessibility.md +470 -0
  6. package/docs/architecture/animation.md +707 -0
  7. package/docs/architecture/container-queries.md +525 -0
  8. package/docs/architecture/custom-layout.md +334 -0
  9. package/docs/architecture/drag-and-drop.md +941 -0
  10. package/docs/architecture/element-layer-contents.md +622 -0
  11. package/docs/architecture/frame-pacing.md +255 -0
  12. package/docs/architecture/grid-layout.md +358 -0
  13. package/docs/architecture/protocol-efficiency.md +357 -0
  14. package/docs/architecture/video.md +1149 -0
  15. package/docs/architecture/windowed-regions.md +854 -0
  16. package/docs/click-to-component.md +125 -0
  17. package/docs/clipboard.md +281 -0
  18. package/docs/components.md +1749 -0
  19. package/docs/dbus.md +277 -0
  20. package/docs/debugging.md +464 -0
  21. package/docs/desktop-calendar.md +284 -0
  22. package/docs/desktop.md +648 -0
  23. package/docs/devtools.md +250 -0
  24. package/docs/drag-and-drop.md +663 -0
  25. package/docs/ecosystem/animation.md +394 -0
  26. package/docs/ecosystem/data-fetching.md +201 -0
  27. package/docs/ecosystem/dev-tooling.md +436 -0
  28. package/docs/ecosystem/forms.md +313 -0
  29. package/docs/ecosystem/headless.md +219 -0
  30. package/docs/ecosystem/i18n.md +89 -0
  31. package/docs/ecosystem/icons.md +410 -0
  32. package/docs/ecosystem/layout.md +216 -0
  33. package/docs/ecosystem/routing.md +146 -0
  34. package/docs/ecosystem/state.md +361 -0
  35. package/docs/ecosystem/testing.md +500 -0
  36. package/docs/ecosystem/theming.md +540 -0
  37. package/docs/ecosystem.md +473 -0
  38. package/docs/elements.md +2762 -0
  39. package/docs/embedding.md +288 -0
  40. package/docs/events.md +1025 -0
  41. package/docs/extending.md +2242 -0
  42. package/docs/eyedropper.md +223 -0
  43. package/docs/filedialog.md +258 -0
  44. package/docs/frame.md +580 -0
  45. package/docs/gl.md +170 -0
  46. package/docs/globalmenu.md +278 -0
  47. package/docs/glx.md +137 -0
  48. package/docs/macos.md +3295 -0
  49. package/docs/notifications.md +164 -0
  50. package/docs/packaging.md +611 -0
  51. package/docs/permissions.md +148 -0
  52. package/docs/react-features.md +430 -0
  53. package/docs/remote.md +223 -0
  54. package/docs/scale.md +339 -0
  55. package/docs/security.md +132 -0
  56. package/docs/styling.md +1928 -0
  57. package/docs/system.md +599 -0
  58. package/docs/testing.md +514 -0
  59. package/docs/typescript.md +117 -0
  60. package/docs/uri-schemes.md +380 -0
  61. package/docs/wayland-backend.md +591 -0
  62. package/docs/wayland.md +545 -0
  63. package/docs/windows-embedding.md +271 -0
  64. package/docs/windows-gl.md +221 -0
  65. package/docs/windows-integrations.md +233 -0
  66. package/docs/windows.md +1404 -0
  67. package/package.json +10 -3
  68. package/src/bus.js +39 -26
  69. package/src/cli/create.d.ts +43 -0
  70. package/src/cli/create.js +305 -0
  71. package/src/cli/env.js +484 -0
  72. package/src/cli/index.js +29 -2
  73. package/src/cli/templates/AGENTS.template.md +195 -0
  74. package/src/cli/templates/DESIGN.template.md +269 -0
  75. package/src/desktopintegration.js +2 -1
  76. package/src/desktopsettings.js +193 -24
  77. package/src/filedialog.js +3 -3
  78. package/src/keyboardstate.js +28 -0
  79. package/src/node.d.ts +10 -10
  80. package/src/portal.js +31 -0
  81. package/src/screencolor.js +5 -10
  82. package/src/types/system.d.ts +8 -6
  83. package/src/wayland/app.js +59 -0
  84. package/src/wayland/clipboard.js +14 -7
  85. package/src/wayland/connection.js +3 -0
  86. package/src/wayland/context2d.js +129 -10
  87. package/src/wayland/protocols/index.json +6 -0
  88. package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
  89. package/src/wayland/seat.js +28 -1
  90. package/src/wayland/swapchain.js +23 -0
  91. package/src/wayland/xkb.js +137 -0
@@ -0,0 +1,591 @@
1
+ # The Wayland backend
2
+
3
+ **Status: a working backend, opt-in.** react-x11's own reconciler renders a
4
+ window on a real Wayland compositor (mutter/GNOME, sway, labwc) with
5
+ decorations — the compositor's where it draws them, client-side where it
6
+ does not — text, shadows, paths, images, live partial repaints, pointer and
7
+ keyboard input decoded against the compositor's keymap, a clipboard, popups,
8
+ layer-shell docks and panels, and a screen capture with an eyedropper on
9
+ top — drawn on the GPU and handed over as dma-buf, on **Node or Bun**, with
10
+ no X server and no Xwayland anywhere in the path.
11
+
12
+ This is the companion to [wayland.md](wayland.md), the research RFC it
13
+ implements. Where the two disagree, this one is the one that ran.
14
+
15
+ ```bash
16
+ REACT_X11_BACKEND=wayland node your-app.mjs # x11-dri >= 0.9 (native socket)
17
+ REACT_X11_BACKEND=wayland bun your-app.mjs # or Bun's bun:ffi transport
18
+ node examples/wayland-gl.mjs # GPU plasma, paced by the compositor
19
+ node examples/wayland-ui.mjs # every drawing primitive, no React
20
+ node --import tsx --test 'test/wayland/*.test.js' # the suite: no display needed
21
+ ```
22
+
23
+ The two packages it needs beyond react-x11's own are optional dependencies,
24
+ installed with it: [`@windowkit/wayland`](https://github.com/windowkit/wayland),
25
+ the protocol layer — a fork of `wayland-client` that passes file descriptors
26
+ — and [`x11-dri`](https://github.com/sidorares/node-x11-dri) 0.9 or later,
27
+ the GPU context and, on Node, the socket.
28
+
29
+ The backend is never chosen by `'auto'`: X11 stays the default on Linux
30
+ because the remote case ([remote.md](remote.md)) is the flagship reason this
31
+ project exists and Wayland has no network transparency.
32
+
33
+ ## How to tell it is not Xwayland
34
+
35
+ A GNOME session answers `$DISPLAY` too, through Xwayland, so "the window
36
+ appeared" proves nothing. Three checks that do:
37
+
38
+ - **Unset `DISPLAY`.** The Wayland backend never opens it; the X11 backend
39
+ cannot start without it.
40
+ ```bash
41
+ env -u DISPLAY REACT_X11_BACKEND=wayland node app.mjs # works only natively
42
+ ```
43
+ - **Ask the app.** `root.app.backend === 'wayland'`, and `root.app.X` is the
44
+ stand-in from `app.js` — no `display.screen`, no `X.require`. The examples
45
+ print the transport they connected through (`connected via x11-dri` or
46
+ `fdpass-bun`) and never import an X module at all.
47
+ - **Ask the desktop.** `xlsclients` and `xwininfo -root -tree` list Xwayland
48
+ clients; a native window is absent from both. GNOME's Looking Glass
49
+ (`Alt+F2`, `lg`, Windows tab) shows it as a `MetaWindowWayland`, and its
50
+ frame is this backend's own titlebar (`decorations.js`) rather than
51
+ mutter's Adwaita frame, which only Xwayland windows get.
52
+
53
+ ## What runs
54
+
55
+ | piece | file | what it does |
56
+ | ------------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
57
+ | connection | `connection.js` | fd-capable transport (native or bun:ffi), vendored protocol JSON |
58
+ | shell | `window.js` | xdg toplevels and popups, deferred `ack_configure`, fractional scale |
59
+ | GPU presentation | `swapchain.js`, `dmabuf.js`, `glcontext.js`, `target.js` | backing target → per-buffer damage → dma-buf |
60
+ | 2d context | `context2d.js`, `glyphatlas.js` | rects/rounded rects (SDF), paths (coverage masks, stencil), exact clips, gradients, patterns, images, text |
61
+ | `<glarea>` | `glarea.js` | a rect of the window's backing target as the area's GL surface; composited panes for its children |
62
+ | input | `seat.js`, `xkb.js`, `keysymnames.js`, `input.js` | pointer frames, keymap parsing, key repeat, routing |
63
+ | touch and tablet | `touch.js`, `tablet.js` | `wl_touch` and `zwp_tablet_v2`, emulating the pointer; raw touches |
64
+ | decorations | `decorations.js`, `ssd.js` | xdg-decoration or KDE's, to hand the frame to the compositor; the client-side frame where neither |
65
+ | layer surfaces | `layershell.js` | docks, panels, wallpapers, overlays — wlr-layer-shell |
66
+ | screen capture | `screencopy.js`, `shm.js` | wlr-screencopy / ext-image-copy-capture into `wl_shm`; the eyedropper |
67
+ | offscreen surfaces | `surface.js`, `device.js` | the paint cache and scroll blits, over a render target; a held context works |
68
+ | clipboard | `clipboard.js`, `fdutil.js` | `wl_data_device` + primary selection, over pipes |
69
+ | screens | `outputs.js` | `wl_output` + xdg_output into `screens.js`: `useScreens()`, the cap |
70
+ | input methods | `textinput.js` | `zwp_text_input_v3`: the compositor's IME into the composition events |
71
+ | the app | `app.js`, `backendwindow.js`, `framewatch.js` | what `createRoot({ backend: 'wayland' })` renders through |
72
+ | tests | `test/wayland/` | an in-process compositor, and the pure parts |
73
+
74
+ ### Running the examples
75
+
76
+ Every example runs on this backend the way it runs on X11, with the display
77
+ taken away so nothing can fall back:
78
+
79
+ ```bash
80
+ env -u DISPLAY REACT_X11_BACKEND=wayland bun examples/widgets.jsx # or node --import tsx
81
+ ```
82
+
83
+ Two are X11 by nature and say so: `examples/wm.jsx` is a window manager and
84
+ `examples/xeyes.jsx` reads the root window's pointer — neither has a
85
+ Wayland counterpart. `examples/viewer3d.jsx` draws its model here through
86
+ its direct half, GLES on the window's own GPU target — the same half X11
87
+ uses under `NTK_GL_POLICY=direct`.
88
+
89
+ Two environment variables help when a desktop offers no screenshot API:
90
+ `REACT_X11_WAYLAND_TRACE=1` writes a line per presented frame, and
91
+ `REACT_X11_WAYLAND_SNAPSHOT=/tmp/frame.png` writes the backing target of
92
+ the first window at its 30th present (`REACT_X11_WAYLAND_SNAPSHOT_AT`
93
+ changes which) — the pixels the compositor was handed, read back from the
94
+ GPU. A third, `REACT_X11_WAYLAND_FRAME_STALL_MS`, is how long a frame
95
+ callback may go unanswered before the backend says the compositor has
96
+ stopped showing the window (2000; `0` turns that off) — see
97
+ [A surface the compositor stops showing stops drawing](#a-surface-the-compositor-stops-showing-stops-drawing).
98
+
99
+ The GPU context is made on `glPolicy.devicePath`, else on
100
+ `REACT_X11_GL_DEVICE`, else on the first render node, and a device named
101
+ either way is the only one tried. A machine with no render node at all — a
102
+ GitHub-hosted runner, a Hyper-V or VirtualBox guest — usually still has a
103
+ card node Mesa renders on in software through kms_swrast, so the backend
104
+ then tries each `/dev/dri/card*` in turn. On a hosted runner that is
105
+ `card1`, and headless weston with its GL renderer accepts the buffers it
106
+ makes, as `scripts/wayland-ci-probe.mjs` measured.
107
+
108
+ ## What was measured
109
+
110
+ Everything below was run on this machine (mutter, virtio-gpu/virgl over an
111
+ Apple M1 Pro host, 2488×1668 @ 75Hz). Numbers from a VM's GPU are the VM's;
112
+ the protocol numbers are not.
113
+
114
+ ### The protocol layer is not the cost
115
+
116
+ Asked first, because it decides whether a native protocol bridge is
117
+ justified. Measured with `@windowkit/wayland`, per request and per
118
+ event:
119
+
120
+ | measure | Bun | Node (native socket) |
121
+ | ---------------------------------------- | ---------------------------- | ------------------------- |
122
+ | encode + write, async API | 0.32 M req/s | 0.20 M req/s |
123
+ | encode + write, `$` synchronous API | 0.49 M req/s | **0.67 M req/s** (1.5 µs) |
124
+ | decode + dispatch | 2.3 M ev/s | **3.0 M ev/s** (0.33 µs) |
125
+ | round trip `sync` → `done`, plain socket | 0.141 ms p50 | 0.119 ms p50 |
126
+ | round trip, fd-capable socket | 0.178 ms p50 (reader thread) | **0.097 ms p50** |
127
+
128
+ A frame issues a handful of requests and input peaks near a thousand events
129
+ a second: the JavaScript protocol layer is one to two orders of magnitude
130
+ below anything visible. **No native protocol bridge was built, on purpose.**
131
+ The one native piece is the socket itself, and only because Node cannot
132
+ receive descriptors at all; its round trip turned out lower than Node's own
133
+ `net.Socket`'s, and the thread-hop cost of the Bun transport (~37 µs) is real
134
+ but not worth acting on.
135
+
136
+ ### Where a frame goes
137
+
138
+ | phase | p50 |
139
+ | ---------------------------------- | ------- |
140
+ | `makeCurrent` | 0.04 ms |
141
+ | draw | 0.05 ms |
142
+ | Wayland (attach / damage / commit) | 0.09 ms |
143
+ | **`Surface.swap()`** | 24.5 ms |
144
+
145
+ The cost is `swap()`, and it is environmental: with no compositor at all,
146
+ `swap()` is 0.07 ms and `glFinish()` is ~10 ms — the same at 64×64 as at
147
+ 720×480. Size-independent means host round-trip latency, not fill rate; this
148
+ is virgl in a VM. On bare metal the number should mostly disappear and must
149
+ be re-measured there before any conclusion about the backend's performance is
150
+ drawn from it.
151
+
152
+ The 2d context on the UI demo: 252 quads, 239 glyphs and 2 paths in **23
153
+ draw calls**.
154
+
155
+ ## The design, where it is not the obvious one
156
+
157
+ **A backing target, not the swapchain, is what gets painted.** react-x11's
158
+ renderer repaints dirty rectangles into a persistent surface, as it would an
159
+ X pixmap. A GBM swapchain rotates, so the buffer a frame lands in last showed
160
+ the frame before last, and a partial repaint into it leaves stale pixels
161
+ everywhere else. So the renderer paints into a persistent render target
162
+ (`target.js`) and the changed rectangles are copied into whichever buffer
163
+ comes round. GBM names the buffer only _after_ the swap, so the copy uses the
164
+ union of what every known buffer still owes — and everything, until a full
165
+ rotation has passed without meeting a new buffer. The tests pin the rule
166
+ (`swapchain.test.js`); a React counter ticking at 400 ms presents frames
167
+ with **one** damage rectangle.
168
+
169
+ **Analytic shapes first, stencil second.** Rounded rectangles — most of a
170
+ UI — are signed distance fields in the fragment shader: antialiased without
171
+ multisampling, and every box in a window batches into one draw. Arbitrary
172
+ paths (`<svg>`, `ctx.fill(path2d)`) are flattened by ntk's own `flattenPath`
173
+ and filled stencil-then-cover on the target's stencil attachment. Strokes
174
+ are the union of segment quads and join polygons under the non-zero rule.
175
+ Box shadows are a feathered SDF (`ctx.fillShadow`, which `boxpaint.js` now
176
+ prefers when a context has it) rather than a CPU blur through the paint
177
+ cache.
178
+
179
+ **An image in perspective is one quad.** `drawImageProjected(image, m)`
180
+ draws an image through a 3×3 projection — a plane turned out of the page,
181
+ as CSS `rotateY()` in a `perspective` turns one — with each corner's texture
182
+ coordinates over its w and the fragment dividing them back, which is exact
183
+ across the quad. A context that can only draw through an affine matrix has
184
+ to cut such a plane into tiles, each drawn through the matrix nearest the
185
+ projection over it and clipped to it, and here every tile is a batch of its
186
+ own because the clip is a scissor: `<Html>` drew Zen Garden 219's sidebar
187
+ as 258 of them a frame, at 11 frames a second on a virgl VM, where seven
188
+ tiles ran at 25. X11 has no such call on purpose: RENDER takes a projective
189
+ picture transform, but glamor — Xwayland's, and most Xorg desktops' —
190
+ divides at the corners and folds the image along its diagonal, and pixman
191
+ runs out of 16.16 precision on a plane of any size. The method answers
192
+ false and draws nothing where a corner is behind the viewer, for the caller
193
+ to draw the part in front some other way.
194
+
195
+ **The ack is deferred to the frame that adopts it.** `xdg_surface.configure`
196
+ is acknowledged immediately before the commit of a frame painted at the
197
+ configured size, not on arrival — acking early tells the compositor the
198
+ next commit is at the new size, and a buffer painted at the old one is then
199
+ shown stretched.
200
+
201
+ **Everything the renderer sees is content-relative device pixels.** The
202
+ surface is larger than the content by the client-side frame, and the
203
+ compositor speaks logical pixels; the two conversions happen in
204
+ `backendwindow.js` and `input.js` and nowhere else.
205
+
206
+ **The keymap is parsed, not dlopen'd.** The compositor sends an XKB keymap
207
+ as a file descriptor; `xkb.js` reads keycodes, types (modifier → level
208
+ maps), symbols per group, and resolves virtual modifiers like `LevelThree`
209
+ to real ones through `interpret` + `modifier_map` — so AltGr works because
210
+ the keymap says which bit it is, not because Mod5 was assumed. The output is
211
+ both the X core `keycode2keysyms` table the accelerator code already speaks
212
+ and a full `decode()` for levels 3 and 4. Key repeat is synthesised at the
213
+ seat's advertised rate; compositors do not repeat.
214
+
215
+ **`useKeyboardState()` is the seat's.** There is no XKB extension to ask, so
216
+ the locks and the group come from `wl_keyboard.modifiers` — Num Lock being
217
+ whichever bit the keymap binds `NumLock` to — and the layout codes from the
218
+ keymap's own `xkb_symbols` name, which libxkbcommon writes from the include
219
+ string the rules built: `pc_us_ru_2_inet(evdev)` is `['us', 'ru']`. A keymap
220
+ not built from the rules has no codes to give, and `layouts` stays empty.
221
+ The protocol's one limit: modifiers go to the focused client only, so a lock
222
+ toggled while another app has the keyboard shows when focus comes back.
223
+
224
+ **The input method is told at the end of the frame.** `zwp_text_input_v3`
225
+ wants to know which field has focus, what kind of text it holds, where its
226
+ caret is and what surrounds it, and every one of those changes repaints the
227
+ field — so the backend window calls `textInput.sync()` once per frame after
228
+ the renderer has painted, when the layout is current, and only what changed
229
+ since the last commit is sent. There is no hook into the event manager and
230
+ nothing per field. What comes back — `preedit_string`, `commit_string`,
231
+ `delete_surrounding_text`, applied together on `done` in the order the spec
232
+ fixes — lands on the same `CompositionStart/Update/End` a dead key raises,
233
+ so `<textinput>` and an application's `onCompositionUpdate` cannot tell IBus
234
+ from `compose.js`. Keys the IME consumes never reach `wl_keyboard.key`, so
235
+ key delivery is untouched; a key that arrives is one the IME declined, and
236
+ the client-side composer has its usual go at it. A `sensitive` field offers
237
+ no surrounding text and says `sensitive_data`; `inputMode` on a
238
+ `<textinput>` becomes the content purpose (`email`, `url`, `tel`, …).
239
+
240
+ **A finger and a pen are the pointer.** On X11 a tablet is an XI2 slave of
241
+ the master pointer and a touch emulates it, so every control in the tree is
242
+ written against `mousedown`/`mousemove`/`mouseup` and nothing else. The
243
+ Wayland seat keeps `wl_touch` and the tablet tools as devices of their own,
244
+ so `touch.js` and `tablet.js` make them the pointer here: the first finger
245
+ of a gesture and a tool in proximity emit the seat's own `enter`/`motion`/
246
+ `buttonpress`/`buttonrelease`/`leave`, carrying the touch `down` or tool
247
+ `down` serial (what `xdg_toplevel.move` wants when the press lands on the
248
+ titlebar), and `input.js` routes them through exactly the path the mouse
249
+ takes — frame hit-testing, popup grabs, the cursor, which a tool in
250
+ proximity follows through cursor-shape's tablet device. The emulated events
251
+ alone carry `pointerType` (`'touch'`, `'pen'`, `'eraser'`, `'mouse'`),
252
+ `pressure`, `tiltX`/`tiltY`, `rotation`, `distance` and
253
+ `tangentialPressure` in the DOM's names and ranges, so a mouse event is
254
+ unchanged and the X11 backend never sees a field it did not send. A second
255
+ finger emulates nothing; every finger also arrives raw on the window as
256
+ `touchstart`/`touchmove`/`touchend`/`touchcancel` — ntk's XI2 names, with
257
+ its `touchId` — each with the active `touches`, batched at `wl_touch.frame`.
258
+ Barrel buttons map as the desktop does: BTN_STYLUS is button 3, BTN_STYLUS2
259
+ button 2; `proximity_out` releases whatever is still held.
260
+
261
+ **`createWindow` is synchronous.** React's commit phase cannot await, and
262
+ that is where windows are realised. Nothing about Wayland needs the
263
+ asynchrony — a request is one-way and the client allocates ids — so the
264
+ library grew a synchronous namespace (`Wl_interface.$`) and the only
265
+ genuinely asynchronous step, binding globals, happens once in `open()`.
266
+
267
+ **The monitors are a fold over the registry.** Each `wl_output` is its own
268
+ global, so `outputs.js` binds every one (through a second `wl_registry`,
269
+ because the library files globals by interface name and keeps one) and
270
+ publishes the layout into `screens.js` the way the cocoa backend does:
271
+ names from `wl_output.name` (`DP-1`, `eDP-1`), logical rects from
272
+ xdg_output where the compositor has it and from the mode over the scale
273
+ where it does not, converted to the renderer's device pixels at
274
+ `app.scale`. Hot-plug is `global`/`global_remove` on that registry. Two
275
+ things the protocol does not have are stood in for: a **primary** (the
276
+ output at the origin), and a **work area** — `xdg_toplevel.configure_bounds`
277
+ is the monitor less its panels, sent to a window ahead of its configure, so
278
+ a window's bounds become the usable rect of the output it has entered, and
279
+ every monitor carries its own rect so no head is clamped by another's. The
280
+ densest output also seeds `app.scale` before the first window exists, which
281
+ is the difference between a first layout at 2x and one corrected a frame
282
+ later; and where a compositor offers neither fractional-scale-v1 nor
283
+ `preferred_buffer_scale`, a surface's buffer scale is that of the output it
284
+ entered.
285
+
286
+ **A `<glarea>` is a rectangle of the backing target.** On X11 it is a child
287
+ window and on macOS a sublayer; a subsurface here would be a second
288
+ swapchain, a second frame clock and a second dma-buf import per frame for
289
+ what is already one GL context drawing into one target. So `glarea.js`
290
+ hands the area the shared GLES table with `viewport`, `scissor` and
291
+ `bindFramebuffer(null)` re-based on its rect (the Cocoa backend's own
292
+ trick, prototype-delegated), and the frame loop orders the three layers:
293
+ the tree's 2D is flushed under, the area draws over, and the panes its
294
+ children were painted on — offscreen targets, `createOverlayPane` — are
295
+ blended over that, translucent. One thing a shared target needs that a
296
+ child window never did: when the tree repaints under a static scene, the
297
+ scene is asked for its frame before the present, so the background never
298
+ shows through for a frame.
299
+
300
+ **One GL device, several contexts, and whoever draws takes it.** A window's
301
+ 2d context, the pane a `<glarea>`'s children are painted on and every
302
+ offscreen `Surface` are the same GLES context, differing only in which
303
+ framebuffer they bind and what they left the blend, scissor and stencil
304
+ state saying — unlike an XRender context, which is an encoder aimed at a
305
+ drawable, or a Cocoa one, which carries its state in its bitmap. So
306
+ `device.js` gives the device an owner: a context claims it before it
307
+ touches GL, and claiming is where the target is bound and the state the
308
+ shader was written against is put back. That is what makes ntk's advice —
309
+ hold a surface's context and draw into it whenever you like — true here
310
+ too, where a held context used to draw wherever the device was last pointed
311
+ and say nothing about it (#566); it is also why a node that painted into a
312
+ surface in the middle of the frame does not have to restore the window's
313
+ context afterwards. Foreign GL is the exception it cannot see: a
314
+ `<glarea>` draws through entry points nobody tracks, so the frame loop
315
+ still calls `restoreGLState()` after one.
316
+
317
+ **Paths are antialiased on the CPU, and clips are exact on the stencil.**
318
+ A solid fill that fits the budget (512×512 after cutting to the clip) goes
319
+ through ntk's analytic coverage rasteriser — the same pixels the X11
320
+ backend's local raster and every glyph get — into a per-frame mask atlas,
321
+ drawn like a glyph; gradients and bigger shapes stay on stencil-then-cover.
322
+ `clip()` with anything but one axis-aligned rectangle sets bit 7 of the
323
+ stencil where the shape is (the low seven bits stay the winding count) and
324
+ every batch under it tests that bit; nested clips intersect in place,
325
+ `restore()` re-renders the restored list. The scissor still narrows to the
326
+ bounds, so the test is only paid inside them. `test/wayland/context2d-gpu.test.js`
327
+ reads the pixels back on any machine with a render node.
328
+
329
+ **The glyph seam is public, and spelled the same way on every backend.**
330
+ `drawGlyphs(op, src, positioned)` is how `TextLayout.draw` reaches a
331
+ context, and ntk documents it for callers that place their own glyphs too —
332
+ a terminal grid, a tabular column. The source in that call is spelled
333
+ `ctx.createSolidPicture(r, g, b, a)` and the op `ctx.Render.PictOp.Over`,
334
+ so both of those answer here as they do on X11 and on Cocoa. Implementing
335
+ `drawGlyphs` alone was not half a seam but a silent one: callers
336
+ feature-detected, the test came back false, and the frame drew with every
337
+ cell in its colour and no text in any of them rather than throwing (issue
338
+ #565). What a solid _is_ stays backend-private — an XRender `Picture` on
339
+ X11, the premultiplied colour itself here — but a CSS colour string, a
340
+ premultiplied `[r, g, b, a]` and `null` (meaning the fill style in force)
341
+ are all accepted as sources as well, which is the set the Cocoa backend
342
+ takes.
343
+
344
+ ### A surface the compositor stops showing stops drawing
345
+
346
+ After the first present the frame loop has exactly one source of liveness:
347
+ `wl_surface.frame`. A compositor is entitled never to send one to a surface
348
+ it is not showing — covered, on another workspace, minimised, the screen
349
+ locked — and that is the right way to drive a client, because painting into a
350
+ surface nobody sees is work for nothing. There is no timer-driven fallback
351
+ here and there should not be one.
352
+
353
+ What it costs is that the silence is **not an error**. Nothing rejects, the
354
+ frame stays armed, and every later repaint request returns at its guard while
355
+ React goes on re-rendering; from outside the process, an app that has drawn
356
+ one frame and gone quiet is indistinguishable from a hang (#567 — 135 state
357
+ ticks, one present, exit code 0). So the loop is watched (`framewatch.js`),
358
+ and the parked state is reported twice over:
359
+
360
+ - **On stderr, once per window, in development**, when the window still had
361
+ something to draw. One line, naming the compositor as the reason and saying
362
+ the app is not hung.
363
+ - **In the tree**, as [`useWindowState().presenting`](system.md#presenting-and-the-wayland-frame-clock),
364
+ which also folds into `visible` — so an app can stop animating and
365
+ simulating rather than feeding a loop nobody is turning, which is what
366
+ every map, chart and game on this backend wants anyway.
367
+
368
+ Nothing is re-armed or polled to recover: the frame callback we asked for is
369
+ still queued with the compositor and fires the moment the surface is shown
370
+ again, which un-parks the loop and flips `presenting` back. The deadline is
371
+ two seconds, and `REACT_X11_WAYLAND_FRAME_STALL_MS` changes it — `0` turns the
372
+ watch off entirely, timer, warning and all.
373
+
374
+ ## Decorations: whose frame
375
+
376
+ `decorations.js` draws a titlebar because mutter will not, and that stays
377
+ the default where nothing better is offered. Where a compositor advertises
378
+ `zxdg_decoration_manager_v1` — wlroots (sway, labwc), KDE — each toplevel
379
+ creates a decoration object before its first commit and asks for
380
+ server-side decorations (`ssd.js`). The compositor's answer is a
381
+ `configure(mode)` that is part of the surface's configure sequence, so it
382
+ is adopted on the same ack as the size it arrived with: 'server' switches
383
+ the client-side frame off (insets to zero, no titlebar painted, the content
384
+ grows into the whole surface, which the tree sees as a resize), 'client'
385
+ leaves it on. Before the first frame, where the compositor imposed no size,
386
+ the surface is refitted to the content the tree asked for, so a floating
387
+ window comes up the size it was declared and not a titlebar taller.
388
+
389
+ `createRoot({ decorations })` says what to ask for: `undefined`/`true`/
390
+ `'server'` prefer the compositor's frame, `'client'` never asks for it, and
391
+ `false` — as does `<window decorations={false}>` — wants no frame at all.
392
+ A frameless window still creates the decoration object where the protocol
393
+ exists, to _decline_ server-side: sway frames every toplevel that has not
394
+ said `client_side`.
395
+
396
+ ## Layer surfaces: docks, panels, wallpapers
397
+
398
+ Where the compositor advertises `zwlr_layer_shell_v1`, a `<window>` whose
399
+ `windowType` is one of these is not an `xdg_toplevel` but a layer surface
400
+ (`layershell.js`), the same `wl_surface`, frame loop and input underneath:
401
+
402
+ | `windowType` | layer | anchored to | exclusive zone |
403
+ | ---------------- | ------------ | ------------------------------------------------- | -------------- |
404
+ | `'dock'` | `top` | the edge its shape and position imply (see below) | its thickness |
405
+ | `'desktop'` | `background` | all four edges | -1 |
406
+ | `'notification'` | `top` | top and right, 12px in | 0 |
407
+ | `'splash'` | `overlay` | nothing — the compositor centres it | 0 |
408
+
409
+ A dock's edge is read the way an X window manager reads the same window:
410
+ wide and at `y={0}` is a top panel, wide otherwise a bottom one; tall and at
411
+ `x={0}` a left dock, tall otherwise a right one. Its size is what the tree
412
+ measured (or `width`/`height`), so a 560×56 dock sits centred on its edge
413
+ with 56px reserved; anchoring it to both ends of the edge stretches it:
414
+
415
+ ```jsx
416
+ <window windowType="dock" height={56}>…</window>
417
+ <window windowType="dock" height={40} layerShell={{ anchor: ['bottom', 'left', 'right'] }}>…</window>
418
+ <window layerShell={{ layer: 'overlay', anchor: 'bottom', margin: { bottom: 40 } }}>an OSD</window>
419
+ ```
420
+
421
+ `layerShell` names anything the type's default gets wrong — `layer`,
422
+ `anchor` (an edge, an array of edges, `'all'`, `'none'`), `exclusiveZone`
423
+ (a number, `'auto'` for the thickness, `-1`), `margin`,
424
+ `keyboardInteractivity` (`'none'`, `'on-demand'`, `'exclusive'`),
425
+ `namespace`, `output` — and `layerShell={false}` keeps a dock-typed window
426
+ an ordinary toplevel. Popups from a layer surface work (a dock's menus): the
427
+ `xdg_popup` is created parentless and adopted through
428
+ `zwlr_layer_surface_v1.get_popup`. A stretched axis is the compositor's:
429
+ `resize()` on it is not granted, and the configured size wins. Without
430
+ layer-shell — GNOME — the same JSX is a toplevel, as before.
431
+
432
+ ## Screen capture, and the eyedropper
433
+
434
+ `app.screenCapture` (`screencopy.js`) is there when the compositor offers
435
+ either `ext_image_copy_capture_manager_v1` (wlroots 0.19+, KDE 6.2+) or
436
+ `zwlr_screencopy_manager_v1` (sway 1.10 has this one), and null otherwise
437
+ (GNOME). `capture()` answers one frame of the output as straight RGBA, the
438
+ shape `readback.js`'s `encodePNG` takes; `pixelAt(x, y)` one device pixel
439
+ of it. The pixels land in a `wl_shm` pool (`shm.js`) whose memory is a
440
+ memfd (or an unlinked `/dev/shm` file without x11-dri), read back with
441
+ `fs.readSync` — there is no mmap in Node or Bun, and one copy of one screen
442
+ on a path nobody runs per frame is the cheaper trade than a native binding.
443
+
444
+ `pickColor()` is the eyedropper, and it is what every wlroots colour picker
445
+ does: freeze the output into a capture, show that capture as a layer-shell
446
+ overlay anchored to every edge with an exclusive keyboard, take the click
447
+ _on the overlay_ — whose coordinates are the output's — read the pixel out
448
+ of the capture, and take the overlay down. Escape cancels; Return, space and
449
+ KP_Enter pick at the pointer. It answers `{ r, g, b }` in 0–1, the portal's
450
+ units, so `src/screencolor.js` converts it with the same function as the
451
+ portal's and the cocoa sampler's. There it is rung 3 of the ladder: below
452
+ the Screenshot portal (the desktop's own picker, user-consented — GNOME's
453
+ only route, and what sway with xdg-desktop-portal-wlr uses too) and above
454
+ the X11 grab. `useEyedropper().supported` is true on a bare sway; the rung
455
+ is guarded by `app.backend === 'wayland'` because the capability is a
456
+ Wayland object through and through.
457
+
458
+ Two protocol gaps are worth knowing. `@windowkit/wayland` keeps one
459
+ global per interface name, so the output bound here is the one advertised
460
+ last; `capture({ output })` and `layerShell.output` take another proxy, but
461
+ enumerating them waits on `outputs.js`. And the portal's `parent_window`
462
+ handle comes from **xdg-foreign**: the first dialog a toplevel opens exports
463
+ it (`zxdg_exporter_v2`, kept for the window's lifetime), and the portal gets
464
+ `wayland:<handle>` — modal to the window and stacked over it, as `x11:<xid>`
465
+ is on X11. A compositor without the protocol, or a popup, leaves the handle
466
+ empty and the dialog floats; an `x11:` handle never goes out from this
467
+ backend, because a Wayland window's id names no X window.
468
+
469
+ ## Under a second compositor
470
+
471
+ mutter was the only compositor this backend had met until sway 1.10.1 and
472
+ labwc 0.8.3 ran here, nested (`WLR_BACKENDS=wayland`) and headless
473
+ (`WLR_BACKENDS=headless`), which is also the first time a screenshot of the
474
+ whole path was possible — `grim` needs wlr-screencopy, which mutter
475
+ refuses:
476
+
477
+ ```bash
478
+ WLR_BACKENDS=headless sway -c sway.conf & # output * resolution 1280x800
479
+ WAYLAND_DISPLAY=wayland-1 REACT_X11_BACKEND=wayland node --import tsx examples/simple.jsx &
480
+ WAYLAND_DISPLAY=wayland-1 grim shot.png
481
+ ```
482
+
483
+ What it showed: the window comes up in sway's own frame (title bar and 2px
484
+ border, `default_border normal 2`) with the client-side bar gone and the
485
+ content filling the surface; a `windowType="dock"` window is a bottom layer
486
+ surface with a reserved strip the tiled toplevel keeps clear of; the
487
+ capture matches what grim sees; and the eyedropper's overlay maps and the
488
+ pick resolves from a synthesised click. The one protocol bug it surfaced was
489
+ in the client library rather than the compositor: it refused a null object
490
+ argument (`set_parent(null)`, `set_fullscreen(null)`, `get_popup(null, …)`,
491
+ `get_layer_surface(…, null, …)`). `@windowkit/wayland` 3.1.1 takes `null`
492
+ wherever the protocol marks an argument `allow-null`, and the backend passes
493
+ it directly.
494
+
495
+ ## The transports, and Node
496
+
497
+ Descriptors are the protocol: the keymap, shm pools and clipboard pipes all
498
+ arrive as fds, and Node's sockets abort on them
499
+ ([nodejs/node#53391](https://github.com/nodejs/node/issues/53391), closed).
500
+
501
+ - **`x11-dri` `UnixSocket`** (>= 0.9): a `uv_poll` over an `AF_UNIX` socket,
502
+ `recvmsg`/`sendmsg` with `SCM_RIGHTS`, no thread. Works on Node. Two
503
+ things it had to get right that a first version did not: N-API values made
504
+ from a libuv callback need a handle scope, and callbacks from the loop must
505
+ go through `napi_make_callback` — `napi_call_function` from there runs the
506
+ callback but never drains the promise-job queue, so a client awaiting its
507
+ first reply hangs until an unrelated timer turns the loop. `pipe()`,
508
+ `socketpair()` and `memfdCreate()` came with it.
509
+ - **node-x11's `fdpass-bun.js`**: `bun:ffi` to libc, with a reader thread.
510
+ Bun only. Bun _exports_ libuv's symbol names but several are stubs that
511
+ abort the process (`uv_poll_init` among them), so the native socket is
512
+ never attempted under Bun; there is nothing to probe for safely. Its
513
+ reader thread gave up on a healthy socket when `poll(2)` was interrupted
514
+ sixty-four times in a row, and JavaScriptCore's thread-suspend signal
515
+ (SIGPWR, `tgkill`ed to every JS thread, the poller included) during
516
+ **WebAssembly compilation** — yoga-layout's wasm tiering up ~0.3 s into
517
+ any react-x11 app — produces exactly that burst, as
518
+ `strace -f -e ppoll,tgkill` shows. EINTR is a retry since x11 4.2.1
519
+ ([sidorares/node-x11#297](https://github.com/sidorares/node-x11/pull/297)).
520
+
521
+ ## What is left
522
+
523
+ - **Multisampling.** Stencil-then-cover (gradients, shapes past the CPU
524
+ budget) and stencil clips are aliased at the edge. MSAA is the fix, and it
525
+ is not the few lines it looks like: GLES 3.0 cannot `blitFramebuffer` _into_
526
+ a multisampled framebuffer, so the scroll blits and the partial repaints the
527
+ backing target exists for would have to be re-expressed as textured draws,
528
+ and every read of the target (a present, `getImageData`, a `drawImage` of a
529
+ surface) as a resolve first. virgl here reports `MAX_SAMPLES` of 1, so none
530
+ of it could be validated; it waits for a machine that can run it.
531
+ - **`<Frame>` panes.** The out-of-process pane path (`createPaneHost`) is
532
+ macOS's and Windows' today — the pane presents an IOSurface id there and a
533
+ composition surface handle on Windows. Linux wants the same over a memfd or
534
+ dma-buf fd on the pane channel; until then `<Frame>` here degrades the way
535
+ it does without embedding.
536
+ - **Drag-and-drop.** Wired (`src/wayland/dnd.js`): the drop side answers
537
+ another application's drag over `wl_data_device` enter/motion/leave/drop,
538
+ and the source side offers a payload and hands the gesture to `start_drag`,
539
+ with an in-app drop keeping `e.items` by reference. What is left is the drag
540
+ _icon_: `start_drag` is given a bare (invisible) surface, and rendering the
541
+ tree's `<popup dragPreview>` into an icon surface the frame loop paints is a
542
+ follow-up.
543
+ - **Input methods, the rest of it.** The preedit's cursor is drawn (a heavier
544
+ underline under the segment being converted) but nothing is done with a
545
+ hidden cursor beyond putting the caret at the end, and the only content
546
+ hints sent are `multiline` and `sensitive_data` — no
547
+ `spellcheck`/`auto_capitalization`, which the tree has no notion of.
548
+ - **Tablet pads.** The buttons, rings and strips on the tablet itself
549
+ (`zwp_tablet_pad_v2`) are accepted and ignored: the tree has no vocabulary
550
+ for them.
551
+ - **`useScreens().source` reads `'test'`**, as it does on macOS and Windows:
552
+ all three backends publish through `setScreensForTests`, the one seam
553
+ `screens.js` has, and it stamps the source. A `source` argument on it is the
554
+ fix.
555
+ - **`fillText` only sees the first font family** of a `font` shorthand and
556
+ strokes text as a fill.
557
+ - **`ext-image-copy-capture`** is implemented to the protocol text but has not
558
+ run against a compositor: sway 1.10 advertises only wlr-screencopy, and
559
+ GNOME neither. wlroots 0.19 (sway 1.11) is where to try it.
560
+
561
+ ## Things that are gone, not missing
562
+
563
+ Window managers (`examples/wm.jsx`), global grabs, reading other windows,
564
+ screen coordinates, `GetImage`, XEmbed/`<foreign>`, and `ssh -X` are
565
+ compositor-private by design. Panels and docks are `layer-shell`, and a
566
+ screen read is `screencopy` — both wlroots and KDE, neither GNOME, where the
567
+ same JSX is a toplevel and the same pick is the portal's.
568
+
569
+ ## Changes outside `src/wayland/`
570
+
571
+ - `src/Reconciler.js` — a `'wayland'` rung in `resolveBackend()`. Additive;
572
+ `'auto'` never selects it.
573
+ - `src/nodes/boxpaint.js` — prefers `ctx.fillShadow` when a context has it.
574
+ - `src/nodes/preedit.js` — the composition may carry a cursor
575
+ (`cursorBegin`/`cursorEnd` on the composition event); without one the
576
+ caret is at the end of the preedit, as before. `src/events.js` —
577
+ `_composition` passes those extra fields through. Additive on both
578
+ backends.
579
+ - `src/screencolor.js` — rung 3 of the eyedropper's ladder, the Wayland
580
+ backend's own picker, guarded by `app.backend === 'wayland'`; and the
581
+ portal's `parent_window` handle is an xdg-foreign export on that backend
582
+ (`portalParentFor` in `src/portal.js`, shared with the file dialog),
583
+ where an X window id would be a lie.
584
+ - [`@windowkit/wayland`](https://github.com/windowkit/wayland) 3.1.1 — a
585
+ fork of [`wayland-client`](https://github.com/sdumetz/node-wayland-client)
586
+ 3.0.0: fd send/receive, the `$` synchronous request namespace, callback
587
+ requests returning their `done` payload, and `null` for `allow-null`
588
+ arguments.
589
+ - `x11-dri` 0.9.0 — `UnixSocket`, `pipe`, `socketpair`, `memfdCreate`; a
590
+ self-test that passes a real descriptor through a socketpair.
591
+ - `x11` 4.2.1 — `fdpass-bun.js` retries a `poll(2)` that EINTR interrupted.