react-x11 2.52.0 → 2.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
package/docs/elements.md CHANGED
@@ -1,13 +1,9 @@
1
1
  # Elements
2
2
 
3
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.
4
+ windows of the display system. Everything else is a retained lightweight
5
+ node — one [yoga-layout](https://www.yogalayout.dev/) node each — painted
6
+ into the owning window's double-buffered 2d context on ntk's frame clock.
11
7
 
12
8
  ## `style` and props
13
9
 
@@ -24,14 +20,14 @@ blocks and `createStyles`.
24
20
 
25
21
  ## Layout properties (all drawn elements + windows)
26
22
 
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).
23
+ Lengths are logical pixels — a number, or `'12px'` — and `'50%'` / `'auto'`
24
+ pass through to yoga where it takes them. `em`, `rem`,
25
+ `vw`/`vh`/`vmin`/`vmax` and `calc()`/`min()`/`max()`/`clamp()` resolve to
26
+ pixels for their node, and again when what they are relative to changes; a
27
+ CSS unit react-x11 does not read (`ex`, `cqw`, `pt`) is an error naming it.
28
+ CSS's shorthands are read too — `padding: '8px 16px'`,
29
+ `border: '1px solid $border'`, `inset: 0`
30
+ ([styling.md](styling.md#lengths-and-shorthands)).
35
31
 
36
32
  - **Size**: `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
37
33
  `maxHeight`, `aspectRatio`. `<image>` and `<svg>` keep their aspect ratio
@@ -43,9 +39,9 @@ they are relative to moves; a unit CSS has and react-x11 does not read (`ex`,
43
39
  `flexBasis`, `gap`, `rowGap`, `columnGap`
44
40
  - **Position**: `position` (`relative`, `absolute`, `static`, `sticky`, or
45
41
  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
42
+ `left`, `start`, `end`, and `inset` for all four. Under `sticky` the
43
+ insets are how close to the nearest scroll pane's edges the box may come,
44
+ not offsets — see [styling.md](styling.md#sticky-positioning) and
49
45
  [custom positions](styling.md#custom-positions)
50
46
  - **Grid**: `display: 'grid'` arranges the children as a CSS grid —
51
47
  `gridTemplateColumns`, `gridTemplateRows`, `gridTemplateAreas`,
@@ -65,16 +61,11 @@ they are relative to moves; a unit CSS has and react-x11 does not read (`ex`,
65
61
 
66
62
  ### Everything shrinks, nothing shrinks to nothing
67
63
 
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:
64
+ `flexShrink` is `1`, as in CSS, but **an item never shrinks below what is
65
+ inside it**: every item has a floor of its own min-content size (CSS's
66
+ `min-width: auto` on a flex item). A row of chips squeezes until it wraps; a
67
+ label squeezes to its longest word; a `height: 40` row stays 40 tall. To say
68
+ otherwise:
78
69
 
79
70
  | write | to mean |
80
71
  | --------------------------------- | ----------------------------------------- |
@@ -82,32 +73,23 @@ Three ways a style says otherwise:
82
73
  | `minWidth: 0` / `minHeight: 0` | this may shrink past its content, to zero |
83
74
  | `overflow: 'hidden'` / `'scroll'` | the same, and clip what no longer fits |
84
75
 
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`
76
+ The last two are CSS's own rule, and why a `<box overflow="scroll">` — a
77
+ viewport smaller than its content — gets `minWidth: 0` and `minHeight: 0`
89
78
  for free.
90
79
 
91
80
  One deliberate difference from CSS: **a size that was named is kept**. CSS
92
81
  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.
82
+ `height: 40` box squashes to nothing in a column too short for it; here it
83
+ stays 40. Say `minHeight: 0` to get CSS's answer.
102
84
 
103
85
  ### `flex`, the shorthand
104
86
 
105
87
  `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
88
+ share of what is left, from a base size of nothing" — the right thing for a
89
+ pane that should fill its window rather than its content. `flex: 'auto'`
90
+ grows and shrinks from the content's own size, and `flex: 'none'` does
91
+ neither. CSS's long form reads too — `'1 1 0'`, `'0 0 240px'`,
92
+ `'2 200px'`. A longhand written beside it wins, so
111
93
  `{ flex: 1, flexBasis: 'auto' }` reads the way it does in CSS.
112
94
 
113
95
  ## Paint properties
@@ -122,12 +104,11 @@ content. `flex: 'auto'` grows and shrinks from the content's own size, and
122
104
  (`'solid'` default, `'dashed'`), or `border: '1px solid $border'` for the
123
105
  first three — `border: '1px solid'` is drawn in `currentColor`
124
106
  - 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
107
+ `borderWidth`, `borderTopColor`/… fall back to `borderColor`, and
108
+ `borderBottom: '1px solid $border'` sets one side's pair — a blockquote's
109
+ bar is `borderLeftWidth: 3` on the quote box, a table rule
129
110
  `borderBottomWidth: 1` on the row. `borderRadius` requires uniform
130
- borders — a non-uniform border paints square
111
+ borders; a non-uniform border paints square
131
112
  ([styling.md](styling.md#per-side-borders))
132
113
  - `zIndex` — paint/hit order among siblings (stable sort)
133
114
  - `objectFit` — where an `<image>`'s or a `<video>`'s picture goes in its
@@ -135,11 +116,11 @@ $border'` sets one side's pair. A blockquote's bar is
135
116
  `'contain'` (`<video>`'s), `'cover'`, `'none'`, `'scale-down'`
136
117
  - `outlineWidth`, `outlineColor`, `outlineOffset` — the focus ring, painted
137
118
  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
119
+ surrounds. A focusable node draws one on `:focus-visible` unasked;
120
+ `outlineWidth: 0` — or `outline: 'none'` — opts out
140
121
  ([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
122
+ - `opacity` — how opaque the node is **with everything inside it**, 0 to 1:
123
+ the subtree is drawn once and composited at that alpha, so a card, its
143
124
  icon and its text fade as one. `0` draws nothing and is still hit
144
125
  ([styling.md](styling.md#opacity))
145
126
  - `transition` — `120`, or `{ backgroundColor: 120, left: 200 }`: how long a
@@ -150,25 +131,21 @@ $border'` sets one side's pair. A blockquote's bar is
150
131
  ([styling.md](styling.md#window-size-queries))
151
132
 
152
133
  `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.
134
+ arrows, … — ntk's cursor names) and `pointerEvents` are style too.
155
135
  `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`:
136
+ `'box-none'` (React Native's) takes out only the node itself — its children
137
+ still take the pointer, and a press on its own area goes to what is behind:
138
+ the value for an overlay of controls, a `<glarea>`'s children included, that
139
+ must not swallow the clicks between them. So is `hitSlop`:
161
140
 
162
141
  ```jsx
163
142
  <box style={{ height: 16, hitSlop: { top: 4, bottom: 4 } }} /> // 24px target
164
143
  ```
165
144
 
166
145
  `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.
146
+ affects **hit testing only** — not paint, not layout — so a 16px control
147
+ can take the 24px target WCAG 2.2 SC 2.5.8 asks for without misaligning its
148
+ row. Where slop overlaps a sibling, the one painted on top wins.
172
149
 
173
150
  ## Interaction props
174
151
 
@@ -178,37 +155,30 @@ stay inside it, focus is restored when it unmounts), `disabled` (never
178
155
  focusable, and the trigger for a `:disabled` style block), and the event
179
156
  handlers listed in [events.md](events.md).
180
157
 
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
158
+ `onLayout` reports layout: `{x, y, width, height}` in logical pixels, the
159
+ position within the parent _as laid out_ (a scroll does not fire it), after
160
+ the first layout and then on each change. It arrives after the pass, so
161
+ setting state from it is safe. Use it when the reaction to a size is not a
162
+ style ([react-features.md](react-features.md#measuring-a-node)); when it
163
+ is, a `'@container'` block answers in the same frame
189
164
  ([styling.md](styling.md#container-queries)).
190
165
 
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).
166
+ `role` and the `aria-*` props are read by the built-in AT-SPI bridge so
167
+ screen readers see the tree; inert where no assistive technology listens,
168
+ with sensible defaults per element ([accessibility.md](accessibility.md)).
196
169
 
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).
170
+ `dropAccept` + `onDrop` accept a drop and `draggable` + `dragData` start
171
+ one, with other applications as well as inside the app; the props alone
172
+ register the node ([drag-and-drop.md](drag-and-drop.md)).
202
173
 
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.
174
+ `selectable` makes an element a **selection surface** — see
175
+ [Selecting text](#selecting-text).
206
176
 
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).
177
+ `scale` zooms a subtree like CSS `zoom`, not a transform: every length under
178
+ the element, and its own style, is multiplied, and text is shaped at the
179
+ drawn size — what an element owning a viewport (a graph pane, a map) mounts
180
+ its content inside. Windows take no `scale`
181
+ ([scale.md](scale.md#a-subtree-of-its-own)).
212
182
 
213
183
  ---
214
184
 
@@ -217,62 +187,59 @@ a viewport (a graph pane, a map) mounts its content inside. Windows take no
217
187
  A real toplevel window — an X11 window, or an `NSWindow` on the Cocoa
218
188
  backend; the flex, paint and event root for its subtree.
219
189
 
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.
190
+ | prop | |
191
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------- |
192
+ | `title` | window title (UTF-8, `WM_NAME` + `_NET_WM_NAME`) |
193
+ | `width`, `height`, `x`, `y` | geometry, not yoga style; a size is pixels or `'auto'` (default, [below](#natural-size)) |
194
+ | `backgroundColor` | full-window clear color (default white) |
195
+ | `onResize(ev)` | the geometry changed; the tree reflows itself. Fires for **moves** too |
196
+ | `onExpose(ev)` | after a repaint was required |
197
+ | `onCloseRequest(ev)` | WM close button; replaces the default answer |
198
+ | `states` | EWMH `_NET_WM_STATE`, controlled |
199
+ | `fullscreen`, `alwaysOnTop` | boolean sugar for two of those states |
200
+ | `decorations` | `false` asks the WM for no titlebar or border |
201
+ | `transparent` | 32-bit ARGB visual: rounded, translucent windows |
202
+ | `transientFor` | ICCCM `WM_TRANSIENT_FOR`: the window this one belongs to |
203
+ | `onStatesChange(states)` | what the window manager actually did |
204
+ | `onClientMessage(ev)` | a ClientMessage addressed to this window — EWMH, XEmbed, the tray |
205
+ | `theme` | palette `$token` style values resolve against, for this subtree |
206
+ | `embeddable` | never maps itself; an embedder ([`<foreign>`](embedding.md)) does. Set by a [`<Frame>`](frame.md) pane's root |
207
+ | `hidden` | realized and laid out, never mapped while true |
208
+ | `frameRate` | frame pacing: `'display'` (default), `'adaptive'`, `'throughput'`, fps, `{ budget, minFps, maxFps }` |
209
+
210
+ Each is described below. A geometry change sends only what changed, so a
211
+ new size leaves the window where the window manager or the user put it.
212
+ Windows may be nested (real X11 child windows). **Ref**: the live window —
213
+ on X11 the ntk `Window`, with `getContext('2d')`, `requestAnimationFrame`,
214
+ `setCursor` and the rest of the ntk API.
244
215
 
245
216
  #### On the Cocoa backend
246
217
 
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.
218
+ Everything about _drawing_ works — geometry, background, resize, close,
219
+ `transparent`, `theme`, `frameRate`, `hidden`. What a **window manager**
220
+ does differs, since macOS has none:
221
+
222
+ | prop | on Cocoa |
223
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
224
+ | `minWidth`/`minHeight`/`maxWidth`/`maxHeight` | honoured |
225
+ | `resizable`, `decorations` | honoured, but read **once, at creation** |
226
+ | `transparent`, `borderRadius` | always available: every window composites |
227
+ | `states: ['demands_attention']` | the Dock bounce, held until the state is removed |
228
+ | `fullscreen`, `states: ['maximized' \| 'hidden']` | the full-screen Space, the zoom, the Dock |
229
+ | `onStatesChange`, `useWindowState()` | fire for full screen (from the transition's start), zoom, Dock, key window; `desktop`/`obscured` never change |
230
+ | `embeddable` | a `<Frame>` pane, through shared surfaces ([frame.md](frame.md)) |
231
+
232
+ **Inert** — accepted and, today, silently ignored, so shared code stays
233
+ branch-free: every other `_NET_WM_STATE` (`sticky`, `below`,
234
+ `skip_taskbar`, `shaded`, …), `appId`, `windowType`, `gravity`, the
235
+ increment and aspect hints, and `transientFor` (a managed `<Dialog>` already
236
+ floats on its own window). `onClientMessage` never fires. Why:
237
+ [macos.md](macos.md#windowing-semantics-what-maps-what-bends-what-breaks).
270
238
 
271
239
  ### Natural size
272
240
 
273
241
  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:
242
+ the screen**. `'auto'` says so explicitly, per axis:
276
243
 
277
244
  ```jsx
278
245
  <window title="prefs" /> {/* natural, both ways */}
@@ -280,91 +247,64 @@ independent:
280
247
  <window width="auto" maxWidth={720} minHeight={200} /> {/* auto within your own bounds */}
281
248
  ```
282
249
 
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.
250
+ This is CSS's shrink-to-fit — a top-level window has nothing to stretch
251
+ into; stretching into the screen is `fullscreen`. The size is worked out
252
+ before the window is created, so it never appears at the wrong size.
288
253
 
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:
254
+ **How it is measured** — the second step is where the surprises are:
295
255
 
296
256
  1. The tree is laid out with no available width, so nothing wraps and the
297
257
  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.
258
+ 2. That is clamped into `[minWidth, min(maxWidth, the screen)]` — the size
259
+ hints bound the auto size as `min-width`/`max-width` bound `width: auto`;
260
+ `minWidth` wins a conflict.
302
261
  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.
262
+ there: a paragraph that had to wrap is taller than step 1 said.
263
+
264
+ Unlike in CSS, the screen cap beats min-content: what does not fit is cut.
265
+
266
+ **The cap** is the usable area of the monitor the window opens on — its
267
+ Xinerama rect less the panels in `_NET_WORKAREA` — read once while
268
+ `createRoot()` connects; the screen on a server with neither; none in the
269
+ headless mock. The window manager's frame is not counted, so a window at
270
+ the cap is a titlebar too tall and the WM trims it.
271
+
272
+ **Afterwards, auto follows the content**, growing and shrinking with it,
273
+ until the user resizes the window or the window manager imposes a size;
274
+ then the window is theirs. A numeric `width` does the same, and `'auto'`
275
+ again hands it back. The window's own resize requests coming back are not a
276
+ takeover.
277
+
278
+ On X11 a re-fit is laid out once, at the new size: the frame waits for the
279
+ server to apply it (up to 150 ms, if the window manager refuses), so grown
280
+ content is never drawn cut off; the old pixels stay up meanwhile.
338
281
  `REACT_X11_NO_REFIT_HOLD=1` turns the wait off
339
- ([debugging.md](debugging.md#react_x11_no_refit_hold1)).
282
+ ([debugging.md](debugging.md#react_x11_no_refit_hold1)). On Cocoa a window
283
+ is a whole number of **points**, rounded up (100.5 makes 101), and still
284
+ follows its content.
340
285
 
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.
286
+ - A scroll container reports its **content** height, so an auto window
287
+ around a long list opens screen-tall. Give the window a `height`, or the
288
+ scroller a `maxHeight`.
289
+ - An auto width is the **unwrapped** row, so it depends on the UI font.
290
+ - A window tracking its content costs an extra layout pass on each frame
291
+ that lays out; one with both sizes given pays nothing.
345
292
 
346
293
  ### A floor the content decides
347
294
 
348
295
  `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.
296
+ can be drawn at**, measured and handed to the window manager so the user
297
+ cannot drag the window below it. Opt-in — Qt and GTK do it by default —
298
+ since CSS lets content overflow.
351
299
 
352
300
  ```jsx
353
301
  <window minWidth="auto" minHeight="auto"> {/* never smaller than my content */}
354
302
  <window width={900} minWidth="auto" /> {/* my opening size, the content's floor */}
355
303
  ```
356
304
 
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
305
  **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:
306
+ and the floor is how far it still reached. Each node contributes what its
307
+ own style lets it shrink to:
368
308
 
369
309
  | the node | contributes |
370
310
  | --------------------------------- | ---------------- |
@@ -375,55 +315,27 @@ what _doesn't_ count:
375
315
  | a `minWidth: 0` of its own | nothing |
376
316
  | a `minWidth: 240` of its own | 240 |
377
317
 
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.
318
+ A registered element contributes its `measureContent` answer for the
319
+ smallest size ([extending.md](extending.md#a-size-of-your-own)). This is
320
+ every node's own floor
321
+ ([above](#everything-shrinks-nothing-shrinks-to-nothing)) read at the root,
322
+ with the same escape hatch: a node that names a floor of its own stops its
323
+ contents counting, so a window around a scrolling pane is floored by
324
+ everything _except_ that pane.
388
325
 
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:
326
+ `maxWidth`/`maxHeight="auto"` mean **the size the content wanted** — the
327
+ natural size, not the floor. Where nothing can shrink they are equal, so
328
+ `minWidth="auto" maxWidth="auto"` is a fixed-size dialog, as is
329
+ `resizable={false}`.
398
330
 
399
331
  - **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.)
332
+ tiling window manager may not, and the renderer will not fight it — clip
333
+ gracefully anyway.
334
+ - **`minHeight="auto"` is for the current width**, re-sent as that changes:
335
+ a paragraph is tallest at its narrowest.
408
336
  - **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.
337
+ user takes over; an `'auto'` bound does not. It costs a layout pass per
338
+ axis on frames that lay out, and is sent only when it moves.
427
339
 
428
340
  ### `hidden` — realized, laid out, not on screen
429
341
 
@@ -431,25 +343,19 @@ Three things worth knowing before reaching for it:
431
343
  <window hidden={minimized} title="scratch" />
432
344
  ```
433
345
 
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.
346
+ The window exists at its (measured) size with a live tree, but is not
347
+ mapped while `hidden`; clearing it maps the window where it stands. Nothing
348
+ unmounts, so state, subscriptions and the window survive — declarative
349
+ "minimize by unmapping", and how [`Tooltip`](components.md#tooltip)
350
+ measures a label before placing it.
351
+
352
+ `<Activity mode="hidden">` or a suspended `<Suspense>` compose with it: the
353
+ window shows only while **neither** hides it. Hiding releases the keyboard
354
+ focus inside ([focus-follows-visibility](events.md)); the reveal restores it
355
+ unless something else took it. On a managed `<window>` hiding is ICCCM's
356
+ _withdraw_, so on reveal the window manager may frame or place it anew, and
357
+ `states` are re-applied rather than remembered; a `<popup>` just unmaps and
358
+ maps.
453
359
 
454
360
  ### `frameRate` — pacing the frames under a flood
455
361
 
@@ -457,89 +363,62 @@ it anew and `states` are re-declared rather than remembered.
457
363
  <window frameRate="adaptive">…</window>
458
364
  ```
459
365
 
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
366
+ By default a window paints every frame the clock gives it — on X11 as often
367
+ as the server keeps up, on macOS at the display's rate — right for a UI
368
+ answering input. For content that changes faster than anyone reads — a
369
+ terminal under `cat`, a socket-fed chart, a simulation — most of the JS
370
+ thread then goes to frames nobody sees, starving the producer.
371
+
372
+ `frameRate` prices frames in CPU time. Under `'adaptive'` painting may take
373
+ a quarter of the wall time while busy: credit accrues at that rate, each
374
+ frame spends what it cost (the flush, and on macOS the present), and a frame
375
+ claimed in debt waits until the credit is back to zero. So:
376
+
377
+ - **Idle is immediate**: the first frame after a pause paints on the next
378
+ tick, whatever the last cost.
379
+ - **Cheap is unthrottled**: a frame cheaper than its credit is never held,
380
+ so a blit-scrolled list keeps the display's rate.
381
+ - **A rare expensive frame is free**: the bucket holds a 50ms burst. A
382
+ _stream_ of them waits `cost × (1/budget − 1)` each — 18ms after a 6ms
383
+ frame.
384
+ - **A flood that ends is unthrottled at once**: the debt never exceeds one
385
+ frame's cost.
386
+
387
+ `minFps` bounds staleness: the screen is never more than `1000 / minFps` ms
388
+ behind the last paint. `maxFps` caps the rate, cheap frames included.
389
+
390
+ | value | `budget` | `minFps` | `maxFps` | when |
391
+ | ---------------------------- | -------: | -------: | -------: | --------------------------------------------------------- |
392
+ | `'display'` (the default) | 1 | none | none | a UI answering input: every frame, after React's batching |
393
+ | `'adaptive'` | 0.25 | 20 | none | fed off an input path: a terminal, a live chart |
394
+ | `'throughput'` | 0.1 | 10 | 30 | output over display: may lag up to 100ms during a flood |
395
+ | a number, `60` | 1 | none | 60 | a ceiling and nothing else |
396
+ | `{ budget, minFps, maxFps }` | | | | any subset; the rest are `'display'`'s |
397
+
398
+ A budget below 1 with no `minFps` warns in development: nothing would bound
399
+ the wait after an expensive frame.
400
+
401
+ Precedence: **`REACT_X11_FRAME_RATE`** (`adaptive`, `display`,
402
+ `throughput` or a number) overrides everything, so an A/B run needs no code
403
+ change; then **the prop**; then **`createRoot({ frameRate })`**, the
404
+ default for every window of that root; then `'display'`. A `<popup>` takes
405
+ its own prop the same way; a [`<glarea>`](#glarea) takes its window's unless
520
406
  it names one.
521
407
 
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.
408
+ The pacer never changes what is painted (held damage accumulates into the
409
+ next frame) and never delays the answer to input: a click or key paints at
410
+ once ([events.md](events.md)). It prices JS-thread time only, so on X11 it
411
+ rarely holds a frame, and it is inert where a backend has backpressure of
412
+ its own. `REACT_X11_TRACE=requests` prints `waited=` on each held frame
413
+ ([debugging.md](debugging.md)). Design notes:
414
+ [frame pacing](../design/architecture/frame-pacing.md).
534
415
 
535
416
  ### `onResize` fires for moves
536
417
 
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:
418
+ `onResize` reports _any_ geometry change: under a window manager that moves
419
+ windows opaquely, a title-bar drag fires it per pointer step at the same
420
+ size. The renderer spends nothing on a move; a handler should check which it
421
+ got:
543
422
 
544
423
  ```jsx
545
424
  <window
@@ -550,17 +429,16 @@ event says which it was:
550
429
  />
551
430
  ```
552
431
 
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.
432
+ `ev.previous` is the geometry these compare against. Once a reparenting
433
+ window manager has framed the window, `ev.x`/`ev.y` are frame-relative, not
434
+ screen coordinates.
557
435
 
558
436
  ### Stacking
559
437
 
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.
438
+ Nested `<window>`s stack like drawn siblings — later on top, `zIndex` over
439
+ document order — and reordering them in JSX restacks the real windows.
440
+ Top-level windows are the window manager's to stack, so their tree order
441
+ means nothing; use `alwaysOnTop`.
564
442
 
565
443
  ```jsx
566
444
  <window>
@@ -570,24 +448,19 @@ nothing at all when mount order already agrees.
570
448
  </window>
571
449
  ```
572
450
 
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
451
  ### Window manager hints
578
452
 
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.
453
+ Read by the window manager, applied at mount and on update (unchanged
454
+ values are not re-sent):
583
455
 
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 |
456
+ - `resizable={false}` pins min and max size to the current size.
457
+ - Size hints: `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `widthInc`,
458
+ `heightInc`, `baseWidth`, `baseHeight`, `minAspect`, `maxAspect`,
459
+ `gravity`.
460
+ - `appId`: `'instance'`, `['instance', 'Class']` or `{instance, class}`.
461
+ - `windowType`: `'dialog'`, `'utility'`, `'tooltip'`… or an array of
462
+ fallbacks.
463
+ - `decorations={false}` asks for no titlebar or border.
591
464
 
592
465
  ```jsx
593
466
  <window width={400} height={300} resizable={false} windowType="dialog" />
@@ -595,26 +468,19 @@ are free, so no `sizeHints` object is needed.
595
468
  <window width={400} height={300} minWidth="auto" /> {/* measured — see above */}
596
469
  ```
597
470
 
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
471
  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
- ```
472
+ are the real geometry the user can drag, `minWidth`/`maxHeight` what the WM
473
+ enforces, and the _contents_ are laid out by `style`. The four bounds also
474
+ take `'auto'` ([a floor the content decides](#a-floor-the-content-decides)),
475
+ and on an auto-sized window they bound the natural size, so
476
+ `<window resizable={false} windowType="dialog" />` is pinned at its measured
477
+ size — the fixed-size dialog.
611
478
 
612
479
  ### `transientFor` — a window that belongs to another
613
480
 
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.
481
+ Without it, a secondary window is an unrelated application to the window
482
+ manager: its own taskbar and alt-tab entry, placed anywhere, stacked
483
+ independently, not minimised with its owner.
618
484
 
619
485
  ```jsx
620
486
  const main = useRef(null);
@@ -623,84 +489,48 @@ const main = useRef(null);
623
489
  <window transientFor={main} windowType="dialog" title="Preferences">…</window>
624
490
  ```
625
491
 
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.
492
+ Takes a ref to a `<window>`/`<popup>` or to **any drawn node** (resolved to
493
+ its window, so an out-of-flow `<box>` in the owner is a good handle), a raw
494
+ XID, `'root'` (this client's whole window group), or `null` to clear. A ref
495
+ still null at mount — an owner mounted as a sibling in the same commit — is
496
+ retried next frame.
497
+
498
+ Set it **with** `windowType="dialog"`. Under EWMH a managed transient with
499
+ no type already is a dialog (kept out of taskbar and pager), but a `dialog`
500
+ with no owner has nothing to stack above or iconify with. Stacking above
501
+ the owner and iconifying with it are window-manager policy (quartz-wm does
502
+ neither). **Inert on override-redirect windows** — every `<popup>` by
503
+ default — which the WM never manages; ntk warns if asked.
504
+
505
+ `windowIdOf(refOrInstance)` is the same resolution: the XID, or null. An
506
+ xdg-desktop-portal `parent_window` is `` `x11:${id.toString(16)}` `` —
507
+ lowercase hex, **no** `0x`. Today's portal backends tolerate a prefix, so
508
+ the mistake goes unnoticed; Qt's parser returns 0 on failure, which would
509
+ mean a silently unparented, non-modal dialog. `useWindowId(ref)` is the
510
+ hook form, returning a getter like `useAnchor`.
511
+
512
+ `decorations={false}` writes `_MOTIF_WM_HINTS` (Mutter, KWin, Xfwm,
513
+ Openbox, i3); a window manager that ignores it decorates anyway.
672
514
 
673
515
  ### Closing a window
674
516
 
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:
517
+ Every window the window manager manages advertises `WM_DELETE_WINDOW`, with
518
+ or without `onCloseRequest` — otherwise the close button would kill the
519
+ connection mid-frame, skipping effect cleanup. Windows the WM does not
520
+ frame (a child `<window>`, an override-redirect `<popup>`) do not; a
521
+ [managed `<popup>`](#a-managed-popup-is-a-dialog) does.
689
522
 
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. |
523
+ - **No `onCloseRequest`**: the **primary window** unmounts the tree and
524
+ closes the connection — effects clean up and the process ends on a drained
525
+ loop. Any other window refuses, with a dev warning.
526
+ - **`onCloseRequest`**: only your handler runs — `setOpen(false)` for a
527
+ dialog, a "save your work?" prompt, `root.unmount()` to quit.
694
528
 
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:
529
+ The primary window is the first top-level `<window>` that is not
530
+ `transientFor` another and has no `windowType`; a lone window is primary
531
+ whatever its type. Secondary windows refuse because closing a
532
+ `{open && <window/>}` behind React's back would leave a window the app
533
+ believes open and can never reopen. Give a dialog a handler:
704
534
 
705
535
  ```jsx
706
536
  {
@@ -717,15 +547,13 @@ a dialog wants a handler:
717
547
  ### Window state
718
548
 
719
549
  `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.
550
+ `maximized_vert`/`maximized_horz`), `shaded`, `skip_taskbar`, `skip_pager`,
551
+ `hidden`, `fullscreen`, `above`, `below`, `demands_attention`, `focused`.
552
+ `fullscreen` and `alwaysOnTop` are sugar for `fullscreen` and `above`,
553
+ unioned with `states`. On the [Cocoa backend](macos.md) `fullscreen` is the
554
+ full-screen Space, `maximized` the zoom, `hidden` the Dock, and
555
+ `demands_attention` bounces the Dock icon until the app is activated or the
556
+ state removed ([desktop.md](desktop.md)); the rest are inert.
729
557
 
730
558
  ```jsx
731
559
  <window
@@ -735,29 +563,20 @@ the rest have no AppKit equivalent and are inert there.
735
563
  />
736
564
  ```
737
565
 
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`.
566
+ **Controlled, and diffed against the previous props** — not the window's
567
+ current state, which the window manager changes behind the app's back
568
+ (maximize, a hotkey leaving fullscreen, a tiling WM). A state is re-sent
569
+ only when the app changes it, so a WM that disagreed is not fought.
570
+ `onStatesChange` reports what the WM did; without it the property is not
571
+ watched. States are applied **before mapping**, so a window opens fullscreen
572
+ without flashing at normal size. On XQuartz, whose quartz-wm lacks
573
+ `_NET_WM_STATE_ABOVE`, `alwaysOnTop` uses Apple-WM window levels.
754
574
 
755
575
  ### `onClientMessage` — speaking a protocol of your own
756
576
 
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.
577
+ ClientMessage carries nearly every X11 convention — EWMH requests, XEmbed,
578
+ the system tray, protocols between copies of one app. `onClientMessage`
579
+ gets every one addressed to this window:
761
580
 
762
581
  ```jsx
763
582
  <window
@@ -768,50 +587,33 @@ every one of them that was addressed to this window.
768
587
  />
769
588
  ```
770
589
 
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
590
+ `ev.messageType` is the atom's **name** (no interning needed), `ev.atom` its
591
+ id, `ev.format` 8, 16 or 32, `ev.data` the payload at that width — 5, 10 or
592
+ 20 values. No setup; a window without the prop pays nothing.
593
+
594
+ - **Order is preserved**, as chunked protocols need
595
+ (`_NET_SYSTEM_TRAY_BEGIN_MESSAGE` then `_NET_SYSTEM_TRAY_MESSAGE_DATA`).
596
+ `messageType` is `null` only for an atom the **server** does not know — a
597
+ broken sender.
598
+ - **Scoped to the window the message names**, unlike
599
+ `useApp().X.on('event')`, which sees every window's events and outlives
600
+ the element. Messages to other windows, the EWMH ones sent to the root
601
+ included, do not arrive; a window manager wants the raw stream
795
602
  (`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`:
603
+ - **`preventDefault()` stops react-x11's own handling** — today XDND, for a
604
+ window speaking the drag protocol itself. The close button is
605
+ `onCloseRequest`'s.
606
+
607
+ Send with `useApp().X.SendClientMessage(destination, aboutWindow, atom,
608
+ format, data, eventMask)`, or ntk's `window.sendClientMessage(name, data)`,
609
+ which interns the atom. EWMH messages _about_ a window go to the **root**;
610
+ to another application's window pass event mask `0`. Taking a selection
611
+ needs a timestamp, not `CurrentTime`:
809
612
  [`lastInputTime` and `serverTime`](clipboard.md#owning-a-selection-of-your-own).
810
613
 
811
614
  ### Kiosk
812
615
 
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:
616
+ No `kiosk` prop — it is a combination:
815
617
 
816
618
  ```jsx
817
619
  <window
@@ -822,16 +624,15 @@ no decoration, no taskbar entry, nothing above it, and no pointer:
822
624
  />
823
625
  ```
824
626
 
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).
627
+ `cursor: 'none'` hides the pointer; an unset `cursor` inherits the root
628
+ window's ([styling.md](styling.md)).
828
629
 
829
630
  ### `transparent` — rounded corners and translucency
830
631
 
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:
632
+ `transparent` creates the window on a 32-bit ARGB visual: what the tree
633
+ leaves unpainted stays empty, and a compositor blends it with what is
634
+ behind. With `borderRadius` in the window's own `style` that gives rounded
635
+ corners with an **antialiased** edge (alpha, not a 1-bit Shape mask):
835
636
 
836
637
  ```jsx
837
638
  <popup
@@ -843,40 +644,20 @@ possible, and it is the main reason to reach for it:
843
644
  </popup>
844
645
  ```
845
646
 
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.
647
+ `backgroundColor` may be translucent; unset, the window is empty but for
648
+ what the tree paints. A window's `borderRadius` works **only** with
649
+ `transparent`; on an opaque window it is ignored (the corners would show
650
+ the server's white).
859
651
 
860
652
  ### When transparency is not available
861
653
 
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:
654
+ **On the Cocoa backend it always is.** On X11 it needs a **depth-32
655
+ TrueColor visual** (XQuartz has none) and a **running compositor** (Mutter,
656
+ KWin, picom, …); without one, painted-away corners show **black**.
657
+ react-x11 prevents that: where transparency would not be seen, the window
658
+ is filled edge to edge, its `borderRadius` ignored and a translucent
659
+ `backgroundColor` flattened — square and opaque, with nothing asked of the
660
+ app. Design the other side with `'@supports transparency'`:
880
661
 
881
662
  ```jsx
882
663
  <popup
@@ -891,62 +672,49 @@ through the `'@supports transparency'` style block:
891
672
  />
892
673
  ```
893
674
 
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:
675
+ The block ([Capability queries](styling.md#capability-queries)) is answered
676
+ per window and re-resolved live: a compositor started mid-session rounds the
677
+ popup without a remount, which is why `transparent` takes the 32-bit visual
678
+ even when nothing composites yet. `useSupports('transparency')` asks the
679
+ same of the display, before any window exists:
904
680
 
905
681
  ```jsx
906
682
  const canBlend = useSupports('transparency');
907
683
  const margin = canBlend ? 26 : 0; // room for a client-drawn shadow
908
684
  ```
909
685
 
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).
686
+ `REACT_X11_NO_TRANSPARENCY=1` shows the fallback on a compositing display:
687
+ `transparent` is ignored and both answer false
688
+ ([debugging.md](debugging.md)). Toggling `transparent` on a mounted window
689
+ does nothing until it remounts (change its `key`): the visual is fixed at
690
+ creation.
916
691
 
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.
692
+ Input still hits the full rectangle — corners are invisible, not
693
+ click-through (the Shape input region is reachable through the ntk ref).
694
+ Child `<window>`s are composited by the X server, not the compositor, so a
695
+ transparent window nested in another blends against nothing.
926
696
 
927
697
  ## `<popup>`
928
698
 
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) |
699
+ An override-redirect top-level window at **screen coordinates** — menus,
700
+ tooltips, dropdowns — that the window manager ignores (no decorations, no
701
+ focus stealing). Its place in the JSX tree does not affect its place on
702
+ screen; it is its own paint and event root, and conditional rendering
703
+ controls its lifetime. `anchor` places it against a node
704
+ ([below](#anchor--a-popup-that-places-itself)); `x`/`y` place it in screen
705
+ logical pixels, e.g. at `ev.screenX`/`screenY`. Same props as `<window>`,
706
+ **including [natural size](#natural-size)** — a menu sized by its rows.
707
+
708
+ | prop | |
709
+ | ------------------ | ------------------------------------------------------------------------------------------------ |
710
+ | `anchor` | place against a node, a rect inside one, or a screen rect, at the content's size |
711
+ | `grab` | hold a pointer grab while up — how menus behave on X (below) |
712
+ | `grabKeyboard` | take the keyboard while up — for a popover a tray click opened (below) |
713
+ | `transparent` | rounded corners and translucency ([above](#transparent--rounded-corners-and-translucency)) |
714
+ | `onDismiss` | a press landed outside the popup: close it |
715
+ | `trapFocus` | own a focus scope: a modal ([events.md](events.md#focus-scopes-modals)) |
716
+ | `overrideRedirect` | `false` makes it a WM-managed window — a real dialog (below) |
717
+ | `dragPreview` | a drag preview following the pointer, never a drop target ([drag-and-drop.md](drag-and-drop.md)) |
950
718
 
951
719
  ### `anchor` — a popup that places itself
952
720
 
@@ -973,82 +741,51 @@ function Completions({ editorRef, caret, matches, onPick }) {
973
741
  ```
974
742
 
975
743
  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.
744
+ label, at most 220 tall and scrolling past that, flipped above the line near
745
+ the screen's bottom — no font measured, size stated or position named.
980
746
 
981
747
  `anchor` takes [`anchorRect`'s options](components.md#useanchorref--anchorrectnode-options)
982
748
  — `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.
749
+ the node (or a ref to one) it hangs off; `x`/`y` are ignored while it is
750
+ set. The popup is measured, placed (flip and screen-edge clamp depend on its
751
+ size) and only then created, so it appears at the right size in the right
752
+ place. It then follows everything that can move either half: the anchor's
753
+ layout, an ancestor scrolling, the owner window moving, its own content
754
+ growing.
755
+
756
+ `at` is a rect **inside** the node, in its coordinates —
757
+ `{ x: caretX, y: lineTop, width: 1, height: lineHeight }` — read by the
758
+ flip, the alignment and `offset` as a small widget there would be; `{x, y}`
759
+ alone is a point.
760
+
761
+ An anchor ref not attached yet (written _above_ its trigger) is waited for,
762
+ one frame, rather than opening at the screen corner. **When the anchor
763
+ scrolls out of view the popup unmaps**, and returns — re-taking its
764
+ `grab` — with the anchor; a real window cannot be clipped by the anchor's
765
+ ancestors. To _close_ it instead, place it yourself with
766
+ [`useAnchorTracking`](components.md#useanchorref--anchorrectnode-options)'s
767
+ `onOutOfView`.
768
+
769
+ A popup never takes the X input focus, but nodes in it can hold the **owner
770
+ window's** focus — with `trapFocus` and `autoFocus` that is a modal
771
+ ([Focus inside a `<popup>`](events.md#focus-inside-a-popup)). One with no
772
+ owner window takes keys with `grabKeyboard`
773
+ ([below](#a-popover-under-a-tray-item)).
774
+
775
+ **`grab` makes a menu dismissable.** Without it, a press elsewhere —
776
+ another app, the root window, even this app's own window frame (the window
777
+ manager's) — never reaches the client, and the menu stays open. With it the
778
+ press is redirected to the popup, lands outside it, and `onDismiss` fires.
779
+ The app's own windows still get their presses, so submenus and the owner
780
+ keep working; only a menu's root popup needs the grab. `Select`,
781
+ `ContextMenu` and `MenuBar` do this.
1044
782
 
1045
783
  ### A popover under a tray item
1046
784
 
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:
785
+ A tray click has no node: [`useTray`](desktop.md#the-tray)'s `onClick`
786
+ reports the item's screen frame. `anchor` takes it as `rect` instead of
787
+ `to`, with the same flip, clamp and natural size, kept on that monitor and
788
+ below the menu bar:
1052
789
 
1053
790
  ```jsx
1054
791
  function MenuBarApp() {
@@ -1070,64 +807,28 @@ function MenuBarApp() {
1070
807
  }
1071
808
  ```
1072
809
 
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.
810
+ `rect` is logical screen pixels; `{ x, y }` alone is a point — all a
811
+ freedesktop tray reports. It moves only when replaced, and is never out of
812
+ view. Its monitor is the one the **whole rect** overlaps most (a menu-bar
813
+ item's frame starts above its display's top), else the nearest.
1077
814
 
1078
- ### Centred over something
815
+ **`grabKeyboard`**: a menu-bar app has no window for keys to arrive
816
+ through, so the popup takes the keyboard while up:
1079
817
 
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.
818
+ | backend | what it is |
819
+ | ------- | ------------------------------------------------------------------------------- |
820
+ | X11 | a keyboard grab, taken and dropped with the map, as `grab` is for the pointer |
821
+ | macOS | a key-capable window at the popup level, activating the app — `NSPopover`'s way |
822
+ | Wayland | nothing more: a grabbing popup has the keyboard already |
1086
823
 
1087
- ```jsx
1088
- <popup
1089
- anchor={{ rect: ownerRect, placement: 'center' }}
1090
- width={360}
1091
- height="auto"
1092
- >
1093
- ```
824
+ On macOS the window's kind is fixed at creation, so the prop does not change
825
+ on a mounted popup. The popover closes on `onBlur` there (another app takes
826
+ the key window) and on `onDismiss` on X; pass both.
1094
827
 
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:
828
+ **Shortcuts work with nothing passed**: a binding belongs to the top-level
829
+ `<window>`, or in an app without one, to the root-level `<popup>` holding
830
+ the keyboard. So do `useFileDialog`'s parent, the global menu and
831
+ `useWindowState`.
1131
832
 
1132
833
  ```jsx
1133
834
  function Controls({ onClose, onToggle }) {
@@ -1137,75 +838,76 @@ function Controls({ onClose, onToggle }) {
1137
838
  }
1138
839
  ```
1139
840
 
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.
841
+ A binding with neither a window nor a popup above it — above the
842
+ `if (!open)`, say — never fires; development says so once, and
843
+ [`scope`](events.md#accelerators) anchors one at a node you name.
844
+
845
+ ### Centred over something
846
+
847
+ `placement: 'center'` puts the popup **over** its anchor, middle on middle,
848
+ clamped to the monitor. Over a window's rect it is a dialog —
849
+ [`Dialog`](components.md#dialog) passes its owner's screen rect as `rect`:
850
+
851
+ ```jsx
852
+ <popup
853
+ anchor={{ rect: ownerRect, placement: 'center' }}
854
+ width={360}
855
+ height="auto"
856
+ >
857
+ ```
858
+
859
+ It is centred **once**: when its size changes later it keeps its top edge
860
+ and horizontal middle, moving only to stay on the monitor, so what the user
861
+ is reading does not jump. A managed one grows from wherever it was moved. A
862
+ new `rect`, or the `to` node moving, centres it again.
1145
863
 
1146
864
  ### A managed `<popup>` is a dialog
1147
865
 
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.
866
+ `overrideRedirect` defaults to `true` — what makes a menu a menu. `false`
867
+ hands the popup to the window manager: framed, titled, movable, with a
868
+ close button (`onCloseRequest`). With `transientFor` it is a real dialog,
869
+ above its owner, out of the taskbar and alt-tab, minimising with the owner.
1154
870
 
1155
871
  ```jsx
1156
872
  <popup overrideRedirect={false} transientFor={anchor} windowType="dialog"
1157
873
  title="Preferences" trapFocus>
1158
874
  ```
1159
875
 
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.
876
+ Turn `grab` off with it — a grab swallows the press that starts a WM drag —
877
+ so a press outside no longer dismisses it, as befits a dialog.
878
+ [`Dialog`](components.md#dialog) does this; `managed={false}` returns to
879
+ override-redirect.
1167
880
 
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.
881
+ `windowType` defaults to `"popup_menu"`; name yours (`"tooltip"`,
882
+ `"dropdown_menu"`, … — `Select`'s sheet and `Tooltip` do). It is
883
+ **additive**: the popup stays override-redirect, and compositors use the
884
+ type for consistent shadows and animations.
1177
885
 
1178
886
  ## `<box>`
1179
887
 
1180
888
  The flex container. All layout + paint + interaction props above.
1181
889
  **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.
890
+ window, valid after layout). With `overflow: 'scroll'` it is also the
891
+ **scroll container**.
1185
892
 
1186
893
  ## Scrolling: `overflow: 'scroll'` {#scrolling}
1187
894
 
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**:
895
+ There is no scrolling element: a `<box>` or `<window>` with
896
+ `overflow: 'scroll'` is a clipped viewport over its overflowing children, on
897
+ **both axes**:
1191
898
 
1192
899
  ```jsx
1193
900
  <box style={{ overflow: 'scroll', flexGrow: 1 }}>{rows}</box>
1194
901
  ```
1195
902
 
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.
903
+ The wheel scrolls it; a draggable thumb is drawn on each overflowing axis.
904
+ `overflow: 'hidden'` clips without scrolling. Being a style, it toggles
905
+ without a remount (`overflow={dense ? 'scroll' : 'visible'}` keeps the node,
906
+ children and state); a box that stops scrolling resets its offset to 0, as
907
+ in CSS.
1205
908
 
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:
909
+ **It is a tab stop and takes keys** whenever it has somewhere to scroll, so
910
+ unfocusable content (a log, a long `<text>`) is readable without a pointer:
1209
911
 
1210
912
  | key | |
1211
913
  | ------------------- | ------------------------------------ |
@@ -1214,46 +916,29 @@ log, a long `<text>`, a rendered document) be read without a pointer at all:
1214
916
  | Space / Shift+Space | the same, for the hand already there |
1215
917
  | Home / End | the top and the bottom |
1216
918
 
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.
919
+ One that fits its content is no tab stop, takes no keys and no wheel: **the
920
+ wheel chains past it** to the next scroll container out. The keys are a
921
+ default action — your `onKeyDown` runs first, and `preventDefault()` cancels
922
+ them. A pane with an `onClick` pages with Space and activates with Enter
923
+ ([events.md](events.md#space-and-enter-are-a-click)); a focusable row
924
+ _inside_ a pane takes both keys and pages with neither.
1240
925
 
1241
- ### The layout defaults it brings
926
+ A press on the bar never reaches the row behind it. A thumb drag keeps its
927
+ grip; a track press pages towards it. Drags move by whole device pixels, as
928
+ the wheel does ([extending.md](extending.md#scrolling-content-you-painted)),
929
+ so nothing is drawn off the pixel grid. `<textarea>` behaves the same, and a
930
+ bar press there never moves the caret.
1242
931
 
1243
- `overflow: 'scroll'` folds three CSS idioms into the resolved style, so a
1244
- scroll container does not have to restate them:
932
+ ### The layout defaults it brings
1245
933
 
1246
934
  | default | when |
1247
935
  | ------------------------------ | ---------------------------------------- |
1248
936
  | `flexBasis: 0` | `flexGrow > 0` and no `width` / `height` |
1249
937
  | `minWidth: 0` / `minHeight: 0` | always |
1250
938
 
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.
939
+ `min-*: 0` lets a viewport be smaller than its content; the zero basis is
940
+ half of `flex: 1`. Without them a header/scroll/footer window grows past its
941
+ bounds and pushes the footer out of view. An explicit value always wins.
1257
942
 
1258
943
  ### Props
1259
944
 
@@ -1264,102 +949,72 @@ view. Set either yourself to opt out; an explicit value always wins.
1264
949
  | `scrollbar={false}` | hide the drawn scrollbars |
1265
950
  | `scrollbarColor` | thumb color |
1266
951
 
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
952
+ **Ref**: the node, plus `scrollTo` / `scrollBy` / `scrollIntoView(node)` /
953
+ `canScroll(dx, dy)` and `scrollX` / `scrollY` / `contentWidth` /
954
+ `contentHeight`. In TypeScript, `useRef<ScrollableNode>(null)` types them;
955
+ `DrawnNode` still works for a box you do not scroll.
956
+
957
+ `scrollTo(y)` scrolls vertically; `scrollTo({x, y})` either axis, leaving an
958
+ omitted one; `scrollBy` likewise. `canScroll(dx, dy)` says whether there is
959
+ room on the axis a delta names — what the wheel asks each node on its way
960
+ out, and what your own element implements
1279
961
  ([extending.md](extending.md#scrolling-content-you-painted)).
1280
962
 
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.
963
+ Horizontal content comes from children that will not shrink (a row of
964
+ fixed-width cells). Its extent is measured **through the subtree**, like
965
+ `scrollWidth`: a row stretched to the viewport whose cells overflow it
966
+ still scrolls; a node clipping its own children ends the measurement. A
967
+ horizontal wheel axis scrolls sideways, as does **Shift + vertical
968
+ wheel**. With both bars, each stops short of the other's corner. Distances
969
+ are the device's: with XI2 a touchpad scrolls by the fraction of a notch it
970
+ measured. A wheel notch and an arrow key are 48 pixels
971
+ ([events.md](events.md#wheel)).
972
+
973
+ `onViewport` fires from **layout**, so it arrives before anyone scrolls —
974
+ what a virtualized list (`Table`) needs to decide how many rows to build,
975
+ since an effect cannot read the size off the ref yet. `onLayout`
976
+ ([Interaction props](#interaction-props)) is the same signal on any element,
977
+ without the content size.
978
+
979
+ `scrollIntoView(node)` scrolls the minimum on both axes to show a
980
+ descendant; one larger than the viewport gets CSSOM's `block: nearest`
981
+ (left alone if it covers the viewport, else brought in by the nearer edge).
982
+ It is safe right after the node mounts — it resolves on the next layout
983
+ pass — and a node already in view leaves the pane alone.
984
+
985
+ `scrollTo` answers inside the call from the last pass's extent: the offset
986
+ moves and `onScroll` fires before it returns. If a pass is owed, whatever
987
+ that extent could not satisfy is re-applied against the new content before
988
+ the frame paints, so restoring a position from a mount-time
989
+ `useLayoutEffect` shows it in the first frame, and `scrollTo(Infinity)` from
990
+ a layout effect in the commit that added rows shows the end in the first
991
+ frame that has them. `Infinity` means the end wherever the pass finds it
992
+ (as the End key does) — prefer it to `contentHeight`, the last pass's
993
+ measurement, which falls short when a commit adds more than a viewport. A
994
+ `scrollBy` before the pass continues from where the pass puts the pane.
995
+ Until then `scrollX`/`scrollY` read the call's answer; `onScroll` reports
996
+ the pass's afterwards.
997
+
998
+ `onScroll` fires for **every** offset change. Wheel, keys, bars and calls on
999
+ a laid-out pane report inside the call. Layout's own moves — a held
1000
+ `scrollTo` landing, `scrollIntoView` resolving (focus on a row below the
1001
+ fold), the clamp when content shrinks or the viewport grows — report after
1002
+ the frame that shows them, only if the offset moved. The payload is read at
1003
+ delivery, so a handler mirroring the offset into state ends where the pane
1004
+ did.
1348
1005
 
1349
1006
  ### On a `<window>`
1350
1007
 
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`.
1008
+ `<window style={{ overflow: 'scroll' }}>` scrolls the window's own content
1009
+ with the same wheel, keys and bars; it stays a `frame` to a screen reader.
1010
+ Its ref is still ntk's `Window`, so reach the scroll API through a ref on a
1011
+ box inside it, or the node in `onViewport`.
1356
1012
 
1357
1013
  ## `<text>`
1358
1014
 
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.
1015
+ Shaped, wrapped text (bidi, ligatures, kerning, font fallback). Strings and
1016
+ numbers are legal only inside `<text>`; nested `<text>`s are **style spans**
1017
+ of one paragraph, wrapped as one.
1363
1018
 
1364
1019
  | prop | |
1365
1020
  | --------------------------------------------------- | ------------------------------------------------------ |
@@ -1376,72 +1031,51 @@ run list so wrapping spans the whole content.
1376
1031
  | `textOverflow` | `clip` (default) or `ellipsis`, for what was cut |
1377
1032
 
1378
1033
  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`
1034
+ `letterSpacing` and the two feature properties **inherit**, into spans and
1035
+ across `<box>`es alike
1036
+ ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)); the
1037
+ rest belong to the `<text>` that owns the box.
1038
+
1039
+ `{ textWrap: 'nowrap', textOverflow: 'ellipsis' }` is one line ending in
1040
+ `…`; an ellipsis with no `maxLines` means one line. Only the pixels are cut
1041
+ — the accessible name, caret indices and `textContent()` keep the whole
1042
+ string
1043
+ ([styling.md](styling.md#keeping-text-on-one-line-and-saying-when-it-did-not-fit)).
1044
+ A font file of the app's own opens with `loadFont`/`useFont`, which return
1045
+ the family name for `fontFamily`
1396
1046
  ([styling.md](styling.md#a-font-file-of-your-own)).
1397
1047
 
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).
1048
+ `start`/`end` follow the box's **direction**, not the string's first strong
1049
+ character: the paragraph is laid out at that base level, so `"(12) files"`
1050
+ in an RTL panel punctuates as RTL, and text with no strong characters still
1051
+ lands on the right side
1052
+ ([styling.md](styling.md#direction-and-the-logical-edges)).
1404
1053
 
1405
1054
  ### Variable fonts
1406
1055
 
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:
1056
+ `fontWeight` drives a variable font's `wght` axis, so one file serves every
1057
+ weight, those between named instances included; `fontVariationSettings`
1058
+ sets other axes by tag:
1413
1059
 
1414
1060
  ```jsx
1415
1061
  <text style={{ fontFamily: 'Inter', fontWeight: 460 }}>
1416
1062
  Serious by default.
1417
1063
  </text>
1418
- ```
1419
-
1420
- `fontVariationSettings` is for the other axes, by OpenType tag:
1421
-
1422
- ```jsx
1423
1064
  <text style={{ fontVariationSettings: { wdth: 87.5, slnt: -8 } }}>
1424
1065
  condensed
1425
1066
  </text>
1426
1067
  ```
1427
1068
 
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`.
1069
+ It inherits, and spans may override it. Unknown axes are ignored and values
1070
+ clamped, so it is safe on any face; it compares **by value**, so a literal
1071
+ is fine. Faces are instantiated on demand but never quantized: a slider
1072
+ bound to an axis with `step={1}` asks for hundreds of faces as it drags —
1073
+ step the control. Use the `.ttf`; a variable `.woff2` cannot be
1074
+ instantiated (ntk's `docs/fonts.md`).
1441
1075
 
1442
1076
  ### `textRendering`
1443
1077
 
1444
- _ntk ≥ 7.2.0._ CSS's property, picking which glyph path draws the text.
1078
+ CSS's property: which glyph path draws the text.
1445
1079
 
1446
1080
  | value | |
1447
1081
  | -------------------- | --------------------------------------------------------- |
@@ -1456,37 +1090,19 @@ _ntk ≥ 7.2.0._ CSS's property, picking which glyph path draws the text.
1456
1090
  </text>
1457
1091
  ```
1458
1092
 
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.
1093
+ The cached path puts each glyph on a whole pixel, so animating `wght` makes
1094
+ single glyphs jump while neighbours stand still; `geometricPrecision`
1095
+ places them where shaping asked, and the line slides. It inherits (a span
1096
+ may override it) and **never reflows** — only draw-time rounding differs —
1097
+ so it is safe to flip on a state change. It rasterizes outlines on every
1098
+ draw and caches nothing: right for animated display text, wrong for a
1099
+ static paragraph, hence `auto`.
1480
1100
 
1481
1101
  ### Letter spacing and figures
1482
1102
 
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:
1103
+ `fontVariantNumeric: 'tabular-nums'` gives every digit one width, so a
1104
+ changing number (a readout beside a slider) stops shuffling its row; most
1105
+ UI faces (SF Pro, Inter) default to proportional figures.
1490
1106
 
1491
1107
  ```jsx
1492
1108
  <text style={{ fontSize: 30, fontVariantNumeric: 'tabular-nums' }}>
@@ -1494,26 +1110,24 @@ value. Tabular figures hold it still:
1494
1110
  </text>
1495
1111
  ```
1496
1112
 
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.
1113
+ Keywords are CSS's: `lining-nums`/`oldstyle-nums`,
1114
+ `proportional-nums`/`tabular-nums`, `diagonal-fractions`/`stacked-fractions`,
1115
+ `ordinal`, `slashed-zero` — several at once, one per pair
1116
+ (`'tabular-nums slashed-zero'`); `'normal'` restores the face's own.
1501
1117
 
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.
1118
+ `fontFeatureSettings` takes any OpenType feature by tag: an array to turn
1119
+ on, or an object of tag to on, off or alternate index. It wins where both
1120
+ name a feature; missing features are ignored; it compares by value.
1505
1121
 
1506
1122
  ```jsx
1507
1123
  <text style={{ fontFeatureSettings: ['tnum', 'zero'] }}>0.10</text>
1508
1124
  <text style={{ fontFeatureSettings: { ss01: true, salt: 2 } }}>Rag</text>
1509
1125
  ```
1510
1126
 
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.
1127
+ `letterSpacing` is CSS's, in logical pixels, after every character (the
1128
+ last on a line too); negative tightens. Spaced text drops the optional
1129
+ ligatures (`liga`, `clig`, `dlig`, `hlig`) — name one in
1130
+ `fontFeatureSettings` to keep it.
1517
1131
 
1518
1132
  ```jsx
1519
1133
  <text style={{ fontSize: 11, fontWeight: 600, letterSpacing: 1.26 }}>
@@ -1521,37 +1135,24 @@ every character, the last on a line included, and negative to tighten.
1521
1135
  </text>
1522
1136
  ```
1523
1137
 
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.
1138
+ All three inherit, the two feature properties independently as in CSS (a
1139
+ `fontVariantNumeric` below replaces the one above, leaving an inherited
1140
+ `fontFeatureSettings` alone). All three re-measure on change, and none may
1141
+ go in a state block.
1532
1142
 
1533
1143
  ### Tiny text
1534
1144
 
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`.
1145
+ Text under **6 logical pixels** is painted as one strip of its ink per line
1146
+ instead of glyphs — in a minimap, a zoomed-out graph or a grid of thousands
1147
+ of cells, it is an illegible smudge anyway, and the strip costs one fill.
1148
+ Layout is unchanged, and the size is logical, so it holds on a 2x panel.
1149
+ `createRoot({ textStripBelow })` moves the threshold (`0` keeps glyphs at
1150
+ every size); `REACT_X11_TEXT_STRIP_BELOW` sets it without `createRoot`.
1548
1151
 
1549
1152
  ## Selecting text
1550
1153
 
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.
1154
+ Labels are not selectable, as `Gtk.Label` is not: most desktop text is
1155
+ chrome, and a stray drag lighting up a caption is noise. An element opts in:
1555
1156
 
1556
1157
  ```jsx
1557
1158
  <box selectable style={{ padding: 12 }}>
@@ -1560,43 +1161,34 @@ selectable when an element says so.
1560
1161
  </box>
1561
1162
  ```
1562
1163
 
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
1164
+ It becomes a **surface**: a drag selects across all text under it, a double
1165
+ click takes a word, a triple click a block, Ctrl+A the surface, Ctrl+C
1166
+ copies. Releasing the button hands the text to **PRIMARY**, so a middle
1167
+ click in a terminal pastes it ([clipboard.md](clipboard.md)); macOS has no
1168
+ PRIMARY, so only a copy reaches the pasteboard
1570
1169
  ([clipboard.md](clipboard.md#primary-is-x11-only)).
1571
1170
 
1572
1171
  ### What is in a surface
1573
1172
 
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:
1173
+ Everything under it that answers for its own text, in document order:
1174
+ `<text>`, and any element implementing the accessors in
1175
+ [extending.md](extending.md#answering-for-your-own-text) — a terminal, log
1176
+ view or code editor from outside this package joins the same drag, with no
1177
+ registration. **Not** in it:
1581
1178
 
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.
1179
+ - anything under `selectable={false}` (CSS's `user-select: none`) — a
1180
+ list's bullets, a table's chrome, a caption inside a document;
1181
+ - `<textinput>`, `<textarea>` and any element declaring `hasOwnSelection` —
1182
+ they keep their own selection, and a press in one is theirs;
1183
+ - `<svg>`, and registered elements that draw text without exposing
1184
+ character geometry.
1589
1185
 
1590
1186
  ### What a copy assembles
1591
1187
 
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.
1188
+ Separators come from **layout**, not markup: text sharing a band of pixels
1189
+ is joined with a **tab**, text starting below with a **newline** — cells and
1190
+ rows for a table, a paragraph per line for prose. An item excluded with
1191
+ `selectable={false}` is absent from the copy as from the highlight.
1600
1192
 
1601
1193
  ```jsx
1602
1194
  // copies as "name\tsize\nnotes\t4 kB"
@@ -1612,50 +1204,37 @@ paragraph per line.
1612
1204
  </box>
1613
1205
  ```
1614
1206
 
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
1207
  ### The props
1619
1208
 
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 |
1209
+ | prop | |
1210
+ | ----------------------- | ----------------------------------------------------------------------------------- |
1211
+ | `selectable` | `true` starts a surface; `false` opts a subtree out of the one above it |
1212
+ | `selectionColor` | highlight behind selected text (default: theme's `selection`, a tint of its accent) |
1213
+ | `onSelectionChange(ev)` | `ev.text` is what a copy would take; `ev.isCollapsed` whether it is empty |
1625
1214
 
1626
- And on the surface's node, through a ref: `selectAll()`, `clearSelection()`,
1215
+ On the surface's node, through a ref: `selectAll()`, `clearSelection()`,
1627
1216
  `selectedText()`, `setSelection(anchor, focus)` — each end
1628
- `{ node, index }`, indices in code points — and `textSelection`, a snapshot of
1629
- `{ isCollapsed, text, ranges }`.
1217
+ `{ node, index }`, indices in code points — and `textSelection`, a snapshot
1218
+ of `{ isCollapsed, text, ranges }`.
1630
1219
 
1631
1220
  ### One selection, and where the keys go
1632
1221
 
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.
1222
+ **One selection is visible per application**: selecting in a document
1223
+ collapses a field's highlight and vice versa — which keeps PRIMARY honest.
1648
1224
 
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)).
1225
+ A surface is a **focus target** (Ctrl+A and Ctrl+C must arrive somewhere),
1226
+ so it is in the Tab cycle; `tabIndex={-1}` removes it but keeps it
1227
+ press-focusable — right for a document in a larger UI, wrong for a reader
1228
+ whose window is the document. The pointer is an I-beam over it unless
1229
+ `cursor` says otherwise. The selection reaches assistive technology through
1230
+ `<text>`'s AT-SPI text interface
1231
+ ([accessibility.md](accessibility.md#text-controls)).
1652
1232
 
1653
1233
  ### The geometry underneath
1654
1234
 
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:
1235
+ Four accessors on every drawn node build the selection, and are public — a
1236
+ caret rect is what an autocomplete popup anchors to
1237
+ (`<popup anchor={{ to, at }}>`), a hit test turns a click into an offset:
1659
1238
 
1660
1239
  ```js
1661
1240
  const node = paragraphRef.current;
@@ -1665,14 +1244,11 @@ node.textCaretRect(14); // { x, y, width: 0, height }
1665
1244
  node.textRangeRects(4, 14); // the bands a highlight over [4, 14) fills
1666
1245
  ```
1667
1246
 
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.
1247
+ Indices are **code points**, not UTF-16 units. Rects are in the owning
1248
+ window's device pixels, like `abs` and `contentBox()`; mouse `x`/`y` are
1249
+ logical ([scale.md](scale.md)). `textRangeRects` returns a band per line
1250
+ _and_ per direction run, since a range crossing from Latin into Arabic
1251
+ covers separate stretches of pixels.
1676
1252
 
1677
1253
  ---
1678
1254
 
@@ -1680,36 +1256,30 @@ nobody selected.
1680
1256
 
1681
1257
  ![textinput](img/textinput.png)
1682
1258
 
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).
1259
+ Single-line editable text; caret and selection positions are exact across
1260
+ kerning, shaping and trailing whitespace.
1261
+
1262
+ | prop | |
1263
+ | --------------------------------- | ---------------------------------------------------------------------------------------------- |
1264
+ | `value` + `onChange(ev)` | controlled mode (display follows the prop) |
1265
+ | `defaultValue` | uncontrolled mode |
1266
+ | `onSubmit(ev)` | Enter/Shift+Enter; with Ctrl, Alt or Super, a shortcut's ([events.md](events.md#accelerators)) |
1267
+ | `name` | field name, echoed on the event |
1268
+ | `placeholder`, `placeholderColor` | shown when empty |
1269
+ | `maxLength` | code-point limit |
1270
+ | `selectionColor`, `caretColor` | selection/caret paint |
1271
+ | `inputMode` | IME hint: `numeric`, `decimal`, `tel`, `email`, `url` ([events.md](events.md#input-methods)) |
1272
+ | text style props | as `<text>` |
1273
+
1274
+ The field follows its box's **direction** as `<text>` does: value,
1275
+ placeholder and caret start at the base direction's start edge (the right
1276
+ under `direction: 'rtl'`), the value is shaped at that base level, and
1277
+ clicks, caret, selection and horizontal scroll mirror together
1278
+ ([styling.md](styling.md#inside-an-editable-field)).
1708
1279
 
1709
1280
  ### The change event
1710
1281
 
1711
- `onChange` and `onSubmit` get a synthetic event, the same shape every other
1712
- handler in the library gets, rather than a bare string:
1282
+ `onChange` and `onSubmit` get a synthetic event, not a bare string:
1713
1283
 
1714
1284
  ```jsx
1715
1285
  <textinput
@@ -1728,139 +1298,99 @@ handler in the library gets, rather than a bare string:
1728
1298
  | `ev.nativeEvent` | the X key event, or **null** — see below |
1729
1299
  | `preventDefault`, `stopPropagation` | present for uniformity; nothing reads them |
1730
1300
 
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).
1301
+ - **`ev.target.value` is the new value even when controlled**, before the
1302
+ parent re-renders — as in the DOM, which is why react-hook-form and formik
1303
+ work ([ecosystem/forms.md](ecosystem/forms.md)). `ev.target` is the live
1304
+ node, so read later it shows the current value; `ev.value` is a snapshot.
1305
+ - **`ev.nativeEvent` can be null** — a paste resolving, an undo or a
1306
+ pushed-back value has no X event. Guard it (downshift reads
1307
+ `nativeEvent.preventDownshiftDefault` unguarded).
1308
+
1309
+ `node.value = ''` sets the text without firing `onChange`, as on a DOM
1310
+ input — how react-hook-form's `register()` resets a field.
1311
+
1312
+ Keys: click/drag selection, double-click word, triple-click all, **dead
1313
+ keys and Compose** ([events.md](events.md#composition)), Backspace/Delete,
1314
+ arrows (+Shift extends), Ctrl+arrows and Ctrl+Backspace/Delete by word,
1315
+ Home/End, Ctrl+A, Ctrl+C/X/V on CLIPBOARD, middle-click paste from PRIMARY,
1316
+ selections (select-all included) owning PRIMARY
1317
+ ([X11 only](clipboard.md#primary-is-x11-only)),
1318
+ **Ctrl+Z / Ctrl+Shift+Z** (and Ctrl+Y) to undo and redo, and a
1319
+ **right-click menu**. On Cocoa the keys are the Mac's: ⌘A, ⌘C/X/V,
1320
+ ⌘Z/⇧⌘Z; ⌥ moves and deletes by word; ⌘←/→ and ⌘↑/↓ go to the ends of the
1321
+ line and the value; ⌘⌫ deletes to line start; ⌃A, ⌃E, ⌃K, ⌃Y and AppKit's
1322
+ other Emacs keys work while other ⌃ chords type nothing; ⌥ types what the
1323
+ layout puts on a key (`ß` for ⌥S) ([events.md](events.md#primary-modifier)).
1324
+ Focusable by default; shows the text cursor. `ev.preventDefault()` in
1325
+ `onKeyDown`/`onMouseDown` suppresses the built-in editing. Copying and
1326
+ pasting from elsewhere: [clipboard.md](clipboard.md).
1768
1327
 
1769
1328
  ### The right-click menu
1770
1329
 
1771
1330
  ![the built-in edit menu](img/textinput-menu.png)
1772
1331
 
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).
1332
+ Undo / Redo / Cut / Copy / Paste / Select All, with no wiring — each row
1333
+ live only when it would act, each running its shortcut's code.
1334
+ Right-clicking **inside** a selection keeps it; outside, the caret moves
1335
+ there first. Arrows walk enabled rows, Enter chooses, Escape or a press
1336
+ outside closes; the selection stays highlighted meanwhile.
1337
+
1338
+ **Paste** is greyed when nothing owns CLIPBOARD — from a session's second
1339
+ menu on, since the field starts watching when its menu first opens; without
1340
+ XFixes it stays enabled. On macOS it tracks the pasteboard's change count, a
1341
+ quarter second behind another app's copy
1342
+ ([clipboard.md](clipboard.md#knowing-when-it-changes)).
1343
+
1344
+ `contextMenu={false}` plus a `ContextMenu` of your own replaces it
1345
+ (`onContextMenu` still fires); `ev.preventDefault()` in `onContextMenu`
1346
+ suppresses it once. `openEditMenu(node, at, actions)` opens the same menu
1347
+ for anything else that edits or selects text
1348
+ ([extending.md](extending.md#the-standard-edit-menu)).
1802
1349
 
1803
1350
  ### `sensitive`
1804
1351
 
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)).
1352
+ `<textinput sensitive>`'s text **never reaches a selection**: Ctrl+C and the
1353
+ copy half of Ctrl+X do nothing, the menu has no Cut or Copy (absent, not
1354
+ greyed — greyed over a password looks like a bug), and selecting does not
1355
+ take PRIMARY. The caret, selection, arrows, undo and pasting _in_ are
1356
+ untouched. The clipboard, unlike the screen, is readable by every client on
1357
+ the display, and clipboard managers record it. `PasswordInput` sets it while
1358
+ the secret is revealed ([components.md](components.md#passwordinput)). On
1359
+ Wayland the field is also declared `sensitive_data` to the input method,
1360
+ with no surrounding text offered ([events.md](events.md#input-methods)).
1822
1361
 
1823
1362
  **Ref**: the node, plus `value`, `undo()` / `redo()` and `canUndo` /
1824
1363
  `canRedo` — enough for a toolbar button beside the field.
1825
1364
 
1826
1365
  ### Undo
1827
1366
 
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.
1367
+ Consecutive typing is one undo step, so Ctrl+Z takes back a word, not a
1368
+ keystroke; runs of Backspace coalesce too. A run ends at whitespace, at
1369
+ anything moving the caret (arrows, Home/End, a click, focus leaving), and
1370
+ around edits that are their own step: a paste, a cut, a replaced selection,
1371
+ Ctrl+Backspace (⌥⌫, ⌘⌫ and ⌃K on a Mac), a `<textarea>` newline. Undo
1372
+ restores the selection and puts the caret where the undone edit started. A
1373
+ composed character is one entry: Ctrl+Z after `dead_acute` `e` steps over
1374
+ `é`.
1375
+
1376
+ The stack holds up to 200 snapshots. A **controlled** `value` reports the
1377
+ restored text through `onChange` and waits for the prop, as typing does, so
1378
+ a parent filtering values has the same say over an undo. A value changed
1379
+ from outside (a form reset) is its own entry.
1847
1380
 
1848
1381
  ## `<textarea>`
1849
1382
 
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.
1383
+ Multi-line editable text on `<textinput>`'s editing core. It word-wraps at
1384
+ the content width; Enter and Shift+Enter insert a newline; Ctrl+Enter (⌘↩
1385
+ on Cocoa) fires `onSubmit`, and with no `onSubmit` is left to a shortcut, as
1386
+ Alt+Enter and Super+Enter are except on Cocoa
1387
+ ([events.md](events.md#accelerators)). Up/Down move between visual lines
1388
+ keeping a goal column, Home/End go to the ends of the visual line, selection
1389
+ spans lines, and the view scrolls to follow the caret (and with the wheel).
1390
+ On Cocoa ⌘←/→ are Home/End, ⌘↑/↓ go to the ends of the value and ⌥↑/↓ to
1391
+ the ends of a paragraph ([events.md](events.md#primary-modifier)). A field
1392
+ whose text fits passes the wheel out to the pane or window behind it, as a
1393
+ `<box overflow="scroll">` does.
1864
1394
 
1865
1395
  | prop | |
1866
1396
  | --------------- | -------------------------------------------------------------- |
@@ -1877,26 +1407,16 @@ window behind it rather than swallowing it.
1877
1407
  | `cacheKey` | the source's identity, when `src` is re-derived per render |
1878
1408
  | `alt` | the accessible name — what a screen reader says ([accessibility.md](accessibility.md)) |
1879
1409
 
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.
1410
+ `src`, `picture` and `drawable` are mutually exclusive; passing two throws.
1411
+ Size by `style`: with one dimension set the other follows the aspect ratio;
1412
+ with neither, the natural size (a server-side source's descriptor size),
1413
+ shrunk to the width on offer. `objectFit` places the picture, centred:
1414
+ `'fill'` (default) stretches, `'contain'` fits inside, `'cover'` fills and
1415
+ crops, `'none'` keeps its size, `'scale-down'` is the smaller of `'none'`
1416
+ and `'contain'`.
1888
1417
 
1889
1418
  ```jsx
1890
1419
  <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
1420
  <image src={photo} style={{ width: 96, height: 96, objectFit: 'cover' }} />
1901
1421
  ```
1902
1422
 
@@ -1909,24 +1429,20 @@ the smaller of `'none'` and `'contain'`, all centred.
1909
1429
  <image src={ntkImage} /> // an ntk Image (or Surface)
1910
1430
  ```
1911
1431
 
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.
1432
+ A path (or `new URL('./logo.png', import.meta.url)`) decodes asynchronously:
1433
+ the element is 0×0 until it lands, then reflows — style a `width` and
1434
+ `height` to avoid the jump. Raw RGBA and an ntk `Image` are synchronous;
1435
+ encoded bytes are synchronous where the decoder is (PNG and JPEG under Node)
1436
+ and land a moment later otherwise (any format under Bun, WebP elsewhere).
1920
1437
  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
1438
+ RGBA, as `getImageData` returns. Sources are treated as immutable: pass a
1439
+ new object when the pixels change — encoded bytes too, which under Bun are
1924
1440
  read on another thread after the commit.
1925
1441
 
1926
1442
  #### Formats, and what decodes them
1927
1443
 
1928
- The format comes from the bytes, never from a file name. Which decoder
1929
- reads them depends on the runtime:
1444
+ The format comes from the bytes, never a file name; the decoder from the
1445
+ runtime:
1930
1446
 
1931
1447
  | format | under Bun | under Node |
1932
1448
  | ---------------- | -------------------------------------------------- | ------------------------------------------ |
@@ -1935,14 +1451,10 @@ reads them depends on the runtime:
1935
1451
  | GIF, BMP | `Bun.Image`, off the JavaScript thread | not decoded — the error says to convert |
1936
1452
  | TIFF, HEIC, AVIF | `Bun.Image` through ImageIO (macOS), WIC (Windows) | not decoded; nor under Bun on Linux |
1937
1453
 
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:
1454
+ Under Bun every format decodes on a worker thread (a 1080p JPEG costs the
1455
+ JavaScript thread ~110ms in jpeg-js), so bytes land a frame or so late, like
1456
+ a file. Where that matters — a test measuring the first frame — decode
1457
+ yourself; the result is used as-is everywhere:
1946
1458
 
1947
1459
  ```jsx
1948
1460
  import { decodeImage } from 'ntk/image'; // PNG and JPEG, synchronous
@@ -1950,45 +1462,26 @@ import { decodeImage } from 'ntk/image'; // PNG and JPEG, synchronous
1950
1462
  const logo = <image src={decodeImage(bytes)} />;
1951
1463
  ```
1952
1464
 
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.
1465
+ `decodeImageBytes` from `react-x11/ntk` is the same ladder for an element
1466
+ that draws its own images (a document's `<img>`, a CSS background): an
1467
+ `Image`, or a promise of one where decoding is async; it throws or rejects,
1468
+ naming what to convert to, where `<image>` would show nothing.
1469
+
1470
+ Between the runtimes:
1471
+
1472
+ - **Animations show their first frame** — animated WebP anywhere, GIF under
1473
+ Bun (Bun 1.4's libwebp cannot decode animated WebP; those go to the
1474
+ JavaScript decoder).
1475
+ - **JPEG EXIF orientation is applied under Bun, not Node**: rotate portrait
1476
+ phone photos first, or use Bun.
1477
+ - **No decoder, no picture**: GIF under Node or HEIC under Bun on Linux
1478
+ logs what to convert to and shows nothing, like a missing file.
1479
+
1480
+ Pixels are uploaded once and composited every repaint. An ntk `Image` is
1481
+ used as-is and never destroyed by the element, so one `Image` in many
1482
+ `<image>`s is one decode and one upload. On Cocoa the element frees its
1483
+ upload of an `Image` it decoded; the upload of yours is freed when your
1484
+ `Image` is garbage-collected (ntk's `destroy()` frees only X copies).
1992
1485
 
1993
1486
  ### `src={{ symbol }}` — the platform's own icons
1994
1487
 
@@ -1999,22 +1492,19 @@ only its X copies.
1999
1492
  </box>
2000
1493
  ```
2001
1494
 
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:
1495
+ A name instead of pixels: the platform's icon in the **text colour** (what
1496
+ `currentColor` is to `<svg>`). The names are those `useTray`'s `icon` and a
1497
+ menu item's `iconName` take:
2006
1498
 
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` |
1499
+ | backend | names |
1500
+ | ------------- | ---------------------------------------------------------------------------------------------------- |
1501
+ | macOS | SF Symbols, like `speaker.wave.3.fill`, through `@windowkit/appkit` |
1502
+ | X11 / Wayland | freedesktop icon theme (the settings daemon's, else Adwaita, then hicolor), like `audio-volume-high` |
2011
1503
 
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:
1504
+ **Sized like the surrounding text**: an SF Symbol takes the inherited
1505
+ `fontSize` and `fontWeight` as point size and weight; an icon theme is asked
1506
+ for the matching size (16 beside 14px text), square. A styled
1507
+ `width`/`height` fits it into that box, centred. Source keys:
2018
1508
 
2019
1509
  | key | |
2020
1510
  | --------------- | ------------------------------------------------------------------------------ |
@@ -2023,74 +1513,51 @@ fitted into that box, centred. The source can say otherwise:
2023
1513
  | `scale` | `'small'`, `'medium'` (default) or `'large'`, beside the text. SF Symbols only |
2024
1514
  | `variableValue` | 0 to 1, how much of a variable symbol shows. SF Symbols, macOS 13 and later |
2025
1515
 
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.
1516
+ On Linux a `-symbolic` icon is preferred and takes the text colour; a
1517
+ full-colour one shows as it is. Lookup follows the Icon Theme
1518
+ Specification, read at the first symbol. **An unknown name takes no room
1519
+ and draws nothing** (one dev warning). The two icon sets do not share
1520
+ names, so pick by platform.
2036
1521
 
2037
1522
  ### `cacheKey` — when the buffer is new but the picture is not
2038
1523
 
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.
1524
+ A component re-deriving its bytes per render (decoding a stream, slicing a
1525
+ capture) passes a new `src` each time — a fresh decode and upload. A
1526
+ `cacheKey`, the `<canvas cacheKey>` contract for sources, vouches that the
1527
+ buffer is the same picture: nothing is redone, and `<image>`s sharing a key
1528
+ share one decoded copy, freed with the last.
2046
1529
 
2047
1530
  ```jsx
2048
1531
  <image src={frame.rgba} cacheKey={`frame:${frame.serial}`} />
2049
1532
  ```
2050
1533
 
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).
1534
+ The key must name the content, or stale pixels show (development warns
1535
+ when two sizes collide under one key). Ignored for an ntk `Image`; refused
1536
+ with `picture`/`drawable`.
2056
1537
 
2057
1538
  ### `picture` / `drawable` — pixels the server already has
2058
1539
 
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.
1540
+ Content already on the X server — an offscreen pixmap, `NameWindowPixmap`
1541
+ from Composite, a cached tile — composites straight from that resource,
1542
+ with no readback, upload or round trip:
2064
1543
 
2065
1544
  ```jsx
2066
1545
  <image picture={{ id: pic, width: 64, height: 64 }} />
2067
1546
  <image drawable={{ id: pixmap, width: 128, height: 96, depth: 32 }} />
2068
1547
  ```
2069
1548
 
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`.
1549
+ You state the size (asking would be a round trip); descriptors compare by
1550
+ value. Scaling is server-side and bilinear; drawing a `picture` scaled sets
1551
+ its transform and filter for the composite and resets them to
1552
+ identity/nearest after, so a picture with a transform of its own should be
1553
+ drawn 1:1. `drawable` wraps the pixmap or window in a Picture the element
1554
+ owns (freed on unmount; the drawable stays yours); `depth` is 24 (default,
1555
+ the screen's and a Composite window pixmap's), 32 for argb, or 8, which
1556
+ composites as ink through its alpha (a mask preview); a `picture`'s format
1557
+ was fixed when it was created. Both accept richer objects with those
1558
+ fields — an ntk `Pixmap` goes straight in as `drawable`. Both are
1559
+ **X11-only**: on Cocoa they draw nothing (one dev warning); pass pixels to
1560
+ `src`.
2094
1561
 
2095
1562
  ## `<video>` {#video}
2096
1563
 
@@ -2101,32 +1568,29 @@ useEffect(() => startDecoder(file, frames), [frames, file]);
2101
1568
  <video frames={frames} style={{ width: '100%', backgroundColor: '#000' }} />;
2102
1569
  ```
2103
1570
 
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.
1571
+ | prop | |
1572
+ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
1573
+ | `frames` | a `VideoFrames` sink you push decoded frames into — every backend |
1574
+ | `src` | a path or URL the platform plays, where `useSupports('mediaPlayback')` (below) |
1575
+ | `poster` | an `<image src>` value, shown while there is no frame |
1576
+ | `autoPlay`, `loop`, `muted`, `volume`, `playbackRate` | HTML's names and defaults — nothing plays until asked; for `src` |
1577
+ | `paused` | controlled when present; for `src` |
1578
+ | `onLoadedMetadata` | `{ width, height, duration }` once the size is known (a sink: on mount, `duration: Infinity`) |
1579
+ | `onPlay`, `onPause`, `onEnded`, `onTimeUpdate` | for `src` |
1580
+ | `onError` | the source failed, or `src` has no player; unhandled, one console warning |
1581
+
1582
+ `frames` and `src` are exclusive; passing both throws. It is a drawn node like
1583
+ `<image>`, so a video in a form sits under its popups and inside its scroll
1584
+ panes, which a child window or `<glarea>` cannot. **Size** is HTML's: the
1585
+ stream's size in logical pixels (1280x720 is 1280 wide at any scale), else
1586
+ the poster's, else 300x150; one styled axis derives the other from the
1587
+ ratio. `objectFit` defaults to `'contain'`; `backgroundColor` fills the
1588
+ bars.
2125
1589
 
2126
1590
  ### `VideoFrames`, the sink
2127
1591
 
2128
- react-x11 decodes nothing; where the bytes come from is the application's
2129
- business, and every source ends in `push`:
1592
+ react-x11 decodes nothing; `useVideoFrames` / `createVideoFrames` make a
1593
+ sink, and every source ends in `push`:
2130
1594
 
2131
1595
  ```js
2132
1596
  import { spawn } from 'node:child_process';
@@ -2158,61 +1622,60 @@ function startDecoder(file, frames) {
2158
1622
  }
2159
1623
  ```
2160
1624
 
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.
1625
+ - **Formats**: `'NV12'` (Y, then Cb/Cr pairs — hardware decoders),
1626
+ `'I420'` (Y, Cb, Cr — `yuv420p`, most software decoders) and `'BGRA'`
1627
+ (fourth byte ignored: frames are opaque). `colorSpace` (`'bt709'`
1628
+ default, `'bt601'`, `'bt2020'`) and `range` (`'video'` default, `'full'`)
1629
+ say what YCbCr numbers mean.
1630
+ - **`preferredFormats`** is the display's order, cheapest first:
1631
+ `NV12, I420, BGRA` where something native takes YCbCr as it is — a layer
1632
+ on macOS, a shader on Wayland, an Xv port on X11 — and `BGRA` where the
1633
+ frame is converted in JavaScript. On X11 it is `BGRA`, not kept, for the
1634
+ few round trips until the port answers after the first sink is made.
1635
+ Every format works everywhere.
1636
+ - **The newest frame is the frame**: a push replaces the last, shown or not;
1637
+ nothing queues, and the decoder's clock is the pacing. A push hands its
1638
+ planes over until the next push — they are read then, not copied at the
1639
+ call — so a decoder reusing one buffer pushes it again after writing.
2175
1640
  - **`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.
1641
+ Buffer back to back, with per-plane row bytes when padded; a plane too
1642
+ small is a `TypeError` naming it and the bytes needed. `close()` drops the
1643
+ frame and ignores later pushes, so a decoder racing an unmount loses
1644
+ quietly.
1645
+ - **Nothing to free**: a sink owns nothing native.
2182
1646
 
2183
1647
  ### Lifted, drawn, and what each costs
2184
1648
 
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.
1649
+ On **macOS** a video goes on its own layer above the window's bitmap when
1650
+ nothing is painted over it, it is not fading and no rounded clipping
1651
+ ancestor holds it ([macos.md](macos.md)); then a `push` costs well under a
1652
+ millisecond and **no window frame**. Otherwise — a tooltip over it, a
1653
+ dialog's dim, a rounded card — it is **drawn**: a push claims the picture,
1654
+ and the next frame composites it in paint order, in the same colours.
1655
+
1656
+ On **X11, Wayland and Windows** it is always drawn, and what differs is how
1657
+ the frame reaches the element's surface:
1658
+
1659
+ - **Wayland**: planes as textures, the matrix in a shader — about 0.2ms of
1660
+ CPU for a 1080p NV12 frame, against 7.6ms converted in JavaScript.
1661
+ - **X11 with an Xv image adaptor** (glamor, under Xorg or Xwayland): the
1662
+ frame goes to the server as decoded, through shared memory, and its GPU
1663
+ converts it into a hidden window copied into the surface — 1.5ms a 1080p
1664
+ NV12 frame against 15ms. Full range, BT.2020, the first frame of a size
1665
+ and a window off screen go the JavaScript way.
1666
+ - **X11 without one, and Windows**: converted to RGBA in JavaScript unless
1667
+ BGRA (hence their preference); X11 uploads through shared memory where
1668
+ available.
1669
+
1670
+ No colour management is applied on any of them. `examples/video.jsx` plays
1671
+ a file through whichever path the display has.
2207
1672
 
2208
1673
  ### `src` — the platform's player
2209
1674
 
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.
1675
+ Where `useSupports('mediaPlayback')` is true — macOS (AVFoundation), and a
1676
+ `<Frame>` pane whose host plays it ([frame.md](frame.md#sizing-stacking-input))
1677
+ — `src` is a path, `file://` or `http(s)` URL (HLS included), played with
1678
+ the platform's decoder, audio, seeking and streaming.
2216
1679
 
2217
1680
  ```jsx
2218
1681
  const ref = useRef(null);
@@ -2230,28 +1693,20 @@ const ref = useRef(null);
2230
1693
  <button onPress={() => ref.current.seek(30)} />;
2231
1694
  ```
2232
1695
 
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:
1696
+ Laid out at 300x150 (or the poster's size) until ready, then at the clip's
1697
+ size from `onLoadedMetadata`. Prop changes go to the same player. The `ref`
1698
+ is HTMLMediaElement's imperative half: `play()` (restarts an ended item),
1699
+ `pause()`, `seek(seconds)` (exact, not keyframe), `currentTime`,
1700
+ `duration`, `paused`. `onPlay`/`onPause` fire as it starts and stops,
1701
+ `onTimeUpdate` four times a second while playing and after a seek,
1702
+ `onEnded` at the end of a non-looping item, `onError` when the item fails.
1703
+ Drawn, the picture is copied at the clip's frame rate; lifted, it costs the
1704
+ window nothing. There is no `controls`: the transport is your React.
1705
+
1706
+ Where it is false — X11 and Wayland for good, Windows today, macOS on an
1707
+ older bridge — the element reports one `NoMediaPlaybackError`
1708
+ (`code: 'ENOMEDIAPLAYBACK'`) through `onError` on a microtask and shows its
1709
+ poster, never a black box. Ask first:
2255
1710
 
2256
1711
  ```jsx
2257
1712
  const playback = useSupports('mediaPlayback');
@@ -2262,7 +1717,7 @@ const playback = useSupports('mediaPlayback');
2262
1717
 
2263
1718
  ## `<canvas>`
2264
1719
 
2265
- The escape hatch: a retained node whose content you paint.
1720
+ The escape hatch: a retained node you paint.
2266
1721
 
2267
1722
  ```jsx
2268
1723
  <canvas
@@ -2275,31 +1730,22 @@ The escape hatch: a retained node whose content you paint.
2275
1730
  ```
2276
1731
 
2277
1732
  `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:
1733
+ conical, `setLineDash`, round caps/joins, images, text), translated to the
1734
+ node's origin and clipped to it. `onDraw` runs on every repaint of the
1735
+ window. `fillRects` sends many same-colour rectangles (a sparkline, a heat
1736
+ map, terminal cells) as one request, with `fillRect`'s fill style, alpha,
1737
+ composite op and clip:
2285
1738
 
2286
1739
  ```jsx
2287
1740
  ctx.fillStyle = theme.accent;
2288
1741
  ctx.fillRects(bars); // [[x, y, w, h], …] or one flat [x, y, w, h, x, …]
2289
1742
  ```
2290
1743
 
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
1744
  ### Raw pixels — the one call that is not translated
2296
1745
 
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:
1746
+ `putImageData` and `getImageData` ignore the transform _and_ the clip (the
1747
+ HTML canvas rule), so `ctx.putImageData(data, 0, 0)` lands at the
1748
+ **window** corner, silently. `onDraw`'s second argument carries the origin:
2303
1749
 
2304
1750
  ```jsx
2305
1751
  <canvas
@@ -2310,19 +1756,15 @@ the **window** corner, not in the node, and nothing errors on the way.
2310
1756
  />
2311
1757
  ```
2312
1758
 
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.
1759
+ `x`/`y` are the node's place in the window in a live paint and the cache
1760
+ surface's origin under a [`cacheKey`](#cachekey--draw-it-once), so
1761
+ offsetting by them is right in both. In development an out-of-box
1762
+ `putImageData` warns once.
2318
1763
 
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:
1764
+ For pixels that last, draw an `Image` instead (ntk's, from
1765
+ [`react-x11/ntk`](extending.md#the-subpath-exports)): `drawImage` honours
1766
+ the transform and clip (a half-scrolled canvas stays cut), and the image
1767
+ keeps its server-side pixmap rather than re-uploading every repaint.
2326
1768
 
2327
1769
  ```jsx
2328
1770
  import { Image } from 'react-x11/ntk';
@@ -2333,38 +1775,27 @@ const img = new Image({ width, height, data: rgba }); // keep it memoized
2333
1775
 
2334
1776
  ### `cacheKey` — draw it once
2335
1777
 
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.
1778
+ With a `cacheKey` the canvas is rendered once and composited on later
1779
+ repaints; canvases sharing a key share the copy.
2340
1780
 
2341
1781
  ```jsx
2342
1782
  <canvas cacheKey={`spark:${series.id}:${w}x${h}`} onDraw={drawSparkline} />
2343
1783
  ```
2344
1784
 
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.
1785
+ Opt-in, since `onDraw` is an opaque closure only you can read. **The key
1786
+ must name every input the drawing reads**, or stale pixels show; develop
1787
+ with `REACT_X11_PAINT_CACHE=verify`, which reports exactly that
1788
+ ([Runtime diagnostics](debugging.md#the-paint-cache--react_x11_no_paint_cache-react_x11_paint_cacheverify)).
1789
+ Leave it unset for anything animated or driven by state outside the props.
1790
+ `<svg>` builds its key itself.
2359
1791
 
2360
1792
  ### `mono` — one colour, and the colour out of the key
2361
1793
 
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:
1794
+ `mono` promises **the drawing is one colour, and not its own to choose**:
1795
+ `fillStyle` and `strokeStyle` arrive preset from the node's resolved
1796
+ `color` — its own, else inherited
1797
+ ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)) — so
1798
+ `:hover`, `:disabled` and a theme flip all reach the same `onDraw`:
2368
1799
 
2369
1800
  ```jsx
2370
1801
  const chevron = (ctx, { width: w, height: h }) => {
@@ -2389,38 +1820,20 @@ const chevron = (ctx, { width: w, height: h }) => {
2389
1820
  />;
2390
1821
  ```
2391
1822
 
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.
1823
+ With a `cacheKey`, a `mono` drawing is cached as **coverage** and coloured
1824
+ at composite time, so one copy serves every ink (as `<svg>` does for
1825
+ `currentColor`); without `mono`, each colour is its own rasterization. It
1826
+ works without `cacheKey`, but together is the point. The
1827
+ [system icon set](components.md#system-icons) is twelve of these.
2412
1828
 
2413
1829
  ---
2414
1830
 
2415
1831
  ## `<glarea>`
2416
1832
 
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).
1833
+ An OpenGL surface in the layout — on X11 the only drawn element owning a
1834
+ real X window; other backends give it a surface of their own
1835
+ ([gl.md](gl.md)). On X11 under the default `glPolicy` it needs a server with
1836
+ **indirect GLX** (`+iglx` / `AllowIndirectGLX`, off by default on many).
2424
1837
 
2425
1838
  ```jsx
2426
1839
  <glarea
@@ -2437,43 +1850,33 @@ off by default on many).
2437
1850
  />
2438
1851
  ```
2439
1852
 
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.
1853
+ - `onDraw(gl, { width, height, node })` — draw one frame; the viewport is
1854
+ set and buffers cleared before it, `SwapBuffers` follows.
1855
+ - `onCreated(gl, info)` — once, when the context is current: one-time GL
1856
+ state, texture uploads, display-list compilation.
2444
1857
  - `clearColor` — CSS colour or `[r, g, b, a]` floats (default black).
2445
1858
  - `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.
1859
+ changes; `'always'` renders continuously on ntk's frame clock.
1860
+ - `frameRate` — [`<window frameRate>`](#framerate--pacing-the-frames-under-a-flood)'s
1861
+ values, priced by what `onDraw` and the swap cost; defaults to the owning
1862
+ window's. Name `'display'` for a scene wanting every frame while its
1863
+ window streams; `'always'` under `'adaptive'` loops at the budget.
2454
1864
  - `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.
1865
+ `{ DEPTH_SIZE: 24 }`; one query per app, shared by every `<glarea>`.
1866
+ - `onError(err)` — no GL surface (no GLX, no matching visual); unhandled, a
1867
+ console warning.
1868
+
1869
+ On X11's default indirect path `gl` is fixed-function OpenGL 1.4 (immediate
1870
+ mode, matrices, lighting, textures, display lists) — no shaders or vertex
1871
+ arrays, which GLX does not encode. **Put geometry in display lists**: each
1872
+ immediate-mode vertex crosses the wire every frame; a compiled list is one
1873
+ `CallList`. The direct path and Cocoa give `onDraw` a WebGL-shaped table
1874
+ with shaders ([gl.md](gl.md)).
1875
+
1876
+ The surface sits above everything its window paints. **Its own children are
1877
+ drawn above it**: laid out like a `<box>`'s, cut to its box — a legend, a
1878
+ toolbar, a label — and painted on **panes** over the surface in the same
1879
+ frame.
2477
1880
 
2478
1881
  ```jsx
2479
1882
  <glarea style={{ flexGrow: 1 }} clearColor="#0e1320" onDraw={drawMap}>
@@ -2491,86 +1894,48 @@ same frame and from the same damage as everything else.
2491
1894
  </glarea>
2492
1895
  ```
2493
1896
 
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.
1897
+ - **Cocoa**: one transparent layer over the whole surface, composited by
1898
+ Core Animation — a translucent fill, antialiased edge or shadow blends
1899
+ with the GL frame.
1900
+ - **X11**: one child window per child, stacked above the surface in the
1901
+ children's order and cut (SHAPE) to where the child painted half its
1902
+ alpha or more; the GL frame shows everywhere else. A translucent pixel
1903
+ inside the cut blends with `clearColor`, not the frame — so a translucent
1904
+ fill is tinted by `clearColor`, and no compositor helps. Without SHAPE
1905
+ (`react-x11/test`'s server) a pane is the child's whole rectangle,
1906
+ `clearColor` where unpainted.
1907
+
1908
+ A child that only moves (its `left` or `top`) has its pixels copied, not
1909
+ repainted — only uncovered strips are drawn, and panes it passes over are
1910
+ untouched — so content panned over a scene is cheap; scroll panes among the
1911
+ children scroll the same way.
1912
+ [`REACT_X11_NO_SCROLL_BLIT=1`](debugging.md#react_x11_no_scroll_blit1)
1913
+ turns the copies off.
1914
+
1915
+ `useSupports('glOverlay')` says whether children are drawn: true everywhere
1916
+ but **XQuartz**, where the macOS window server composites every GL surface
1917
+ above all X drawing — children are laid out but not drawn, the pointer over
1918
+ them is the surface's, and development says so once. Draw the overlay
1919
+ beside the surface or in GL there, or use the Cocoa backend.
2550
1920
 
2551
1921
  Limits worth knowing:
2552
1922
 
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.
1923
+ - **Overlapping surfaces:** both overlays are above both frames on Cocoa;
1924
+ on X11 they stack by creation order.
1925
+ - **Scroll panes:** a surface scrolled partly out of a pane is not clipped
1926
+ by it — its window and panes are real windows at its rect.
1927
+ - **Promotion:** on Cocoa a child is never lifted onto its own layer for an
1928
+ animation (it would sit under the surface); its transitions run on the
1929
+ frame clock.
1930
+
1931
+ **The pointer over the surface is the tree's**, on every backend:
1932
+ `onMouseDown`, `onMouseMove`, `onMouseUp`, `onClick` (double click:
1933
+ `detail: 2`), `onContextMenu`, `onMouseEnter`/`onMouseLeave`,
1934
+ `:hover`/`:active` and `onWheel` fire at the child under the pointer, or the
1935
+ `<glarea>` between children, and bubble. A press focuses the nearest
1936
+ focusable one; `ev.capturePointer()` keeps a drag that leaves the surface.
1937
+ The surface and its children win hit tests over what the tree has behind;
1938
+ `pointerEvents: 'none'` on the surface lets the pointer through.
2574
1939
 
2575
1940
  ```jsx
2576
1941
  <glarea
@@ -2583,52 +1948,29 @@ instead.
2583
1948
  />
2584
1949
  ```
2585
1950
 
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.
1951
+ An element built on `<glarea>` must **not listen for the pointer on
1952
+ `node.window`**: on X11 that diverts every press and wheel from the tree.
1953
+ `node.forwardsPointer` (`GlAreaNode.prototype.forwardsPointer` from
1954
+ `react-x11/node`, to ask without rendering) is `true` where core delivers
1955
+ the pointer — the switch for dropping such a listener.
1956
+
1957
+ A scene graph over `onDraw` is `@react-x11/components/three`. See
1958
+ `examples/viewer3d.jsx` for the raw form, and `examples/labs/gl-overlay.jsx`
1959
+ for a HUD over a surface on both backends, and beside it on XQuartz.
2607
1960
 
2608
1961
  ---
2609
1962
 
2610
1963
  ## `<foreign>`
2611
1964
 
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.
1965
+ **X11 only**; no other backend has cross-process window embedding, and
1966
+ none is planned. On Cocoa, Wayland, Windows and the headless mock it lays
1967
+ out as an empty box, draws nothing, never calls `onReady`, and calls
1968
+ `onError` once. Ask first with `useSupports('embedding')`. (`<Frame>`, which
1969
+ uses it on X11 to host a pane of your _own_ app, works on Cocoa and Windows
1970
+ another way — [frame.md](frame.md).)
2626
1971
 
2627
1972
  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.
1973
+ pane, a video surface, a docked tray icon:
2632
1974
 
2633
1975
  ```jsx
2634
1976
  <foreign
@@ -2639,82 +1981,59 @@ a react-x11 app a _host_ rather than a drawer of its own pixels. Needs ntk
2639
1981
  />
2640
1982
  ```
2641
1983
 
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.
1984
+ - `windowId` — the window to embed; changing it hands the old client back
1985
+ first. **Omit it** to adopt whatever is put inside this node, as
1986
+ `xterm -into WID` and `mpv --wid=WID` need.
1987
+ - `onReady({ windowId })` — this node's container id, offered before
1988
+ anything is embedded, so a program can be spawned into it.
1989
+ - `onEmbedded({ id, xembed, version })` — a client is in; `xembed: false`
1990
+ (no `_XEMBED_INFO`, plain reparenting) is the common case.
2652
1991
  - `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.
1992
+ - `onRequestFocus()` — the client asked for focus; it is given through the
1993
+ focus manager, so the tree observes it normally.
1994
+ - `onError(err)` — the embed failed, or (once, at mount) the backend has no
1995
+ embedding; unhandled, a console warning (for the latter, one per app).
2658
1996
  - `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.
1997
+ - `backgroundColor` in `style` shows while there is no client.
2661
1998
 
2662
1999
  **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).
2000
+ set; it never destroys it.** Layout treats it as a leaf; its X window
2001
+ follows that rect, and the client gets the synthetic `ConfigureNotify`
2002
+ ICCCM requires, in root coordinates. It stacks above everything drawn in
2003
+ the parent and takes no children, so an overlay belongs in a sibling
2004
+ `<popup>`. Pointer events over it are the client's.
2005
+
2006
+ **While it holds focus, the app's handlers see every key first, and
2007
+ anything they do not `preventDefault()` is forwarded to the client.** That,
2008
+ the XEmbed focus messages, the tab chain crossing the boundary and the one
2009
+ case the rule cannot cover are in [embedding.md](embedding.md).
2680
2010
 
2681
2011
  ---
2682
2012
 
2683
2013
  ## 3D scene: `<mesh>`, `<group>`, geometries, materials
2684
2014
 
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).
2015
+ These live in [`@react-x11/components/three`](ecosystem.md), a scene graph
2016
+ with its own reconciler over `<glarea>`, rendering on **both** GL backends
2017
+ from the same JSX. Core keeps the surface and `onDraw` ([gl.md](gl.md)).
2689
2018
 
2690
2019
  ## Vector drawings
2691
2020
 
2692
2021
  `<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.
2022
+ size and scales into the content box it is given. For documents use
2023
+ [`@react-x11/components`](https://github.com/sidorares/react-x11-components)'
2024
+ `<Markdown>` (GFM) and `<Formula>` (KaTeX), with `<RichText>` and
2025
+ `<CodeBlock>` beneath: built from host elements, they select across blocks
2026
+ and re-render only the changed block while content streams in. (The
2027
+ `<markdown>`, `<html>` and `<tex>` elements were removed in 2.0.)
2705
2028
 
2706
2029
  ### `<svg>`
2707
2030
 
2708
2031
  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:
2032
+ transforms, basic text). Sized like `<image>`: natural `viewBox` size,
2033
+ aspect-preserving when the style sets one of `width`/`height`, scaled into
2034
+ the content box when it sets both. Content is **JSX children, like SVG in
2035
+ React DOM** — camelCase props (`strokeWidth`, `fillRule`; `viewBox` as-is),
2036
+ re-rendered on any prop change:
2718
2037
 
2719
2038
  ```jsx
2720
2039
  <svg viewBox="0 0 24 24" style={{ width: 40, height: 40 }}>
@@ -2723,24 +2042,21 @@ any prop change:
2723
2042
  </svg>
2724
2043
  ```
2725
2044
 
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
2045
  | prop | |
2732
2046
  | --------- | --------------------------------- |
2733
2047
  | children | declarative SVG elements |
2734
2048
  | `source` | SVG markup string |
2735
2049
  | `viewBox` | coordinate system (children form) |
2736
2050
 
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:
2051
+ Children win over `source` when both are given. Unsupported tags are
2052
+ skipped. Per-child event handlers are not dispatched — put them on the
2053
+ `<svg>`.
2054
+
2055
+ **`currentColor`** in `fill`/`stroke` resolves as in CSS: the node's own
2056
+ `color`, else the inherited ink, else the palette's `text`
2057
+ ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)) — so
2058
+ one drawing serves every state, and an unstyled one is readable on a dark
2059
+ desktop:
2744
2060
 
2745
2061
  ```jsx
2746
2062
  <svg
@@ -2754,9 +2070,10 @@ readable on a dark desktop rather than black on it:
2754
2070
  />
2755
2071
  ```
2756
2072
 
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.
2073
+ A drawing painted entirely in `currentColor`, or in one colour, is cached
2074
+ as coverage: recolouring is a composite, not a re-render, and every colour
2075
+ shares one copy. Two colours or a gradient are baked in.
2076
+ `REACT_X11_DEBUG_PAINT_CACHE=1` shows what is kept.
2077
+
2078
+ Design notes behind this page — mechanism, history, measurements:
2079
+ [design/elements.md](../design/elements.md).