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,2762 @@
1
+ # Elements
2
+
3
+ Only `<window>`, `<popup>`, `<glarea>` and `<foreign>` are backed by real
4
+ windows of the display system — on X11, X windows created top-down in
5
+ React's commit phase so every `CreateWindow` names its actual parent — and,
6
+ for `<foreign>`, so that no `ReparentWindow` is ever issued from a render
7
+ React might discard.
8
+ Everything else is a retained lightweight node — one
9
+ [yoga-layout](https://www.yogalayout.dev/) node each — painted into the
10
+ owning window's double-buffered 2d context on ntk's frame clock.
11
+
12
+ ## `style` and props
13
+
14
+ Everything CSS has a concept for goes in **`style`**; everything else is a
15
+ prop. No name means both, which is why `<window width>` is unambiguously the
16
+ real window's geometry. See [styling.md](styling.md) for arrays, state
17
+ blocks and `createStyles`.
18
+
19
+ ```jsx
20
+ <box style={{ flexDirection: 'row', gap: 8 }} onClick={pick}>
21
+ <text style={{ fontSize: 14, color: '#2d3436' }}>hello</text>
22
+ </box>
23
+ ```
24
+
25
+ ## Layout properties (all drawn elements + windows)
26
+
27
+ Lengths are logical pixels — a number, or the same number written `'12px'`
28
+ — and `'50%'` / `'auto'` pass through to yoga where it takes them. `em`,
29
+ `rem`, `vw`/`vh`/`vmin`/`vmax` and `calc()`/`min()`/`max()`/`clamp()` over
30
+ them resolve to pixels for the node they are written on, and again when what
31
+ they are relative to moves; a unit CSS has and react-x11 does not read (`ex`,
32
+ `cqw`, `pt`) is an error naming it. CSS's shorthands are read too —
33
+ `padding: '8px 16px'`, `border: '1px solid $border'`, `inset: 0`; see
34
+ [styling.md](styling.md#lengths-and-shorthands).
35
+
36
+ - **Size**: `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
37
+ `maxHeight`, `aspectRatio`. `<image>` and `<svg>` keep their aspect ratio
38
+ when only one of `width`/`height` is given
39
+ - **Flex**: `flexDirection` (`row`, `column`, `row-reverse`,
40
+ `column-reverse`), `justifyContent` (`flex-start`, `center`, `flex-end`,
41
+ `space-between`, `space-around`, `space-evenly`), `alignItems`,
42
+ `alignSelf`, `alignContent`, `flexWrap`, `flex`, `flexGrow`, `flexShrink`,
43
+ `flexBasis`, `gap`, `rowGap`, `columnGap`
44
+ - **Position**: `position` (`relative`, `absolute`, `static`, `sticky`, or
45
+ one registered with `registerPosition`), `top`, `right`, `bottom`,
46
+ `left`, `start`, `end`, and `inset` for all four. Under `sticky` the insets are how close to the
47
+ nearest scroll pane's edges the box may come, not offsets — see
48
+ [styling.md](styling.md#sticky-positioning) and
49
+ [custom positions](styling.md#custom-positions)
50
+ - **Grid**: `display: 'grid'` arranges the children as a CSS grid —
51
+ `gridTemplateColumns`, `gridTemplateRows`, `gridTemplateAreas`,
52
+ `gridAutoColumns`, `gridAutoRows`, `gridAutoFlow` and `justifyItems` on
53
+ the box, `gridColumn`, `gridRow`, `gridArea` and `justifySelf` on a child
54
+ — see [styling.md](styling.md#grid)
55
+ - **Arrangement**: `layout` (`grid`, `masonry`, `equal-row`, or one
56
+ registered with `registerLayout`) arranges the children in place of
57
+ flexbox, and a child's `layoutItem` is what it tells that layout — see
58
+ [styling.md](styling.md#custom-layouts)
59
+ - **Spacing**: `margin`, `marginTop/Right/Bottom/Left`, `padding`,
60
+ `paddingTop/Right/Bottom/Left`, and the logical pairs `paddingInline`,
61
+ `paddingBlock`, `marginInline`, `marginBlock`. `margin` and `padding` take
62
+ CSS's one-to-four values as one string: `'0 auto'`, `'8px 16px'`
63
+ - **Visibility**: `display` (`flex`, `grid`, `none`), `overflow` (`visible`,
64
+ `hidden`, `scroll`)
65
+
66
+ ### Everything shrinks, nothing shrinks to nothing
67
+
68
+ `flexShrink` is `1`, as in CSS: an item hands back the space it has spare
69
+ when the row it is in runs short, rather than pushing the row past its
70
+ container. And **it never gives up what is inside it** — every item carries a
71
+ floor of its own min-content size, which is CSS's automatic minimum size
72
+ (`min-width: auto` on a flex item) written out by the renderer, since yoga
73
+ implements the shrinking half and not the floor half. A row of chips squeezes
74
+ until it wraps; a label squeezes to its longest word; a `height: 40` row
75
+ stays 40 tall.
76
+
77
+ Three ways a style says otherwise:
78
+
79
+ | write | to mean |
80
+ | --------------------------------- | ----------------------------------------- |
81
+ | `flexShrink: 0` | never give up any space at all |
82
+ | `minWidth: 0` / `minHeight: 0` | this may shrink past its content, to zero |
83
+ | `overflow: 'hidden'` / `'scroll'` | the same, and clip what no longer fits |
84
+
85
+ The last two are CSS's own rule — `min-*: auto` computes to `0` on anything
86
+ whose overflow is not `visible` — and they are what makes a scroll pane
87
+ possible: a viewport is a box that is _allowed_ to be smaller than what is
88
+ inside it. A `<box overflow="scroll">` gets `minWidth: 0` and `minHeight: 0`
89
+ for free.
90
+
91
+ One deliberate difference from CSS: **a size that was named is kept**. CSS
92
+ floors an item at `min(the size it was given, its content)`, so an empty
93
+ `height: 40` box squashes to nothing in a column too short for it; that is
94
+ survivable on the web because a `<div>` is a _block_ container and its
95
+ children are not flex items at all, and it is not survivable here, where
96
+ every box lays its children out with flex. Say `minHeight: 0` to get CSS's
97
+ answer for one.
98
+
99
+ The measurement costs the tree an extra layout pass per axis, on frames that
100
+ change the layout — a scroll, which moves no box, keeps the single pass it
101
+ always had.
102
+
103
+ ### `flex`, the shorthand
104
+
105
+ `flex: 1` is `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }` — "take this
106
+ share of what is left, from a base size of nothing" — which is what makes it
107
+ the right thing for a pane that should fill its window rather than its
108
+ content. `flex: 'auto'` grows and shrinks from the content's own size, and
109
+ `flex: 'none'` does neither. CSS's long form reads too — `'1 1 0'`,
110
+ `'0 0 240px'`, `'2 200px'`. A longhand written beside it wins, so
111
+ `{ flex: 1, flexBasis: 'auto' }` reads the way it does in CSS.
112
+
113
+ ## Paint properties
114
+
115
+ - `backgroundColor` — any CSS color string (`'#2980b9'`,
116
+ `'rgba(0,0,0,.5)'`, `'red'`); `background` sets it and a gradient
117
+ (`backgroundImage`) in one
118
+ - any colour may be `'currentColor'`, the node's ink — its own `color` or
119
+ the one it inherits, followed when it moves
120
+ ([styling.md](styling.md#currentcolor))
121
+ - `borderWidth`, `borderColor`, `borderRadius`, `borderStyle`
122
+ (`'solid'` default, `'dashed'`), or `border: '1px solid $border'` for the
123
+ first three — `border: '1px solid'` is drawn in `currentColor`
124
+ - per-side borders: `borderTopWidth`/`Right`/`Bottom`/`Left` override
125
+ `borderWidth` the way `paddingLeft` overrides `padding`, and
126
+ `borderTopColor`/… fall back to `borderColor`; `borderBottom: '1px solid
127
+ $border'` sets one side's pair. A blockquote's bar is
128
+ `borderLeftWidth: 3` on the quote box, not a 3px sibling; a table rule is
129
+ `borderBottomWidth: 1` on the row. `borderRadius` requires uniform
130
+ borders — a non-uniform border paints square
131
+ ([styling.md](styling.md#per-side-borders))
132
+ - `zIndex` — paint/hit order among siblings (stable sort)
133
+ - `objectFit` — where an `<image>`'s or a `<video>`'s picture goes in its
134
+ content box, CSS's `object-fit`, centred: `'fill'` (`<image>`'s default),
135
+ `'contain'` (`<video>`'s), `'cover'`, `'none'`, `'scale-down'`
136
+ - `outlineWidth`, `outlineColor`, `outlineOffset` — the focus ring, painted
137
+ outside the border box and invisible to yoga, so it never moves what it
138
+ surrounds. A focusable node draws one on `:focus-visible` without being
139
+ asked; `outlineWidth: 0` — or `outline: 'none'` — opts out
140
+ ([styling.md](styling.md#the-focus-ring))
141
+ - `opacity` — how opaque the node is **with everything inside it**, from 0
142
+ to 1: the subtree is drawn once and composited at that alpha, so a card, its
143
+ icon and its text fade as one. `0` draws nothing and is still hit
144
+ ([styling.md](styling.md#opacity))
145
+ - `transition` — `120`, or `{ backgroundColor: 120, left: 200 }`: how long a
146
+ change to that property takes ([styling.md](styling.md#transitions))
147
+ - any value may be a **theme token**: `'$panel'` resolves against the nearest
148
+ `theme` prop above the node ([styling.md](styling.md#theme-tokens))
149
+ - `'@width >= 600'` blocks restyle for the window's size, layout included
150
+ ([styling.md](styling.md#window-size-queries))
151
+
152
+ `cursor` (`'pointer'`, `'text'`, `'wait'`, `'move'`, `'crosshair'`, resize
153
+ arrows, … — the ntk cursor name map) and `pointerEvents` are style too: CSS
154
+ has both, and React Native has been moving `pointerEvents` the same way.
155
+ `pointerEvents: 'none'` takes a node and its subtree out of hit testing;
156
+ `'box-none'`, React Native's, takes out only the node itself — its children
157
+ still take the pointer, and a press on its own area goes to what is behind
158
+ it. That is the value for an overlay of controls that must not swallow the
159
+ clicks between them, a `<glarea>`'s children over a scene whose input lives
160
+ on the element behind the surface included. So is `hitSlop`:
161
+
162
+ ```jsx
163
+ <box style={{ height: 16, hitSlop: { top: 4, bottom: 4 } }} /> // 24px target
164
+ ```
165
+
166
+ `hitSlop: 4` grows every side, the object form only the sides it names. It
167
+ is **hit testing and nothing else** — not paint, not yoga — which is what
168
+ lets a 16px control answer over the 24px WCAG 2.2 SC 2.5.8 wants without a
169
+ taller control misaligning the row it sits in. The slop may overlap a
170
+ sibling's box; hit testing runs front to back over paint order, so the
171
+ sibling on top keeps its own pixels either way.
172
+
173
+ ## Interaction props
174
+
175
+ `focusable`, `tabIndex` (sequential focus order; `-1` is focusable but not
176
+ tabbable), `autoFocus`, `trapFocus` (own a focus scope — Tab and presses
177
+ stay inside it, focus is restored when it unmounts), `disabled` (never
178
+ focusable, and the trigger for a `:disabled` style block), and the event
179
+ handlers listed in [events.md](events.md).
180
+
181
+ `onLayout` is the one handler that reports layout rather than input: after a
182
+ layout pass moved or resized the element it gets `{x, y, width, height}` in
183
+ logical pixels — the position within the parent _as laid out_, so a scroll
184
+ does not fire it — once after the first layout and then only when something
185
+ changed. It arrives after the pass, so state set from it is safe. It is how
186
+ a component reacts to its size when the reaction is not a style
187
+ ([react-features.md](react-features.md#measuring-a-node)); where it is one,
188
+ a `'@container'` block answers in the same frame
189
+ ([styling.md](styling.md#container-queries)).
190
+
191
+ Every element also takes `role` and the `aria-*` props — the web's
192
+ accessibility vocabulary, read by the built-in AT-SPI bridge so screen
193
+ readers see the tree. They are inert where no assistive technology is
194
+ listening, and the defaults already say something sensible for every
195
+ element: [accessibility.md](accessibility.md).
196
+
197
+ Drag and drop is two more prop families on the same elements —
198
+ `dropAccept` + `onDrop` to accept a drop, `draggable` + `dragData` to start
199
+ a drag — and works with other applications as well as inside the app.
200
+ Their presence is what registers the node, so there is nothing else to
201
+ mount: [drag-and-drop.md](drag-and-drop.md).
202
+
203
+ `selectable` makes an element a **selection surface**: text inside it can be
204
+ dragged over, copied and, on X11, handed to the PRIMARY selection. See
205
+ [Selecting text](#selecting-text) below.
206
+
207
+ `scale` zooms a subtree, CSS `zoom` rather than a transform: every length
208
+ under the element — and the element's own style — is multiplied by it, and
209
+ text is shaped at the size it will be drawn at. It is what an element owning
210
+ a viewport (a graph pane, a map) mounts its content inside. Windows take no
211
+ `scale`: [scale.md](scale.md#a-subtree-of-its-own).
212
+
213
+ ---
214
+
215
+ ## `<window>`
216
+
217
+ A real toplevel window — an X11 window, or an `NSWindow` on the Cocoa
218
+ backend; the flex, paint and event root for its subtree.
219
+
220
+ | prop | |
221
+ | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
222
+ | `title` | window title (UTF-8, via `WM_NAME` + `_NET_WM_NAME`) |
223
+ | `width`, `height`, `x`, `y` | window geometry (window state, not yoga style — the user may resize). A size is pixels or `'auto'`, and leaving it out means `'auto'` (below). A change sends only what changed: a new size leaves the window where the window manager or the user put it |
224
+ | `backgroundColor` | full-window clear color (default white) |
225
+ | `onResize(ev)` | ConfigureNotify — the tree reflows automatically. Fires for **moves** too (see below) |
226
+ | `onExpose(ev)` | after a repaint was required |
227
+ | `onCloseRequest(ev)` | WM close button — replaces the default answer (see below); `WM_DELETE_WINDOW` is advertised either way |
228
+ | `states` | EWMH `_NET_WM_STATE` — controlled, see below |
229
+ | `fullscreen`, `alwaysOnTop` | boolean sugar for two of those states |
230
+ | `decorations` | `false` asks the WM for no titlebar or border |
231
+ | `transparent` | 32-bit ARGB visual — rounded, translucent windows (below) |
232
+ | `transientFor` | ICCCM `WM_TRANSIENT_FOR` — the window this one belongs to (below) |
233
+ | `onStatesChange(states)` | what the window manager actually did |
234
+ | `onClientMessage(ev)` | a ClientMessage addressed to this window — EWMH, XEmbed, the tray (below) |
235
+ | `theme` | palette that `$token` style values resolve against, for this subtree |
236
+ | `embeddable` | created but never self-mapped: the window waits for an embedder ([`<foreign>`](embedding.md) on the other side), which maps it after the reparent. What a [`<Frame>`](frame.md) pane's root window sets |
237
+ | `hidden` | realized and laid out, never mapped while true (below) |
238
+ | `frameRate` | how the window paces its frames under a stream of changes: `'display'` (the default), `'adaptive'`, `'throughput'`, a ceiling in fps, or `{ budget, minFps, maxFps }` (below) |
239
+
240
+ Windows may be nested inside other windows (real X11 child windows).
241
+ **Ref**: the live window object — on X11 the ntk `Window`, with
242
+ `getContext('2d')`, `requestAnimationFrame`, `setCursor` and the whole ntk
243
+ API behind it.
244
+
245
+ #### On the Cocoa backend
246
+
247
+ Everything above that is about _drawing_ — geometry, background, resize,
248
+ close, `transparent`, `theme`, `frameRate`, `hidden` — works. What a
249
+ **window manager** does is where the two part company, because macOS has no
250
+ separate one to negotiate with, and the honest list is short:
251
+
252
+ | prop | on Cocoa |
253
+ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
254
+ | `minWidth`/`minHeight`/`maxWidth`/`maxHeight` | honoured — they become the window's own min/max size |
255
+ | `resizable`, `decorations` | honoured, but read **once, at creation**: they are `NSWindow` style options rather than hints, so changing either after mount does nothing |
256
+ | `transparent`, `borderRadius` | always available; every window composites, so there is no "if a compositor is running" |
257
+ | `states: ['demands_attention']` | the Dock bounce, held until the state is removed |
258
+ | `fullscreen`, `states: ['maximized' \| 'hidden']` | the green button's full-screen Space, the zoom, and the Dock, through `toggleFullScreen:`, `zoom:` and `miniaturize:` |
259
+ | every other `_NET_WM_STATE` | inert — `sticky`, `below`, `skip_taskbar`, `shaded` and the rest have no AppKit equivalent |
260
+ | `onStatesChange`, and `useWindowState()` | fire for full screen (from the transition's begin), the zoom, the Dock and the key window; `desktop` and `obscured` never change |
261
+ | `appId`, `windowType`, `gravity`, the increment and aspect hints | inert. They are messages to a window manager, and there is not one |
262
+ | `transientFor` | inert today — a stub waiting on `addChildWindow`; a managed `<Dialog>` already floats on its own window |
263
+ | `onClientMessage` | never fires — X11's ClientMessage has no counterpart |
264
+ | `embeddable` | means a `<Frame>` pane, and works — but through shared surfaces rather than reparenting ([frame.md](frame.md)) |
265
+
266
+ Inert means **inert, not fatal, and today also silent**: the prop is
267
+ accepted and does nothing, so shared application code stays branch-free. See
268
+ [macos.md](macos.md#windowing-semantics-what-maps-what-bends-what-breaks)
269
+ for why that is the policy and what it costs.
270
+
271
+ ### Natural size
272
+
273
+ A window with no `width`/`height` is **sized from its content and capped at
274
+ the screen**. `'auto'` says the same thing out loud, and the two axes are
275
+ independent:
276
+
277
+ ```jsx
278
+ <window title="prefs" /> {/* natural, both ways */}
279
+ <window width={600} height="auto" /> {/* height follows the content */}
280
+ <window width="auto" maxWidth={720} minHeight={200} /> {/* auto within your own bounds */}
281
+ ```
282
+
283
+ This is CSS's `width: auto`, read the way CSS reads it for a box whose
284
+ containing block is the viewport but which is not in flow — a float, an
285
+ abspos, an inline-block: **shrink-to-fit**. A top-level window has no
286
+ container to stretch into, and stretching into the screen is what
287
+ `fullscreen` means.
288
+
289
+ The size is worked out **before `CreateWindow`**, so the window is created
290
+ the right size rather than resized into it after mapping. Nothing is ever on
291
+ screen at the wrong size, and there is no jump to watch for.
292
+
293
+ **How it is measured**, which is worth knowing because the second step is
294
+ where the surprises are:
295
+
296
+ 1. The tree is laid out with no available width, so nothing wraps and the
297
+ root reports its **max-content** width.
298
+ 2. That is clamped into `[minWidth, min(maxWidth, the screen)]` — the same
299
+ size-hint props the window manager enforces also bound the auto size,
300
+ exactly as `min-width`/`max-width` bound `width: auto` in CSS. Where the
301
+ two disagree, `minWidth` wins, which is CSS's order too.
302
+ 3. The tree is laid out **again at that width**, and the height comes from
303
+ there. A paragraph that had to wrap is taller than step 1 said.
304
+
305
+ Where this parts company with CSS: shrink-to-fit is
306
+ `min(max(min-content, available), max-content)`, and that `max(…)` means a
307
+ CSS box never shrinks below its min-content size even when it overflows. A
308
+ window cannot be wider than the screen, so the cap wins and the content is
309
+ cut instead.
310
+
311
+ **The cap** is the usable area of the monitor the window will open on:
312
+ Xinerama's per-monitor rects — so a window on a two-head desktop is bounded
313
+ by one screen and not by both — with `_NET_WORKAREA` taken off it per axis,
314
+ so a panel is not space to grow into. Both are read once while `createRoot()`
315
+ connects. On a server with neither, the cap is the screen; with no display to
316
+ ask at all — the headless mock — there is no cap. The window manager's frame
317
+ is _not_ modelled, so a window that reaches the cap is a titlebar taller than
318
+ the work area and the WM will trim it.
319
+
320
+ **Afterwards, auto keeps up with the content until something else sets the
321
+ size.** Add a row and the window grows, take it away and the window shrinks
322
+ back; the moment the user drags an edge, or a window manager answers with a
323
+ size of its own, the window is theirs and stops re-fitting. Setting `width` to
324
+ a number is the app doing the same thing, and setting it back to `'auto'`
325
+ hands it back. Only a size the window did not ask for counts: its own
326
+ requests come back from the server as echoes, sometimes late and sometimes
327
+ behind a move that still carries the old size, and none of those is anybody
328
+ taking the window over.
329
+
330
+ **A re-fit is laid out once, at the new size.** On X11 a window takes a size
331
+ when the server answers the request, a round trip later, so the frame that
332
+ re-fits waits for that answer before it lays out, and the old pixels stay
333
+ up meanwhile. Laid out at the size it still had, content that grew would be
334
+ drawn for a round trip wrapped and cut off at the bottom of a window too
335
+ small for it — a tooltip whose value gained a word. A request answered with
336
+ no new size (a window manager refusing it) gives up the wait after 150 ms.
337
+ Cocoa takes a size on the call, so there is nothing to wait for there.
338
+ `REACT_X11_NO_REFIT_HOLD=1` turns the wait off
339
+ ([debugging.md](debugging.md#react_x11_no_refit_hold1)).
340
+
341
+ On the Cocoa backend a window is a whole number of **points**, so content
342
+ 100.5 points tall makes a window 101 points tall — rounded up, so nothing is
343
+ cut — and that is still the window's own size rather than one somebody else
344
+ chose: it goes on following its content.
345
+
346
+ ### A floor the content decides
347
+
348
+ `minWidth`/`minHeight` also take `'auto'`: **the smallest size the content
349
+ can be drawn at**, measured from the tree and handed to the window manager,
350
+ which is what stops the user dragging the window below it.
351
+
352
+ ```jsx
353
+ <window minWidth="auto" minHeight="auto"> {/* never smaller than my content */}
354
+ <window width={900} minWidth="auto" /> {/* my opening size, the content's floor */}
355
+ ```
356
+
357
+ This is what Qt and GTK both do, and in both it is the default rather than
358
+ something you ask for: a Qt layout's `minimumSize()` becomes the window's
359
+ via `QLayout::SetDefaultConstraint`, and GTK writes the widget tree's
360
+ minimum into the WM geometry hints. Here it is opt-in, because a renderer
361
+ whose layout is CSS has no floor unless one is asked for — CSS lets content
362
+ overflow.
363
+
364
+ **How the floor is measured**: the tree is laid out with _no room at all_,
365
+ and the floor is how far it still reached. What each node contributes is
366
+ whatever its own style lets it shrink to, which makes the interesting part
367
+ what _doesn't_ count:
368
+
369
+ | the node | contributes |
370
+ | --------------------------------- | ---------------- |
371
+ | a box that named a size | that size |
372
+ | a wrapping row | its widest item |
373
+ | text | its longest word |
374
+ | `overflow: 'scroll'` / `'hidden'` | nothing |
375
+ | a `minWidth: 0` of its own | nothing |
376
+ | a `minWidth: 240` of its own | 240 |
377
+
378
+ A registered element contributes whatever its `measureContent` answers when
379
+ asked for the smallest size it can be drawn at — see
380
+ [extending.md](extending.md#a-size-of-your-own).
381
+
382
+ This is the same measurement every node's own floor comes from (above), read
383
+ at the top of the tree — so the escape hatch is the same one, and it is the
384
+ one CSS, Qt and GTK all spell: `min-width: 0`, `QScrollArea`,
385
+ `min-content-width`. A node that named a floor of its own is taken at its
386
+ word and its contents stop counting, so a window around a scrolling pane is
387
+ floored by everything _except_ that pane.
388
+
389
+ `maxWidth`/`maxHeight` take `'auto'` as well, and it means the other half of
390
+ the pair: **the size the content wanted** — the natural size above, not the
391
+ floor. `'auto'` reads as "ask the content", and the content's answer to _how
392
+ big_ is not its answer to _how small_. On a tree where nothing can shrink
393
+ the two are the same number, which is what makes `minWidth="auto"
394
+ maxWidth="auto"` the fixed-size dialog — the same thing `resizable={false}`
395
+ says more briefly.
396
+
397
+ Three things worth knowing:
398
+
399
+ - **A floor is a request.** mutter, kwin and xfwm enforce it at the drag; a
400
+ tiling window manager may size the window however it likes. It stops the
401
+ user, it does not relieve the app of clipping gracefully — and the
402
+ renderer will not fight a window manager that ignores it.
403
+ - **`minHeight="auto"` is measured for the width the window has**, and
404
+ re-sent as that changes. A minimum height is a height _for a width_ — a
405
+ paragraph is tallest at its narrowest — and `WM_NORMAL_HINTS` holds two
406
+ independent numbers with no way to say so. (GTK answers the same question
407
+ at its minimum _width_, which is a taller floor than a wide window needs.)
408
+ - **The floor outlives the size.** An `'auto'` size stops tracking once the
409
+ user takes the window over; an `'auto'` bound does not, because stopping
410
+ them going too far is the whole job. It is measured on every frame that
411
+ lays out — one extra layout pass per axis asked for — and written only on
412
+ the frames it actually moves.
413
+
414
+ Three things worth knowing before reaching for it:
415
+
416
+ - A scroll container reports its **content** height, not a viewport height,
417
+ so an auto window around a long list opens as tall as the screen. Give the
418
+ window a `height`, or the scrolling box a `maxHeight`, if the scrolling is
419
+ meant to happen inside a smaller window.
420
+ - The natural size is measured with **no available width**, where nothing
421
+ wraps and nothing is short of room to shrink into, so an auto width is the
422
+ **unwrapped** row. That makes the width font-dependent: the same window is
423
+ wider under a wide UI face than under a narrow one.
424
+ - Measuring costs an extra layout pass per frame that lays out, for as long
425
+ as the window is still tracking. A window with both sizes given pays
426
+ nothing.
427
+
428
+ ### `hidden` — realized, laid out, not on screen
429
+
430
+ ```jsx
431
+ <window hidden={minimized} title="scratch" />
432
+ ```
433
+
434
+ The X window exists at its size — auto sizes included, the content is really
435
+ measured — and the tree behind it is live; the window is simply never mapped
436
+ while `hidden` is true. Clearing it maps the window where it stands. Unlike
437
+ conditional rendering nothing unmounts in between, so component state,
438
+ subscriptions and the X window itself all survive a hide: this is the
439
+ declarative form of "minimize by unmapping", and what
440
+ [`Tooltip`](components.md#tooltip) measures an element label in before
441
+ placing it.
442
+
443
+ A subtree hidden by React — `<Activity mode="hidden">`, a `<Suspense>` that
444
+ suspended — composes rather than competes: the window is on screen only
445
+ while **neither** says hidden. Hiding releases the keyboard focus of
446
+ anything inside (the [focus-follows-visibility](events.md) rule), and the
447
+ reveal hands it back unless something else took it meanwhile.
448
+
449
+ On a `<popup>` the pair of unmap/map is the whole story. On a managed
450
+ `<window>` an unmap is ICCCM's _withdraw_ — the window manager forgets the
451
+ window, and the re-map is a fresh `MapRequest`, so a WM may frame or place
452
+ it anew and `states` are re-declared rather than remembered.
453
+
454
+ ### `frameRate` — pacing the frames under a flood
455
+
456
+ ```jsx
457
+ <window frameRate="adaptive">…</window>
458
+ ```
459
+
460
+ By default a window paints every frame the clock gives it: on X11 as often
461
+ as the server keeps up, on macOS at the display's own rate. That is right
462
+ for a UI that answers input — a keystroke, a hover, a scroll — where every
463
+ frame is one someone is waiting for. It is wrong for a window whose content
464
+ changes faster than anyone can read it: a terminal under `cat`, a chart fed
465
+ by a socket, a simulation ticking as fast as it can. On a 120Hz panel such a
466
+ window paints 120 times a second, and where a frame costs a few
467
+ milliseconds of the JS thread — full-window CoreGraphics passes on macOS —
468
+ most of that thread goes to painting screens nobody sees, and the producer
469
+ gets what is left. Measured on a streaming terminal, that was a flood four
470
+ times slower on macOS than under XQuartz on the same machine, and the whole
471
+ difference was frame count times per-frame cost.
472
+
473
+ `frameRate` prices frames in CPU time rather than counting them. A window
474
+ under `'adaptive'` keeps a budget: painting may take a quarter of the wall
475
+ time while the window is busy. Credit accrues at that rate, a frame spends
476
+ what it measurably cost — the flush, and on macOS the present — and a claim
477
+ that finds the window in debt waits for the credit to reach zero, and no
478
+ longer. Four things follow, and they are what the tests pin:
479
+
480
+ - **Idle is immediate.** A quiet window has a full bucket, so the first
481
+ claim after a pause — a keystroke, a click — paints on the next tick,
482
+ whatever the last frame cost.
483
+ - **Cheap is unthrottled.** A frame that costs less than the credit it
484
+ accrued leaves the bucket where it was. A blit-scrolled list keeps the
485
+ display's rate: its frames are memmoves and a strip.
486
+ - **A rare expensive frame is free.** The bucket holds a burst — 50ms of
487
+ paint, the floor's interval — so the one relayout in a scroll costs
488
+ credit, the credit refills, and nothing waited. Only a _stream_ of
489
+ expensive frames drains it, and then each waits `cost × (1/budget − 1)`:
490
+ a 6ms frame is followed by 18ms of the thread for everything else.
491
+ - **A flood that ends is un-throttled at once.** The debt is never more than
492
+ one frame's cost, so the prompt that appears when the flood stops waits
493
+ for that and nothing more.
494
+
495
+ Two more numbers bound the rule. `minFps` is the floor: whatever the debt,
496
+ the screen is never more than `1000 / minFps` ms behind the last paint — a
497
+ promise about staleness, not a rate, since a single frame can take longer.
498
+ `maxFps` is a ceiling on top of the display's, cheap frames included — the
499
+ knob every terminal already has.
500
+
501
+ | value | `budget` | `minFps` | `maxFps` | when |
502
+ | ---------------------------- | -------: | -------: | -------: | -------------------------------------------------------------------------------------- |
503
+ | `'display'` (the default) | 1 | none | none | a UI answering input: every frame the clock gives, after React's own batching |
504
+ | `'adaptive'` | 0.25 | 20 | none | a window fed off an input path: a terminal, a live chart, a scene that streams |
505
+ | `'throughput'` | 0.1 | 10 | 30 | output matters more than the display: the screen may lag by up to 100ms during a flood |
506
+ | a number, `60` | 1 | none | 60 | a ceiling and nothing else — the fixed cap |
507
+ | `{ budget, minFps, maxFps }` | | | | the three numbers, any subset; the ones left out are `'display'`'s |
508
+
509
+ A budget below 1 with no `minFps` warns in development: after an expensive
510
+ frame the next may wait three times its cost with nothing to bound it, and
511
+ `{ budget, minFps: 20 }` is what `'adaptive'` does.
512
+
513
+ Three sources, in order of precedence: **`REACT_X11_FRAME_RATE`** in the
514
+ environment (`adaptive`, `display`, `throughput` or a number) overrides
515
+ everything, the way `REACT_X11_BACKEND` does, so an A/B run and a field
516
+ diagnosis need no code change; then **the prop**; then
517
+ **`createRoot({ frameRate })`**, the default for every window of that root;
518
+ then `'display'`. A `<popup>` paces itself the same way, from its own prop.
519
+ A [`<glarea>`](#glarea) takes the window's policy for its own frames unless
520
+ it names one.
521
+
522
+ What the pacer does not do. It never changes what a frame paints: a held
523
+ claim's damage accumulates on the window exactly as it does between two
524
+ ticks of the clock, and the frame that follows paints all of it. It never
525
+ holds the answer to an input: a click or a key paints the window at once
526
+ (the early flush in [events.md](events.md)), held claims included, and the
527
+ debt is paid then. And it prices the JS thread's time only — on X11 the
528
+ server's work is fenced by the frame clock already, so the pacer rarely
529
+ holds a frame there; where a backend has backpressure of its own, the
530
+ pacer is inert. `REACT_X11_TRACE=requests` prints `waited=` on every frame
531
+ the pacer held ([debugging.md](debugging.md)), and
532
+ [architecture/frame-pacing.md](architecture/frame-pacing.md) is the design
533
+ record, with the measurements.
534
+
535
+ ### `onResize` fires for moves
536
+
537
+ `onResize` is X's `ConfigureNotify`, which reports _any_ geometry change:
538
+ under a window manager that moves windows opaquely, dragging one by its
539
+ title bar delivers one per pointer step, all the same size. The renderer
540
+ already discriminates — a move costs no layout and no repaint — but a
541
+ handler of your own has to, or it does its work per step of a drag. The
542
+ event says which it was:
543
+
544
+ ```jsx
545
+ <window
546
+ onResize={(ev) => {
547
+ if (ev.resized) refit(ev.width, ev.height);
548
+ if (ev.moved) rememberPosition(ev.x, ev.y);
549
+ }}
550
+ />
551
+ ```
552
+
553
+ `ev.previous` carries the geometry these are measured against. Note that
554
+ `ev.x`/`ev.y` are frame-relative once a reparenting window manager has
555
+ framed the window, so they are not screen coordinates — the renderer
556
+ resolves the real origin itself for popup anchoring.
557
+
558
+ ### Stacking
559
+
560
+ Nested `<window>`s stack the way drawn siblings paint: the later sibling
561
+ sits on top, and `zIndex` wins over document order. Reordering them in JSX
562
+ restacks the real windows — one `ConfigureWindow` pass per commit, and
563
+ nothing at all when mount order already agrees.
564
+
565
+ ```jsx
566
+ <window>
567
+ <window key="back" /> {/* bottom */}
568
+ <window key="front" /> {/* on top */}
569
+ <window key="always" zIndex={1} /> {/* above both, wherever it sits here */}
570
+ </window>
571
+ ```
572
+
573
+ Top-level windows are the window manager's to stack — it redirects the
574
+ request — so their order in the tree carries no stacking meaning. Use
575
+ `alwaysOnTop` (below) for those.
576
+
577
+ ### Window manager hints
578
+
579
+ Properties the window manager reads (ntk ≥ 3.5.0). All work at mount and
580
+ update; unchanged values are not re-sent. The size hints are flat props like
581
+ the geometry they constrain — with style in its own channel the yoga names
582
+ are free, so no `sizeHints` object is needed.
583
+
584
+ | prop | |
585
+ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
586
+ | `resizable` | `false` pins min and max size to the current size |
587
+ | size hints | `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `widthInc`, `heightInc`, `baseWidth`, `baseHeight`, `minAspect`, `maxAspect`, `gravity` |
588
+ | `appId` | `'instance'`, `['instance', 'Class']` or `{instance, class}` |
589
+ | `windowType` | `'dialog'`, `'utility'`, `'tooltip'`… or an array of fallbacks |
590
+ | `decorations` | `false` asks for no titlebar or border |
591
+
592
+ ```jsx
593
+ <window width={400} height={300} resizable={false} windowType="dialog" />
594
+ <window width={400} height={300} minWidth={320} minHeight={200} />
595
+ <window width={400} height={300} minWidth="auto" /> {/* measured — see above */}
596
+ ```
597
+
598
+ The four bounds also take `'auto'`, which asks the content instead of naming
599
+ a number: [a floor the content decides](#a-floor-the-content-decides).
600
+
601
+ On a `<window>` these names are the window's, not yoga's: `width`/`height`
602
+ are the real geometry the user can drag, and `minWidth`/`maxHeight` are what
603
+ the WM enforces. A window's _contents_ are laid out by its `style`. The size
604
+ hints do double duty on an auto-sized window, where they also bound the
605
+ natural size (above) — so `resizable={false}` pins the window at whatever its
606
+ content measured, which is the fixed-size dialog:
607
+
608
+ ```jsx
609
+ <window resizable={false} windowType="dialog" />
610
+ ```
611
+
612
+ ### `transientFor` — a window that belongs to another
613
+
614
+ Without it, every secondary top-level window an app opens is, to the window
615
+ manager, an unrelated second application: its own taskbar and alt-tab entry,
616
+ placed wherever new windows go, stacked independently, and it does not
617
+ minimise with the window it belongs to.
618
+
619
+ ```jsx
620
+ const main = useRef(null);
621
+
622
+ <window ref={main} title="editor">…</window>
623
+ <window transientFor={main} windowType="dialog" title="Preferences">…</window>
624
+ ```
625
+
626
+ Takes a ref to a `<window>`/`<popup>`, a ref to **any drawn node** (resolved
627
+ to the window that owns it — so an out-of-flow `<box>` inside the owner is a
628
+ perfectly good handle), a raw XID, `'root'` for "transient for this client's
629
+ whole window group", or `null` to clear.
630
+
631
+ Measured against quartz-wm — the thinnest EWMH implementation in the stack,
632
+ so this is a floor rather than a best case:
633
+
634
+ | the client sets | the WM allows | the WM adds to `_NET_WM_STATE` |
635
+ | --------------------- | ------------------------------------------------------------- | ------------------------------ |
636
+ | nothing | close, minimize, move, resize, maximize horz/vert, fullscreen | — |
637
+ | `transientFor` | close, minimize, move, resize | skip_taskbar, skip_pager |
638
+ | `windowType="dialog"` | close, minimize, move, resize | skip_taskbar, skip_pager |
639
+
640
+ Rows two and three matching is the EWMH rule working as specified: a managed
641
+ window with `WM_TRANSIENT_FOR` and no `_NET_WM_WINDOW_TYPE` **is** a dialog.
642
+ The converse does not hold — a `dialog` type with no owner is a dialog
643
+ belonging to nobody, with nothing to stack above or iconify with — so set
644
+ both, which is what real toolkits do. A fuller window manager also restacks
645
+ the transient above its owner and iconifies it alongside; quartz-wm does
646
+ neither, and both are policy rather than spec.
647
+
648
+ **It is inert on an override-redirect window**, which is every `<popup>` by
649
+ default: the WM never manages those, so nothing reads the property. ICCCM
650
+ 4.1.2.6 draws exactly that distinction — `WM_TRANSIENT_FOR` for windows the
651
+ WM manages, override-redirect plus a pointer grab for menus. ntk warns if
652
+ you ask it to write the property on one.
653
+
654
+ Resolution happens in the **commit phase**, not at element creation: a React
655
+ ref is not something ntk should be asked to understand. Refs attach in the
656
+ layout phase, after every mutation, so on the commit that mounts two sibling
657
+ `<window>`s the second realizes while the first one's ref is still null —
658
+ that owner is retried on the frame the mount schedules rather than dropped.
659
+
660
+ `windowIdOf(refOrInstance)` is the same resolution, exported: it returns the
661
+ XID a ref points at, or null. It is also what an xdg-desktop-portal
662
+ `parent_window` handle needs — `` `x11:${id.toString(16)}` ``, lowercase hex
663
+ with **no** `0x` prefix. Both shipping portal backends happen to tolerate a
664
+ prefix, which is exactly why it is easy to get wrong and never notice; Qt's
665
+ parser returns 0 on failure with no error path, so the symptom would be a
666
+ silently unparented, non-modal dialog. `useWindowId(ref)` is the hook form
667
+ and returns a getter, like `useAnchor`.
668
+
669
+ `decorations={false}` writes `_MOTIF_WM_HINTS` — honoured by Mutter, KWin,
670
+ Xfwm, Openbox and i3. A window manager that ignores it simply decorates the
671
+ window; there is no way to force the matter.
672
+
673
+ ### Closing a window
674
+
675
+ Every window the window manager manages advertises `WM_DELETE_WINDOW` in
676
+ `WM_PROTOCOLS`, whether or not you pass `onCloseRequest`. That property is
677
+ what lets the WM _ask_ a window to close; a client without it can only be
678
+ shot, so the close button becomes `XKillClient` — the connection dies
679
+ mid-frame, effects never clean up, and IceWM asks the user to confirm the
680
+ kill first. Advertising it is not an opt-in feature, it is the difference
681
+ between closing and crashing, so react-x11 does it for you.
682
+
683
+ Windows the WM does not frame never advertise it, because nothing would read
684
+ it: a child `<window>` (a region inside another window) and an
685
+ override-redirect `<popup>`. A [managed `<popup>`](#a-managed-popup-is-a-dialog)
686
+ is a real dialog and does.
687
+
688
+ What a close request _does_ is the part you can change:
689
+
690
+ | | |
691
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
692
+ | no `onCloseRequest` | the **primary window** unmounts the tree and closes the connection — effects clean up and the process ends on a drained loop. Any other window refuses, and warns in dev. |
693
+ | `onCloseRequest` | nothing happens except your handler: `setOpen(false)` for a dialog, a "save your work?" prompt, `root.unmount()` for a quit of your own. |
694
+
695
+ The primary window is inferred, not declared: the first top-level `<window>`
696
+ that is not `transientFor` another and has no `windowType` of its own. A
697
+ one-window app — nearly every app — is unambiguous, and a lone window is the
698
+ app whatever type it declares.
699
+
700
+ The refusal for secondary windows is deliberate. A `{open && <window/>}` was
701
+ opened by a `setOpen(true)` somewhere, and closing it behind React's back
702
+ would leave a window the app still believes is open and can never reopen. So
703
+ a dialog wants a handler:
704
+
705
+ ```jsx
706
+ {
707
+ showSettings && (
708
+ <window
709
+ title="Settings"
710
+ transientFor={main}
711
+ onCloseRequest={() => setShowSettings(false)}
712
+ />
713
+ );
714
+ }
715
+ ```
716
+
717
+ ### Window state
718
+
719
+ `states` is EWMH `_NET_WM_STATE`: `modal`, `sticky`, `maximized` (or
720
+ `maximized_vert`/`maximized_horz` individually), `shaded`, `skip_taskbar`,
721
+ `skip_pager`, `hidden`, `fullscreen`, `above`, `below`, `demands_attention`,
722
+ `focused`. `fullscreen` and `alwaysOnTop` are boolean sugar for the two
723
+ everyone reaches for — `fullscreen` and `above` — and they union with
724
+ `states` rather than competing with it. On the [cocoa backend](macos.md)
725
+ `fullscreen` is the green button's full-screen Space, `maximized` the zoom,
726
+ `hidden` the Dock, and `demands_attention` bounces the Dock icon until the app
727
+ is activated, stopping when the state is removed ([desktop.md](desktop.md));
728
+ the rest have no AppKit equivalent and are inert there.
729
+
730
+ ```jsx
731
+ <window
732
+ states={['skip_taskbar']}
733
+ fullscreen={isFullscreen}
734
+ onStatesChange={(states) => setFullscreen(states.includes('fullscreen'))}
735
+ />
736
+ ```
737
+
738
+ **These are controlled props, and the reason is not React convention.** On X
739
+ the window manager changes state behind the app's back constantly: the user
740
+ hits maximize, a global hotkey leaves fullscreen, a tiling WM has its own
741
+ opinion. react-x11 therefore diffs `states` against the _previous props_,
742
+ never against what the window currently has — so a prop is re-sent only when
743
+ the app changes its mind, and a WM that disagreed is not fought on the next
744
+ commit. `onStatesChange` is the other half: it reports what the WM actually
745
+ did, and subscribing to it is what makes react-x11 watch the property, so a
746
+ window without a handler costs nothing.
747
+
748
+ States are applied **before the window is mapped**, which is what EWMH 7.7
749
+ requires and the only way to open already fullscreen instead of flashing at
750
+ the normal size first.
751
+
752
+ `alwaysOnTop` falls back to Apple-WM window levels on XQuartz, where
753
+ quartz-wm does not support `_NET_WM_STATE_ABOVE`.
754
+
755
+ ### `onClientMessage` — speaking a protocol of your own
756
+
757
+ Nearly every convention layered over X11 is carried by ClientMessage: EWMH's
758
+ requests to the window manager, XEmbed, the system tray, and whatever two
759
+ copies of one application agree between themselves. `onClientMessage` is
760
+ every one of them that was addressed to this window.
761
+
762
+ ```jsx
763
+ <window
764
+ onClientMessage={(ev) => {
765
+ if (ev.messageType !== '_NET_SYSTEM_TRAY_OPCODE') return;
766
+ if (ev.data[1] === SYSTEM_TRAY_REQUEST_DOCK) dock(ev.data[2]);
767
+ }}
768
+ />
769
+ ```
770
+
771
+ `ev.messageType` is the atom's **name**, which is the point: without it a
772
+ handler has to intern its atoms first and compare numbers, and cannot say
773
+ anything at all until those round trips have landed. `ev.atom` is the id it
774
+ came in as, `ev.format` is 8, 16 or 32, and `ev.data` is the payload at that
775
+ width — 5 values at 32, 10 at 16, 20 at 8.
776
+
777
+ Nothing has to be armed. A ClientMessage reaches its window's owner whatever
778
+ event mask that window selected, so a `<window>` without the prop pays
779
+ nothing and one with it needs no other setup.
780
+
781
+ Three things worth knowing:
782
+
783
+ - **Messages arrive in the order they were sent.** The chunked protocols
784
+ depend on it — `_NET_SYSTEM_TRAY_BEGIN_MESSAGE` and the
785
+ `_NET_SYSTEM_TRAY_MESSAGE_DATA` pieces after it reassemble by arrival order
786
+ and nothing else. Naming a type this connection has never seen costs one
787
+ `GetAtomName`, and everything behind it waits rather than overtaking it.
788
+ `messageType` is therefore `null` only for an atom the **server** does not
789
+ know, which is a broken sender.
790
+ - **It is scoped to the window the message names**, not to the connection —
791
+ that is the whole difference from `useApp().X.on('event')`, which sees
792
+ every event for every window and outlives the element. A message aimed at
793
+ another window, including the EWMH ones sent to the root, does not arrive
794
+ here; a window manager wants the raw stream for those
795
+ (`examples/wm-core.js`).
796
+ - **`preventDefault()` stops react-x11 acting on the message itself**, which
797
+ today means XDND — for a window answering the drag protocol on its own
798
+ terms. It does not cover the WM close button; `onCloseRequest` is that
799
+ seam.
800
+
801
+ Sending is `useApp().X.SendClientMessage(destination, aboutWindow, atom,
802
+ format, data, eventMask)`, or ntk's `window.sendClientMessage(name, data)`
803
+ which interns the atom for you. Note the two window arguments: EWMH messages
804
+ about a window are delivered to the **root**, and messages to another
805
+ application's window pass an event mask of `0`.
806
+
807
+ Selections are the other half of most of these protocols, and taking one
808
+ needs a timestamp rather than `CurrentTime`:
809
+ [`lastInputTime` and `serverTime`](clipboard.md#owning-a-selection-of-your-own).
810
+
811
+ ### Kiosk
812
+
813
+ There is no `kiosk` prop, because it is not one thing — it is a window with
814
+ no decoration, no taskbar entry, nothing above it, and no pointer:
815
+
816
+ ```jsx
817
+ <window
818
+ fullscreen
819
+ decorations={false}
820
+ states={['above', 'skip_taskbar', 'skip_pager']}
821
+ style={{ cursor: 'none', flexGrow: 1 }}
822
+ />
823
+ ```
824
+
825
+ `cursor: 'none'` hides the pointer outright (ntk ≥ 4.2.0). It is not the
826
+ same as leaving `cursor` unset, which inherits whatever the root window's
827
+ cursor is — see [styling.md](styling.md).
828
+
829
+ ### `transparent` — rounded corners and translucency
830
+
831
+ `transparent` creates the window on a 32-bit ARGB visual, so it has a real
832
+ alpha channel: what the tree does not paint stays empty, and a compositor
833
+ blends it against whatever is behind. That is what makes rounded corners
834
+ possible, and it is the main reason to reach for it:
835
+
836
+ ```jsx
837
+ <popup
838
+ transparent
839
+ grab
840
+ style={{ backgroundColor: 'rgba(24, 24, 30, 0.86)', borderRadius: 14 }}
841
+ >
842
+ …
843
+ </popup>
844
+ ```
845
+
846
+ Two props do the work together. `transparent` gives the window somewhere to
847
+ put transparency; `borderRadius` in the window's own `style` rounds the
848
+ background painted into it, and the corners it gives up are the corners the
849
+ desktop then shows through. The edge is **antialiased**, because it is alpha
850
+ rather than the Shape extension's 1-bit mask — no `XShapeCombineMask`, no
851
+ jagged diagonal.
852
+
853
+ `backgroundColor` may be translucent, and leaving it unset gives a window
854
+ that is empty except for what the tree paints.
855
+
856
+ `borderRadius` on a window is meaningful **only** with `transparent`. On an
857
+ opaque window it is ignored: giving up the corners there would only expose
858
+ the server's white, which is worse than square.
859
+
860
+ ### When transparency is not available
861
+
862
+ **On the Cocoa backend it always is** — every window composites and every
863
+ window has an alpha channel, so `transparent` and a `borderRadius` on a
864
+ window simply work, and the rest of this section is about X11.
865
+
866
+ Transparency there needs two things, and either can be missing: a
867
+ **depth-32 TrueColor visual** (XQuartz has none), and a **running
868
+ compositor** (Mutter, KWin, picom, …) to blend the alpha channel. Without a
869
+ compositor the X server shows the raw pixels, and a corner you painted away
870
+ is not transparent — it is **black**.
871
+
872
+ react-x11 never lets that happen. When transparency would not actually be
873
+ seen, the window is filled edge to edge and `borderRadius` on it is ignored:
874
+ you get the square opaque popup, not a black-cornered one. A translucent
875
+ `backgroundColor` is flattened rather than composited onto the last frame.
876
+ Nothing is required of the application for that floor to hold.
877
+
878
+ What the application _does_ control is the design on the other side of it,
879
+ through the `'@supports transparency'` style block:
880
+
881
+ ```jsx
882
+ <popup
883
+ transparent
884
+ style={{
885
+ backgroundColor: '#1c1c22', // square and opaque, works everywhere
886
+ '@supports transparency': {
887
+ backgroundColor: 'rgba(24, 24, 30, 0.86)',
888
+ borderRadius: 14,
889
+ },
890
+ }}
891
+ />
892
+ ```
893
+
894
+ See [Capability queries](styling.md#capability-queries). The block is
895
+ answered per window and re-resolved live, so a compositor being switched on
896
+ mid-session turns the popup rounded without a remount — which is why
897
+ `transparent` still takes the 32-bit visual when nothing is compositing yet.
898
+ A visual is a `CreateWindow` field and cannot be changed afterwards; what
899
+ the window paints can.
900
+
901
+ For decisions that are not styling, `useSupports('transparency')` answers
902
+ the same question about the display, and can be asked before any window
903
+ exists:
904
+
905
+ ```jsx
906
+ const canBlend = useSupports('transparency');
907
+ const margin = canBlend ? 26 : 0; // room for a client-drawn shadow
908
+ ```
909
+
910
+ To **look at the fallback** on a display that composites perfectly well,
911
+ run with `REACT_X11_NO_TRANSPARENCY=1`: `transparent` is ignored, the window
912
+ is created on the ordinary visual, and both the style block and
913
+ `useSupports` answer false. Otherwise the only way to see the design you
914
+ wrote for XQuartz is to stop the compositor, which takes the rest of your
915
+ desktop with it. See [debugging.md](debugging.md).
916
+
917
+ Toggling the `transparent` prop itself on a mounted window does nothing
918
+ until it remounts (change its `key`) — again because the visual is fixed at
919
+ creation. Requires **ntk ≥ 6.6.0**.
920
+
921
+ Two things a transparent window does **not** change. Input still hits the
922
+ full rectangle — the corners are invisible, not click-through; use the Shape
923
+ extension's input region through the ntk ref if that matters. And child
924
+ `<window>`s are composited by the X server, not the compositor, so nesting a
925
+ transparent window inside another window blends against nothing.
926
+
927
+ ## `<popup>`
928
+
929
+ An override-redirect top-level window at **screen coordinates** — the
930
+ window manager ignores it (no decorations, no focus stealing): menus,
931
+ tooltips, dropdowns. May appear anywhere in the JSX tree (its position in
932
+ the tree does not affect its position on screen); it is its own paint and
933
+ event root. Give it an `anchor` and it places itself against a node
934
+ ([below](#anchor--a-popup-that-places-itself)); `x`/`y` are there for the
935
+ placements that are nobody's node — `ev.screenX`/`screenY`, the pointer on
936
+ the virtual screen, in the logical pixels `x`/`y` are themselves in. Same props as `<window>` — **including
937
+ [natural size](#natural-size)**, which is what a menu sized by its own rows
938
+ is — and conditional rendering controls its lifetime.
939
+
940
+ | prop | |
941
+ | ------------------ | -------------------------------------------------------------------------------------------------------------- |
942
+ | `anchor` | place against a node, a rect inside one, or a rect on the screen, at whatever size the content turns out to be |
943
+ | `grab` | hold a pointer grab while the popup is up — how menus behave on X (below) |
944
+ | `grabKeyboard` | take the keyboard while the popup is up — what a popover a tray click opened needs (below) |
945
+ | `transparent` | 32-bit ARGB visual: rounded corners and translucency ([above](#transparent--rounded-corners-and-translucency)) |
946
+ | `onDismiss` | a press landed outside the popup: close it |
947
+ | `trapFocus` | own a focus scope: a modal (see [events.md](events.md#focus-scopes-modals)) |
948
+ | `overrideRedirect` | `false` makes it a WM-managed window instead — a real dialog (below) |
949
+ | `dragPreview` | this popup is a drag preview following the pointer, never a drop target — [drag-and-drop.md](drag-and-drop.md) |
950
+
951
+ ### `anchor` — a popup that places itself
952
+
953
+ ```jsx
954
+ function Completions({ editorRef, caret, matches, onPick }) {
955
+ if (!matches.length) return null;
956
+ return (
957
+ <popup
958
+ anchor={{ to: editorRef, at: caret, placement: 'bottom' }}
959
+ maxHeight={220}
960
+ >
961
+ <box
962
+ style={{ overflow: 'scroll', padding: 4, backgroundColor: '$surface' }}
963
+ >
964
+ {matches.map((m) => (
965
+ <box key={m.label} onClick={() => onPick(m)} style={{ padding: 6 }}>
966
+ <text>{m.label}</text>
967
+ </box>
968
+ ))}
969
+ </box>
970
+ </popup>
971
+ );
972
+ }
973
+ ```
974
+
975
+ That is the whole completion list: **at the caret**, as wide as its widest
976
+ label, no taller than 220 and scrolling past that, flipped above the line
977
+ when it is near the bottom of the screen. Nothing in it measures a font,
978
+ states a size, or names a position — `caret` is a rect the editor already
979
+ knows, in its own coordinates.
980
+
981
+ `anchor` takes [`anchorRect`'s options](components.md#useanchorref--anchorrectnode-options)
982
+ — `placement`, `align`, `offset`, `alignOffset`, `alignTo`, `at` — plus `to`,
983
+ the node (or a ref to one) it hangs off. `x`/`y` are ignored while it is set.
984
+
985
+ **Why the popup and not the application.** A `<popup>` with no `width` is
986
+ sized from its content like any other window, and that size is settled
987
+ _inside_ `realize()` — after the content is measured, before `CreateWindow`.
988
+ Which side the popup flips to and how far it is pulled back from a screen
989
+ edge are both functions of that size, so the earliest anyone can work out the
990
+ position is the moment after the measurement — which is already too late for
991
+ React to have passed one in. Doing it here keeps the rule the natural size
992
+ established: the window is **born** the right size in the right place, rather
993
+ than mapped somewhere provisional and corrected a frame later.
994
+
995
+ Afterwards it keeps up with everything that can move either half — the
996
+ anchor's own layout, an ancestor of it scrolling, the owner window being
997
+ dragged by the window manager, its own content growing a row.
998
+
999
+ `at` is a rect **inside** the node, in the node's own coordinates:
1000
+
1001
+ ```js
1002
+ const caret = { x: caretX, y: lineTop, width: 1, height: lineHeight };
1003
+ ```
1004
+
1005
+ Everything reads it — the side that flips, the edge that aligns, the gap
1006
+ `offset` leaves — so a popup at a caret behaves like one hung off a small
1007
+ widget that happens to be there. `width`/`height` may be left out, and
1008
+ `{x, y}` alone is a point.
1009
+
1010
+ A popup whose anchor is a ref that has not attached yet — one written
1011
+ _above_ its own trigger in the JSX — waits for it rather than opening in the
1012
+ corner of the screen. That is one frame, and it is the same rule as the one
1013
+ below: an anchored popup is on screen only while there is something for it
1014
+ to point at.
1015
+
1016
+ **When the anchor scrolls out of view the popup unmaps** and comes back,
1017
+ where it belongs — and holding its `grab` again, since X drops a pointer
1018
+ grab whose window stops being viewable — when the anchor does. A popup is a real X window rather
1019
+ than an element its ancestors clip, so a caret that has scrolled out of the
1020
+ editor leaves a list floating over a document it no longer points into —
1021
+ and there is no position that fixes that. Whether it should _close_ instead
1022
+ is React state and therefore the application's: do the placement yourself
1023
+ with [`useAnchorTracking`](components.md#useanchorref--anchorrectnode-options)
1024
+ and its `onOutOfView`.
1025
+
1026
+ A popup never receives the X input focus, but nodes inside it can hold the
1027
+ **owner window's** focus and receive keys — with `trapFocus` and `autoFocus`
1028
+ that is a modal dialog. See
1029
+ [Focus inside a `<popup>`](events.md#focus-inside-a-popup). A popup that has
1030
+ no owner window to receive keys for it takes them itself with
1031
+ `grabKeyboard` ([below](#a-popover-under-a-tray-item)).
1032
+
1033
+ **`grab` is what makes a menu dismissable.** Without it, a press that lands
1034
+ anywhere else — another application, the root window, or even this app's own
1035
+ **window frame**, which belongs to the window manager — never reaches the
1036
+ client, so the menu stays open behind whatever was clicked. With the grab,
1037
+ that press is redirected to the popup instead, arrives outside its bounds,
1038
+ and `onDismiss` fires. The client's own windows still receive their own
1039
+ presses (X owner-events), so submenus and the owner window keep working
1040
+ normally, and only the root popup of a menu needs the grab. Needs
1041
+ ntk ≥ 3.7.0; on older ntk the popup behaves as it did before.
1042
+
1043
+ `Select`, `ContextMenu` and `MenuBar` already do this.
1044
+
1045
+ ### A popover under a tray item
1046
+
1047
+ A tray click has no node behind it: [`useTray`](desktop.md#the-tray)'s
1048
+ `onClick` reports the item's frame on the screen, where the platform knows
1049
+ it. `anchor` takes that as `rect`, in place of `to`, and places against it
1050
+ with the same flip, clamp and natural size — kept on the monitor the rect is
1051
+ on, and below the menu bar, since that is not usable area:
1052
+
1053
+ ```jsx
1054
+ function MenuBarApp() {
1055
+ const [open, setOpen] = useState(null);
1056
+ const close = () => setOpen(null);
1057
+ useTray({ icon: 'waveform', onClick: setOpen });
1058
+ if (!open) return null;
1059
+ return (
1060
+ <popup
1061
+ anchor={{ rect: open, placement: 'bottom', align: 'center', offset: 6 }}
1062
+ grab
1063
+ grabKeyboard
1064
+ onDismiss={close}
1065
+ onBlur={close}
1066
+ >
1067
+ <Controls onClose={close} />
1068
+ </popup>
1069
+ );
1070
+ }
1071
+ ```
1072
+
1073
+ `rect` is logical screen pixels, the unit a popup's `x`/`y` are in, and
1074
+ `{ x, y }` alone is a point, which is all a freedesktop tray reports: its
1075
+ protocol has no item rect. Nothing moves the rect but a new one, and it is
1076
+ never out of view.
1077
+
1078
+ ### Centred over something
1079
+
1080
+ `placement: 'center'` puts the popup **over** its anchor rather than beside
1081
+ it — the middle of one on the middle of the other, clamped into the monitor,
1082
+ with nothing to flip to. Over a window's rect it is a dialog, and that is
1083
+ what [`Dialog`](components.md#dialog) does: it takes its owner window's rect
1084
+ on the screen when it opens and hands it over as `rect`, so the popup is
1085
+ measured, centred at the size it measured and only then created.
1086
+
1087
+ ```jsx
1088
+ <popup
1089
+ anchor={{ rect: ownerRect, placement: 'center' }}
1090
+ width={360}
1091
+ height="auto"
1092
+ >
1093
+ ```
1094
+
1095
+ A centred popup is centred **once**. When its own size changes afterwards —
1096
+ its content growing a line — it keeps its top edge and its horizontal middle
1097
+ and grows from there, pulled back only as far as it takes to stay on the
1098
+ monitor: re-centring would move the top edge by half the growth, and with it
1099
+ whatever the user was reading or about to press. A managed one grows from
1100
+ wherever the window manager or the user has put it since, and is moved only
1101
+ to keep it on the monitor. A new `rect` (or the `to` node moving) centres it
1102
+ again.
1103
+
1104
+ **The monitor comes from the whole rect**, not from a corner of it. A
1105
+ menu-bar item's frame starts a few points _above_ the top of the display it
1106
+ is on, so its top-left corner is outside that display — and on a desk where
1107
+ a taller neighbour reaches down past that edge, inside a different one. The
1108
+ monitor the rect overlaps most is the one it is on; a rect that overlaps
1109
+ none takes the nearest.
1110
+
1111
+ **`grabKeyboard` is the other half.** A menu-bar app has no window of its
1112
+ own, so the keys a popup's content would normally get through its owner
1113
+ window arrive nowhere. With it the popup takes the keyboard while it is up:
1114
+
1115
+ | backend | what it is |
1116
+ | ------- | ---------------------------------------------------------------------------------------------- |
1117
+ | X11 | a keyboard grab, taken and dropped with the map, as `grab` is for the pointer |
1118
+ | macOS | a window AppKit can make key, at the popup level, shown activating the app — `NSPopover`'s way |
1119
+ | Wayland | nothing more: a grabbing popup has the keyboard already |
1120
+
1121
+ On macOS the window's kind is decided when the popup is created, so the prop
1122
+ does not change on a mounted popup. There the popover closes on `onBlur` —
1123
+ clicking any other app takes the key window away — and on X on `onDismiss`,
1124
+ the press outside the pointer grab reports. Written with both, it closes the
1125
+ same way on either.
1126
+
1127
+ **Shortcuts in the popover work, with nothing passed.** A binding belongs to
1128
+ the tree's top-level `<window>`, and in an app that has none it belongs to
1129
+ the root-level `<popup>` that took the keyboard — this one, which is where
1130
+ the keys are arriving anyway:
1131
+
1132
+ ```jsx
1133
+ function Controls({ onClose, onToggle }) {
1134
+ useAccelerator([['space']], onToggle);
1135
+ useAccelerator([['Escape']], onClose);
1136
+ return <box style={{ padding: 12, gap: 8 }}>…</box>;
1137
+ }
1138
+ ```
1139
+
1140
+ The same goes for everything else that resolves an owner window on its own:
1141
+ `useFileDialog`'s parent, the global menu, `useWindowState`. A binding written
1142
+ where there is neither a window nor a popup under it — above the `if (!open)`
1143
+ in the app above, say — hangs off nothing and never fires; development says so
1144
+ once, and [`scope`](events.md#accelerators) anchors one at a node you name.
1145
+
1146
+ ### A managed `<popup>` is a dialog
1147
+
1148
+ `overrideRedirect` defaults to `true` and is what makes a menu a menu.
1149
+ Passing `false` gives up all of that on purpose and hands the window to the
1150
+ window manager: it is reparented into a frame, so it has a titlebar, can be
1151
+ moved, and has a WM close button (`onCloseRequest`). Together with
1152
+ `transientFor` that is a real dialog — above its owner, out of the taskbar
1153
+ and alt-tab list, minimising with the owner.
1154
+
1155
+ ```jsx
1156
+ <popup overrideRedirect={false} transientFor={anchor} windowType="dialog"
1157
+ title="Preferences" trapFocus>
1158
+ ```
1159
+
1160
+ Turn `grab` off with it. A client-side pointer grab over a window the WM is
1161
+ trying to let the user drag swallows the press that would start the drag.
1162
+ That also means a press outside no longer dismisses it — which is correct: a
1163
+ real dialog does not close because you clicked elsewhere.
1164
+
1165
+ The [`Dialog`](components.md#dialog) component does exactly this, and takes
1166
+ `managed={false}` to go back to the override-redirect shape.
1167
+
1168
+ Defaults to `windowType="popup_menu"` — the least-wrong answer for a popup
1169
+ that declares nothing; pass `windowType` to say what yours is
1170
+ (`"tooltip"`, `"dropdown_menu"`, …). The built-in widgets do:
1171
+ `Select`'s sheet is `dropdown_menu`, `Tooltip` is
1172
+ `tooltip`. The hint is **additive** — override-
1173
+ redirect is what keeps the WM from moving or decorating the popup, and
1174
+ stays on. The EWMH spec asks for the type hint on override-redirect
1175
+ windows too, so compositing managers can give menus and tooltips
1176
+ consistent shadows and animations.
1177
+
1178
+ ## `<box>`
1179
+
1180
+ The flex container. All layout + paint + interaction props above.
1181
+ **Ref**: the retained node — `abs` (`{x, y, width, height}` within the
1182
+ window, valid after layout).
1183
+
1184
+ With `overflow: 'scroll'` it is also the **scroll container** — see below.
1185
+
1186
+ ## Scrolling: `overflow: 'scroll'` {#scrolling}
1187
+
1188
+ There is no scrolling _element_. A `<box>` — or a `<window>` — whose style
1189
+ says `overflow: 'scroll'` becomes a clipped viewport over its own
1190
+ overflowing children, on **both axes**:
1191
+
1192
+ ```jsx
1193
+ <box style={{ overflow: 'scroll', flexGrow: 1 }}>{rows}</box>
1194
+ ```
1195
+
1196
+ Wheel events scroll it by default; a scrollbar thumb is drawn on each axis
1197
+ that overflows, and the thumb can be dragged. `overflow: 'hidden'` still
1198
+ means what it always did — clip and do not scroll.
1199
+
1200
+ Because the switch is a style and not an element, a pane can start and stop
1201
+ scrolling without remounting, which is what makes `overflow={dense ?
1202
+ 'scroll' : 'visible'}` cheap: the node, its children and their state all
1203
+ survive the change. When a box stops being a scroll container its offset
1204
+ resets to 0, as it does in CSS.
1205
+
1206
+ **It is also a tab stop, and answers the keyboard**, whenever it has
1207
+ somewhere to scroll — which is what lets a pane of unfocusable content (a
1208
+ log, a long `<text>`, a rendered document) be read without a pointer at all:
1209
+
1210
+ | key | |
1211
+ | ------------------- | ------------------------------------ |
1212
+ | arrows | one wheel notch (48px) on that axis |
1213
+ | PageUp / PageDown | a viewport, keeping a sliver of it |
1214
+ | Space / Shift+Space | the same, for the hand already there |
1215
+ | Home / End | the top and the bottom |
1216
+
1217
+ One that fits its content is not a tab stop, answers no keys and takes no
1218
+ wheel: it is a box with a clip, and stopping on it would be a stop that does
1219
+ nothing. **The wheel chains past it** to the next scroll container out — the
1220
+ same thing a browser does — so declaring a pane scrollable never steals a
1221
+ gesture the window would have answered. The keys are a default action, so an
1222
+ `onKeyDown` of your own runs first and `preventDefault()` cancels them.
1223
+
1224
+ A pane that also has an `onClick` keeps Space for paging: paging is the
1225
+ pane's own default action and runs ahead of the keyboard activation Space
1226
+ would otherwise be, so such a pane pages with Space and activates with Enter
1227
+ ([events.md](events.md#space-and-enter-are-a-click)). A focusable row _inside_
1228
+ a pane takes both keys and pages with neither — a default action runs on the
1229
+ focused node, and that is the row.
1230
+
1231
+ The bar belongs to the scroller, not to the content painted under it — the
1232
+ same rule a browser applies — so a press on the thumb never reaches the row
1233
+ behind it. Dragging keeps the grip where it was taken, so the thumb does not
1234
+ jump to the pointer, and a press on the track pages towards it, like
1235
+ PageUp/PageDown. A drag moves the pane by whole device pixels, as a wheel
1236
+ does ([extending.md](extending.md#scrolling-content-you-painted)), so the
1237
+ scroll stays a blit and nothing in the pane is drawn off the pixel grid.
1238
+ `<textarea>` behaves the same way, and there a bar press never moves the
1239
+ caret.
1240
+
1241
+ ### The layout defaults it brings
1242
+
1243
+ `overflow: 'scroll'` folds three CSS idioms into the resolved style, so a
1244
+ scroll container does not have to restate them:
1245
+
1246
+ | default | when |
1247
+ | ------------------------------ | ---------------------------------------- |
1248
+ | `flexBasis: 0` | `flexGrow > 0` and no `width` / `height` |
1249
+ | `minWidth: 0` / `minHeight: 0` | always |
1250
+
1251
+ `min-*: 0` is the CSS spec's own rule — `min-*: auto` computes to `0` on a
1252
+ flex item whose overflow is not `visible` — and it is what lets a viewport be
1253
+ smaller than what is inside it, which is what scrolling is. The zero basis is
1254
+ half of what `flex: 1` means. Without them a header/scroll/footer window
1255
+ grows past its own bounds as rows are added and the footer is pushed out of
1256
+ view. Set either yourself to opt out; an explicit value always wins.
1257
+
1258
+ ### Props
1259
+
1260
+ | prop | |
1261
+ | ------------------- | -------------------------------------------------------------------------------- |
1262
+ | `onScroll(ev)` | `{scrollX, scrollY, contentWidth, contentHeight, viewportWidth, viewportHeight}` |
1263
+ | `onViewport(ev)` | `{width, height, contentWidth, contentHeight}` whenever they change |
1264
+ | `scrollbar={false}` | hide the drawn scrollbars |
1265
+ | `scrollbarColor` | thumb color |
1266
+
1267
+ **Ref**: the node, plus `scrollTo` / `scrollBy` / `scrollIntoView(node)` and
1268
+ `scrollX` / `scrollY` / `contentWidth` / `contentHeight`. In TypeScript,
1269
+ `useRef<ScrollableNode>(null)` types those in; `DrawnNode` still works for a
1270
+ box you do not scroll.
1271
+
1272
+ `scrollTo(y)` takes a number for the vertical axis; `scrollTo({x, y})` moves
1273
+ either, leaving alone whichever you omit. `scrollBy` matches.
1274
+ `scrollIntoView(node)` scrolls the minimum amount on both axes, and a node
1275
+ larger than the viewport that already covers it not at all.
1276
+ `canScroll(dx, dy)` answers whether there is room to move on the axis a delta
1277
+ names — what the wheel asks each node on its way out, and the one method an
1278
+ element of your own implements to be asked it too
1279
+ ([extending.md](extending.md#scrolling-content-you-painted)).
1280
+
1281
+ Horizontal content comes from children that will not shrink — a row of
1282
+ fixed-width cells, say. The extent is measured **through the subtree**, the
1283
+ way `scrollWidth` is in a browser: a row that stretches to the viewport
1284
+ while its own cells overflow it still reports something to scroll. Anything
1285
+ that clips its own children ends that measurement, since their overflow
1286
+ belongs to them.
1287
+
1288
+ A device with a horizontal axis scrolls sideways with it; **Shift + vertical
1289
+ wheel** does too, for the mice and touchpads that have none. When both bars
1290
+ show, each stops short of the other's corner.
1291
+
1292
+ The distance is the device's, not a constant: where the server has XI2 a
1293
+ touchpad's scroll arrives as the fraction of a notch it measured, so a pane
1294
+ moves by a few pixels rather than by 48 at a time. A wheel notch is still 48
1295
+ pixels, and so is an arrow key ([events.md](events.md#wheel)).
1296
+
1297
+ `onViewport` fires from **layout**, not from scrolling, so it arrives for a
1298
+ list nobody has scrolled yet. That is what a virtualized list needs before
1299
+ it can decide how many rows are worth building: layout runs on the frame
1300
+ clock, after the commit that mounted the node, so an effect cannot read the
1301
+ size off the ref. `Table` is built on it. `onLayout`
1302
+ ([Interaction props](#interaction-props)) is the same signal on any element;
1303
+ `onViewport` adds the content size a scroller measures.
1304
+
1305
+ `scrollIntoView(node)` scrolls the minimum amount that makes a descendant
1306
+ node fully visible — for one larger than the viewport, CSSOM View's
1307
+ `block: nearest`: left where it covers the viewport, and otherwise brought
1308
+ in by the edge that moves it least — and is safe to call from an effect right after that
1309
+ node mounts: the request is resolved on the next layout pass, when the
1310
+ node actually has geometry. Asking repaints nothing by itself. A request
1311
+ that finds the node in view already leaves the pane as it was, so focus
1312
+ moving between the fields of a form in a pane costs what it costs outside
1313
+ one: the two focus rings. One that does move the pane is a scroll like any
1314
+ other: where the backend can copy pixels, what stays on screen is copied
1315
+ and only the strip the move uncovers is repainted.
1316
+
1317
+ `scrollTo` answers inside the call, from the extent the last layout pass
1318
+ measured: the offset moves and `onScroll` fires before it returns. When a
1319
+ pass is already owed, the part of the request that extent could not answer
1320
+ is kept as well, and the pass answers it again against the content it
1321
+ measures, before the frame paints. Two cases need that:
1322
+
1323
+ - A pane no pass has laid out yet has no extent at all. Restoring a list's
1324
+ position from a mount-time `useLayoutEffect` shows the list at that
1325
+ position in its first frame.
1326
+ - Rows added in the commit the request comes from are not in the extent
1327
+ yet. A log that follows its new lines with `scrollTo(Infinity)` from a
1328
+ layout effect shows each one at the end, in the first frame that has it.
1329
+
1330
+ `Infinity` means the end, wherever the pass finds it, and the End key goes
1331
+ there the same way. Prefer it to `contentHeight`, which is the last pass's
1332
+ measurement too: a target computed from it falls short when a commit adds
1333
+ more than a viewport. A `scrollBy` made before that pass moves on from where
1334
+ the pass puts the pane. Until the pass, `scrollX`/`scrollY` read the call's
1335
+ answer, and `onScroll` reports the pass's once it is over.
1336
+
1337
+ `onScroll` fires for **every** move of the offsets, not only the ones a call
1338
+ made. The wheel, the keys, a bar and `scrollTo`/`scrollBy` on a laid-out
1339
+ pane report inside the call. Layout moves the offsets on its own, and a
1340
+ browser fires `scroll` for each: the held `scrollTo` above landing, a
1341
+ `scrollIntoView` resolving (focus landing on a row below the fold is one),
1342
+ and the clamp that pulls the offset back when the content shrinks or the
1343
+ viewport grows under it, as when rows are filtered out of a list scrolled
1344
+ to its end. Those report once the pass is over, like `onViewport`: after
1345
+ the frame that shows the new offset, and only if the pass moved it. The
1346
+ payload is read when the report is delivered, so a handler that mirrors the
1347
+ offset into state, as `Table` does, ends where the pane did.
1348
+
1349
+ ### On a `<window>`
1350
+
1351
+ `<window style={{ overflow: 'scroll' }}>` scrolls the window's own content,
1352
+ with no inner pane at all — the same wheel, keys and bars. The window stays
1353
+ a `frame` to a screen reader whatever its overflow says. Its ref is still
1354
+ ntk's `Window`, so reach the scroll API through a ref on a box inside it, or
1355
+ through the node in `onViewport`.
1356
+
1357
+ ## `<text>`
1358
+
1359
+ Shaped, wrapped text through ntk's text stack (bidi, ligatures, kerning,
1360
+ font fallback). Strings/numbers are only legal inside `<text>`; nested
1361
+ `<text>` elements are **style spans** — the paragraph is laid out as one
1362
+ run list so wrapping spans the whole content.
1363
+
1364
+ | prop | |
1365
+ | --------------------------------------------------- | ------------------------------------------------------ |
1366
+ | `color` | text color — inherited (see below) |
1367
+ | `fontSize`, `fontFamily`, `fontWeight`, `fontStyle` | ntk font style (fontconfig lookup + fallback) |
1368
+ | `fontVariationSettings` | a variable font's axes — see below |
1369
+ | `textRendering` | which glyph path to draw with — see below |
1370
+ | `letterSpacing` | space after every character, in px — see below |
1371
+ | `fontVariantNumeric`, `fontFeatureSettings` | figures and OpenType features — see below |
1372
+ | `textAlign` | `left`, `right`, `center`, `start`, `end` (bidi-aware) |
1373
+ | `lineHeight` | multiplier over the natural font line height |
1374
+ | `textWrap` | `wrap` (default) or `nowrap` |
1375
+ | `maxLines` | how many lines are kept — unlimited by default |
1376
+ | `textOverflow` | `clip` (default) or `ellipsis`, for what was cut |
1377
+
1378
+ The first four rows, `fontVariationSettings`, `textRendering`,
1379
+ `letterSpacing` and the two feature properties **inherit** — from a nested
1380
+ span's point of view that has always been true, and it is equally true
1381
+ across a `<box>`, so a caption block is written once around the labels in it
1382
+ ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)).
1383
+ The rest do not: they shape the box the lines flow in, which belongs to the
1384
+ `<text>` that owns it.
1385
+
1386
+ `maxLines` and `textOverflow` are how a label says it did not fit —
1387
+ `{ textWrap: 'nowrap', textOverflow: 'ellipsis' }` is one line ending in a
1388
+ `…` at the pixel the box ends at, and an ellipsis with no `maxLines` means
1389
+ one line. The cut is a fact about the pixels only: the accessible name, the
1390
+ caret indices and `textContent()` are all still the whole string. See
1391
+ [styling.md](styling.md#keeping-text-on-one-line-and-saying-when-it-did-not-fit).
1392
+
1393
+ A face the app supplies itself — one it ships, or one the user picked in a
1394
+ dialog — is opened with `loadFont`/`useFont`, which hand back the family
1395
+ name to put in `fontFamily`
1396
+ ([styling.md](styling.md#a-font-file-of-your-own)).
1397
+
1398
+ `start` and `end` are resolved against the box's own **direction**, not
1399
+ against the first strong character in the string — the box says which way it
1400
+ reads and the paragraph is laid out at that base level, so `"(12) files"` in
1401
+ an RTL panel punctuates as an RTL sentence and a `<text>` with no strong
1402
+ characters at all still lands on the right side.
1403
+ See [styling.md](styling.md#direction-and-the-logical-edges).
1404
+
1405
+ ### Variable fonts
1406
+
1407
+ _ntk ≥ 7.1.0._
1408
+
1409
+ A variable font is one file with a continuous design space, and
1410
+ `fontWeight` already drives its `wght` axis — so a family with one variable
1411
+ file behaves like a family with nine faces, and like nine hundred, since
1412
+ the weights between the named instances are the point of an axis:
1413
+
1414
+ ```jsx
1415
+ <text style={{ fontFamily: 'Inter', fontWeight: 460 }}>
1416
+ Serious by default.
1417
+ </text>
1418
+ ```
1419
+
1420
+ `fontVariationSettings` is for the other axes, by OpenType tag:
1421
+
1422
+ ```jsx
1423
+ <text style={{ fontVariationSettings: { wdth: 87.5, slnt: -8 } }}>
1424
+ condensed
1425
+ </text>
1426
+ ```
1427
+
1428
+ It inherits into spans like any other text style prop, and a span may
1429
+ override it. Axes the font does not have are ignored and values are clamped
1430
+ to each axis's range, so it is safe to set without checking the face first.
1431
+ It is compared **by value**, so the object literal above is fine — a fresh
1432
+ one with the same numbers on the next render costs nothing.
1433
+
1434
+ ntk instantiates on demand and rasterizes only the glyphs actually drawn,
1435
+ at the size drawn; both its caches are bounded. What it does not do is
1436
+ quantize for you, so a slider bound straight to an axis with `step={1}`
1437
+ really will ask for hundreds of distinct faces as it is dragged — step the
1438
+ control, not the font. See ntk's `docs/fonts.md` for the whole picture,
1439
+ including why a variable `.woff2` cannot be instantiated and you want the
1440
+ `.ttf`.
1441
+
1442
+ ### `textRendering`
1443
+
1444
+ _ntk ≥ 7.2.0._ CSS's property, picking which glyph path draws the text.
1445
+
1446
+ | value | |
1447
+ | -------------------- | --------------------------------------------------------- |
1448
+ | `auto` (default) | ntk decides from the size — right for UI text |
1449
+ | `geometricPrecision` | glyph origins exactly where shaping asked; nothing cached |
1450
+ | `optimizeSpeed` | ntk's cached glyph bitmaps, at any size |
1451
+ | `optimizeLegibility` | accepted, means `auto` — there is no hinting to turn on |
1452
+
1453
+ ```jsx
1454
+ <text style={{ fontSize: 96, textRendering: 'geometricPrecision' }}>
1455
+ Serious by default.
1456
+ </text>
1457
+ ```
1458
+
1459
+ **Why display text wants it.** On the cached path a glyph's advance is baked
1460
+ into the server-side glyphset as a whole number, because a cached bitmap can
1461
+ only land on a whole pixel. Animate a variable font's `wght` and the true
1462
+ advances move by hundredths of a pixel — hundredths that accumulate along
1463
+ the line until one glyph crosses a rounding boundary and jumps a whole pixel
1464
+ on its own while its neighbours stand still. `geometricPrecision` puts every
1465
+ glyph where shaping asked, so the line slides instead of stepping.
1466
+
1467
+ It inherits into spans and a span may override it, so a headline and the
1468
+ body text under it can take different paths in one paragraph.
1469
+
1470
+ **It never reflows.** Only draw-time rounding differs; ntk's layout measures
1471
+ byte-identically for every value, down to per-run offsets. Changing it drops
1472
+ the cached layout — the value rides on the spans inside it — and repaints,
1473
+ without marking anything dirty for yoga. So it is safe to flip on a state
1474
+ change without the tree moving.
1475
+
1476
+ The cost is real: the precise path rasterizes outlines on every draw and
1477
+ caches nothing. That is the right trade for text being animated and the
1478
+ wrong one for a paragraph that never changes, which is why `auto` is the
1479
+ default rather than the other way round.
1480
+
1481
+ ### Letter spacing and figures
1482
+
1483
+ _ntk ≥ 8.9.0 on X11, `@windowkit/appkit` ≥ 0.11.0 on Cocoa._
1484
+
1485
+ `fontVariantNumeric: 'tabular-nums'` gives every digit one width. Most UI
1486
+ faces — SF Pro, Inter — draw proportional figures by default, where a `1` is
1487
+ much narrower than an `8`, so a number that changes while you watch it
1488
+ changes width too, and a readout beside a slider shuffles its row on every
1489
+ value. Tabular figures hold it still:
1490
+
1491
+ ```jsx
1492
+ <text style={{ fontSize: 30, fontVariantNumeric: 'tabular-nums' }}>
1493
+ {hz} Hz
1494
+ </text>
1495
+ ```
1496
+
1497
+ The keywords are CSS's: `lining-nums` or `oldstyle-nums`, `proportional-nums`
1498
+ or `tabular-nums`, `diagonal-fractions` or `stacked-fractions`, `ordinal` and
1499
+ `slashed-zero` — several at once, at most one from each pair, as in
1500
+ `'tabular-nums slashed-zero'`. `'normal'` puts the face's own figures back.
1501
+
1502
+ `fontFeatureSettings` is any OpenType feature, by its four-letter tag: an
1503
+ array of the ones to turn on, or an object of tag to on, off, or the
1504
+ alternate a feature picks.
1505
+
1506
+ ```jsx
1507
+ <text style={{ fontFeatureSettings: ['tnum', 'zero'] }}>0.10</text>
1508
+ <text style={{ fontFeatureSettings: { ss01: true, salt: 2 } }}>Rag</text>
1509
+ ```
1510
+
1511
+ Where the two name the same feature, `fontFeatureSettings` wins. A feature
1512
+ the face does not have is ignored, and the settings are compared by value,
1513
+ so an object literal is fine.
1514
+
1515
+ `letterSpacing` is CSS's `letter-spacing`, in logical pixels: added after
1516
+ every character, the last on a line included, and negative to tighten.
1517
+
1518
+ ```jsx
1519
+ <text style={{ fontSize: 11, fontWeight: 600, letterSpacing: 1.26 }}>
1520
+ NOISE TYPE
1521
+ </text>
1522
+ ```
1523
+
1524
+ Spaced text drops the optional ligatures — `liga`, `clig`, `dlig` and
1525
+ `hlig` — because a ligature is one glyph, with no gap in its middle to open.
1526
+ Name one in `fontFeatureSettings` to keep it anyway.
1527
+
1528
+ All three inherit, and the two feature properties inherit apart, as CSS
1529
+ inherits them: a `fontVariantNumeric` below replaces the one above and
1530
+ leaves an inherited `fontFeatureSettings` alone. All three can move glyphs,
1531
+ so a change re-measures, and none of them may go in a state block.
1532
+
1533
+ ### Tiny text
1534
+
1535
+ Text smaller than **6 logical pixels** is painted as a strip of its ink over
1536
+ the band its letters sit in, one rectangle per line, instead of as glyphs. A
1537
+ zoomed-out view — a minimap, a graph at a tenth of its size, a grid of five
1538
+ thousand cells — is a screen of labels nobody can read, each of which would
1539
+ cost a glyph run at the price of a legible one; below a legible size a label
1540
+ is a smudge of its ink, and the strip is that smudge for one fill. Layout is
1541
+ untouched: the text is still shaped, the strip is as wide as the line. The
1542
+ size is logical because legibility is physical — a 5px label is the same
1543
+ size on a 2x panel as on a 1x monitor.
1544
+
1545
+ `createRoot({ textStripBelow })` moves the line, in logical pixels; `0`
1546
+ keeps glyphs at every size. `REACT_X11_TEXT_STRIP_BELOW` is the same line
1547
+ for a process that cannot reach `createRoot`.
1548
+
1549
+ ## Selecting text
1550
+
1551
+ _Issue #259._ A label is not selectable, the way `Gtk.Label` is not: a
1552
+ desktop application is full of text that is chrome rather than content, and
1553
+ a stray drag lighting up a button's caption is noise. Text becomes
1554
+ selectable when an element says so.
1555
+
1556
+ ```jsx
1557
+ <box selectable style={{ padding: 12 }}>
1558
+ <text style={{ fontSize: 20, fontWeight: 'bold' }}>Release notes</text>
1559
+ <text>Everything below this heading can be dragged over and copied.</text>
1560
+ </box>
1561
+ ```
1562
+
1563
+ That one prop is the whole feature. The element becomes a **surface**: a
1564
+ drag inside it selects across every piece of text under it, a double click
1565
+ takes a word, a triple click takes a block, Ctrl+A takes the surface and
1566
+ Ctrl+C copies it. Releasing the button hands the text to **PRIMARY**, so a
1567
+ middle click in a terminal pastes it — which is what selecting text means
1568
+ on X11 ([clipboard.md](clipboard.md)). macOS has no PRIMARY, so there a
1569
+ release copies nothing and only a copy reaches the pasteboard
1570
+ ([clipboard.md](clipboard.md#primary-is-x11-only)).
1571
+
1572
+ ### What is in a surface
1573
+
1574
+ Everything under it that can answer for its own text, in document order.
1575
+ `<text>` can; so can any element that implements the accessors in
1576
+ [extending.md](extending.md#answering-for-your-own-text) — a terminal, a log
1577
+ view, a code editor written outside this package are all selected across by
1578
+ the same drag, with no registration call.
1579
+
1580
+ What is **not** in it:
1581
+
1582
+ - anything under a `selectable={false}`, which is CSS's `user-select: none`.
1583
+ A list's bullets, a table's chrome, a button's caption inside a document;
1584
+ - `<textinput>` and `<textarea>`, and any element that says
1585
+ `hasOwnSelection` — they keep their own selection, and a press in one is
1586
+ theirs;
1587
+ - `<svg>`, and any registered element that draws its own text, which draw
1588
+ their text through an ntk widget that exposes no character geometry to us.
1589
+
1590
+ ### What a copy assembles
1591
+
1592
+ The separators come from the **layout**, not from the markup: core cannot
1593
+ know that one `<text>` is a table cell and another is a paragraph, and
1594
+ asking every application to say so would be a second authoring model for
1595
+ something the screen already shows. Two pieces of text sharing a band of
1596
+ pixels are joined with a **tab**; one that starts below the last is joined
1597
+ with a **newline**. For a table laid out as rows of cells that is exactly
1598
+ "cells with tabs, rows with newlines", and for ordinary prose it is one
1599
+ paragraph per line.
1600
+
1601
+ ```jsx
1602
+ // copies as "name\tsize\nnotes\t4 kB"
1603
+ <box selectable>
1604
+ <box style={{ flexDirection: 'row' }}>
1605
+ <text style={{ width: 120 }}>name</text>
1606
+ <text>size</text>
1607
+ </box>
1608
+ <box style={{ flexDirection: 'row' }}>
1609
+ <text style={{ width: 120 }}>notes</text>
1610
+ <text>4 kB</text>
1611
+ </box>
1612
+ </box>
1613
+ ```
1614
+
1615
+ A list marker excluded with `selectable={false}` is absent from the copied
1616
+ text as well as from the highlight, which is the point of excluding it.
1617
+
1618
+ ### The props
1619
+
1620
+ | prop | |
1621
+ | ----------------------- | ------------------------------------------------------------------------------------------- |
1622
+ | `selectable` | `true` starts a surface; `false` opts a subtree out of the one above it |
1623
+ | `selectionColor` | the highlight behind selected text (default: the theme's `selection`, a tint of its accent) |
1624
+ | `onSelectionChange(ev)` | `ev.text` is what a copy would put on the clipboard, `ev.isCollapsed` whether it is empty |
1625
+
1626
+ And on the surface's node, through a ref: `selectAll()`, `clearSelection()`,
1627
+ `selectedText()`, `setSelection(anchor, focus)` — each end
1628
+ `{ node, index }`, indices in code points — and `textSelection`, a snapshot of
1629
+ `{ isCollapsed, text, ranges }`.
1630
+
1631
+ ### One selection, and where the keys go
1632
+
1633
+ **Only one selection is visible in an application at a time.** Selecting in
1634
+ a document collapses the highlight in the field beside it, and selecting in
1635
+ a field clears the document — the two never both claim to be showing the
1636
+ selection, which is a rule core holds rather than one every surface has to
1637
+ keep. That is also what makes PRIMARY honest: there is one selection on the
1638
+ display, so there is one here.
1639
+
1640
+ A surface is a **focus target**, because Ctrl+A and Ctrl+C are keystrokes
1641
+ and a keystroke has to arrive somewhere. That puts it in the Tab cycle;
1642
+ `tabIndex={-1}` takes it back out while leaving it focusable by a press —
1643
+ which is the right choice for a document inside a larger UI, and the wrong
1644
+ one for a reader whose window is the document.
1645
+
1646
+ The pointer shows an I-beam over a surface, the way it does over a field.
1647
+ `style={{ cursor: … }}` overrides it as usual.
1648
+
1649
+ The selection is reported to assistive technology as it moves: a `<text>`
1650
+ already exposed AT-SPI's text interface, and the range a reader has dragged
1651
+ across now travels through it ([accessibility.md](accessibility.md#text-controls)).
1652
+
1653
+ ### The geometry underneath
1654
+
1655
+ The selection is built out of four accessors that every drawn node answers,
1656
+ and they are public in their own right — a caret rect is what an
1657
+ autocomplete popup anchors to (`<popup anchor={{ to, at }}>`), and a hit
1658
+ test is how an app turns a click into a character offset:
1659
+
1660
+ ```js
1661
+ const node = paragraphRef.current;
1662
+ node.textContent(); // 'Everything below this heading…'
1663
+ node.textIndexAt(ev.x, ev.y); // 14 — window coordinates in, a code point out
1664
+ node.textCaretRect(14); // { x, y, width: 0, height }
1665
+ node.textRangeRects(4, 14); // the bands a highlight over [4, 14) fills
1666
+ ```
1667
+
1668
+ Indices are **code points**, not UTF-16 units, so an emoji is one position.
1669
+ Rectangles are in the owning window's coordinates — the same space as
1670
+ `abs` and `contentBox()`, which is device pixels; a mouse event's `x`/`y`
1671
+ are logical ([scale.md](scale.md)). `textRangeRects` returns
1672
+ one band per line _and_ one per direction run inside a line: a range that
1673
+ crosses from Latin into Arabic covers two separate stretches of pixels, and
1674
+ a single rectangle drawn between two caret positions would paint over text
1675
+ nobody selected.
1676
+
1677
+ ---
1678
+
1679
+ ## `<textinput>`
1680
+
1681
+ ![textinput](img/textinput.png)
1682
+
1683
+ Single-line editable text. Caret and selection geometry come from ntk's
1684
+ `TextLayout.caretPosition`/`indexAt` (ntk ≥ 3.3.0), so positions are exact
1685
+ across kerning, shaping boundaries and trailing whitespace.
1686
+
1687
+ | prop | |
1688
+ | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
1689
+ | `value` + `onChange(ev)` | controlled mode (display follows the prop) |
1690
+ | `defaultValue` | uncontrolled mode |
1691
+ | `onSubmit(ev)` | Enter or Shift+Enter; with Ctrl, Alt or Super held it is a shortcut's ([events.md](events.md#accelerators)) |
1692
+ | `name` | field name, echoed on the event |
1693
+ | `placeholder`, `placeholderColor` | shown when empty |
1694
+ | `maxLength` | code-point limit |
1695
+ | `selectionColor`, `caretColor` | selection/caret paint |
1696
+ | `inputMode` | what an input method should expect — the DOM's `numeric`, `decimal`, `tel`, `email`, `url` ([events.md](events.md#input-methods)) |
1697
+ | text style props | as `<text>` |
1698
+
1699
+ The text style props include `textAlign`, and the field is laid out at its
1700
+ box's **direction** the way a `<text>` is: the value, the placeholder and the
1701
+ caret sit at the base direction's start edge — the right-hand one under
1702
+ `direction: 'rtl'` — and the value is shaped at that base level rather than
1703
+ at its own first strong character, which is what makes `"(1) 12:30 — نص"`
1704
+ punctuate the same way in a field and in the paragraph beside it. Clicking,
1705
+ the caret, the selection bands and the horizontal scroll are read from one
1706
+ placement, so they mirror together. See
1707
+ [styling.md](styling.md#inside-an-editable-field).
1708
+
1709
+ ### The change event
1710
+
1711
+ `onChange` and `onSubmit` get a synthetic event, the same shape every other
1712
+ handler in the library gets, rather than a bare string:
1713
+
1714
+ ```jsx
1715
+ <textinput
1716
+ name="email"
1717
+ value={email}
1718
+ onChange={(ev) => setEmail(ev.target.value)}
1719
+ />
1720
+ ```
1721
+
1722
+ | on the event | |
1723
+ | ----------------------------------- | --------------------------------------------------- |
1724
+ | `ev.value`, `ev.target.value` | the text **after** the edit |
1725
+ | `ev.name`, `ev.target.name` | the `name` prop |
1726
+ | `ev.type` | `'change'` or `'submit'` |
1727
+ | `ev.target`, `ev.currentTarget` | the node (they are the same — this does not bubble) |
1728
+ | `ev.nativeEvent` | the X key event, or **null** — see below |
1729
+ | `preventDefault`, `stopPropagation` | present for uniformity; nothing reads them |
1730
+
1731
+ Two details worth knowing:
1732
+
1733
+ - **`ev.target.value` is the new value even in controlled mode**, where
1734
+ `props.value` is still the old string until the parent re-renders. That is
1735
+ what makes `e.target.value` mean here what it means in the DOM, and it is
1736
+ why react-hook-form and formik work — see
1737
+ [docs/ecosystem/forms.md](ecosystem/forms.md). It is true _while the
1738
+ handler runs_: `ev.target` is the live node, so a handler that stashes the
1739
+ event and reads `ev.target.value` later sees whatever the control holds
1740
+ then. `ev.value` is a snapshot and always safe.
1741
+ - **`ev.nativeEvent` can be null.** A keystroke carries the X key event that
1742
+ produced it, but an edit can also come from a paste resolving, an undo, or
1743
+ a value the parent pushed back — none of which has an X event behind it.
1744
+ Guard it. (Not academic: downshift reads
1745
+ `event.nativeEvent.preventDownshiftDefault` unguarded, so a null there is a
1746
+ TypeError rather than a no-op.)
1747
+
1748
+ The node behind `ev.target` is writable, too. `node.value = ''` sets the text
1749
+ without firing `onChange`, the way assigning to a DOM input's `value` does —
1750
+ that is how react-hook-form's `register()` resets a field through its ref.
1751
+
1752
+ Interactions: click/drag selection, double-click word select, triple-click
1753
+ select all, **dead keys and Compose**
1754
+ ([events.md](events.md#composition)), Backspace/Delete, arrows (+Shift extends), Ctrl+arrows
1755
+ and Ctrl+Backspace/Delete by word, Home/End, Ctrl+A,
1756
+ Ctrl+C/X/V on CLIPBOARD, middle-click paste from PRIMARY, selections own
1757
+ PRIMARY (X11 conventions, select-all included; [X11 only](clipboard.md#primary-is-x11-only)), **Ctrl+Z / Ctrl+Shift+Z** (Ctrl+Y too) to undo
1758
+ and redo, and a **right-click menu**. On the Cocoa backend the keys are the
1759
+ Mac's instead: the chords are ⌘ — ⌘A, ⌘C/X/V, ⌘Z / ⇧⌘Z — ⌥ moves and deletes
1760
+ by word, ⌘←/→ and ⌘↑/↓ go to the ends of the line and of the value, ⌘⌫
1761
+ deletes to the start of the line, ⌃A, ⌃E, ⌃K, ⌃Y and the rest of AppKit's
1762
+ Emacs keys work while any other ⌃ chord types nothing, and ⌥ types what the
1763
+ layout puts on a key, `ß` for ⌥S ([events.md](events.md#primary-modifier)).
1764
+ Focusable by default; shows the text cursor. `ev.preventDefault()` in your
1765
+ `onKeyDown`/`onMouseDown` suppresses the built-in editing behavior. To copy
1766
+ or paste from anywhere else — a canvas, a list, your own menu item — see
1767
+ [clipboard.md](clipboard.md).
1768
+
1769
+ ### The right-click menu
1770
+
1771
+ ![the built-in edit menu](img/textinput-menu.png)
1772
+
1773
+ Right-clicking gives Undo / Redo / Cut / Copy / Paste / Select All with no
1774
+ wiring, the way a browser gives `<input>` one — each row live only when it
1775
+ would do something, and every row running the same code the keyboard
1776
+ shortcut does. Right-clicking **inside** a selection keeps it (the menu is
1777
+ about to act on it); outside one, the caret moves there first.
1778
+
1779
+ **Paste** is greyed when nothing owns the CLIPBOARD selection. The field
1780
+ subscribes to selection changes the first time its menu opens rather than
1781
+ asking on the way in — asking would mean a round trip against whatever
1782
+ foreign client owns the clipboard, and a wait if that client is wedged, at
1783
+ exactly the moment a menu should already be on screen. So the very first
1784
+ menu of a session still shows Paste enabled; every one after it knows. On
1785
+ a server without XFixes it stays enabled, which is where it started. On
1786
+ macOS it follows the pasteboard's change count, a quarter second behind a
1787
+ copy made in another app ([clipboard.md](clipboard.md#knowing-when-it-changes)).
1788
+
1789
+ Arrows walk the rows, skipping the disabled ones, Enter chooses, Escape or
1790
+ a press anywhere outside closes. The selection stays visibly highlighted
1791
+ while the menu is up, even though the popup holds the keyboard.
1792
+
1793
+ To replace it with your own, set `contextMenu={false}` and render a
1794
+ `ContextMenu` — `onContextMenu` still fires. To suppress it for one event,
1795
+ call `ev.preventDefault()` in an `onContextMenu` handler.
1796
+
1797
+ The menu is not the field's private property: `openEditMenu(node, at,
1798
+ actions)` opens this one for anything that edits or selects text — a code
1799
+ editor, a rendered document with a selection — and `<textinput>`'s own menu
1800
+ is a caller of it, which is what keeps the two the same menu. See
1801
+ [extending.md](extending.md#the-standard-edit-menu).
1802
+
1803
+ ### `sensitive`
1804
+
1805
+ `<textinput sensitive>` is a field whose text **never reaches a selection**.
1806
+ Ctrl+C and the copy half of Ctrl+X do nothing, the menu offers neither Cut
1807
+ nor Copy — absent rows rather than greyed ones, because a greyed Copy over a
1808
+ password reads as a bug in the application — and selecting text does not take
1809
+ PRIMARY, so a middle click in another window cannot spend it. Everything else
1810
+ is untouched: the caret, the selection itself, the arrows, undo, and pasting
1811
+ _in_.
1812
+
1813
+ The line it draws is between what is on screen and what is on the clipboard.
1814
+ The first stops being visible when the field is hidden or the window closes;
1815
+ the second is readable by every client on the display until something else
1816
+ takes the selection, and a clipboard manager will have written it down.
1817
+ `PasswordInput` sets it on the input it shows while the secret is revealed —
1818
+ see [components.md](components.md#passwordinput). An input method is on the
1819
+ same side of the line: on Wayland a `sensitive` field is declared
1820
+ `sensitive_data` and none of its text is offered as surrounding text
1821
+ ([events.md](events.md#input-methods)).
1822
+
1823
+ **Ref**: the node, plus `value`, `undo()` / `redo()` and `canUndo` /
1824
+ `canRedo` — enough for a toolbar button beside the field.
1825
+
1826
+ ### Undo
1827
+
1828
+ Consecutive typing coalesces into one undo step, so Ctrl+Z takes back a
1829
+ word rather than a keystroke; a run of Backspaces coalesces the same way.
1830
+ A run ends at whitespace, at anything that moves the caret (arrows,
1831
+ Home/End, a click, focus leaving the field), and around edits that are
1832
+ their own step whatever surrounds them: a paste, a cut, a replaced
1833
+ selection, Ctrl+Backspace (⌥⌫ and ⌘⌫ on a Mac, and a ⌃K), and a `<textarea>`
1834
+ newline. Undo restores the
1835
+ selection and puts the caret back where the undone edit started.
1836
+
1837
+ A composed character is one entry, not one per keystroke: the accent a dead
1838
+ key is holding is not in the value until it commits, so Ctrl+Z after
1839
+ `dead_acute` `e` steps over `é` rather than into it.
1840
+
1841
+ Undo is a stack of snapshots of the states the control has shown, capped
1842
+ at 200. A **controlled** `value` reports the restored text through
1843
+ `onChange` and waits for the prop to come back, exactly as typing does —
1844
+ so a parent that filters or rejects a value gets the same say over an undo
1845
+ that it has over an edit. A value changed from outside (a form reset)
1846
+ becomes its own entry, and undoing steps back through it.
1847
+
1848
+ ## `<textarea>`
1849
+
1850
+ Multi-line editable text on the same editing core as `<textinput>`:
1851
+ word-wraps at the content width, Enter and Shift+Enter insert a newline
1852
+ (Ctrl+Enter fires `onSubmit`, ⌘↩ on the Cocoa backend, and with no `onSubmit`
1853
+ is left to a shortcut, as Alt+Enter and Super+Enter are everywhere but there —
1854
+ [events.md](events.md#accelerators)), Up/Down move between visual lines keeping a goal column,
1855
+ Home/End go to the start/end of the visual (wrapped) line, selection spans
1856
+ lines, and the view scrolls vertically to follow the caret (mouse wheel
1857
+ scrolls too). On the Cocoa backend ⌘←/→ are Home/End, ⌘↑/↓ go to the ends of
1858
+ the value and ⌥↑/↓ to the ends of a paragraph
1859
+ ([events.md](events.md#primary-modifier)).
1860
+
1861
+ The wheel reaches it through the same chain a `<box overflow="scroll">` is
1862
+ in, so a field whose text fits passes the gesture out to the pane or the
1863
+ window behind it rather than swallowing it.
1864
+
1865
+ | prop | |
1866
+ | --------------- | -------------------------------------------------------------- |
1867
+ | `rows` | preferred height in text lines (default 3) |
1868
+ | everything else | as `<textinput>` (`onSubmit` fires on Ctrl+Enter, ⌘↩ on Cocoa) |
1869
+
1870
+ ## `<image>`
1871
+
1872
+ | prop | |
1873
+ | ---------- | -------------------------------------------------------------------------------------- |
1874
+ | `src` | client-side pixels, in any of the four forms below, or a symbol by name |
1875
+ | `picture` | `{ id, width, height }` — an existing server-side Picture, composited as-is |
1876
+ | `drawable` | `{ id, width, height, depth? }` — an existing Pixmap/Window, composited as-is |
1877
+ | `cacheKey` | the source's identity, when `src` is re-derived per render |
1878
+ | `alt` | the accessible name — what a screen reader says ([accessibility.md](accessibility.md)) |
1879
+
1880
+ One source per element — `src`, `picture` and `drawable` are mutually
1881
+ exclusive, and passing two throws.
1882
+
1883
+ Sized by style — `style={{ width, height }}`, never flat props, since both
1884
+ are style names. With only one of the two set the other follows the natural
1885
+ aspect ratio; with neither, the image measures at its natural size, shrunk
1886
+ to the width on offer. For the server-side sources the "natural size" is the
1887
+ `width`/`height` stated in the descriptor.
1888
+
1889
+ ```jsx
1890
+ <image src={photo} style={{ height: 64 }} /> // width follows the aspect ratio
1891
+ ```
1892
+
1893
+ `objectFit` decides where the picture goes inside that box when the two
1894
+ shapes differ: `'fill'` — the default — stretches it to the box, `'contain'`
1895
+ fits it inside with the background around it, `'cover'` fills the box and
1896
+ cuts what overflows, `'none'` leaves it its own size and `'scale-down'` is
1897
+ the smaller of `'none'` and `'contain'`, all centred.
1898
+
1899
+ ```jsx
1900
+ <image src={photo} style={{ width: 96, height: 96, objectFit: 'cover' }} />
1901
+ ```
1902
+
1903
+ ### `src` — pixels the client has
1904
+
1905
+ ```jsx
1906
+ <image src="./logo.png" /> // file path or file URL
1907
+ <image src={bytes} /> // encoded PNG, JPEG, WebP, …
1908
+ <image src={{ width, height, data }} /> // raw straight RGBA
1909
+ <image src={ntkImage} /> // an ntk Image (or Surface)
1910
+ ```
1911
+
1912
+ A file path (or `new URL('./logo.png', import.meta.url)`) is read and
1913
+ decoded asynchronously; the element measures 0×0 until the decode lands,
1914
+ then reflows. Style it a `width` and `height` and nothing moves when it
1915
+ does. The in-memory forms skip the filesystem — pixels that arrived from a
1916
+ socket, a decoder, or a `getImageData` readback go straight on screen — and
1917
+ raw RGBA and an ntk `Image` are synchronous. Encoded bytes are synchronous
1918
+ where the decoder is (PNG and JPEG under Node) and land a moment later, the
1919
+ way a file does, where it is not: any format under Bun, and WebP elsewhere.
1920
+ Raw `data` is `width × height × 4` bytes of straight (non-premultiplied)
1921
+ RGBA — exactly what `getImageData` hands back — and the object is treated as
1922
+ immutable content: hand over a new object when the pixels change, or the
1923
+ renderer cannot tell. That goes for encoded bytes too, which under Bun are
1924
+ read on another thread after the commit.
1925
+
1926
+ #### Formats, and what decodes them
1927
+
1928
+ The format comes from the bytes, never from a file name. Which decoder
1929
+ reads them depends on the runtime:
1930
+
1931
+ | format | under Bun | under Node |
1932
+ | ---------------- | -------------------------------------------------- | ------------------------------------------ |
1933
+ | PNG, JPEG | `Bun.Image`, off the JavaScript thread | pngjs, jpeg-js — synchronous |
1934
+ | WebP | `Bun.Image`, off the JavaScript thread | image-in-browser, loaded on the first WebP |
1935
+ | GIF, BMP | `Bun.Image`, off the JavaScript thread | not decoded — the error says to convert |
1936
+ | TIFF, HEIC, AVIF | `Bun.Image` through ImageIO (macOS), WIC (Windows) | not decoded; nor under Bun on Linux |
1937
+
1938
+ **Under Bun, Bun decodes everything**, PNG and JPEG included, which the
1939
+ JavaScript decoders could read: libjpeg-turbo, spng and libwebp are built
1940
+ into the runtime and run on a worker thread. A 1080p JPEG takes ~110ms of the
1941
+ JavaScript thread inside jpeg-js and ~7ms under Bun, which is the difference
1942
+ between a stall the user feels and none. What it costs is that bytes under
1943
+ Bun land a frame or so late, like a file. Where that matters — a test that
1944
+ measures the element on its first frame — decode the bytes yourself and pass
1945
+ the result, which is used as-is on every runtime:
1946
+
1947
+ ```jsx
1948
+ import { decodeImage } from 'ntk/image'; // PNG and JPEG, synchronous
1949
+
1950
+ const logo = <image src={decodeImage(bytes)} />;
1951
+ ```
1952
+
1953
+ The WebP decoder under Node is pure JavaScript, imported the first time a
1954
+ WebP arrives so an app that shows none never loads it: the first WebP pays
1955
+ ~140ms to load the decoder, and each one about what jpeg-js costs a JPEG of
1956
+ the same size.
1957
+
1958
+ An element that draws images of its own — a document's `<img>`, a CSS
1959
+ background — decodes through the same ladder, so that an image shows there
1960
+ exactly when it would show in an `<image>`: `decodeImageBytes` from
1961
+ `react-x11/ntk` takes the bytes and answers an `Image`, or a promise of one
1962
+ wherever the table above says the decode is not synchronous, and throws or
1963
+ rejects, saying what to convert to, where an `<image>` would show nothing.
1964
+
1965
+ What differs between the two, so it is not found the hard way:
1966
+
1967
+ - **An animation shows its first frame** — an animated WebP on either
1968
+ runtime, a GIF under Bun. Bun's libwebp cannot decode an animated WebP at
1969
+ all (Bun 1.4), so under Bun that one goes to the JavaScript decoder too.
1970
+ - **A JPEG's EXIF orientation is applied under Bun and not under Node.**
1971
+ Browsers apply it, and so does Bun; jpeg-js does not, so under Node a
1972
+ phone photo taken in portrait shows on its side. Rotate it before
1973
+ handing it over, or run under Bun.
1974
+ - **A format with no decoder says so**: GIF under Node, or HEIC under Bun on
1975
+ Linux, is logged with what to convert it to, and the element shows
1976
+ nothing, the same as a missing file.
1977
+
1978
+ Decoded pixels are uploaded to the server once and composited from there on
1979
+ every repaint (ntk's `Image` caches its upload per connection), so the cost
1980
+ of an image is one `PutImage` at first paint, not one per frame.
1981
+
1982
+ An ntk `Image` is used as-is and never destroyed by the element — identity
1983
+ is yours, which is also the sharing idiom: one `Image` shown by many
1984
+ `<image>`s is one decode and one upload, however many places composite it.
1985
+
1986
+ On the Cocoa backend (macOS's default) the upload is a CoreGraphics bitmap
1987
+ rather than a pixmap — made once per `Image` at its first paint and
1988
+ composited, scaled, from there — so the cost is the same one upload. The
1989
+ element frees it with an `Image` it decoded; for an `Image` of yours it is
1990
+ freed when the `Image` is garbage-collected, since ntk's `destroy()` frees
1991
+ only its X copies.
1992
+
1993
+ ### `src={{ symbol }}` — the platform's own icons
1994
+
1995
+ ```jsx
1996
+ <box style={{ flexDirection: 'row', gap: 6, color: theme.textMuted }}>
1997
+ <image src={{ symbol: 'speaker.wave.3.fill', variableValue: volume }} />
1998
+ <text>Volume</text>
1999
+ </box>
2000
+ ```
2001
+
2002
+ A name instead of pixels: the icon the platform draws for it, in the **text
2003
+ colour** — the colour a `<text>` beside it would be drawn in, which is what
2004
+ `currentColor` is to an `<svg>`. The names are the ones `useTray`'s `icon`
2005
+ and a menu item's `iconName` take:
2006
+
2007
+ | backend | names |
2008
+ | ------------- | ---------------------------------------------------------------------------------------------------------------- |
2009
+ | macOS | SF Symbols, like `speaker.wave.3.fill`, through `@windowkit/appkit` |
2010
+ | X11 / Wayland | the user's freedesktop icon theme — the settings daemon's, else Adwaita, then hicolor — like `audio-volume-high` |
2011
+
2012
+ A symbol is **sized like the text around it**. An SF Symbol takes the
2013
+ `fontSize` and `fontWeight` it inherits as its point size and weight, the
2014
+ way the symbols are designed to sit in a line of text; an icon theme is asked
2015
+ for the size that text calls for — 16 beside 14px text — and its icons are
2016
+ square. Style the element a `width` and `height` of its own and the symbol is
2017
+ fitted into that box, centred. The source can say otherwise:
2018
+
2019
+ | key | |
2020
+ | --------------- | ------------------------------------------------------------------------------ |
2021
+ | `symbol` | the name |
2022
+ | `weight` | what `fontWeight` takes; defaults to the text's. SF Symbols only |
2023
+ | `scale` | `'small'`, `'medium'` (default) or `'large'`, beside the text. SF Symbols only |
2024
+ | `variableValue` | 0 to 1, how much of a variable symbol shows. SF Symbols, macOS 13 and later |
2025
+
2026
+ On Linux a `-symbolic` icon is preferred where the theme has one, and it is
2027
+ the one that takes the text colour — the monochrome set GTK tints the same
2028
+ way. An icon the theme only has in its own colours is shown as it is. The
2029
+ lookup is the Icon Theme Specification's, read when the first symbol is
2030
+ drawn.
2031
+
2032
+ **A name this desktop does not have takes no room and draws nothing**, and
2033
+ says so once in development. An app that runs on both systems picks its
2034
+ names by platform, as it does for the tray: the two icon sets do not share
2035
+ their names.
2036
+
2037
+ ### `cacheKey` — when the buffer is new but the picture is not
2038
+
2039
+ A component that re-derives its bytes per render — decoding a protocol
2040
+ stream, slicing a capture — hands `<image>` a structurally new `src` every
2041
+ time, and without help that is a fresh decode and a fresh upload each
2042
+ render. `cacheKey` is the `<canvas cacheKey>` contract applied to sources:
2043
+ an unchanged key vouches that the new buffer is the same picture, so nothing
2044
+ is re-decoded or re-uploaded — and two `<image>`s with one key share one
2045
+ decoded copy, freed when the last of them unmounts.
2046
+
2047
+ ```jsx
2048
+ <image src={frame.rgba} cacheKey={`frame:${frame.serial}`} />
2049
+ ```
2050
+
2051
+ The key must name the content: a key that stays the same while the pixels
2052
+ change shows the old pixels. Development warns when two sizes collide under
2053
+ one key, which is the cheap symptom of that mistake. The key is not
2054
+ consulted for an ntk `Image` (the object is its own identity), and is
2055
+ refused with `picture`/`drawable` (there is nothing client-side to cache).
2056
+
2057
+ ### `picture` / `drawable` — pixels the server already has
2058
+
2059
+ When the content is already a server-side Picture or Drawable — a pixmap the
2060
+ app rendered offscreen, `NameWindowPixmap` from Composite, a cached tile —
2061
+ showing it must not mean reading it back and uploading it again. These
2062
+ composite straight from the existing resource: one `RenderComposite` into
2063
+ the window, no `PutImage`, no round trip.
2064
+
2065
+ ```jsx
2066
+ <image picture={{ id: pic, width: 64, height: 64 }} />
2067
+ <image drawable={{ id: pixmap, width: 128, height: 96, depth: 32 }} />
2068
+ ```
2069
+
2070
+ The caller states the size because asking the server for it would be a round
2071
+ trip, which these props exist to avoid. The descriptor is compared by value,
2072
+ so an object literal rebuilt every render costs nothing. Scaling (a box
2073
+ styled to a different size than the source) is server-side, through the
2074
+ picture transform with bilinear filtering — note that drawing a `picture`
2075
+ scaled sets that picture's transform and filter for the composite and resets
2076
+ them to identity/nearest after, the same bracket ntk puts around its own
2077
+ uploads; a picture that must keep a transform of its own should be
2078
+ composited 1:1.
2079
+
2080
+ `drawable` wraps the pixmap or window in a Picture the element creates and
2081
+ owns (freed on unmount; the drawable stays yours). `depth` picks the format
2082
+ it is composited through: 24 — the screen's default depth, what a window
2083
+ pixmap from Composite is — is the default, 32 is argb, and 8 composites as
2084
+ ink through its alpha, which is what previewing a mask looks like. A
2085
+ `picture` needs no depth: its format was fixed when it was created.
2086
+
2087
+ Both name X server resources, so both are X11-only: the Cocoa backend has no
2088
+ X server to composite from, and there they draw nothing (development warns
2089
+ once). Hand `src` the pixels instead.
2090
+
2091
+ Both descriptors also accept the richer objects that already carry these
2092
+ fields — an ntk `Pixmap` is `{ id, width, height, depth }` and goes straight
2093
+ in as `drawable`.
2094
+
2095
+ ## `<video>` {#video}
2096
+
2097
+ ```jsx
2098
+ // frames the application decodes — every backend
2099
+ const frames = useVideoFrames({ width: 1280, height: 720, format: 'NV12' });
2100
+ useEffect(() => startDecoder(file, frames), [frames, file]);
2101
+ <video frames={frames} style={{ width: '100%', backgroundColor: '#000' }} />;
2102
+ ```
2103
+
2104
+ | prop | |
2105
+ | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
2106
+ | `frames` | a `VideoFrames` sink the application pushes decoded frames into (`useVideoFrames`, `createVideoFrames`) — works on every backend |
2107
+ | `src` | a path or URL the platform plays itself, where `useSupports('mediaPlayback')` is true; elsewhere one `NoMediaPlaybackError` through `onError` |
2108
+ | `poster` | an `<image src>` value, shown until there is a frame and whenever there is none |
2109
+ | `autoPlay`, `loop`, `muted`, `volume`, `playbackRate` | HTML's names and defaults — nothing plays until asked; for `src` |
2110
+ | `paused` | controlled when present; for `src` |
2111
+ | `onLoadedMetadata` | `{ width, height, duration }` once the stream's size is known — for a sink, once it is mounted, with a `duration` of `Infinity`, a live source's |
2112
+ | `onPlay`, `onPause`, `onEnded`, `onTimeUpdate` | for `src`; the `ref` has `play()`, `pause()`, `seek(seconds)`, `currentTime`, `duration` and `paused` |
2113
+ | `onError` | the source failed, or there is no player for `src`; without a handler, one console warning |
2114
+
2115
+ `frames` and `src` are exclusive, and passing both throws. The element is a
2116
+ drawn node like `<image>` — in the layout, in the paint order, hit like
2117
+ anything else — so a video in a form sits under the form's popups and inside
2118
+ its scroll panes, which a child window or a `<glarea>` can do neither of.
2119
+
2120
+ **Size.** HTML's: the stream's size in logical pixels — a 1280x720 stream is
2121
+ 1280 wide at any scale, as an image is — then the poster's, then 300x150. A
2122
+ style that names one axis gets the other from the picture's ratio, larger
2123
+ than the stream as well as smaller. `objectFit` is `'contain'` unless the
2124
+ style says otherwise, and `backgroundColor` is what shows in the bars.
2125
+
2126
+ ### `VideoFrames`, the sink
2127
+
2128
+ react-x11 decodes nothing; where the bytes come from is the application's
2129
+ business, and every source ends in `push`:
2130
+
2131
+ ```js
2132
+ import { spawn } from 'node:child_process';
2133
+
2134
+ // ffmpeg decoding a file into NV12 frames on a pipe, paced to the clip's rate
2135
+ function startDecoder(file, frames) {
2136
+ const ff = spawn('ffmpeg', [
2137
+ '-re',
2138
+ '-i',
2139
+ file,
2140
+ '-f',
2141
+ 'rawvideo',
2142
+ '-pix_fmt',
2143
+ frames.format.toLowerCase(),
2144
+ '-s',
2145
+ `${frames.width}x${frames.height}`,
2146
+ '-',
2147
+ ]);
2148
+ const size = frames.width * frames.height * 1.5; // NV12
2149
+ let pending = Buffer.alloc(0);
2150
+ ff.stdout.on('data', (chunk) => {
2151
+ pending = Buffer.concat([pending, chunk]);
2152
+ while (pending.length >= size) {
2153
+ frames.push(pending.subarray(0, size)); // one buffer, planes back to back
2154
+ pending = pending.subarray(size);
2155
+ }
2156
+ });
2157
+ return () => ff.kill();
2158
+ }
2159
+ ```
2160
+
2161
+ - **Formats.** `'NV12'` (Y, then Cb/Cr pairs — what a hardware decoder
2162
+ emits), `'I420'` (Y, Cb, Cr — `yuv420p`, most software decoders' default)
2163
+ and `'BGRA'` (the fourth byte ignored: a frame is opaque). `colorSpace`
2164
+ (`'bt709'` by default, `'bt601'`, `'bt2020'`) and `range` (`'video'` by
2165
+ default, `'full'`) say what a YCbCr frame's numbers mean.
2166
+ - **`preferredFormats`** is this display's order, cheapest first, for a
2167
+ decoder that can choose: `NV12, I420, BGRA` on macOS, where a YCbCr frame
2168
+ goes on a layer as it is, and `BGRA` wherever a 2D context draws it. Every
2169
+ format works everywhere; the conversion is charged where it happens.
2170
+ - **The newest frame is the frame.** A push replaces the last one, shown or
2171
+ not; nothing queues, and the decoder's own clock is the pacing. A push
2172
+ hands its planes over until the next push — they are read then, not
2173
+ copied at the call — so a decoder that writes every frame into one buffer
2174
+ pushes it again after writing.
2175
+ - **`push(planes, { strides, time })`** takes a Buffer per plane or one
2176
+ Buffer of them back to back, with each plane's bytes per row when a
2177
+ decoder pads them; a plane too small for the frame is a `TypeError` naming
2178
+ the plane and the bytes it needs. `close()` drops the frame and ignores
2179
+ what comes after, so a decoder racing an unmount loses quietly.
2180
+ - **Nothing to free.** A sink owns nothing native: what shows it makes its
2181
+ surfaces and lets them go when it stops showing it.
2182
+
2183
+ ### Lifted, drawn, and what each costs
2184
+
2185
+ On **macOS** a video with a frame to show goes on a layer of its own above
2186
+ the window's bitmap, under the same rule that lifts an animation
2187
+ ([macos.md](macos.md)): nothing painted over it, no fade, no rounded
2188
+ clipping ancestor that cannot hold it. There every `push` writes the frame
2189
+ into a surface the render server shows as it is and repoints the layer —
2190
+ 0.07ms for a 1080p NV12 frame, 0.27ms for an I420 one interleaved on the
2191
+ way — with **no frame of the window's own**. The rest of the time — a
2192
+ tooltip over it, a dialog's dim, a card with rounded corners — it is
2193
+ **drawn**: a push claims the picture, and the next frame copies the newest
2194
+ frame into a surface of the element's own and composites it in paint
2195
+ order. The bridge converts a YCbCr frame into that surface in the colours
2196
+ the layer shows it in, so the picture does not change shade on the way
2197
+ between the two.
2198
+
2199
+ On **X11, Wayland and Windows** it is always drawn, the frame converted to
2200
+ RGBA in JavaScript unless it is BGRA — which is why those displays prefer
2201
+ BGRA. On X11 the upload rides ntk's shared-memory path where the server has
2202
+ it. No transfer function is applied there, as nothing else on those
2203
+ displays is colour-managed either.
2204
+
2205
+ `borderRadius` on an ancestor that clips the video costs the layer, not the
2206
+ picture: the video is drawn for as long as the rounded clip holds it.
2207
+
2208
+ ### `src` — the platform's player
2209
+
2210
+ Where `useSupports('mediaPlayback')` is true, `src` is a file or URL the
2211
+ platform plays with its own decoder, audio, seeking and streaming — on macOS
2212
+ AVFoundation, over a bridge with its player verbs (`createPlayer`), and in
2213
+ a `<Frame>` pane, whose host plays it and sends the frames down
2214
+ ([frame.md](frame.md#sizing-stacking-input)). A path, a `file://` URL and an
2215
+ `http(s)` one, HLS included, all play.
2216
+
2217
+ ```jsx
2218
+ const ref = useRef(null);
2219
+ <video
2220
+ ref={ref}
2221
+ src="/Users/me/clip.mp4"
2222
+ autoPlay
2223
+ muted
2224
+ onLoadedMetadata={({ duration }) => setDuration(duration)}
2225
+ onTimeUpdate={({ currentTime }) => setAt(currentTime)}
2226
+ onEnded={() => setDone(true)}
2227
+ style={{ width: 640 }}
2228
+ />;
2229
+ // a transport bar of your own
2230
+ <button onPress={() => ref.current.seek(30)} />;
2231
+ ```
2232
+
2233
+ It is laid out at 300x150 — or the poster's size — until the item is ready,
2234
+ and at the clip's size from `onLoadedMetadata` on. `paused` is controlled
2235
+ when present; `autoPlay`, `loop`, `muted`, `volume` and `playbackRate` are
2236
+ HTML's, and a change to any of them goes to the same player. The `ref` is
2237
+ HTMLMediaElement's imperative half: `play()` (from the start if the item has
2238
+ ended), `pause()`, `seek(seconds)` — exact, not to the nearest keyframe —
2239
+ and `currentTime`, `duration` and `paused`. `onPlay` and `onPause` fire when
2240
+ it starts and stops, `onTimeUpdate` four times a second while it plays and
2241
+ after a seek, `onEnded` at the end of an item that does not loop, and
2242
+ `onError` when the item fails.
2243
+
2244
+ Lifted, the picture is the player's own layer and costs the window nothing
2245
+ while it plays; painted over, the frame showing is copied into the element's
2246
+ surface at the clip's own frame rate for as long as it plays, in the same
2247
+ colours. There is no `controls` attribute: the transport is your React,
2248
+ over or beside the video.
2249
+
2250
+ Where `useSupports('mediaPlayback')` is false — X11 and Wayland for good,
2251
+ Windows today, and macOS over an older bridge — the element reports one
2252
+ `NoMediaPlaybackError` (`code: 'ENOMEDIAPLAYBACK'`) through `onError`, on a
2253
+ microtask, and shows its poster: never a black box that looks like a video
2254
+ that has not started. Ask before rendering one:
2255
+
2256
+ ```jsx
2257
+ const playback = useSupports('mediaPlayback');
2258
+ {
2259
+ playback ? <video src={file} autoPlay /> : <video frames={decoded} />;
2260
+ }
2261
+ ```
2262
+
2263
+ ## `<canvas>`
2264
+
2265
+ The escape hatch: a retained node whose content you paint.
2266
+
2267
+ ```jsx
2268
+ <canvas
2269
+ style={{ flexGrow: 1 }}
2270
+ onDraw={(ctx, { width, height, node }) => {
2271
+ ctx.fillStyle = 'tomato';
2272
+ ctx.fillRect(0, 0, width / 2, height);
2273
+ }}
2274
+ />
2275
+ ```
2276
+
2277
+ `ctx` is ntk's canvas-like 2d context (paths, transforms, gradients incl.
2278
+ conical, `setLineDash`, round caps/joins, images, text — XRender-backed),
2279
+ translated to the node's origin and clipped to its bounds. `onDraw` runs on
2280
+ every repaint of the window.
2281
+
2282
+ A drawing made of many small rectangles in one colour — a sparkline, a heat
2283
+ map, row striping, terminal cells — has a batch primitive, and the
2284
+ difference is a whole frame's worth of requests:
2285
+
2286
+ ```jsx
2287
+ ctx.fillStyle = theme.accent;
2288
+ ctx.fillRects(bars); // [[x, y, w, h], …] or one flat [x, y, w, h, x, …]
2289
+ ```
2290
+
2291
+ `fillRects` is `fillRect` per rectangle — the same fill style, alpha,
2292
+ composite op and clip — sent as a single `Render.FillRectangles` where the
2293
+ loop would have sent one composite each.
2294
+
2295
+ ### Raw pixels — the one call that is not translated
2296
+
2297
+ Everything on the context honours "you are at the node's origin" — except
2298
+ the raw-pixel pair. `putImageData` and `getImageData` ignore the current
2299
+ transform _and_ the clip (the HTML canvas rule, kept by ntk), so their
2300
+ coordinates are the drawable's own: `ctx.putImageData(data, 0, 0)` lands at
2301
+ the **window** corner, not in the node, and nothing errors on the way.
2302
+ `onDraw`'s second argument carries the node's origin for exactly this case:
2303
+
2304
+ ```jsx
2305
+ <canvas
2306
+ onDraw={(ctx, { width, height, x, y }) => {
2307
+ ctx.putImageData({ width, height, data: rgba }, x, y); // in the node
2308
+ }}
2309
+ style={{ width, height }}
2310
+ />
2311
+ ```
2312
+
2313
+ `x`/`y` are wherever the drawing is going — in a live paint the node's place
2314
+ in the window, under a [`cacheKey`](#cachekey--draw-it-once) the origin of
2315
+ the cache surface — so offsetting by them is correct in both. In
2316
+ development, a `putImageData` whose pixels land outside the node's box
2317
+ warns, once, naming this rule.
2318
+
2319
+ Better, for pixels that survive more than one frame: upload through an
2320
+ `Image` source (ntk's, re-exported — see [the subpath
2321
+ exports](extending.md#the-subpath-exports)) and draw it.
2322
+ `drawImage` goes through the transform and the clip like everything else — a
2323
+ half-scrolled-out canvas stays cut at the viewport, which no raw write is —
2324
+ and the image caches its server-side pixmap instead of re-uploading the
2325
+ bytes on every repaint:
2326
+
2327
+ ```jsx
2328
+ import { Image } from 'react-x11/ntk';
2329
+
2330
+ const img = new Image({ width, height, data: rgba }); // keep it memoized
2331
+ <canvas onDraw={(ctx) => ctx.drawImage(img, 0, 0, width, height)} />;
2332
+ ```
2333
+
2334
+ ### `cacheKey` — draw it once
2335
+
2336
+ A drawing that does not change between frames does not have to be redrawn
2337
+ between frames. Give the canvas a `cacheKey` and its content is rendered once
2338
+ and composited on later repaints; two canvases with the same key share the one
2339
+ rendered copy.
2340
+
2341
+ ```jsx
2342
+ <canvas cacheKey={`spark:${series.id}:${w}x${h}`} onDraw={drawSparkline} />
2343
+ ```
2344
+
2345
+ Opt-in, and the reason matters: `onDraw` is an opaque closure. Nothing in the
2346
+ renderer can know what it reads — a prop, a ref, a clock — and its identity
2347
+ changes on every render unless you memoize it, so it is not a key either. Only
2348
+ you know, so you say.
2349
+
2350
+ **The key must name every input the drawing reads.** One that leaves something
2351
+ out shows stale pixels, which is the hardest kind of bug to see. Develop with
2352
+ `REACT_X11_PAINT_CACHE=verify`, which turns exactly that mistake into a loud
2353
+ complaint — see [Runtime diagnostics](debugging.md#the-paint-cache--react_x11_no_paint_cache-react_x11_paint_cacheverify).
2354
+ Leave `cacheKey` unset for anything animated, or driven by state outside the
2355
+ props.
2356
+
2357
+ `<svg>` does this automatically: its content is fully described by
2358
+ its props, so the renderer can build the key itself.
2359
+
2360
+ ### `mono` — one colour, and the colour out of the key
2361
+
2362
+ `mono` is a promise about the drawing: **everything it paints is one colour,
2363
+ and it is not the drawing's to choose.** `onDraw` then names no colour at all
2364
+ — `fillStyle` and `strokeStyle` arrive preset from the node's resolved
2365
+ `color`, which is its own if it named one and otherwise the ink it inherits
2366
+ ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)), so a
2367
+ mark inside a dimmed or hovered row follows it with nothing said:
2368
+
2369
+ ```jsx
2370
+ const chevron = (ctx, { width: w, height: h }) => {
2371
+ ctx.lineWidth = 1.5;
2372
+ ctx.beginPath();
2373
+ ctx.moveTo(w * 0.25, h * 0.375);
2374
+ ctx.lineTo(w * 0.5, h * 0.65);
2375
+ ctx.lineTo(w * 0.75, h * 0.375);
2376
+ ctx.stroke(); // no strokeStyle: the ink is style.color
2377
+ };
2378
+
2379
+ <canvas
2380
+ mono
2381
+ cacheKey="chevron"
2382
+ onDraw={chevron}
2383
+ style={{
2384
+ width: 12,
2385
+ height: 12,
2386
+ color: '$textMuted',
2387
+ ':disabled': { color: '$border' },
2388
+ }}
2389
+ />;
2390
+ ```
2391
+
2392
+ The promise is what lets one drawing serve every state a control puts it in:
2393
+ `:hover`, `:disabled` and a theme flip all reach the same `onDraw`, and none of
2394
+ them is the drawing's business.
2395
+
2396
+ **What it buys at the cache.** With a `cacheKey`, a `mono` drawing is kept as
2397
+ **coverage** rather than as pixels, with the colour applied at composite time —
2398
+ so the colour leaves the key and one rendered copy serves every ink. That is
2399
+ the trick the glyph cache runs on text, and the one `<svg>` gets for a
2400
+ `fill="currentColor"` document; a closure cannot be scanned the way a document
2401
+ can, so here you say it. Without `mono`, each colour of the same shape is a
2402
+ separate rasterization and a separate pixmap.
2403
+
2404
+ Needs **ntk ≥ 7.3.3**, which `package.json` carries: earlier versions
2405
+ composite coverage as empty under a clip they cannot express as a rectangle
2406
+ ([sidorares/ntk#243](https://github.com/sidorares/ntk/issues/243)), and a
2407
+ widget sits inside clips like that routinely.
2408
+
2409
+ `mono` works without `cacheKey` (the ink is preset either way), but the two
2410
+ together are the point. The [system icon set](components.md#system-icons) is
2411
+ twelve of these.
2412
+
2413
+ ---
2414
+
2415
+ ## `<glarea>`
2416
+
2417
+ An OpenGL surface in the layout — on X11 the only drawn element that owns a
2418
+ real X window, because GLX needs a drawable created for a GL-capable visual
2419
+ and cannot share the XRender pipeline the rest of the tree paints through
2420
+ (NEXT_STEPS §4); the other backends give it a surface of their own
2421
+ ([gl.md](gl.md)). On X11 under the default `glPolicy` it needs ntk ≥ 3.6.0
2422
+ and a server with **indirect GLX** enabled (`+iglx` / `AllowIndirectGLX` —
2423
+ off by default on many).
2424
+
2425
+ ```jsx
2426
+ <glarea
2427
+ style={{ flexGrow: 1 }}
2428
+ clearColor="#0b1021"
2429
+ frameLoop="always"
2430
+ onCreated={(gl) => gl.Enable(gl.DEPTH_TEST)}
2431
+ onDraw={(gl, { width, height }) => {
2432
+ gl.MatrixMode(gl.PROJECTION);
2433
+ gl.LoadIdentity();
2434
+ gl.Frustum(-1, 1, -height / width, height / width, 2, 20);
2435
+ // ... immediate-mode or display-list drawing
2436
+ }}
2437
+ />
2438
+ ```
2439
+
2440
+ - `onDraw(gl, { width, height, node })` — draw one frame. The viewport is
2441
+ set and the buffers are cleared before it, `SwapBuffers` follows it.
2442
+ - `onCreated(gl, info)` — runs once, when the context is current: one-time
2443
+ GL state, texture uploads, display-list compilation.
2444
+ - `clearColor` — CSS colour or `[r, g, b, a]` floats (default black).
2445
+ - `frameLoop` — `'demand'` (default) redraws on prop, size and expose
2446
+ changes only; `'always'` renders continuously on ntk's frame clock.
2447
+ - `frameRate` — how those frames are paced when they are expensive: the
2448
+ same values as [`<window frameRate>`](#framerate--pacing-the-frames-under-a-flood),
2449
+ priced by what `onDraw` and the swap cost the JS thread. Defaults to the
2450
+ owning window's, so a `<window frameRate="adaptive">` paces the scene in
2451
+ it too; name `'display'` here for a scene that wants every frame while the
2452
+ window around it streams. A `frameLoop` of `'always'` under `'adaptive'`
2453
+ is a loop at the budget rather than at the display.
2454
+ - `glx` — a visual spec for ntk's `chooseGLXConfig`, e.g.
2455
+ `{ DEPTH_SIZE: 24 }`. One query per app, shared by every `<glarea>`.
2456
+ - `onError(err)` — no GL surface (no GLX, no matching visual). Without a
2457
+ handler the failure is a console warning.
2458
+
2459
+ On X11's indirect path, the default, `gl` is ntk's indirect-GLX context:
2460
+ fixed-function OpenGL 1.4 (immediate mode, matrices, lighting, textures,
2461
+ display lists) — no shaders, no vertex arrays, since the GLX protocol does
2462
+ not encode them. **Geometry belongs in display lists**: every
2463
+ immediate-mode vertex is a command on the wire, so a mesh re-sent per frame
2464
+ costs kilobytes per frame, while a compiled list costs one `CallList`. The
2465
+ direct path and the Cocoa backend hand `onDraw` a WebGL-shaped table with
2466
+ shaders instead ([gl.md](gl.md)).
2467
+
2468
+ The surface is stacked above everything drawn in its window, so nothing the
2469
+ window paints can overlap it. **Its own children can: they are 2D content
2470
+ drawn above the surface.** Layout treats a `<glarea>` as a flex container
2471
+ like a `<box>` — it is sized and positioned like any node, its window follows
2472
+ that rect, and its children are laid out inside it and cut to its box: a
2473
+ legend in a corner, a toolbar along an edge, a label over the scene. They are
2474
+ ordinary nodes in every way but one. The window's own paint cannot reach
2475
+ above the surface, so they are painted on **panes** stacked over it, in the
2476
+ same frame and from the same damage as everything else.
2477
+
2478
+ ```jsx
2479
+ <glarea style={{ flexGrow: 1 }} clearColor="#0e1320" onDraw={drawMap}>
2480
+ <box
2481
+ style={{
2482
+ position: 'absolute',
2483
+ left: 12,
2484
+ top: 12,
2485
+ padding: 8,
2486
+ backgroundColor: '#f4f5f8',
2487
+ }}
2488
+ >
2489
+ <text>Legend</text>
2490
+ </box>
2491
+ </glarea>
2492
+ ```
2493
+
2494
+ What a pane is depends on the backend, and so does what translucent means:
2495
+
2496
+ | | Cocoa | X11 |
2497
+ | -------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2498
+ | a pane | one transparent layer over the whole surface, above the GL layer | one child window per child, as big as its paint reach, stacked directly above the surface's window in the children's order; where two overlap, the upper pane holds the lower child's pixels too |
2499
+ | translucency | composited by Core Animation: a translucent fill, an antialiased edge or a shadow blends with the frame | partly: a pane is cut with SHAPE to where its child painted half its alpha or more, and the frame shows everywhere else; a pixel inside the cut that is translucent — an antialiased edge, a translucent fill — blends with the `clearColor`, not the frame |
2500
+ | between the children | the GL frame | the GL frame |
2501
+
2502
+ So on X11 expect a translucent fill to be tinted by the `clearColor`
2503
+ rather than to show the scene: the frame shows around a child, not through
2504
+ it. X11 cannot do better without a compositor, and a compositor would not
2505
+ help: it blends top-level windows, not the children inside one. The cut
2506
+ needs the SHAPE extension, which every real X server has; without it — the
2507
+ in-process server in `react-x11/test` — a pane is the rectangle of its
2508
+ child's reach, and whatever the child leaves unpainted there shows the
2509
+ `clearColor`. A window per child keeps each pane as small as what it
2510
+ holds.
2511
+
2512
+ A pane is its child's, and holds that child and what is under it, never
2513
+ what is over it. So a child that moves — a card dragged across a graph —
2514
+ takes its pane along and repaints nothing when nothing is under it, and the
2515
+ panes it passes over are not touched at all. A pane's window takes its new
2516
+ place and the pixels that go there in one batch of requests, and the
2517
+ server keeps what each pane and the surface show while they are mapped
2518
+ (backing store), so what a moving pane uncovers is back at once rather than
2519
+ after an Expose and a repaint.
2520
+
2521
+ `useSupports('glOverlay')` says whether a connection draws a surface's
2522
+ children at all. Every backend does, on every display but one.
2523
+
2524
+ **XQuartz draws nothing over a GL surface.** There every surface, direct
2525
+ (Apple-DRI) or indirect, is composited by the macOS window server above
2526
+ everything the X server draws in the window. A pane stacked over the
2527
+ surface in X is drawn under it on screen, and nothing on the X side changes
2528
+ that: cutting the surface's window down with SHAPE still leaves the frame
2529
+ covering all of it. A pane over the _whole_ surface does show, but only
2530
+ because the frame then disappears. So on XQuartz the children are laid out
2531
+ and not drawn, the pointer over them is the surface's,
2532
+ `useSupports('glOverlay')` is false, and a development build says so once.
2533
+ Ask the hook and draw the overlay another way there, beside the surface or
2534
+ in GL, or run on the Cocoa backend (the default on macOS), which composites
2535
+ it.
2536
+
2537
+ **A child that only moves is not repainted.** A pane keeps what it drew
2538
+ between frames, so when all that changed about one of the surface's children
2539
+ is where it sits — its `left` or `top`, and nothing inside it — the frame
2540
+ moves that child's pixels on the pane and repaints only what the copy cannot
2541
+ supply: the strips the move uncovered, and the other children's pixels it
2542
+ passed over or under. A layer of mounted content panned across a scene,
2543
+ such as a graph's node bodies over its GL edges, then costs a copy and a few
2544
+ strips a frame rather than a repaint of everything on show. A scroll pane
2545
+ among the children scrolls the same way, moving the pane's pixels. A child
2546
+ that also changed size, or drew anything differently in the same commit, is
2547
+ repainted as before, and
2548
+ [`REACT_X11_NO_SCROLL_BLIT=1`](debugging.md#react_x11_no_scroll_blit1) turns
2549
+ every such copy off.
2550
+
2551
+ Limits worth knowing:
2552
+
2553
+ - **Overlapping surfaces:** where two surfaces overlap, both overlays are
2554
+ above both frames on the Cocoa backend, and stack by when each was made
2555
+ on X11.
2556
+ - **Scroll panes:** a surface scrolled partly out of a scroll pane is not
2557
+ clipped by it. Its window and its panes are real windows placed at its
2558
+ rect.
2559
+ - **Promotion:** a child of the surface is never lifted onto a layer of its
2560
+ own for an animation on the Cocoa backend. That layer would sit under the
2561
+ surface. Its transitions run on the frame clock instead.
2562
+
2563
+ **The pointer over the surface is the tree's**, on every backend, exactly
2564
+ as over a `<box>`: `onMouseDown`, `onMouseMove`, `onMouseUp`, `onClick` (a
2565
+ double click is `detail: 2`), `onContextMenu`, `onMouseEnter` and
2566
+ `onMouseLeave`, `:hover` and `:active`, and `onWheel` fire at the child
2567
+ under the pointer, or at the `<glarea>` between its children, and bubble to
2568
+ the ancestors. A press focuses the nearest focusable one, and
2569
+ `ev.capturePointer()` keeps a drag that wanders off the surface. The
2570
+ window's hit test knows the surface and its children are on top: a point
2571
+ inside the rect lands on them, not on whatever the tree has behind.
2572
+ `pointerEvents: 'none'` on the surface lets the pointer through to that
2573
+ instead.
2574
+
2575
+ ```jsx
2576
+ <glarea
2577
+ onMouseDown={(ev) => {
2578
+ ev.capturePointer();
2579
+ orbit.start(ev.x, ev.y);
2580
+ }}
2581
+ onMouseMove={(ev) => orbit.drag(ev.x, ev.y)}
2582
+ onWheel={(ev) => zoom(ev.deltaY)}
2583
+ />
2584
+ ```
2585
+
2586
+ How it gets there differs underneath and nowhere above. On X11 the surface
2587
+ is a child window that **selects no input of its own**, so the server
2588
+ reports what happens over it to the window the tree lives in — in that
2589
+ window's coordinates, under that window's grab, and with its XI2 selection,
2590
+ so the wheel over a surface is the same wheel as anywhere else in the
2591
+ window. On the Cocoa backend the surface is a layer and the events were the
2592
+ window's all along.
2593
+
2594
+ One rule follows for an element built on `<glarea>`: **do not listen for
2595
+ the pointer on `node.window`**. ntk selects whatever its window is listened
2596
+ to for, and X then delivers the event there instead of to the tree — every
2597
+ press, and the wheel with them, since both are ButtonPress. Listening there
2598
+ was the only way to hear a press over a surface before core delivered it;
2599
+ `node.forwardsPointer` is `true` where it does
2600
+ (`GlAreaNode.prototype.forwardsPointer` from `react-x11/node`, to ask
2601
+ without rendering), and is the switch for dropping such a listener.
2602
+
2603
+ `onDraw` is the raw escape hatch, and a scene graph over it is
2604
+ `@react-x11/components/three`. See `examples/viewer3d.jsx` for the raw
2605
+ form, and `examples/labs/gl-overlay.jsx` for a surface with a HUD over it on
2606
+ both backends, and beside it on XQuartz.
2607
+
2608
+ ---
2609
+
2610
+ ## `<foreign>`
2611
+
2612
+ **X11 only.** Cross-process window embedding does not exist on macOS —
2613
+ there is no equivalent of handing another application's window to your
2614
+ layout, and none is planned. Where there is none — the Cocoa, Wayland and
2615
+ Windows backends, and the headless mock tests render on — this element
2616
+ refuses: it lays out as an empty box and draws nothing, never calls
2617
+ `onReady`, and calls `onError` once to say why. Ask before rendering one:
2618
+
2619
+ ```jsx
2620
+ const embedding = useSupports('embedding');
2621
+ ```
2622
+
2623
+ `<Frame>`, which uses it on X11 to host a pane of your _own_ application,
2624
+ does work on the Cocoa and Windows backends too ([frame.md](frame.md)) — it
2625
+ just gets there another way.
2626
+
2627
+ Another process's top-level X window, laid out as an element — a terminal
2628
+ pane, a video surface, a docked tray icon. The second element that owns a
2629
+ real X window without painting into its parent, and the only one that makes
2630
+ a react-x11 app a _host_ rather than a drawer of its own pixels. Needs ntk
2631
+ ≥ 7.4.0.
2632
+
2633
+ ```jsx
2634
+ <foreign
2635
+ windowId={terminal.windowId}
2636
+ style={{ flexGrow: 1, backgroundColor: '#101014' }}
2637
+ onEmbedded={({ xembed }) => setMode(xembed ? 'xembed' : 'reparented')}
2638
+ onClientGone={() => respawn()}
2639
+ />
2640
+ ```
2641
+
2642
+ - `windowId` — the window to embed. Changing it hands the old client back
2643
+ before the new one is taken. **Omit it** to adopt whatever is put inside
2644
+ this node instead, which is what `xterm -into WID` and `mpv --wid=WID`
2645
+ need.
2646
+ - `onReady({ windowId })` — this node's own container id, offered before
2647
+ anything is embedded, so a program can be spawned into it. Never called
2648
+ where there is no embedding, so the id is always a real window's.
2649
+ - `onEmbedded({ id, xembed, version })` — a client is in. `xembed: false`
2650
+ is a client that set no `_XEMBED_INFO` and got plain reparenting, which
2651
+ is the common case rather than the fallback.
2652
+ - `onClientGone()` — destroyed, or reparented away by someone else.
2653
+ - `onRequestFocus()` — the client asked for the focus. It is then given
2654
+ through the focus manager, so the rest of the tree observes it normally.
2655
+ - `onError(err)` — the embed failed, or — once, at mount — this backend has
2656
+ no embedding at all. Without a handler, a console warning; for the second
2657
+ kind, one per app however many `<foreign>`s ask.
2658
+ - `focusable` — default `true`, like `<textinput>`.
2659
+ - `backgroundColor` in the `style` is what shows in the rect with no client
2660
+ in it: the container window's background, painted by the server.
2661
+
2662
+ **Unmount reparents the client back to the root and drops it from the save
2663
+ set. It never destroys it** — a React tree changing shape is not a reason
2664
+ for another application to lose its window.
2665
+
2666
+ Layout treats it as a leaf, and its X window follows that rect; every change
2667
+ also sends the client the synthetic ICCCM 4.1.5 `ConfigureNotify` with
2668
+ root-relative coordinates. Stacking is `<glarea>`'s: the child window sits
2669
+ above everything drawn in the parent, so 2D content cannot overlap it — and
2670
+ unlike a `<glarea>`, `<foreign>` takes no children to draw over it, so an
2671
+ overlay belongs in a sibling `<popup>` — and pointer events over it are the
2672
+ client's.
2673
+
2674
+ Keyboard focus is where this element has a rule of its own: **while a
2675
+ `<foreign>` holds focus, the application's handlers see every key first and
2676
+ anything they do not consume with `preventDefault()` is forwarded to the
2677
+ client.** That, the XEmbed focus messages, the tab chain crossing the
2678
+ boundary and the one case the rule cannot cover are all in
2679
+ [embedding.md](embedding.md).
2680
+
2681
+ ---
2682
+
2683
+ ## 3D scene: `<mesh>`, `<group>`, geometries, materials
2684
+
2685
+ Moved to [`@react-x11/components/three`](ecosystem.md), which is a scene
2686
+ graph with its own reconciler over the `<glarea>` above — and, unlike the
2687
+ version that lived here, one that renders on **both** GL backends from the
2688
+ same JSX. Core keeps the surface and `onDraw`; see [gl.md](gl.md).
2689
+
2690
+ ## Vector drawings
2691
+
2692
+ `<svg>` is a drawing, laid out like an `<image>`: it reports an intrinsic
2693
+ size and scales into the content box it is given.
2694
+
2695
+ Documents used to live here too — `<markdown>`, `<html>` and `<tex>`, thin
2696
+ wrappers over ntk's document widgets. They were removed in react-x11 2.0:
2697
+ each rendered a whole document as one opaque drawing, which foreclosed the
2698
+ two things a document most needs — text selection across blocks, and
2699
+ re-rendering only the block that changed while content streams in. Their
2700
+ successors are components in
2701
+ [`@react-x11/components`](https://github.com/sidorares/react-x11-components),
2702
+ composed from public host elements rather than wrapping a widget:
2703
+ `<Markdown>` (GFM, selectable, streaming-friendly) and `<Formula>` (KaTeX,
2704
+ selectable), with `<RichText>` and `<CodeBlock>` underneath them.
2705
+
2706
+ ### `<svg>`
2707
+
2708
+ ntk `SvgView` (static SVG via Path2D — paths, shapes, gradients,
2709
+ transforms, basic text). Sized like `<image>`, and by the same style
2710
+ properties: natural `viewBox` size, aspect-preserving when the style sets
2711
+ only one of `style={{ width, height }}`, and scaled into the content box
2712
+ when it sets both.
2713
+
2714
+ Content is **JSX children, like SVG in React DOM** — SVG elements are
2715
+ declarative children with camelCase props (`strokeWidth`, `fillRule`;
2716
+ native-camelCase attributes like `viewBox` stay as-is), re-rendered on
2717
+ any prop change:
2718
+
2719
+ ```jsx
2720
+ <svg viewBox="0 0 24 24" style={{ width: 40, height: 40 }}>
2721
+ <circle cx={12} cy={12} r={10} fill={active ? '#2980b9' : '#ccc'} />
2722
+ <path d="M8 12l3 3 5-6" stroke="white" strokeWidth={2} fill="none" />
2723
+ </svg>
2724
+ ```
2725
+
2726
+ A `source` markup string is also accepted (children win when both are
2727
+ present). Supported elements/attributes are SvgView's (unsupported tags
2728
+ are skipped); per-child event handlers are not dispatched — put handlers
2729
+ on the `<svg>` element itself.
2730
+
2731
+ | prop | |
2732
+ | --------- | --------------------------------- |
2733
+ | children | declarative SVG elements |
2734
+ | `source` | SVG markup string |
2735
+ | `viewBox` | coordinate system (children form) |
2736
+
2737
+ **`currentColor` and recolouring.** `fill="currentColor"` and
2738
+ `stroke="currentColor"` resolve the way the word does in CSS: the node's own
2739
+ `color` if it named one, otherwise the ink it inherits, and under that the
2740
+ palette's `text`
2741
+ ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)). So one
2742
+ drawing serves every state the UI puts it in, and an unstyled one is
2743
+ readable on a dark desktop rather than black on it:
2744
+
2745
+ ```jsx
2746
+ <svg
2747
+ source={icons.gauge}
2748
+ style={{
2749
+ width: 20,
2750
+ height: 20,
2751
+ color: '$fg',
2752
+ ':hover': { color: '$accent' },
2753
+ }}
2754
+ />
2755
+ ```
2756
+
2757
+ A drawing whose paint is entirely `currentColor` — or entirely one colour — is
2758
+ cached as coverage rather than as pixels, so recolouring it is a composite and
2759
+ not a re-render, and every colour of it shares one rendered copy. Drawings with
2760
+ two colours or a gradient bake their colours in, which is right: those colours
2761
+ belong to the drawing rather than to the UI. Nothing to configure either way.
2762
+ `REACT_X11_DEBUG_PAINT_CACHE=1` shows what is being kept.