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/extending.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # Extending the element vocabulary
2
2
 
3
- `<box>`, `<text>`, `<glarea>` and the rest are built in, but the list is not
4
- closed. `registerElement()` adds an element from outside the package — a
5
- `<sparkline>`, a tray icon, an XEMBED `<foreign>` — so a sibling package can
6
- ship one without forking react-x11 and without the core growing.
3
+ `<box>`, `<text>`, `<glarea>` and the rest are built in; `registerElement()`
4
+ adds more from outside the package — a `<sparkline>`, a tray icon, an XEMBED
5
+ `<foreign>` — so a sibling package can ship one without forking react-x11.
7
6
 
8
7
  ```js
9
8
  import { registerElement } from 'react-x11/host';
@@ -44,146 +43,124 @@ registerElement('sparkline', {
44
43
  />
45
44
  ```
46
45
 
47
- Register before you render. The registry is consulted when an element is
46
+ Register before you render: the registry is consulted when an element is
48
47
  first created, so a late registration only affects trees mounted after it.
49
48
 
49
+ Design notes (history, measurements, how core serves each hook):
50
+ [design/extending.md](../design/extending.md).
51
+
50
52
  ## The definition
51
53
 
52
- | field | |
53
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
54
- | `create(props, app, hostCtx)` | builds the node. `app` is the ntk connection — the second argument every built-in node constructor takes. Must return a `Node`. |
55
- | `drawn` | default `true`: lays out with yoga and paints into the owning window. `false` for a node that owns a real child X window (see below). |
56
- | `semanticNames` | prop names this element owns even though they are also style names |
57
- | `selfDamagedProps` | prop names whose damage the element's own `applyProps` claims (see [Drawing a scene](#drawing-a-scene-into-one-node)) |
58
- | `childrenAllowed` | default `true`; `false` rejects children by name instead of laying out something that never paints |
59
- | `override` | replace an existing registration. Off by default — two packages claiming one name is a conflict worth hearing about |
60
-
61
- Two of these exist because getting them wrong fails a long way from the
62
- cause, so they are worth understanding rather than copying:
63
-
64
- **`drawn` decides whether your element paints at all.** `paintOrder()`
65
- filters children on the set of drawn kinds; a node missing from it lays out
66
- correctly, reports a sensible `abs` rect, and never appears on screen, with
67
- no error anywhere. `registerElement` puts you in that set unless you opt out.
68
-
69
- **`semanticNames` is the difference between DEV and production.** react-x11
70
- throws in development on `<box width={10} />`, because `width` is a style
71
- property written flat — a real mistake with an unhelpful failure otherwise.
72
- An element whose own vocabulary overlaps the style vocabulary (`width`,
73
- `color`, `fontSize`) therefore has to say so, or it throws on its own props
74
- in development and works in production. `<window width>` is the built-in
75
- precedent: on a `<window>` it is the X window's width and never style.
76
-
77
- `react-x11/style`'s `isStyleProp(name)` is how to check whether a name you
78
- are about to use needs declaring.
54
+ | field | |
55
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
56
+ | `create(props, app, hostCtx)` | builds and returns the `Node`; `app` is the ntk connection |
57
+ | `drawn` | default `true`: laid out by yoga, painted into the window. `false`: it owns a child X window (below) |
58
+ | `semanticNames` | prop names this element owns even though they are also style names |
59
+ | `selfDamagedProps` | props whose damage your own `applyProps` claims ([Drawing a scene](#drawing-a-scene-into-one-node)) |
60
+ | `childrenAllowed` | default `true`; `false` rejects children by name rather than laying out what never paints |
61
+ | `override` | replace an existing registration. Off by default: two packages claiming one name is a conflict |
62
+
63
+ **`drawn` decides whether your element paints at all.** A kind outside the
64
+ set of drawn kinds lays out, reports a sensible `abs`, and never appears on
65
+ screen, with no error. `registerElement` adds you unless you opt out.
66
+
67
+ **`semanticNames` keeps development and production alike.** In development
68
+ react-x11 throws on `<box width={10} />`, a style property written flat. An
69
+ element whose own props overlap the style vocabulary (`width`, `color`,
70
+ `fontSize`) must list them, or it throws on its own props in development
71
+ only. `<window width>` is the built-in precedent: the X window's width,
72
+ never style. `isStyleProp(name)` from `react-x11/style` says whether a name
73
+ needs declaring.
79
74
 
80
75
  ## Registering under hot reload {#hot-reload}
81
76
 
82
- Register at module scope — the pattern everything above shows, and the one
83
- tree-shaking forces on a component library. That module scope re-runs when
84
- [`react-x11/refresh`](ecosystem/dev-tooling.md#react-refresh) hot-reloads
85
- the module, so `registerElement` has a re-registration policy rather than
86
- a footnote:
87
-
88
- - **The same `create` reference is never a conflict.** A definition
89
- arriving twice replaces silently, latest flags winning.
90
- - **Under an active hot-reload session, any duplicate replaces silently.**
91
- The session begins with the first hot re-import and covers the rest of
92
- the process: from then on, "already registered" would only mean
93
- "registered by the previous version of yourself". Mounted nodes keep the
94
- old prototype until they remount — the same staleness contract Fast
95
- Refresh has for classes; new mounts get the new definition.
96
- - **Outside a session, a duplicate still throws.** Two packages claiming
97
- one element name in a plain run is a conflict to be told about, and
98
- `override: true` remains the way to say a replacement is deliberate.
99
-
100
- One thing the hot loader adds: inside a hot module, call it through a
101
- namespace or default import (`Host.registerElement(...)`) — a _named_
102
- import called at module top level is a transform error there, because
103
- named-import bindings initialize in a microtask after a reload.
77
+ Register at module scope (tree-shaking requires it of a component library).
78
+ That scope re-runs when
79
+ [`react-x11/refresh`](ecosystem/dev-tooling.md#react-refresh) hot-reloads the
80
+ module, so:
81
+
82
+ - **The same `create` reference is never a conflict**: it replaces
83
+ silently, latest flags winning.
84
+ - **During a hot-reload session, any duplicate replaces silently.** The
85
+ session starts at the first hot re-import and lasts for the process.
86
+ Mounted nodes keep the old prototype until they remount (Fast Refresh's
87
+ contract for classes); new mounts get the new definition.
88
+ - **Outside a session, a duplicate throws**; `override: true` says a
89
+ replacement is deliberate.
90
+
91
+ In a hot module, call it through a namespace or default import
92
+ (`Host.registerElement(...)`): a _named_ import called at module top level
93
+ is a transform error there, because named-import bindings initialize a
94
+ microtask after a reload.
104
95
 
105
96
  ## The node contract
106
97
 
107
- `Node` already implements the whole reconciler-facing surface, so an element
108
- that only draws needs a constructor and a `paint`. What you may override:
109
-
110
- | method | when |
111
- | ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
112
- | `paint(ctx)` | draw. Call `super.paint(ctx)` first for background, border and clip, then draw inside `this.abs`. |
113
- | `paintContent(ctx)` | draw _between_ the background and the children — where the built-ins draw, and what a scroller needs (below) |
114
- | `opaqueRect()` | the rect you cover with opaque pixels on every paint, so a pass inside it skips the fills under you (below) |
115
- | `viewportFixedRects()` | what you draw fixed to your scroll pane's viewport, so its scroll's blit repaints it rather than copying it |
116
- | `paintOverhang()` | how far you draw past your box, so claims and culls cover it (below) |
117
- | `applyProps(next, prev)` | props changed. Call `super.applyProps(next, prev)`; invalidate if you cache anything derived. |
118
- | `paintChanged(next, prev)` | did anything you draw change? Only for an element that claims its own damage (below) |
119
- | `measureContent(constraints)` | your content has a size of its own (below). Leaves only — an element that measures has no children. |
120
- | `default*(ev)` | the element's own behaviour for a key, a press or focus (below) — what makes it _interactive_. |
121
- | `hitTest(x, y)` | non-rectangular hit areas. The default walks children in reverse paint order. |
122
- | `insertBefore` / `removeChild` | only if children mean something structural to you |
123
- | `destroySubtree()` | release anything you allocated (pixmaps, fonts, timers). Call `super.destroySubtree()`. |
124
-
125
- And what you read:
98
+ `Node` implements the whole reconciler-facing surface; an element that only
99
+ draws needs a constructor and a `paint`. What you may override:
100
+
101
+ | method | when |
102
+ | ------------------------------ | ------------------------------------------------------------------------------------------- |
103
+ | `paint(ctx)` | draw. Call `super.paint(ctx)` first (background, border, clip), then draw inside `this.abs` |
104
+ | `paintContent(ctx)` | draw _between_ background and children — where built-ins draw; what a scroller needs |
105
+ | `opaqueRect()` | the rect you cover opaquely every paint, so a pass inside skips the fills under it |
106
+ | `viewportFixedRects()` | what you draw fixed to your scroll pane's viewport, so its blit repaints it |
107
+ | `paintOverhang()` | how far you draw past your box, so claims and culls cover it |
108
+ | `applyProps(next, prev)` | props changed. Call `super`; invalidate anything you derived |
109
+ | `paintChanged(next, prev)` | did anything you draw change? Only if you claim your own damage |
110
+ | `measureContent(constraints)` | your content has a size of its own. Leaves only: no children |
111
+ | `default*(ev)` | your behaviour for a key, a press or focus — what makes it _interactive_ |
112
+ | `hitTest(x, y)` | non-rectangular hit areas. The default walks children in reverse paint order |
113
+ | `insertBefore` / `removeChild` | only if children mean something structural to you |
114
+ | `destroySubtree()` | release what you allocated (pixmaps, fonts, timers). Call `super` |
115
+
116
+ What you read:
126
117
 
127
118
  - `this.props` — the current props, replaced wholesale on update.
128
- - `this.style` — the flattened `style` prop with the active `:hover` /
129
- `:focus` / `:active` / `:disabled` blocks overlaid, and `$token`
130
- references already resolved against the theme. **Everything that paints or
131
- lays out reads this, never `props`.**
132
- - `this.abs` — position and size within the owning window, valid after
133
- layout.
134
- - `this.contentBox()` — `abs` inset by the border and the padding: where the
135
- content goes, and where every built-in draws.
136
- - `this.scale` — device pixels per logical pixel ([scale.md](scale.md)).
137
- `this.style`, `this.abs`, `contentBox()`, the paint context, every rect you
138
- hand `invalidate` or `scrollContents` and every rect you report to a screen
139
- reader are device pixels already; a synthetic event's `x`/`y` are logical,
140
- so a hit test against `abs` reads `ev.nativeEvent.x`/`y`, or multiplies.
141
- On a 1x display the two units coincide — which is exactly how an element
142
- that mixes them passes every headless test and then hovers, drags and
143
- frames at half the distance on a retina panel.
144
- `renderX11(element, { scale: 2 })` is the test that catches it. It is a
145
- per-node number rather than a per-window one — an enclosing `scale` prop
146
- zooms a subtree ([scale.md](scale.md#a-subtree-of-its-own)) — so read it
147
- off the node rather than caching one for the tree.
148
- - `this.resolvedTextStyle()` — the text style this node resolves to, ready to
149
- hand to `app.fonts.layout` (below).
150
- - `this.resolveColor(value)` — a colour from this node's style as one a
151
- context can paint: the value itself, or for
152
- [`currentColor`](styling.md#currentcolor) the node's ink. Every colour
153
- core paints goes through it, so a colour of yours read off `this.style`
154
- should too — `ctx.strokeStyle = this.resolveColor(this.style.borderColor)`
155
- — or it paints nothing where the app wrote the keyword. A node whose
156
- style paints in `currentColor` repaints when its ink moves, without being
157
- asked.
158
- - `this.direction` — which way this node reads, resolved: `'ltr'` or
159
- `'rtl'`, never `'inherit'`. Yoga has already mirrored the boxes, so this
160
- is for what you draw _inside_ one — which side a mark sits on, which way
161
- a chevron points, what a pointer coordinate means
119
+ - `this.style` — the flattened `style` with the active `:hover` / `:focus` /
120
+ `:active` / `:disabled` blocks overlaid and `$token`s resolved against the
121
+ theme. **Everything that paints or lays out reads this, never `props`.**
122
+ - `this.abs` — position and size within the owning window, after layout.
123
+ - `this.contentBox()` — `abs` inset by border and padding: where the content
124
+ goes, and where every built-in draws.
125
+ - `this.scale` — device pixels per logical pixel ([scale.md](scale.md)),
126
+ per node (an enclosing `scale` prop zooms a subtree —
127
+ [scale.md](scale.md#a-subtree-of-its-own)), so read it off the node.
128
+ `style`, `abs`, `contentBox()`, the paint context and every rect you hand
129
+ `invalidate`, `scrollContents` or a screen reader are device pixels; a
130
+ synthetic event's `x`/`y` are logical, so hit-test `abs` with
131
+ `ev.nativeEvent.x`/`y`, or multiply. They coincide at 1x, so test with
132
+ `renderX11(element, { scale: 2 })`.
133
+ - `this.resolvedTextStyle()` — this node's text style, ready for
134
+ `app.fonts.layout` (below).
135
+ - `this.resolveColor(value)` — a style colour as a context can paint it: the
136
+ value, or the node's ink for [`currentColor`](styling.md#currentcolor).
137
+ Pass every colour you read off `this.style` through it
138
+ (`ctx.strokeStyle = this.resolveColor(this.style.borderColor)`), or the
139
+ keyword paints nothing. A node whose style paints in `currentColor`
140
+ repaints when its ink moves.
141
+ - `this.direction` — `'ltr'` or `'rtl'`, never `'inherit'`. Yoga has already
142
+ mirrored the boxes; this is for what you draw inside one — which side a
143
+ mark sits on, which way a chevron points, what a pointer coordinate means
162
144
  ([styling.md](styling.md#direction-and-the-logical-edges)).
163
145
  - `this.root` — the owning `<window>` node; `this.app` — the ntk connection.
164
146
  - `this.theme` — the nearest theme at or above this node.
165
- - `this.paintDamage()` — the rect the pass being painted covers, or `null`
166
- for a full one. Only an element that draws a whole scene needs it
167
- ([below](#drawing-a-scene-into-one-node)).
168
-
169
- To ask for a repaint: `this.invalidate(layout, damage, reason)`. Pass the
170
- damage — `this` is usually it, or a rect when you know a tighter one. An
171
- invalidation with no bound repaints the whole window, which is this
172
- renderer's main performance bug class (see [debugging.md](debugging.md)).
173
- `reason` joins the closed set the diagnostics print. Damage is collected on
174
- the window node and `invalidate` on any node forwards there, so an element
175
- that is not attached yet invalidates nothing rather than throwing.
176
-
177
- And to take the keyboard focus: `this.focus()` moves it here as a click
178
- would and hands the node back, `this.blur()` gives it up, `this.focused` and
179
- `this.focusWithin` answer where it is. They claim their own damage — a
180
- focused control repaints itself, and its ring, without being asked.
147
+ - `this.paintDamage()` — the rect the current pass covers, or `null` for a
148
+ full one ([below](#drawing-a-scene-into-one-node)).
149
+
150
+ `this.invalidate(layout, damage, reason)` asks for a repaint. Pass the damage
151
+ — `this`, or a tighter rect: an unbounded one repaints the whole window, the
152
+ main performance bug class here ([debugging.md](debugging.md)). `reason` is
153
+ from the closed set the diagnostics print. On a node not yet attached it
154
+ does nothing.
155
+
156
+ `this.focus()` takes the keyboard focus as a click would and returns the
157
+ node; `this.blur()` gives it up; `this.focused` and `this.focusWithin` say
158
+ where it is. A focus change repaints the control and its ring by itself.
181
159
 
182
160
  ### Text of your own
183
161
 
184
162
  An element that draws text — a badge, a code editor, a terminal — shapes it
185
- through ntk's font manager, and the two things it needs to do that are the
186
- two accessors above:
163
+ through ntk's font manager with the two accessors above:
187
164
 
188
165
  ```js
189
166
  class BadgeNode extends Node {
@@ -202,50 +179,34 @@ class BadgeNode extends Node {
202
179
  }
203
180
  ```
204
181
 
205
- **`contentBox()` is not `abs` minus `style.padding`.** The insets come off
206
- the layout, which is where `padding: '10%'`, the per-side overrides and the
207
- border widths have already been resolved against this frame's size — so the
208
- box an element computes from the style bag agrees with the one `<text>` uses
209
- only by luck, and stops agreeing the next time the vocabulary grows another
210
- edge — the per-side border widths were the last one to arrive.
211
-
212
- **`resolvedTextStyle()` is what makes the type of an app reach your
213
- element.** It is the node's own font properties over what it **inherits**
214
- from the elements around it, and under that what the palette says text with
215
- none of its own is set in — in the shape `fonts.layout` wants (`family`, not
216
- `fontFamily`; `variations`, not `fontVariationSettings`; `features`, what
217
- `fontVariantNumeric` and `fontFeatureSettings` resolve to together). An
218
- element that reads `this.style.fontFamily` instead is one that a
219
- `<ThemeProvider value={{ fontFamily: 'Inter' }}>` does not reach, and one
220
- that a `<box style={{ color: theme.dim }}>` around it does not dim — bugs an
221
- app author can only work around by growing props on your element and
222
- threading them in. Ask, and the element inherits exactly what a `<text>`
223
- sibling inherits, including a `:hover` block on the row it sits in
182
+ **Use `contentBox()`, not `abs` minus `style.padding`.** Its insets come from
183
+ layout, with `padding: '10%'`, per-side overrides and border widths
184
+ resolved; a box derived from the style bag matches `<text>`'s only by luck.
185
+
186
+ **Use `resolvedTextStyle()`, not `this.style.fontFamily`.** It layers the
187
+ node's own font properties over what it inherits from the elements around
188
+ it and the palette's default, in the shape `fonts.layout` wants (`family`,
189
+ not `fontFamily`; `variations`, not `fontVariationSettings`; `features`,
190
+ what `fontVariantNumeric` and `fontFeatureSettings` resolve to). Read the
191
+ style bag instead and a `<ThemeProvider>` font or an enclosing box's
192
+ `color` never reaches your element; ask, and it inherits what a `<text>`
193
+ sibling does, a row's `:hover` included
224
194
  ([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)).
225
195
 
226
- Both are read at paint time. `contentBox()` is computed each call, since the
227
- box changes with every layout; `resolvedTextStyle()` is memoised and dropped
228
- by everything that can move it — a style change, a state block, an
229
- animation tick, a theme swap, an element above it changing its own type — so
230
- reading it per frame costs a property lookup and never goes stale. If the
231
- text is also what _sizes_ the element, shape it in `measureContent` as well;
232
- `<text>` memoizes its layout per max-width for exactly that reason. An
233
- element that keeps a cache of its own keyed on the type — a shaped run, a
234
- rasterized label — should drop it in **`_textStyleMoved(cost)`**, which is
235
- called with `2` when a glyph can have moved and `1` when only the ink or the
236
- glyph rounding did. The default re-measures on the first and repaints on the
237
- second, which is right for most elements.
238
-
239
- **Text drawn where a context is not.** A GL surface's labels, or a signed
240
- distance field made from a string, need the text as pixels rather than
241
- drawn onto a surface — and a surface read back is a round trip for pixels
242
- the engine had, one that answers asynchronously and in the ink the screen
243
- gets (hinted, gamma and contrast applied). A layout answers
244
- **`coverage({ pad })`** instead (#673): how much of each device pixel its
245
- glyphs cover, one byte a pixel, synchronously — the layout box in whole
246
- pixels with `pad` round it, the layout's origin at `(pad, pad)`. Linear,
247
- grayscale, unhinted: the outlines' own coverage, which is what a raster that
248
- will be scaled wants.
196
+ Read both at paint time: `contentBox()` is computed per call;
197
+ `resolvedTextStyle()` is memoised and dropped by whatever can move it (a
198
+ style or state change, an animation tick, a theme swap, an ancestor's type
199
+ changing). If the text also sizes the element, shape it in `measureContent`
200
+ too. A cache of your own keyed on the type — a shaped run, a rasterized
201
+ label — is dropped in **`_textStyleMoved(cost)`**: `2` when a glyph can
202
+ have moved, `1` when only the ink or glyph rounding did. The default
203
+ re-measures on `2` and repaints on `1`.
204
+
205
+ **Text as pixels.** A GL surface's labels or a signed distance field need
206
+ glyph coverage, not a drawn surface. A layout's **`coverage({ pad })`**
207
+ answers synchronously: one byte per device pixel, the layout box in whole
208
+ pixels with `pad` round it, origin at `(pad, pad)` — linear, grayscale,
209
+ unhinted, as a raster that will be scaled wants.
249
210
 
250
211
  ```js
251
212
  const coverage = layout.coverage?.({ pad: 4 }) ?? null;
@@ -254,18 +215,14 @@ if (!coverage) {
254
215
  }
255
216
  ```
256
217
 
257
- It is optional on purpose: ntk's layout answers it (X11, Wayland, the mock),
258
- and so do DirectWrite's (Windows) and CoreText's (macOS), each from the
259
- release that added it. Feature-detect, keep a readback, and never assume
260
- the two are pixel-identical — `draw` snaps and hints, coverage does
261
- neither.
218
+ It is optional — ntk's layout (X11, Wayland, the mock), DirectWrite's and
219
+ CoreText's answer it from the release that added it — so feature-detect and
220
+ keep a readback. It is not `draw` pixel for pixel: `draw` snaps and hints.
262
221
 
263
222
  ### Answering for your own text
264
223
 
265
- _Issue #259._ An element that draws text can also let the user **select**
266
- it. There is no registration call and no list of blessed kinds: a
267
- `selectable` ancestor walks its subtree and asks every node four questions,
268
- and an element that answers them is part of the document.
224
+ A `selectable` ancestor lets the user select the text of any descendant
225
+ that answers four questions; there is no registration.
269
226
 
270
227
  ```js
271
228
  class LogViewNode extends Node {
@@ -317,52 +274,33 @@ class LogViewNode extends Node {
317
274
  }
318
275
  ```
319
276
 
320
- **Two spaces, and both are the ones already in use.** Indices are **code
321
- points** — the space ntk's `TextLayout.caretPosition`/`indexAt` speak, so an
322
- emoji is one position and not two — and rectangles are in the **owning
323
- window's** coordinates, the same as `abs` and `contentBox()`: device pixels.
324
- (A mouse event's `x`/`y` are the same coordinates in logical pixels —
325
- [scale.md](scale.md).) An element that answers in its own local coordinates
326
- is one whose highlight is drawn somewhere else on the screen.
327
-
328
- **Answer from what you draw.** `<text>` computes its layout and its origin
329
- in one place and uses it for painting and for all four accessors, because a
330
- caret answered from a differently-placed layout is a caret in the wrong
331
- place and nothing about it looks like a bug in the accessor. If your element
332
- scrolls, the origin has the scroll offset in it — that is what makes a hit
333
- test right after the user has scrolled.
334
-
335
- **One band per line, and one per direction run.** A selection is contiguous
336
- in _logical_ order and a line is laid out in _visual_ order, so a range that
337
- crosses from Latin into Arabic covers two disjoint stretches of pixels. If
338
- you are drawing with ntk's `TextLayout`, walk `line.runs` and intersect each
339
- one with the range rather than filling from one caret x to the other;
340
- `<text>` does this and `test/text-selection.test.js` pins it.
341
-
342
- **Say if you own a selection of your own.** An element that edits — anything
343
- with a caret the user moves — sets `hasOwnSelection = true` in its
344
- constructor. A `selectable` document then skips its subtree whole and never
345
- takes its presses, which is what keeps a `<textinput>` inside a document
346
- from having half of it lit by somebody else's drag.
347
-
348
- **Presses arrive through the base class.** `Node`'s `defaultMouseDown`,
349
- `defaultMouseDrag`, `defaultMouseUp` and `defaultKeyDown` hand the gesture
350
- to the nearest `selectable` ancestor, so an element that only draws needs to
351
- write none of them. An element that overrides one and does not call `super`
352
- has taken that gesture for itself — right for an editor, and the reason a
353
- drawing element that wants both should end its handler with
354
- `super.defaultMouseDown(ev)`.
355
-
356
- Everything about the surface side — what a copy assembles, what the
357
- separators are, which keys are bound — is in
277
+ - **Indices are code points** (ntk's `TextLayout.caretPosition`/`indexAt`:
278
+ an emoji is one position). **Rects are the owning window's device
279
+ pixels**, like `abs`; a mouse event's `x`/`y` are the same in logical
280
+ pixels ([scale.md](scale.md)).
281
+ - **Answer from what you draw**: one layout and origin for painting and all
282
+ four accessors. If the element scrolls, the origin includes the offset.
283
+ - **One band per line, and per direction run.** A selection is contiguous
284
+ in logical order, a line laid out in visual order, so a Latin-to-Arabic
285
+ range covers disjoint stretches: walk ntk's `line.runs` and intersect each
286
+ with the range.
287
+ - **An element that edits owns its selection**: `hasOwnSelection = true` in
288
+ the constructor, and a `selectable` document skips its subtree and its
289
+ presses.
290
+ - **Presses arrive through the base class.** `Node`'s `defaultMouseDown`,
291
+ `defaultMouseDrag`, `defaultMouseUp` and `defaultKeyDown` hand the
292
+ gesture to the nearest `selectable` ancestor. Override one without
293
+ `super` and you take the gesture (right for an editor); to keep both, end
294
+ with `super.defaultMouseDown(ev)`.
295
+
296
+ What a copy assembles, the separators, the keys:
358
297
  [elements.md](elements.md#selecting-text).
359
298
 
360
299
  ### A size of your own
361
300
 
362
- `<box>` is as big as its style and its children say; a gauge, a chart, a
363
- terminal or an editor is as big as its _content_ says. An element with a size
364
- of its own implements **`measureContent`**, and layout asks it whenever the
365
- size on an axis is the element's to give:
301
+ An element as big as its content — a gauge, a chart, a terminal, an editor
302
+ — implements **`measureContent`**, asked whenever the size on an axis is the
303
+ element's to give:
366
304
 
367
305
  ```js
368
306
  const TICK = 30;
@@ -383,77 +321,61 @@ class GaugeNode extends Node {
383
321
  }
384
322
  ```
385
323
 
386
- `<gauge ticks={6} />` is then 180×24 with nothing in the style saying so, the
387
- way `<textarea rows={6}>` is six lines tall.
388
-
389
- It has to be a **method on the class**: the base `Node` constructor is what
390
- wires it to layout, so an element that assigns it in its own constructor is
391
- too late and never gets asked. `invalidateMeasure()` says so in development.
324
+ `<gauge ticks={6} />` is then 180×24 with no style saying so. It must be a
325
+ **method on the class**: the base constructor wires it to layout, so one
326
+ assigned in your constructor is never asked (`invalidateMeasure()` says so
327
+ in development).
392
328
 
393
329
  #### What it is asked
394
330
 
395
- The argument is `{ width, height, widthMode, heightMode }`. Each mode says
396
- what the number beside it means, and an unbounded axis arrives as `Infinity`
397
- rather than as a sentinel — so `Math.min(preferred, width)` is the right
398
- answer in all three, and the mode is there for the cases where it is not:
399
-
400
- | asked with | the element answers |
401
- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------ |
402
- | `unconstrained` | its natural size — nothing bounds this axis (`<textarea>`: its preferred width, `rows` tall) |
403
- | `at-most` | its natural size, clamped into the offer |
404
- | `at-most` with 0 | **the smallest it can be drawn at** — an editor: a character or two of one line; something that scrolls inside: honestly small |
405
- | `exactly` | the style decided this axis; hand the number back and answer for the other one |
406
-
407
- The `at-most 0` row is the one worth reading twice, because it is not only
408
- the layout that asks it. The renderer measures the smallest size every node
409
- can be drawn at — by laying the tree out with **no room at all** and asking
410
- each leaf directly — and writes that back as the element's own floor, so a
411
- row too narrow for it squeezes it that far and no further
412
- ([elements.md](elements.md#everything-shrinks-nothing-shrinks-to-nothing)).
413
- The same measurement read at the top of the tree is what
414
- `<window minWidth="auto">` sends the window manager. An element that answers
415
- 0 there is saying it can be squeezed to nothing, and both floors come out
416
- without it.
417
-
418
- `at-most` is what is _available_, not a cap that will be enforced: an element
419
- may answer larger and it will be laid out at what it asked for, overflowing
420
- whatever contains it. That is how `<text textWrap="nowrap">` works, and it is
421
- the honest answer for content that genuinely does not reflow — a code block
422
- that scrolls sideways rather than wrapping.
331
+ `{ width, height, widthMode, heightMode }`, an unbounded axis as `Infinity`,
332
+ so `Math.min(preferred, width)` is right in every mode; the mode is for when
333
+ it is not:
334
+
335
+ | asked with | the element answers |
336
+ | ---------------- | -------------------------------------------------------------------------------------- |
337
+ | `unconstrained` | its natural size (`<textarea>`: its preferred width, `rows` tall) |
338
+ | `at-most` | its natural size, clamped into the offer |
339
+ | `at-most` with 0 | **the smallest it can be drawn at** — an editor: a character or two; a scroller: small |
340
+ | `exactly` | the style decided; hand the number back and answer for the other axis |
341
+
342
+ The renderer asks `at-most 0` itself, laying the tree out with no room to
343
+ find each leaf's floor: a row too narrow squeezes the element that far and
344
+ no further ([elements.md](elements.md#everything-shrinks-nothing-shrinks-to-nothing)),
345
+ and at the top of the tree it is what `<window minWidth="auto">` sends the
346
+ window manager. Answer 0 and both floors are lost.
347
+
348
+ `at-most` is available room, not a cap: answer larger and the element is
349
+ laid out at that and overflows, as `<text textWrap="nowrap">` does — right
350
+ for content that does not reflow.
423
351
 
424
352
  #### What it must not do
425
353
 
426
- `measureContent` is a **question about a hypothetical**, asked several times
427
- per layout and at sizes nothing will ever be drawn at. So:
354
+ It is a question about a hypothetical, asked several times per layout at
355
+ sizes nothing will be drawn at:
428
356
 
429
- - **do not read `this.abs`** — it is the result of the layout doing the
430
- asking, and last frame's answer at that;
431
- - **do not paint, invalidate, or set state.** Cache derived work if it is
432
- expensive (`<text>` memoizes its shaped layout per width), but keep the
433
- answer a pure function of the content and the constraints;
434
- - **answer with finite numbers.** Content that has not arrived yet is
435
- `{ width: 0, height: 0 }`, not `undefined` — anything else throws naming
436
- the element, because a `NaN` would otherwise spread silently through every
437
- ancestor's rect.
357
+ - **do not read `this.abs`** — it is last frame's;
358
+ - **do not paint, invalidate, or set state** — cache expensive work (as
359
+ `<text>` memoizes its layout per width), but answer as a pure function of
360
+ content and constraints;
361
+ - **answer finite numbers** — `{ width: 0, height: 0 }` for content not yet
362
+ arrived; anything else throws naming the element.
438
363
 
439
- An element that measures itself **cannot have children**: layout sizes it
440
- from the measure function and gives its children no say, so rendering one
441
- inside it is an error naming both elements.
364
+ An element that measures itself **cannot have children**; rendering one
365
+ inside it is an error naming both.
442
366
 
443
367
  #### Saying the answer moved
444
368
 
445
- Layout caches what an element measured to. `invalidateMeasure(reason?)` is
446
- how the element says that cache is stale — a prop the measurement read
447
- changed, data arrived, a font loaded — and the next frame asks again. Nothing
448
- does this for you: only the element knows which of its inputs the measurement
449
- reads. `reason` joins the closed set the diagnostics print
450
- ([debugging.md](debugging.md#invalidation-reasons)); the default is
451
- `'measure'`.
369
+ Layout caches the measurement. Call `invalidateMeasure(reason?)` when an
370
+ input it reads changes — a prop, arriving data, a loaded font — and the next
371
+ frame asks again; nothing else knows your inputs. `reason` is from the
372
+ diagnostics' closed set ([debugging.md](debugging.md#invalidation-reasons)),
373
+ default `'measure'`.
452
374
 
453
375
  #### Content with an aspect ratio
454
376
 
455
- The `<image>`/`<svg>` shape — a natural size to keep the proportions of — is
456
- one call:
377
+ `intrinsicSize` is the `<image>`/`<svg>` measurement, a natural size whose
378
+ proportions are kept:
457
379
 
458
380
  ```js
459
381
  import { Node, intrinsicSize } from 'react-x11/node';
@@ -465,41 +387,36 @@ class ThumbNode extends Node {
465
387
  }
466
388
  ```
467
389
 
468
- It shrinks to the width on offer with the height following, and scales the
469
- width from a style height when only that axis is fixed — which is what an
470
- `<img>` does, and is the whole of `<image>`'s own measurement.
390
+ It shrinks to the width on offer, the height following, and scales the
391
+ width from a style height when only that is fixed, as `<img>` does.
471
392
 
472
393
  ### Behaviour of your own
473
394
 
474
- An element that _does_ something — an editor, a terminal, a table with cell
475
- editing, a tree with type-ahead — implements the **default actions**. They are
476
- the same seam `<textinput>`'s editing runs on, and the ordering is the whole
477
- point:
395
+ An element that does something — an editor, a terminal, a table with cell
396
+ editing, a tree with type-ahead — implements **default actions**, the seam
397
+ `<textinput>` runs on. Order:
478
398
 
479
- 1. the application's `onKeyDown` / `onMouseDown` / … handlers, capture then
480
- bubble;
481
- 2. **if none of them called `ev.preventDefault()`**, the element's `default*`
399
+ 1. the app's `onKeyDown` / `onMouseDown` / … handlers, capture then bubble;
400
+ 2. **unless one called `ev.preventDefault()`**, the element's `default*`
482
401
  method.
483
402
 
484
- So an app can veto the behaviour, or run something before it, without knowing
485
- how the element is built — which is what `<textinput onKeyDown>` has always
486
- been able to do, and is the reason to implement behaviour here rather than in
487
- a React component wrapping the element.
488
-
489
- | method | when |
490
- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
491
- | `defaultKeyDown(ev)` | a key, delivered to the focused node. `ev.keysym` is the `XK_*` constant — the Latin one, so a chord matches under any layout; `codepoint >= 0x20` is the test for "this is text" |
492
- | `defaultKeyUp(ev)` | that key was released. Rare — nothing in core needs it — and there for an element that answers the whole keystroke rather than the press, such as `<foreign>` forwarding into an embedded client |
493
- | `defaultComposition(ev)` | text still being typed — a dead key, a Compose sequence. `ev.type` is the phase, `ev.data` what to show, or at the end what to insert |
494
- | `defaultMouseDown(ev)` | a press. Place a caret, grab a handle; `ev.capturePointer()` to keep the rest of the gesture |
495
- | `defaultMouseDrag(ev)` | motion while _this_ element holds the press — including outside its box, which `onMouseMove` does not give you |
496
- | `defaultMouseUp(ev)` | that press was released |
497
- | `defaultMouseMove(ev)` | the pointer moved over this element, no button down — light the node, the edge, the handle it is over. The first one after it arrives is the enter |
498
- | `defaultMouseLeave(ev)` | …and the pointer left: put that back |
499
- | `defaultWheel(ev)` | the wheel, before the scroll chain — for the element whose wheel is not a scroll. `ev.preventDefault()` consumes it |
500
- | `defaultContextMenu(ev)` | right-click, after the press: open your own menu. Separate so suppressing it keeps the caret placement |
501
- | `defaultFocus(info)` | this element became the focused node (or its window got the X focus back): start the caret blinking. `info` is `{ reason, backwards }` — `'key'` with a direction is a Tab, which is the only thing that has ever needed it |
502
- | `defaultBlur()` | focus left, or the window lost it: stop it |
403
+ So an app can veto or precede the behaviour without knowing how the element
404
+ is built — the reason to put it here rather than in a wrapping component.
405
+
406
+ | method | when |
407
+ | ------------------------ | ------------------------------------------------------------------------------------------------ |
408
+ | `defaultKeyDown(ev)` | a key, at the focused node. `ev.keysym`: the Latin `XK_*`, any layout; `codepoint >= 0x20`: text |
409
+ | `defaultKeyUp(ev)` | the key was released. Rare: for answering the whole keystroke, as `<foreign>` does |
410
+ | `defaultComposition(ev)` | a dead key or Compose in progress: `ev.type` the phase, `ev.data` what to show or insert |
411
+ | `defaultMouseDown(ev)` | a press: place a caret, grab a handle; `ev.capturePointer()` keeps the gesture |
412
+ | `defaultMouseDrag(ev)` | motion while _this_ element holds the press, even outside its box |
413
+ | `defaultMouseUp(ev)` | that press was released |
414
+ | `defaultMouseMove(ev)` | pointer motion over it, no button down. The first is the enter |
415
+ | `defaultMouseLeave(ev)` | the pointer left: unlight it |
416
+ | `defaultWheel(ev)` | the wheel, before the scroll chain, for a wheel that is not a scroll |
417
+ | `defaultContextMenu(ev)` | right-click, after the press (preventing it keeps the caret): open your menu |
418
+ | `defaultFocus(info)` | it, or its window, got focus. `info`: `{ reason, backwards }`; `'key'` + direction is Tab |
419
+ | `defaultBlur()` | focus left, or the window lost it: stop the caret |
503
420
 
504
421
  ```js
505
422
  import { Node, CARET_BLINK_MS } from 'react-x11/node';
@@ -549,34 +466,19 @@ class EditorNode extends Node {
549
466
  }
550
467
  ```
551
468
 
552
- Details worth having in writing:
553
-
554
- **A gesture is vetoed once, at its press.** `defaultMouseDrag` and
555
- `defaultMouseUp` are the continuation of the press `defaultMouseDown` got, so
556
- a handler that prevented the press means none of the three run — an element
557
- never hears about motion it has no press behind, and does not need a
558
- `dragging` flag of its own to discover that.
559
-
560
- **Hover is motion, not a state you are handed.** `defaultMouseMove` runs for
561
- the element the pointer is over — coalesced to once per frame, like every
562
- motion — and `defaultMouseLeave` when it goes elsewhere, which is everything
563
- a scene element needs to light the node, edge or handle under the pointer and
564
- put it back. There is no `defaultMouseEnter`: the first motion after the
565
- pointer arrives _is_ the enter, and it carries the position an enter would
566
- have to be asked for. A capture suspends the pair — while a gesture owns the
567
- pointer, hover stays frozen where it was and the motion goes to
568
- `defaultMouseDrag` instead. And a node that unmounts while hovered is
569
- _forgotten_ rather than left, exactly as one that unmounts while focused is,
570
- so anything with a lifetime behind the highlight is released in
571
- `destroySubtree` as well.
572
-
573
- **The cursor is the point's.** `defaultCursor` is one cursor for the whole
574
- element. An element that draws a document, a graph or an editor has one per
575
- part — a link, a handle, text — and names it with `cursorAt(x, y)`, asked on
576
- every motion over it with the pointer in device pixels, the unit `abs` is
577
- in. It answers a cursor name, or null for nothing in particular, which falls
578
- through to `defaultCursor` and then to the parent. A `cursor` style on the
579
- element still wins, and a capture freezes the cursor with the rest of hover.
469
+ - **A gesture is vetoed once, at its press**: a prevented press means no
470
+ `defaultMouseDown`, `defaultMouseDrag` or `defaultMouseUp`.
471
+ - **Hover is motion.** `defaultMouseMove` runs for the element under the
472
+ pointer, coalesced to once per frame, and `defaultMouseLeave` when it
473
+ goes; the first motion is the enter (there is no `defaultMouseEnter`).
474
+ A capture freezes hover and sends motion to `defaultMouseDrag`. A node
475
+ unmounted while hovered is forgotten, not left: release in
476
+ `destroySubtree` what the highlight holds.
477
+ - **The cursor is the point's.** `defaultCursor` covers the element;
478
+ `cursorAt(x, y)`, asked on every motion in device pixels like `abs`,
479
+ answers one per part (a link, a handle, text), or null to fall through to
480
+ `defaultCursor`, then the parent. A `cursor` style still wins; a capture
481
+ freezes it.
580
482
 
581
483
  ```js
582
484
  cursorAt(x, y) {
@@ -584,17 +486,12 @@ cursorAt(x, y) {
584
486
  }
585
487
  ```
586
488
 
587
- **The wheel that is not a scroll.** `canScroll`/`scrollBy`
588
- ([below](#scrolling-content-you-painted)) route the wheel for content that
589
- scrolls, and they hand the element deltas only — which is the whole of the
590
- gesture when the answer is "move by that much". It is not the whole of a zoom
591
- about the pointer: that needs the point that must _not_ move (`ev.x`/`ev.y`)
592
- and the modifier that tells a zoom from a pan, and it answers the wheel
593
- whether or not anything "can scroll". `defaultWheel(ev)` is the whole event,
594
- at the node under the pointer, before the chain walks; `ev.preventDefault()`
595
- consumes it and the walk never runs. The deltas are as the device measured
596
- them, fractions included — the whole-pixel rule the chain follows is there
597
- for the scroll blit, and a zoom factor is continuous.
489
+ - **A wheel that is not a scroll** — a zoom about the pointer — needs the
490
+ point (`ev.x`/`ev.y`) and the modifier, not just the deltas
491
+ `canScroll`/`scrollBy` get ([below](#scrolling-content-you-painted)).
492
+ `defaultWheel(ev)` gets the whole event at the node under the pointer
493
+ before the scroll chain walks; `ev.preventDefault()` stops the walk. Its
494
+ deltas keep their fractions.
598
495
 
599
496
  ```js
600
497
  defaultWheel(ev) {
@@ -604,41 +501,24 @@ defaultWheel(ev) {
604
501
  }
605
502
  ```
606
503
 
607
- **Focus is a prerequisite, not a consequence.** Keys are delivered to the
608
- focused node, and nothing focuses an element that has not said it can be:
609
- `focusableByDefault = true` makes it a tab stop and a press target the way
610
- `<textinput>` is one, and an application's `focusable={false}` or `tabIndex`
611
- still overrides it ([events.md](events.md#focus)).
612
-
613
- **Tab is an ordinary key here.** It reaches `defaultKeyDown` before focus
614
- traversal sees it, so an element can keep it — an editor indenting with Tab
615
- does not need a wrapping component to get it, which is what the ordering used
616
- to force. Consuming it means calling `ev.preventDefault()` from the default
617
- action: what that prevents is the default action _after_ this one, which for
618
- Tab is the focus cycle.
619
-
620
- **`super.defaultKeyDown(ev)` is the base class's own behaviour**, and there
621
- are two things in it: the text-selection keys, and Space/Enter dispatching
622
- the node's `onClick` as a click
623
- ([events.md](events.md#space-and-enter-are-a-click)). An override that
624
- answers the keystroke itself — an editor, where Space types a space — simply
625
- does not call it, which is what `<textinput>` does. One that answers a few
626
- keys and leaves the rest, as the scroll pane does, ends its `default:` branch
627
- with `return super.defaultKeyDown(ev)` and keeps both.
628
-
629
- An element that eats Tab has taken the keyboard user's only way out of it, so
630
- it owes them another. The convention, above and worth converging on: **Escape
631
- arms one pass-through Tab.** Escape on its own does nothing visible, the next
632
- Tab leaves, and the one after that indents again — the same bargain a code
633
- editor on the web makes, and the reason a screen-reader user is not stuck in
634
- your element.
635
-
636
- **An element that types text implements `defaultComposition` too.** `é` is
637
- a dead key and then a letter, and the keys that make it never reach
638
- `defaultKeyDown` — so an element that only implements the key path types the
639
- letter and drops the accent, which is the bug
640
- [events.md](events.md#composition) describes. Three phases, and the third
641
- is the only one that inserts anything:
504
+ - **Focus is a prerequisite.** Keys go to the focused node, and only an
505
+ element with `focusableByDefault = true` is focused (a tab stop and press
506
+ target, like `<textinput>`); the app's `focusable={false}` or `tabIndex`
507
+ still overrides it ([events.md](events.md#focus)).
508
+ - **Tab is an ordinary key**: it reaches `defaultKeyDown` before focus
509
+ traversal, and `ev.preventDefault()` there keeps focus where it is.
510
+ - **`super.defaultKeyDown(ev)`** is the text-selection keys, and Space/Enter
511
+ dispatching `onClick` ([events.md](events.md#space-and-enter-are-a-click)).
512
+ An editor that answers every key (Space types a space) skips it, as
513
+ `<textinput>` does; one answering a few keys ends with
514
+ `return super.defaultKeyDown(ev)`, as the scroll pane does.
515
+ - **An element that eats Tab owes a way out.** The convention, shown above:
516
+ **Escape arms one pass-through Tab** — the next Tab leaves, the one after
517
+ indents again — so no keyboard or screen-reader user is trapped.
518
+ - **An element that types text implements `defaultComposition`.** Dead keys
519
+ and Compose sequences never reach `defaultKeyDown`, so without it `é`
520
+ types as `e` ([events.md](events.md#composition)). Only the last phase
521
+ inserts:
642
522
 
643
523
  ```js
644
524
  defaultComposition(ev) {
@@ -652,32 +532,25 @@ defaultComposition(ev) {
652
532
  }
653
533
  ```
654
534
 
655
- **An element that forwards keys somewhere else sets `composes = false`**
656
- instead. Composition is ours to run only when the text lands in our tree;
657
- an element handing the raw key event to another program — `<foreign>` and
658
- its embedded client — wants the dead key itself, and the client's own input
659
- method takes it from there.
660
-
661
- The preedit is deliberately not part of your value: it never reaches
662
- `onChange` or the undo history, a controlled value rewritten mid-composition
663
- cannot corrupt it, and abandoning it — Escape, focus leaving — is dropping a
664
- string rather than undoing an edit. Not implementing it at all is a
665
- supported position for an element that holds no text; the composition then
666
- simply goes nowhere.
667
-
668
- **A caret blinks at `CARET_BLINK_MS`.** It is the cadence `<textinput>` uses,
669
- exported so that two carets on one screen are in step rather than a few tens
670
- of milliseconds apart. Stop the timer in `defaultBlur` _and_ in
671
- `destroySubtree`: a node that unmounts while focused is forgotten rather than
672
- blurred, so `defaultBlur` is not guaranteed to run.
535
+ The preedit is not part of your value: it never reaches `onChange` or the
536
+ undo history, a controlled value rewritten mid-composition cannot corrupt
537
+ it, and abandoning it (Escape, blur) drops a string, not an edit. An
538
+ element holding no text can leave composition out; one that **forwards keys
539
+ to another program** (`<foreign>`) sets `composes = false` so the dead key
540
+ reaches the client's input method.
541
+
542
+ **A caret blinks at `CARET_BLINK_MS`**, `<textinput>`'s cadence, so carets
543
+ blink in step. Stop the timer in `defaultBlur` _and_ `destroySubtree`: a
544
+ node unmounted while focused is never blurred.
673
545
 
674
546
  ### Text a screen reader can read
675
547
 
676
- An element that draws text of its own is, to an assistive technology,
677
- nothing at all: a painted rectangle with no characters, no caret and no
678
- selection in it. `<textinput>` is not — it implements AT-SPI's `Text` and
679
- `EditableText` interfaces in full — and the difference is one method
680
- (issue #257):
548
+ To an assistive technology, an element that draws its own text is a
549
+ rectangle with no characters, caret or selection. `a11yTextState()` gives it
550
+ what `<textinput>` has, through the same code: AT-SPI's `Text` (characters,
551
+ words, lines, caret, selection, and the `text-changed` / `text-caret-moved`
552
+ / `text-selection-changed` events Orca narrates) and, for an editor,
553
+ `EditableText`.
681
554
 
682
555
  ```js
683
556
  class EditorNode extends Node {
@@ -699,76 +572,41 @@ class EditorNode extends Node {
699
572
  }
700
573
  ```
701
574
 
702
- That alone buys the whole read side: character count, reading by character,
703
- word and line, the caret, the selection, and the `text-changed` /
704
- `text-caret-moved` / `text-selection-changed` deltas Orca narrates from —
705
- through the same code that serves `<textinput>`, so a third-party editor is
706
- read by the paths core is read by rather than by a parallel set of them.
707
-
708
- | member | when |
709
- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
710
- | `a11yTextState()` | always, for an element holding text. `null` for one that holds none |
711
- | `notifyA11yTextChanged()` | every edit, caret move, selection change and composition — the state is _pulled_ when this says it moved |
712
- | `a11ySetSelection(start, end)` | an AT moved the caret or selected a range; the caret belongs at `end`. Without it the element is read but not navigated |
713
- | `a11yReplaceText(start, end, text)` | an AT edited: replace that range. Insert, delete and set-the-lot all arrive here. Without it the element is read but not typed in |
714
- | `a11yRole` | the ARIA role this element is when the app writes none — `'textbox'`, `'log'`, `'document'`. Assign it in the constructor |
715
-
716
- **The two tiers are the two kinds of element.** A viewer — a markdown
717
- document, a code block, a log, a terminal — reports `value` and a selection
718
- and stops there: it gets the `Text` interface, a `document` role, and a
719
- selection an AT can read and set, which is the whole of what "select this
720
- paragraph and copy it" needs. An editor adds `editable: true` and
721
- `a11yReplaceText`, which is what turns on the EDITABLE state and the
722
- `EditableText` interface. An element that claims `editable` without
723
- implementing the write **is** still announced as editable and reports its
724
- own edits — it simply is not exposed as one an AT may type into, because an
725
- interface whose every method answered "no" is a lie an AT cannot see
726
- through.
727
-
728
- Four details, each of which is a defect the other way round:
729
-
730
- **Offsets are code points and they index what you draw.** `Array.from(value)`
731
- is the counting, and `value` is the string on the screen — an open
732
- composition included, because every geometric answer beside it (the extents
733
- of character _n_, a magnifier's highlight) is read off the glyphs a sighted
734
- user sees. Say which part is uncommitted with `preedit: { offset, text }`
735
- and its churn is marked as text the user did not type, exactly as
736
- `<textinput>`'s is ([accessibility.md](accessibility.md#while-a-composition-is-open)).
737
- Offsets you report are clamped for you, so a caret left stale between an
738
- edit and your own bookkeeping cannot become an out-of-range answer on the
739
- wire.
740
-
741
- **`notifyA11yTextChanged()` is free when nobody is listening** — one
742
- property read, the hook slots being empty until a bridge or the test spy
743
- fills them. So call it from every path that moves the text, rather than
744
- asking whether accessibility is on.
745
-
746
- **`a11yTextState()` is called several times per change**, for the role, the
747
- states and the diff. Answer from state the element already holds; it must
748
- not shape, copy a buffer, or invalidate.
749
-
750
- **Say `multiline`.** Left unsaid, neither the single-line nor the
751
- multi-line state is claimed — better than a guess read off the current
752
- value, which would report the element's shape _changing_ the first time
753
- somebody pressed Enter.
754
-
755
- The behaviour is assertable without a screen reader, a desktop or a bus:
756
- `renderX11(el, { a11y: true })` hands back a spy that records the same feed
757
- ([accessibility.md](accessibility.md#asserting-what-a-screen-reader-would-hear)),
758
- and `test/a11y-custom-text.test.js` in this repo is a worked editor and a
759
- worked viewer driven through it.
575
+ | member | when |
576
+ | ----------------------------------- | ---------------------------------------------------------------------------------- |
577
+ | `a11yTextState()` | always, for an element holding text; `null` if none |
578
+ | `notifyA11yTextChanged()` | every edit, caret move, selection or composition change; state is then _pulled_ |
579
+ | `a11ySetSelection(start, end)` | an AT moved the caret (to `end`) or selected. Without it: read, not navigated |
580
+ | `a11yReplaceText(start, end, text)` | an AT inserted, deleted or set the lot. Without it: read, not typed in |
581
+ | `a11yRole` | the ARIA role if the app sets none (`'textbox'`, `'log'`…); set in the constructor |
582
+
583
+ - **Viewer or editor.** A viewer (a document, a code block, a log, a
584
+ terminal) reports `value` and a selection: it gets `Text`, a `document`
585
+ role, and a selection an AT can read and set. An editor adds
586
+ `editable: true` and `a11yReplaceText` for the EDITABLE state and
587
+ `EditableText`; `editable` without `a11yReplaceText` is announced as
588
+ editable but not exposed as typeable.
589
+ - **Offsets are code points into what you draw** (`Array.from(value)`), an
590
+ open composition included, since extents and magnifiers follow the
591
+ glyphs. Mark it with `preedit: { offset, text }` and its churn is reported
592
+ as text the user did not type
593
+ ([accessibility.md](accessibility.md#while-a-composition-is-open)).
594
+ Offsets you report are clamped.
595
+ - **`notifyA11yTextChanged()` is free when nobody listens**: call it on
596
+ every path that moves the text.
597
+ - **`a11yTextState()` runs several times per change**: answer from held
598
+ state; never shape, copy a buffer, or invalidate.
599
+ - **Say `multiline`**, or neither state is claimed (a guess from the value
600
+ would flip the first time someone pressed Enter).
601
+
602
+ `renderX11(el, { a11y: true })` returns a spy recording the same feed
603
+ ([accessibility.md](accessibility.md#asserting-what-a-screen-reader-would-hear)).
760
604
 
761
605
  ### A scene a screen reader can walk
762
606
 
763
- _Issue #304._ An element that draws a hundred interactive things is still
764
- one node, so it is one accessible: a graph pane full of selectable,
765
- draggable, connectable nodes reads out as **"Flow graph, group"** and stops
766
- there. Nothing in it can be found, named, activated or told apart, and no
767
- amount of `role`/`aria-*` on the element itself changes that — the objects
768
- an assistive technology would need are not in the tree, because you drew
769
- them.
770
-
771
- Describe them and they are:
607
+ An element that draws many interactive things is one node, so one
608
+ accessible: a graph pane reads as **"Flow graph, group"**, nothing in it
609
+ findable or actionable, whatever `role`/`aria-*` it carries. Describe them:
772
610
 
773
611
  ```js
774
612
  class FlowNode extends Node {
@@ -792,70 +630,48 @@ class FlowNode extends Node {
792
630
  }
793
631
  ```
794
632
 
795
- Each becomes a real child of your element's accessible: named, placed,
796
- walked into, focusable, with `selected` announced as it changes — the same
797
- model a `<box role="listitem" aria-selected>` goes through, because that is
798
- literally what these are read as. `id` is the whole of what core needs from
799
- you: it is what makes the child that was there last frame the same child
800
- this frame, and therefore what keeps every reference a screen reader is
801
- holding alive across a scene you rebuild sixty times a second.
633
+ Each item becomes a real child accessible — named, placed, focusable,
634
+ `selected` announced — read as `<box role="listitem" aria-selected>` would
635
+ be. `id` keeps a child the same child across frames, so a screen reader's
636
+ references survive a scene rebuilt every frame.
802
637
 
803
- | member | when |
804
- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
805
- | `a11yScene()` | always, for an element that draws interactive things. Called once per question about your children — a cheap read, never a pass |
806
- | `notifyA11ySceneChanged()` | the scene moved between commits: a drag, an animation, your own arrow keys. A commit re-reads it on its own |
807
- | `a11ySceneAction(id, action)` | an AT acted on one of them. Return `true` to claim it; anything else keeps core's answer |
638
+ | member | when |
639
+ | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
640
+ | `a11yScene()` | for an element drawing interactive things. Called per question: keep it cheap |
641
+ | `notifyA11ySceneChanged()` | the scene moved between commits (a drag, an animation, your arrow keys); a commit re-reads it itself |
642
+ | `a11ySceneAction(id, action)` | an AT acted on one. Return `true` to claim it, else core answers |
808
643
 
809
644
  What one item may say:
810
645
 
811
- | field | |
812
- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
813
- | `id` | required. Its name for as long as it is drawn — ids are matched within their parent, not globally |
814
- | `rect` | required. Where it is, in window coordinates: where AT focus is drawn and what a magnifier follows |
815
- | `role` | an ARIA role. `'group'` if it says none |
816
- | `name` / `description` | what a screen reader says. An unnamed item announces as having none, which is the defect being loud |
817
- | `states` | `selected`, `checked`, `expanded`, `disabled`, `busy`, and `focused` for your own keyboard cursor |
818
- | `focusable` | default true |
819
- | `props` | anything else an element carries — `aria-posinset`/`aria-setsize` for Orca's "3 of 7", `aria-level`, `aria-valuenow` |
820
- | `children` | a scene with structure: series and points, groups and nodes |
821
-
822
- Four things worth knowing before you write one:
823
-
824
- **`focused` is the only way your cursor is reported.** The window's focus
825
- manager holds your element, not the thing inside it that arrow keys move —
826
- which is right, and it means the state has to come from you. Reporting it
827
- is all that is asked: navigation between items stays yours.
828
-
829
- **Actions fall back rather than fail.** With no `a11ySceneAction`, an
830
- activation is a synthetic click at the item's own rect — you hit-test your
831
- scene already, so the rect is the whole address and an AT's click is
832
- indistinguishable from a mouse user's. `focus` focuses the element,
833
- `scroll` reveals it. Implement the seam for the cases where that is wrong
834
- (an activation that is not a click, a cursor to move, a viewport of your
835
- own to scroll), claim those with `true`, and let the rest fall through.
836
-
837
- **An action needs a role that promises one** — `button`, `option`,
838
- `treeitem`, `checkbox` — _or_ your `a11ySceneAction`, which is the same
839
- promise made directly. A `listitem` with neither is read, not clicked, the
840
- same as everywhere else in ARIA.
841
-
842
- **`a11yScene()` is called once per question**, so answer from what you
843
- already hold. It is the same rule `a11yTextState()` follows, for the same
844
- reason: a shaping pass or a copy of your model behind it turns a screen
845
- reader walking the tree into a frame budget.
846
-
847
- Assertable with no bus and no desktop, through the same spy:
848
- `renderX11(el, { a11y: true })` records an item's states changing exactly as
849
- it records a `<Checkbox>`'s, and `test/a11y-scene.test.js` is a worked graph
850
- pane. The wire itself — children, extents, actions — is covered in
851
- `test/atspi.test.js`.
646
+ | field | |
647
+ | ---------------------- | ------------------------------------------------------------------------------------------- |
648
+ | `id` | required. Its name while it is drawn, matched within its parent |
649
+ | `rect` | required. Window coordinates: where AT focus is drawn, what a magnifier follows |
650
+ | `role` | an ARIA role; `'group'` if none |
651
+ | `name` / `description` | what a screen reader says; an unnamed item announces as having none |
652
+ | `states` | `selected`, `checked`, `expanded`, `disabled`, `busy`; `focused` for your keyboard cursor |
653
+ | `focusable` | default true |
654
+ | `props` | other ARIA: `aria-posinset`/`aria-setsize` (Orca's "3 of 7"), `aria-level`, `aria-valuenow` |
655
+ | `children` | structure: series and points, groups and nodes |
656
+
657
+ - **`focused` is the only way your cursor is reported**: the focus manager
658
+ holds your element, not the item your arrow keys move. Navigation stays
659
+ yours.
660
+ - **Actions fall back.** Without `a11ySceneAction`, an activation is a
661
+ synthetic click at the item's `rect`, as a mouse user's would be; `focus`
662
+ focuses the element; `scroll` reveals the item. Claim with `true` only
663
+ what differs — a non-click activation, a cursor move, your own viewport.
664
+ - **An action needs a role that promises one** (`button`, `option`,
665
+ `treeitem`, `checkbox`) or your `a11ySceneAction`; a bare `listitem` is
666
+ read, not clicked.
667
+
668
+ The same spy records an item's states changing as it does a `<Checkbox>`'s.
852
669
 
853
670
  ### The standard edit menu
854
671
 
855
- A `<textinput>` gets a right-click Undo / Cut / Copy / Paste / Select All
856
- menu without being asked. An element that edits or selects text of its own
857
- gets **the same menu** — not one that looks like it — by naming its verbs
858
- and letting core do the rest (issue #256):
672
+ An element that edits or selects text of its own gets `<textinput>`'s
673
+ right-click Undo / Cut / Copy / Paste / Select All menu — the same one — by
674
+ naming its verbs:
859
675
 
860
676
  ```js
861
677
  import { openEditMenu } from 'react-x11';
@@ -881,20 +697,14 @@ class EditorNode extends Node {
881
697
  }
882
698
  ```
883
699
 
884
- What that buys, and the reason it is worth an export rather than a recipe:
885
- the rows and their order, the enablement rules, Paste watching **selection
886
- ownership** instead of asking the server on the way to opening a menu
887
- ([elements.md](elements.md#the-right-click-menu)), the arrow keys and
888
- Escape, the pointer grab that dismisses it, and handing the keyboard back to
889
- wherever it came from afterwards. `<textinput>` is a caller of this function,
890
- which is what keeps the two from drifting.
891
-
892
- **A verb you leave out is a row that is not there** — not a greyed one. It
893
- was `<textinput sensitive>` that motivated the rule, where a disabled Copy
894
- over a password reads as a bug in the application rather than as a decision,
895
- but it is also exactly what a **read-only** surface needs: a rendered
896
- document that can be selected and copied passes three things and gets a
897
- two-row menu, with no dead Undo, Cut or Paste to explain.
700
+ Core supplies the rows and order, enablement, Paste following **selection
701
+ ownership** rather than asking the server
702
+ ([elements.md](elements.md#the-right-click-menu)), the keys, the dismissing
703
+ grab, and handing the keyboard back.
704
+
705
+ **A verb you leave out is a row that is not there**, not a greyed one. A
706
+ read-only surface passes three things for a two-row menu; no verbs, no
707
+ menu:
898
708
 
899
709
  ```js
900
710
  openEditMenu(
@@ -908,82 +718,51 @@ openEditMenu(
908
718
  );
909
719
  ```
910
720
 
911
- Leave out every verb and nothing opens at all.
721
+ **Enablement is core's.** Answer `canUndo`, `canRedo` and `canSelectAll`
722
+ (default true; false when empty or all selected) and hand over
723
+ `hasSelection`; Cut and Copy follow the selection, Paste what the server
724
+ said about the clipboard. `at` is the pointer in the **owner window's**
725
+ coordinates (a synthetic event's `x`/`y`); the menu is clamped into its
726
+ monitor.
912
727
 
913
- **Enablement is not yours to decide row by row.** You answer three questions
914
- only the target can answer — `canUndo`, `canRedo`, `canSelectAll` (which
915
- defaults to true: the surface is empty, or all of it is selected already) —
916
- and hand over `hasSelection`. Cut and Copy follow the selection, Paste
917
- follows what the server has said about the clipboard. That is the part a
918
- second implementation would get subtly wrong.
728
+ | function | |
729
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------ |
730
+ | `editMenuOpen(node)` | is it up? (it holds the keyboard; keep your selection lit) |
731
+ | `closeEditMenu(node)` | close it if content changed under it or your surface scrolled (a choice, Escape, a press outside close it already) |
919
732
 
920
- `at` is where the pointer was, in the **owner window's** coordinates — the
921
- `x`/`y` a synthetic event carries. A surface with no caret has nothing else
922
- to offer, and this is what it already has; the menu is placed on the screen
923
- from there, clamped into the monitor it opens on.
733
+ All three come from the **package root**, so a component wrapping an
734
+ element can open the menu from `onContextMenu` and a ref.
924
735
 
925
- Two more functions come with it, both for the element rather than the menu:
736
+ ### Scrolling content you painted
926
737
 
927
- | function | |
928
- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
929
- | `editMenuOpen(node)` | is the menu up? An element that paints a selection asks — the popup holds the keyboard, so the element is not focused, and the selection has to stay lit |
930
- | `closeEditMenu(node)` | close it. The menu already closes itself on a choice, Escape and a press outside; this is for content that changed underneath it, or a surface that scrolled |
738
+ `<box overflow="scroll">` scrolls children. An editor, a terminal or a
739
+ canvas-backed table scrolls pixels it draws, and joins the same machinery
740
+ with:
931
741
 
932
- They are exported from the **package root** rather than from
933
- `react-x11/node`, because the caller is not always an element: a component
934
- that wraps one opens the same menu from an `onContextMenu` handler and a ref.
742
+ | method | |
743
+ | ------------------------ | --------------------------------------------------------------------------------- |
744
+ | `canScroll(dx, dy)` | is there room on the axis this delta names? The wheel asks first |
745
+ | `scrollBy(by)` | `scrollBy(dy)` or `scrollBy({x, y})`: move by that much |
746
+ | `measureScrollContent()` | `Scrollable` only: `{ width, height }` the content reaches. Default: the children |
935
747
 
936
- ### Scrolling content you painted
748
+ **The wheel's default action is the first two.** It walks out from the hit
749
+ node asking `canScroll(deltaX, deltaY)`; the first yes gets `scrollBy`, up
750
+ to the `<window>`. Say no when there is nothing to move and the wheel
751
+ chains to what is behind you, as in a browser (`<textarea>` does). Answer
752
+ from the **extent, not the position**: a viewport at its bottom keeps the
753
+ rest of a flick. The hit node's `defaultWheel`
754
+ ([above](#behaviour-of-your-own)) runs before the walk.
937
755
 
938
- A `<box overflow="scroll">` scrolls **children**: layout knows where they
939
- are, so the extent, the bars, the clamping and the wheel all follow from the
940
- tree. An editor, a terminal or a canvas-backed table has no children — its
941
- content is pixels it draws — and it still has a scroll position, an extent
942
- and a wheel gesture to answer. Two methods make it a member of the same
943
- machinery instead of an exception to it (issue #253):
944
-
945
- | method | |
946
- | ------------------------ | --------------------------------------------------------------------------------------------------- |
947
- | `canScroll(dx, dy)` | is there room to move on the axis this delta names? The wheel asks before it scrolls you |
948
- | `scrollBy(by)` | `scrollBy(dy)` or `scrollBy({x, y})` — move by that much |
949
- | `measureScrollContent()` | `Scrollable` only: `{ width, height }`, how far the content reaches. The default walks the children |
950
-
951
- **The wheel's default action is those first two and nothing else.** It walks
952
- out from the node the pointer hit, asks each one `canScroll(deltaX, deltaY)`,
953
- and the first that says yes gets `scrollBy` — up to the `<window>`, where the
954
- walk stops. So an element joins the chain by answering two questions, and it
955
- **chains** by answering the first of them honestly: say no when there is
956
- nothing left to move and the wheel goes to the pane or the window behind you,
957
- which is what a browser does and what a user flicking through a long page
958
- expects. `<textarea>` is the worked example in core — it scrolls wrapped text
959
- it painted, and since it answers `canScroll` off that text, a short one hands
960
- the gesture outward rather than swallowing it.
961
-
962
- The one thing ahead of the walk is the hit node's own `defaultWheel`
963
- ([above](#behaviour-of-your-own)) — for an element whose wheel is not a
964
- scroll at all, a pane that zooms about the pointer. Consuming the event there
965
- is what keeps the walk from running; leaving it alone is what puts the
966
- element back in this chain.
967
-
968
- Answer `canScroll` from the **extent, not the position**: a viewport already
969
- scrolled to its bottom should keep the rest of a flick rather than pass it
970
- to whatever is behind it.
971
-
972
- Both deltas are **whole pixels**, and stay whole however the scroll was
973
- measured. A touchpad on a server with XI2 reports fractions of a notch and
974
- `ev.deltaY` carries them ([events.md](events.md#wheel)), but the default
975
- action spends only the whole part and carries the rest to the next event —
976
- because a fractional scroll offset is one the server-side blit cannot shift,
977
- and a smooth gesture that repainted every frame would cost more than it
978
- looks. So an element joining the chain never has to think about sub-pixels,
979
- and a slow scroll still moves: a pixel at a time.
756
+ Deltas are **whole pixels**: a touchpad's fractions stay on `ev.deltaY`
757
+ ([events.md](events.md#wheel)), and the default action carries them to the
758
+ next event, so a scroll can always be a blit and a slow one still moves.
980
759
 
981
760
  #### The mixin, for everything else
982
761
 
983
- `canScroll` + `scrollBy` buys the wheel. `Scrollable(Node)` buys the rest —
984
- the offsets, `scrollTo`/`scrollBy`/`scrollIntoView`, the drawn scrollbars and
985
- their drags, the scroll keys (arrows, Page, Home/End, Space), the tab stop
986
- and the AT-SPI scroll-pane role — and all of it reads one number pair:
762
+ `Scrollable(Node)` adds the offsets, `scrollTo`/`scrollBy`/`scrollIntoView`,
763
+ drawn scrollbars and their drags, the scroll keys (arrows, Page, Home/End,
764
+ Space), the tab stop and the AT-SPI scroll-pane role, all reading one
765
+ number pair:
987
766
 
988
767
  ```js
989
768
  import { Node, Scrollable } from 'react-x11/node';
@@ -1021,55 +800,29 @@ class EditorNode extends Scrollable(Node) {
1021
800
  }
1022
801
  ```
1023
802
 
1024
- Three things about that, each of which is a bug if you get it the other way
1025
- round:
1026
-
1027
- **Draw in `paintContent`, not in `paint`.** `paint` draws the background,
1028
- then `paintContent`, then the children, the border, and — for a
1029
- `Scrollable` — the scrollbars last. An element that draws after
1030
- `super.paint(ctx)` returns therefore paints _over its own thumb_.
1031
- `paintContent` is the seam every built-in that draws uses, and it is where a
1032
- drawing belongs whether it scrolls or not.
1033
-
1034
- **Offset the drawing yourself, and clip it.** A `<box>`'s children are moved
1035
- by the scroll during layout; nobody moves pixels. `contentBox()` is the
1036
- viewport to clip to and `scrollX`/`scrollY` are what to subtract — they are
1037
- already clamped to the extent you reported, so there is nothing to bound
1038
- again. `scrollX` is a distance from the edge the content _starts_ at, which
1039
- is the right-hand one under `direction: 'rtl'`
1040
- ([styling.md](styling.md#direction-and-the-logical-edges)) — and the `width` you report is that
1041
- same reach, so an element that draws in its own reading direction never has
1042
- to ask which one it is in.
1043
-
1044
- **Do not assign `focusableByDefault`.** `Scrollable` answers it — a pane with
1045
- somewhere to go is a tab stop, one that fits is not — and assigning over the
1046
- getter throws. Override the getter if your element is focusable for a reason
1047
- of its own.
1048
-
1049
- `measureScrollContent` is called at most once per layout pass, from
1050
- `absolutize`, so it may read layout geometry, but it must not paint or
1051
- invalidate. It has to return finite numbers: a `NaN` extent would otherwise
1052
- become a `NaN` offset and a subtree laid out at `NaN`, so returning anything
1053
- else throws naming the element, exactly as `measureContent` does. The answer
1054
- is cached across passes that laid nothing inside the pane out again — a pass
1055
- that merely scrolled reuses it, since a scroll cannot change how far the
1056
- content reaches.
1057
-
1058
- When the drawing changes its own extent — a line was typed, rows arrived —
1059
- say so the way any other layout change is said, with
1060
- `this.invalidate(true, this, 'scroll')`; the next pass asks again. That call
1061
- is also what re-arms the measurement above: content that grows silently and
1062
- never invalidates would scroll against the stale extent.
803
+ - **Draw in `paintContent`, not `paint`.** `paint` draws background,
804
+ `paintContent`, children, border and last a `Scrollable`'s scrollbars;
805
+ drawing after `super.paint(ctx)` covers your own thumb.
806
+ - **Offset and clip yourself**: clip to `contentBox()` and subtract
807
+ `scrollX`/`scrollY`, already clamped to your extent. `scrollX` counts from
808
+ the edge the content starts at — the right one under `direction: 'rtl'`
809
+ ([styling.md](styling.md#direction-and-the-logical-edges)) — as does your
810
+ reported `width`, so drawing in your own direction needs no branch.
811
+ - **Do not assign `focusableByDefault`**: `Scrollable` answers it (a tab stop
812
+ when there is somewhere to go) and assigning throws. Override the getter.
813
+ - **`measureScrollContent` runs at most once per layout pass**, and is
814
+ reused when nothing inside was laid out again (a pure scroll). It may read
815
+ layout geometry; it must not paint or invalidate, and must return finite
816
+ numbers or it throws, naming the element.
817
+ - **When your extent changes** (a typed line, arriving rows), call
818
+ `this.invalidate(true, this, 'scroll')`, or scrolling keeps the stale one.
1063
819
 
1064
820
  #### Scrolling the pixels, not just the offset
1065
821
 
1066
- `scrollBy` moves a number; nobody moves pixels. For a drawing that is cheap
1067
- to redraw, that is the end of it — `paintContent` runs again at the new
1068
- offset and the frame is correct. For one that is expensive per row and
1069
- already lives in a retained buffer — a terminal grid, a log view, a minimap,
1070
- a chart that pans — a scroll changes almost nothing: the band that stays
1071
- visible is the same pixels, one shift away. A `Surface` can move that band
1072
- itself, server-side:
822
+ `scrollBy` moves a number; `paintContent` redraws at the new offset. A
823
+ drawing that is expensive and kept in a retained buffer — a terminal grid, a
824
+ log, a minimap, a panning chart — can shift its surviving band inside a
825
+ `Surface` instead:
1073
826
 
1074
827
  ```js
1075
828
  import { Node, Scrollable } from 'react-x11/node';
@@ -1112,34 +865,21 @@ class GridNode extends Scrollable(Node) {
1112
865
  }
1113
866
  ```
1114
867
 
1115
- `copyWithin(src, dx, dy)` shifts `src` (`{x, y, width, height}`, in surface
1116
- coordinates) in place by an integer delta and answers **whether anything
1117
- survived** — `false` means repaint the lot, which is the branch a fresh
1118
- buffer, a jump to the far end and a resize all take. It is one `CopyArea`
1119
- inside the pixmap: nothing crosses the wire but the request, the overlap is
1120
- safe, and no exposure events come back.
1121
-
1122
- The same element runs unchanged on the Cocoa, Wayland and win32 backends.
1123
- `react-x11/ntk`'s `Surface` asks the app it is handed for the
1124
- implementation — ntk's pixmap on an X connection, a CG bitmap on a Cocoa
1125
- app, a GL render target on a Wayland one, a Direct2D bitmap on a win32 one
1126
- — so the surface above is a bitmap there, `copyWithin` is one in-place copy
1127
- of the band (on win32 it answers `false` for now, the bridge having no
1128
- in-place blit for an offscreen bitmap, so the element repaints the rect),
1129
- and `drawImage` is one composite, with the element naming no backend. The
1130
- one thing to know: where a surface has one graphics state rather than a context per
1131
- call — a bitmap, a render target — `getContext('2d')` answers the same
1132
- context every time and its `destroy()` is a no-op (docs/macos.md "Custom
1133
- drawing on a layer tree").
1134
-
1135
- **Drawing into a surface, whichever backend.** ntk's advice — a caller
1136
- doing many draws takes `getContext('2d')` once and holds it, rather than
1137
- one context per frame — holds everywhere, including the backends where the
1138
- surface is a GPU target and every context in the process shares one GL
1139
- device. A held context there takes the device back whenever it finds it in
1140
- someone else's hands, so its draws land in its own surface even in the
1141
- middle of the window's own paint, and the window's context does the same on
1142
- its way back (issue #566). Nothing to branch on, and nothing to restore:
868
+ `copyWithin(src, dx, dy)` shifts `src` (`{x, y, width, height}`, surface
869
+ coordinates) in place by whole pixels, overlap-safe, and returns **whether
870
+ anything survived**; `false` (a fresh buffer, a far jump, a resize) means
871
+ repaint the lot.
872
+
873
+ `Surface` from `react-x11/ntk` is the backend's own — ntk's pixmap on X11,
874
+ a CG bitmap on Cocoa, a GL render target on Wayland, a Direct2D bitmap on
875
+ win32 — so the element runs unchanged; on win32 `copyWithin` returns
876
+ `false` for now. Where a surface has one graphics state (a bitmap, a render
877
+ target), `getContext('2d')` returns the same context each time and its
878
+ `destroy()` is a no-op.
879
+
880
+ **Hold one context**, taken once with `getContext('2d')`: on every backend,
881
+ shared GL devices included, its draws land in its own surface even
882
+ mid-paint.
1143
883
 
1144
884
  ```js
1145
885
  drawRows(rows) {
@@ -1148,15 +888,11 @@ drawRows(rows) {
1148
888
  }
1149
889
  ```
1150
890
 
1151
- `surface.render(fn)` is the scoped alternative: it hands `fn` a context
1152
- whose transform and clip start clean and flushes when `fn` returns. Use it
1153
- for a whole-surface repaint; hold a context for a grid that is touched a
1154
- few cells at a time. The one thing not to do is mix them and expect the
1155
- held context's transform to survive a `render()` — it is the same context,
1156
- and `render` resets it.
891
+ `surface.render(fn)` is the scoped alternative for a whole-surface repaint:
892
+ a clean transform and clip, flushed when `fn` returns. It is the same
893
+ context, so it resets a held context's transform.
1157
894
 
1158
- If the surface is **opaque** — every pixel of it written, which a terminal's
1159
- grid or a tiled scene is — say so, and the present stops blending:
895
+ **An opaque surface composites with `copy`**, skipping the blend:
1160
896
 
1161
897
  ```js
1162
898
  paintContent(ctx) {
@@ -1167,41 +903,21 @@ paintContent(ctx) {
1167
903
  }
1168
904
  ```
1169
905
 
1170
- `copy` is canvas's "replace these pixels, do not read what is under them",
1171
- and it means the same thing on both backends: XRender's `PictOp.Src` on
1172
- X11, and on Cocoa a row memcpy in place of the CGImage the blend would
1173
- otherwise be built from — a 125x45 terminal grid composites in 0.42ms
1174
- rather than 1.37 (docs/macos.md "Compositing a surface at a translate").
1175
- Set it back, or set it inside a `save`/`restore` pair: it is context state,
1176
- and the context is the window's. It is only right for a surface with no
1177
- transparency to show through, and it is refused rather than approximated by
1178
- a backend that cannot draw it — assign it and read the property back if you
1179
- want to know which you got.
1180
-
1181
- Two things it lines up with. The deltas you are handed are already whole
1182
- pixels — the sub-pixel carry described above exists so that a shift is
1183
- always expressible — so an element scrolling this way never has a fraction
1184
- to round. And `<box overflow="scroll">` has been doing the window-side half
1185
- of this without being asked: a pure scroll of one viewport blits the
1186
- surviving band inside ntk's backing pixmap rather than repainting it. This
1187
- is that optimisation, for a buffer you retained yourself.
1188
-
1189
- An element that draws its content live rather than into a surface asks for
1190
- the window-side half directly, with
1191
- [`scrollContents`](#panning-a-scene-you-drew) — the same shift, one layer
1192
- out.
906
+ Set it back (or `save`/`restore`): it is state on the window's context. Use
907
+ it only where every pixel is opaque. A backend that cannot draw it refuses
908
+ the assignment; read it back to know.
909
+
910
+ `<box overflow="scroll">` blits its own viewport on a pure scroll; an
911
+ element that draws live asks for that with
912
+ [`scrollContents`](#panning-a-scene-you-drew).
1193
913
 
1194
914
  #### Drawing what stays put while the pane scrolls
1195
915
 
1196
- That window-side blit copies everything in the pane's viewport with its
1197
- content, which is right for everything that scrolls. An element that
1198
- paints part of itself **against the pane's viewport** rather than against
1199
- its own box has pixels the copy gets wrong: a document with a fixed
1200
- background or a `position: fixed` header, drawn by one element inside a
1201
- `<box overflow="scroll">`, had its header dragged along with the text and
1202
- its background scrolling away. `viewportFixedRects()` is the element's word
1203
- for those parts: window coordinates, device pixels, like `abs`, or `null`,
1204
- the default.
916
+ The scroll blit copies a pane's whole viewport. An element that paints part
917
+ of itself against that viewport — a fixed background, a `position: fixed`
918
+ header in a document it draws — names it in `viewportFixedRects()` (window
919
+ coordinates, device pixels; default `null`), and a blitting scroll
920
+ repaints those rects where they are and where the copy dragged them:
1205
921
 
1206
922
  ```js
1207
923
  class DocumentNode extends Node {
@@ -1224,28 +940,17 @@ class DocumentNode extends Node {
1224
940
  }
1225
941
  ```
1226
942
 
1227
- A scroll of the pane that blits repaints those rects where they are and
1228
- where the copy dragged their image, as it does the corners of a rounded box
1229
- around the pane; one that pins most of the viewport — a fixed background
1230
- behind all of it — makes the scroll a repaint, which draws it right. Only
1231
- the nearest scroll pane asks: a pane further out moves the inner one with
1232
- its content, and what is fixed to it with it. The answer is read on each
1233
- scroll the pane blits, after layout, and nowhere else.
943
+ Rects covering most of the viewport make the scroll a repaint. Only the
944
+ nearest scroll pane asks (an outer pane moves the inner one with what is
945
+ fixed to it), after layout, on each scroll it blits.
1234
946
 
1235
947
  #### Keeping what the viewport shows while your content moves
1236
948
 
1237
- A document laid out again at another width breaks its text above the fold
1238
- into more lines or fewer, so the paragraph at the top of the viewport moves
1239
- down the document or up it by the difference, and a pane that keeps its
1240
- offset shows the reader whatever is at that offset now: a window dragged
1241
- narrower scrolled them away from where they were reading. A browser keeps
1242
- the paragraph where it was on screen instead, and moves the offset by as
1243
- much (CSS Scroll Anchoring 1). `anchorScrollBy(dx, dy)` is how an element
1244
- asks its scroll pane for that: its content moved by (dx, dy) device pixels
1245
- under the viewport, in the layout pass running now, and the nearest scroll
1246
- pane moves its offsets by as much, after it measures what it holds and
1247
- before it places it, so the frame that lays the content out again shows it
1248
- where it was.
949
+ Laid out at a new width, a document rewraps above the fold and the
950
+ paragraph at the top of the viewport moves. `anchorScrollBy(dx, dy)` keeps
951
+ it on screen, as CSS Scroll Anchoring does: your content moved (dx, dy)
952
+ device pixels in this layout pass, and the nearest scroll pane shifts its
953
+ offsets by as much before placing it, in the same frame.
1249
954
 
1250
955
  ```js
1251
956
  class DocumentNode extends Node {
@@ -1260,37 +965,24 @@ class DocumentNode extends Node {
1260
965
  }
1261
966
  ```
1262
967
 
1263
- Ask it from the layout pass — `measureContent`, or what it calls — where
1264
- the pane places what it holds after; shifts asked in one pass add up, and
1265
- one asked outside a pass waits for the next and asks for it. A `scrollTo`
1266
- the application made since the last pass drops it, since the shift was
1267
- measured against the offsets before that scroll; a `scrollIntoView` lands
1268
- after it; and the result is clamped to what the pane holds, as any scroll
1269
- is. It returns false where nothing scrolls the element.
968
+ Call it in the layout pass. Shifts in one pass add up; one outside a pass
969
+ waits for, and requests, the next. An app `scrollTo` since the last pass
970
+ drops it; a `scrollIntoView` lands after it; the result is clamped. It
971
+ returns false where nothing scrolls the element.
1270
972
 
1271
973
  ### Drawing a scene into one node
1272
974
 
1273
- _Issue #301._ A frame repaints **rects, not windows**: claims coalesce into
1274
- disjoint rectangles, each gets its own clipped pass, and a subtree that lands
1275
- outside the pass being painted emits no drawing at all. All of that has one
1276
- granularity — the retained node — which is exactly right until your element
1277
- _is_ the scene. A graph view, a chart, a timeline, a code editor: the tree
1278
- says "one node, 1100×700", so a pass over the 80×40 box a dragged node moved
1279
- through redraws all three hundred nodes, all seven hundred edges, the grid
1280
- and the minimap into a clip that throws almost all of it away.
1281
-
1282
- Two accessors make such an element a member of the same machinery rather
1283
- than the one exception to it. Both are opt-in, and both keep core's answer
1284
- when you say nothing.
1285
-
1286
- **Painting: `paintDamage()`.** The rect this pass covers, in the owning
1287
- window's coordinates — the same space as `abs`, in device pixels — or `null`
1288
- when the frame is unbounded and everything has to be drawn. Cull
1289
- against it the way core culls the tree, where a pixel short of the rect
1290
- still counts as inside it: antialiasing puts ink just past the bounds you
1291
- computed, and the pass is clipped to the rect, so drawing a near miss costs
1292
- one draw and loses nothing. Here `overlaps(a, b, 1)` asks whether two rects
1293
- come within a pixel of each other:
975
+ A frame repaints damaged **rects**, each in a clipped pass that skips
976
+ subtrees outside it — at node granularity. When your element is the scene (a
977
+ graph, a chart, a timeline, a code editor), a small rect still redraws all
978
+ of it into the clip. Two opt-in accessors fix that; say nothing and core's
979
+ answer stands.
980
+
981
+ **Painting: `paintDamage()`** — this pass's rect (window coordinates,
982
+ device pixels), or `null` when everything must be drawn. Cull with a pixel
983
+ of slack, as core does: antialiasing inks just past computed bounds, and
984
+ the pass is clipped anyway. `overlaps(a, b, 1)` here asks whether two rects
985
+ come within a pixel:
1294
986
 
1295
987
  ```js
1296
988
  class FlowNode extends Node {
@@ -1311,13 +1003,10 @@ class FlowNode extends Node {
1311
1003
  }
1312
1004
  ```
1313
1005
 
1314
- **Committing: `selfDamagedProps`.** `applyProps` damages the whole node when
1315
- any non-event prop changes identity, which is right for every element whose
1316
- node _is_ what changed and always-everything for a scene pane. A controlled
1317
- graph app commits a new `nodes` array per drag step, so each step is one
1318
- scoped claim from the gesture plus one full-pane claim from the commit — and
1319
- the full-pane claim wins. Name the props whose damage your own `applyProps`
1320
- claims and the commit contributes none of its own:
1006
+ **Committing: `selfDamagedProps`.** A new identity for any non-event prop
1007
+ damages the whole node, so a controlled scene committing a new `nodes`
1008
+ array per drag step repaints the pane. Name the props whose damage your
1009
+ `applyProps` claims, and the commit claims none:
1321
1010
 
1322
1011
  ```js
1323
1012
  registerElement('flow', {
@@ -1345,9 +1034,8 @@ class FlowNode extends Node {
1345
1034
  }
1346
1035
  ```
1347
1036
 
1348
- When the answer depends on the _values_ rather than the names, override
1349
- **`paintChanged(next, prev)`** instead — it is what the list feeds, so the
1350
- two are the same seam:
1037
+ To decide on values rather than names, override
1038
+ **`paintChanged(next, prev)`**, which the list feeds:
1351
1039
 
1352
1040
  ```js
1353
1041
  paintChanged(next, prev) {
@@ -1358,53 +1046,27 @@ paintChanged(next, prev) {
1358
1046
  }
1359
1047
  ```
1360
1048
 
1361
- Three things about that contract, in the order they bite:
1362
-
1363
- **Everything you do not recognise goes to `super`.** An element that answers
1364
- "nothing changed" about a prop it does not actually claim shows a stale
1365
- pixel, and nothing comes back to fix it — so the override answers for the
1366
- props it knows and hands the rest to core, which is conservative on purpose.
1367
- That is what keeps an `aria-label`, a `title`, or a prop the element grows
1368
- next year correct without anybody remembering this method exists.
1369
-
1370
- **A style change is not yours to excuse.** `style` is compared by value by
1371
- the caller and never reaches either seam: what it moves is the background,
1372
- the border and the clip that `Node.paint` draws for you.
1373
-
1374
- **Claim before you skip.** `selfDamagedProps` only says the commit will not
1375
- claim — it does not claim anything on your behalf. An element that names a
1376
- prop and then forgets to invalidate for it renders nothing at all on that
1377
- change, which at least fails loudly the first time you drag something.
1378
-
1379
- And one place `paintDamage()` does not belong: inside `paintCached`
1380
- ([below](#drawing-once-instead-of-every-frame)). That draws into a surface in
1381
- its own coordinates, and a copy culled against the window's damage is stored
1382
- half-drawn under a key that says it is whole — so every later frame that hits
1383
- the key gets the hole.
1384
-
1385
- The same scene has a second one-node problem, in the accessible tree rather
1386
- than in the frame: see [A scene a screen reader can
1387
- walk](#a-scene-a-screen-reader-can-walk).
1388
-
1389
- Worth it because the numbers are not small. `@react-x11/components`' `<Flow>`
1390
- on a 300-node / 745-edge scene at 1100×700 costs ~2470 X requests and ~150 ms
1391
- per drag step redrawing the pane, and ~400 requests / ~30 ms with the two
1392
- seams — the same frame, with the parts of it nobody can see left unsent.
1393
- `REACT_X11_DEBUG_PAINT=1` is how to watch it: the pass's rect is stroked in
1394
- the frame's colour, so a scene that is still repainting whole strobes across
1395
- the pane instead of outlining what moved ([debugging.md](debugging.md)).
1049
+ - **Hand what you do not recognise to `super`**, which is conservative, or
1050
+ a prop you never thought of (`aria-label`, `title`) leaves stale pixels.
1051
+ - **A style change never reaches either seam**: core compares `style` by
1052
+ value and repaints what `Node.paint` draws.
1053
+ - **Claim before you skip**: `selfDamagedProps` claims nothing for you; a
1054
+ named prop you forget to invalidate renders nothing.
1055
+ - **Not inside `paintCached`**
1056
+ ([below](#drawing-once-instead-of-every-frame)): a copy culled by window
1057
+ damage is cached half-drawn.
1058
+
1059
+ The accessible tree has the same one-node problem: [A scene a screen reader
1060
+ can walk](#a-scene-a-screen-reader-can-walk). On a 300-node `<Flow>` the
1061
+ seams take a drag step from about 150 ms to 30 ms.
1062
+ `REACT_X11_DEBUG_PAINT=1` strokes each pass's rect, so a scene still
1063
+ repainting whole strobes across the pane ([debugging.md](debugging.md)).
1396
1064
 
1397
1065
  #### Panning a scene you drew
1398
1066
 
1399
- _Issue #303._ The two seams above scope a frame to the part of the scene
1400
- that changed. A **pan** has no such part: it translates every pixel, so the
1401
- honest claim is the whole pane and the honest frame is a full repaint — of a
1402
- picture that is already on screen, one shift away from being right.
1403
-
1404
- That is the shape `<box overflow="scroll">` has been treating as a special
1405
- case since issue #138: blit the band that survives inside ntk's backing
1406
- store, repaint only the strip the shift exposed. `scrollContents` is that
1407
- same dance, for a viewport of your own:
1067
+ A pan moves every pixel of a picture already on screen. `scrollContents`
1068
+ does for your viewport what `<box overflow="scroll">` does: blit the
1069
+ surviving band, repaint the exposed strip.
1408
1070
 
1409
1071
  ```js
1410
1072
  class FlowNode extends Node {
@@ -1426,43 +1088,24 @@ class FlowNode extends Node {
1426
1088
  }
1427
1089
  ```
1428
1090
 
1429
- The call claims `rect` — the conservative answer, and the one that stands if
1430
- anything declines — and arms the frame to blit instead. At frame time core
1431
- asks ntk to move the surviving band and **narrows that claim to the band the
1432
- shift exposed**, which is what `paintDamage()` then hands your paint. So the
1433
- element draws the strip and nothing else without ever asking whether the
1434
- blit happened, and every gate below falls back to repainting `rect`, which is
1435
- the behaviour without the call at all.
1436
-
1437
- Four things worth knowing, in the order they bite:
1438
-
1439
- **`dx`/`dy` are how far the pixels moved.** The sense `Surface.copyWithin`
1440
- and ntk's `scrollRegion` use, not a scroll offset's: panning the scene right
1441
- by ten is `dx: 10`, and the exposed band is down the left edge. Both whole
1442
- device pixels — a fractional shift is not a copy — and `rect` in window
1443
- coordinates (the same space as `abs` and `contentBox()`: device pixels) and
1444
- inside your node.
1445
-
1446
- **You promise one thing: that inside `rect` the frame really is that
1447
- translation.** Everything else is core's to check, and each of them declines
1448
- rather than misrenders — a claim from anywhere else reaching into the rect, a
1449
- sibling drawing over it, a child of your node laid out on top of it, your own
1450
- border ring or rounded corner, a layout pass that moved something, a region
1451
- too small or a shift too large to be worth it. Several pans in one frame
1452
- coalesce into one blit by their net shift; two different regions of one node
1453
- do not, and fall back.
1454
-
1455
- **Diagonal is fine here**, unlike the scroll containers' one-axis-at-a-time
1456
- rule. That rule exists because the L of exposed strips overlaps the scrollbar
1457
- rects and the merges balloon back towards the whole viewport; with no bars
1458
- the L is two disjoint rects and stays two. A drag-pan is diagonal almost
1459
- every frame, so this is the case rather than the corner.
1460
-
1461
- **Every gate above is about `rect`, not about your node.** A pane usually
1462
- has furniture — a minimap, zoom controls, a HUD strip — that has to repaint
1463
- on a pan frame and whose pixels must _not_ ride the blit. A strip along one
1464
- edge can be carved out of the region you shift and claimed the ordinary way;
1465
- the claim lands beside the rect and the frame stays a blit:
1091
+ The call claims `rect` and arms a blit; at frame time core shifts the band
1092
+ and **narrows the claim to the exposed strip**, which `paintDamage()` hands
1093
+ your paint. If anything declines, `rect` is repainted as without the call;
1094
+ the element never needs to know which.
1095
+
1096
+ - **`dx`/`dy` are how far the pixels moved** (`copyWithin`'s sense, not a
1097
+ scroll offset's): panning right by ten is `dx: 10`, exposing the left
1098
+ edge. Whole device pixels; `rect` in window coordinates, inside your
1099
+ node. Diagonal is fine.
1100
+ - **You promise that inside `rect` the frame is exactly that translation.**
1101
+ Core declines on the rest: a claim from elsewhere reaching into `rect` by
1102
+ even a pixel, a sibling drawing over it, a child laid out over it, your
1103
+ own border ring or rounded corner, a layout change, a region too small or
1104
+ a shift too large. Pans in one frame coalesce by net shift; two regions
1105
+ of one node fall back.
1106
+
1107
+ **Furniture along an edge** — a HUD strip — is carved out of the region and
1108
+ claimed beside it; sibling overlay panels outside `rect` work the same way:
1466
1109
 
1467
1110
  ```js
1468
1111
  onDragPan(dx, dy) {
@@ -1475,17 +1118,12 @@ onDragPan(dx, dy) {
1475
1118
  }
1476
1119
  ```
1477
1120
 
1478
- A claim that _reaches into_ the rect — by so much as a pixel — still declines
1479
- the frame (issue #309). Overlay panels mounted as sibling nodes work the same
1480
- way, as long as they sit outside the rect: one drawing over it is dragged
1481
- along by the blit, so it declines instead.
1482
-
1483
- **Furniture in a corner is pinned, not carved.** What shifts is one
1484
- rectangle, so carving a minimap out of the bottom-right corner takes the rows
1485
- beside it too, and zoom controls in the other bottom corner make that a band
1486
- the pane's full width — repainted on every pan frame, with every card, label
1487
- and edge in it (issue #682). Hand over the whole pane instead, and name the
1488
- furniture that stays put:
1121
+ **Furniture in a corner is pinned**, since carving takes the rows beside it.
1122
+ Pass the rects that stay put; core repaints each, and the stale image the
1123
+ copy dragged, every pan frame — so the furniture may change (a minimap's
1124
+ viewport box) and claim inside its rect without declining. A claim
1125
+ elsewhere in the region still declines, as does furniture over three
1126
+ quarters of the pane.
1489
1127
 
1490
1128
  ```js
1491
1129
  onDragPan(dx, dy) {
@@ -1499,40 +1137,15 @@ onDragPan(dx, dy) {
1499
1137
  }
1500
1138
  ```
1501
1139
 
1502
- The copy drags a stale image of each piece along by (dx, dy), and core
1503
- repaints the pinned rect and that image: two small rects a piece, on every
1504
- pan frame, whether the furniture changed or not. So what the furniture shows
1505
- can follow the pan — a minimap's viewport box — without a claim of its own,
1506
- and a claim it makes anyway is yours as long as it lies inside a pinned rect.
1507
- A claim anywhere else in the region still declines the frame, and so does
1508
- furniture that is most of the pane: past three quarters of it repainted, the
1509
- blit is buying a shift and paying for the region anyway.
1510
-
1511
- **A rounded box around your element is pinned the same way.** The arc of a
1512
- rounded corner does not translate, but it lives in the four radius-sized
1513
- squares at the corners and nowhere else, so core pins those squares itself:
1514
- the whole region shifts, and each square is repaired where it is and where
1515
- the copy dragged it — a few pixels a corner (issue #691). A graph pane
1516
- inside a rounded card is the common shape. Its own border ring and corners
1517
- are different: those are the element's paint, not its drawing, and they
1518
- decline the blit.
1519
-
1520
- The rects a pan frame repaints — its strips, the furniture, the corners —
1521
- are merged down to a few passes, and merged together, so the small ones
1522
- join their neighbours before a large box can swallow them: a diagonal pan
1523
- of a rounded pane with furniture in both bottom corners repaints under a
1524
- fifth of it.
1525
-
1526
- A node mounted over the region inside a pinned rect — a zoom button, a
1527
- legend — is furniture too: it does not decline the blit, and its pixels are
1528
- repaired with the rest. Its claims carry the pixel of slop every node's do,
1529
- so the rect to pin is the one it claims, `node.paintBounds()`, or its hover
1530
- declines the frame it lands in.
1531
-
1532
- **Unless its pixels are meant to move with it.** A graph whose cards carry
1533
- real widgets mounts them beside the pane, all in one box it moves with the
1534
- pan — and that box is over the rect every frame. Name it as a rider and it
1535
- rides the blit instead of declining it:
1140
+ Core pins the four radius-sized corners of a rounded box around your
1141
+ element itself; your element's own border ring and corners decline. A node
1142
+ mounted over the region inside a pinned rect (a zoom button, a legend) is
1143
+ furniture too: pin the rect it claims, `node.paintBounds()`, slop included,
1144
+ or its hover declines.
1145
+
1146
+ **Riders move with the pan.** Widgets mounted beside the pane in a box moved
1147
+ by exactly (dx, dy) this frame, and changed in no other way, ride the blit
1148
+ when named:
1536
1149
 
1537
1150
  ```js
1538
1151
  onDragPan(dx, dy) {
@@ -1543,45 +1156,20 @@ onDragPan(dx, dy) {
1543
1156
  }
1544
1157
  ```
1545
1158
 
1546
- A rider promises what your drawing promises: that it moves by exactly
1547
- (dx, dy) this frame and changes nothing else. Then its commit claims nothing,
1548
- the blit carries its pixels with yours, and its move claims only what it
1549
- leaves or reaches _outside_ the rect — so clip it to your element, with a
1550
- box around it that paints nothing of its own, and that is nothing at all.
1551
- Anything else it does — moving by some other amount, a widget in it
1552
- changing, one mounting as the pan brings it in — is claimed the ordinary
1553
- way, which declines that frame's blit; the next one blits again.
1554
-
1555
- **Zoom is not a blit.** Scaling resamples; it is a full repaint and should
1556
- be. That is the right trade: a zoom is a gesture step, a pan is sixty of them
1557
- a second.
1558
-
1559
- The protocol bench prices it at five diagonal pan steps over a 374-cell scene
1560
- — 627 requests / 308 composites / 0.06 Mpx, against 6550 / 3269 / 2.38 for
1561
- the same five steps under `REACT_X11_NO_SCROLL_BLIT=1`, which is exactly the
1562
- fallback every gate here takes. With a HUD strip claimed beside the region
1563
- they cost 1993 / 993 / 0.43 — the blit plus the strip. With furniture in two
1564
- bottom corners they cost 1622 / 790 / 0.30 pinned, where carving the band
1565
- between the corners costs 2736 / 1349 / 0.70, and 1892 / 920 / 0.31 in a
1566
- rounded pane.
1567
-
1568
- If your element keeps its drawing in a `Surface` of its own rather than
1569
- drawing it live, the shift you want is
1570
- [`copyWithin`](#scrolling-the-pixels-not-just-the-offset) on that surface —
1571
- same idea, one layer in.
1159
+ A rider's commit then claims nothing, and its move only what lies outside
1160
+ `rect` — nothing, if a box that paints nothing clips it to your element.
1161
+ Anything else it does is claimed normally and declines that frame's blit.
1572
1162
 
1573
- ### An element that covers its box
1163
+ **Zoom is not a blit**: scaling resamples and repaints. A retained `Surface`
1164
+ shifts with [`copyWithin`](#scrolling-the-pixels-not-just-the-offset)
1165
+ instead. `REACT_X11_NO_SCROLL_BLIT=1` forces the fallback
1166
+ ([debugging.md](debugging.md#react_x11_no_scroll_blit1)).
1574
1167
 
1575
- A terminal, a media frame, a chart with a retained surface: an element that
1576
- composites a bitmap of its own over its whole content box, every paint,
1577
- leaves nothing of what was painted under it. Core does not know that. A
1578
- pass inside the element still fills the window's background, the
1579
- background of every ancestor and the element's own before `paintContent`
1580
- composites the bitmap over all three — composites the server ran for
1581
- nothing on X11, and on macOS full-area CoreGraphics passes that were a
1582
- fifth of a streaming terminal's frame.
1168
+ ### An element that covers its box
1583
1169
 
1584
- `opaqueRect()` is the element's word for it:
1170
+ An element compositing an opaque bitmap over its whole content box every
1171
+ paint (a terminal, a media frame) hides what is under it, but core still
1172
+ fills the window's, ancestors' and its own backgrounds first unless told:
1585
1173
 
1586
1174
  ```js
1587
1175
  class TerminalNode extends Node {
@@ -1597,52 +1185,33 @@ class TerminalNode extends Node {
1597
1185
  }
1598
1186
  ```
1599
1187
 
1600
- A pass that lies inside the answer is painted without the fills that would
1601
- be under it — the window's background, this node's own and every
1602
- ancestor's. Whole pixels only, window coordinates like `abs`: a fractional
1603
- edge is antialiased and an antialiased pixel is not opaque, so core takes
1604
- the whole pixels inside what you answer. A clipping ancestor shrinks the
1605
- cover to what reaches the surface, a rounded one by its radius all round.
1606
- The promise is yours to keep — every pixel, alpha one, on every paint,
1607
- whatever the props — and an element that is translucent, or paints a
1608
- background only sometimes, answers `null`, the default. The answer is read
1609
- per pass, so it may follow `contentBox()`, and it costs nothing on a pass
1610
- it does not cover.
1611
-
1612
- **The same promise lets a move be copied** (issue #681). A subtree whose
1613
- only change in a frame is where it is — a terminal dragged across a
1614
- desktop, a card of widgets dragged across a graph — is already on the
1615
- surface, one shift away. When something in it covers its box, nothing under
1616
- it shows through there, so core copies its pixels where they went
1617
- (`CopyArea` on X11, the backing's own copy on Cocoa and Windows) and paints
1618
- only what the copy cannot supply: the strip the move uncovered, the
1619
- corners a radius gives up, and whatever is drawn over it. What covers is
1620
- an element answering `opaqueRect()`, a box with a `backgroundColor`
1621
- nothing shows through, or either of those inside boxes that hold nothing
1622
- else — the wrappers a card is positioned and scaled by. A card with no background of its own is drawn over what is
1623
- under it, and is repainted where it was and where it went.
1624
-
1625
- The frame's other claims are sorted by whose they are. One from an element
1626
- painted **under** the moved subtree is repainted around the copy but not
1627
- through it, since nothing of it shows there — a graph pane that claims the
1628
- box its card moved through, with the card's mounted body over it, pays for
1629
- the card's header and edges and not for the body. One from inside the
1630
- subtree, over it, or from a claim core cannot place is repainted in full,
1631
- and where the copy carried what it covered. When those repaint most of the
1632
- copy, or would take more damage rects than the window affords a frame —
1633
- on X11, four: a rounded card's corners and the strip it uncovered are
1634
- already five — the move is repainted where it was and where it went, as
1635
- it always was. `REACT_X11_NO_SCROLL_BLIT=1` turns the copy off with the
1636
- other blits ([debugging.md](debugging.md#react_x11_no_scroll_blit1)).
1188
+ A pass inside the answer skips those fills. Window coordinates like `abs`;
1189
+ core uses the whole pixels inside it; clipping ancestors shrink it, rounded
1190
+ ones by their radius. The promise — every pixel, alpha one, every paint,
1191
+ whatever the props — is yours: a translucent element, or one with an
1192
+ occasional background, answers `null` (the default). It is read per pass, so it may follow
1193
+ `contentBox()`.
1194
+
1195
+ **It also lets a move be copied.** When a subtree only moved this frame (a
1196
+ terminal across a desktop, a card across a graph) and something in it
1197
+ covers its box, core copies its pixels and repaints only the uncovered
1198
+ strip, the corners a radius gives up, and what is drawn over it. Covering
1199
+ means an `opaqueRect()` element or a box with a `backgroundColor` nothing
1200
+ shows through, possibly inside boxes holding nothing else. A card without
1201
+ a background is repainted where it was and where it went.
1202
+
1203
+ Other claims that frame: one from an element **under** the subtree repaints
1204
+ around the copy, not through it; one from inside, over, or unplaceable
1205
+ repaints in full. If those cover most of the copy, or need more damage
1206
+ rects than a frame allows (four on X11: a rounded card's corners plus strip
1207
+ are five), the move repaints in both places. `REACT_X11_NO_SCROLL_BLIT=1`
1208
+ turns it off ([debugging.md](debugging.md#react_x11_no_scroll_blit1)).
1637
1209
 
1638
1210
  ### An element that draws past its box
1639
1211
 
1640
- Core claims a node's damage, culls it out of a pass and sizes a fade's
1641
- surface by its box — grown by its outline and its shadow, the two things a
1642
- box draws outside itself. An element that draws anything else out there has
1643
- to say how far, or it is claimed by its box alone: when it changes, the old
1644
- ink's edge outside the box stays on the screen and the new one's is not
1645
- drawn, and scrolled just out of a pane it drops the ink it still puts in.
1212
+ Core claims, culls and sizes a fade's surface by a node's box grown by its
1213
+ outline and shadow. Declare ink beyond that, or it is left on screen when
1214
+ the element changes and dropped when scrolled just out of a pane:
1646
1215
 
1647
1216
  ```js
1648
1217
  class RichTextNode extends Node {
@@ -1654,17 +1223,13 @@ class RichTextNode extends Node {
1654
1223
  }
1655
1224
  ```
1656
1225
 
1657
- Device pixels, every side. It is read when core rebuilds its cached paint
1658
- bounds, which a claim of the node does, so answer a bound for anything the
1659
- element may draw rather than for what it drew last — the claim after a
1660
- change is what has to cover the frame before's ink.
1226
+ Device pixels, every side, read when core rebuilds cached paint bounds (on
1227
+ a claim): bound anything the element may draw, not what it drew last.
1661
1228
 
1662
1229
  ### Drawing once instead of every frame
1663
1230
 
1664
- A drawing that does not change between frames does not have to be redrawn
1665
- between frames. Implement two more methods and your element is rendered once
1666
- and composited on later repaints, sharing one rendered copy with every other
1667
- node that produces the same key:
1231
+ A drawing that does not change can be rendered once and composited on later
1232
+ repaints, one copy shared by every node producing the same key:
1668
1233
 
1669
1234
  | method | |
1670
1235
  | ----------------------- | ----------------------------------------------------------------------------- |
@@ -1692,40 +1257,28 @@ paintCached(ctx, box) {
1692
1257
  }
1693
1258
  ```
1694
1259
 
1695
- `format: 'a8'` stores coverage rather than colour and paints through `tint`,
1696
- so one rendered copy serves every colour the drawing is ever asked for — the
1697
- right choice for a monochrome drawing, and it keeps the colour out of the key.
1260
+ `format: 'a8'` stores coverage and paints through `tint`: one copy serves
1261
+ every colour of a monochrome drawing, and colour stays out of the key.
1698
1262
 
1699
1263
  **The key is the entire correctness surface.** It must name every input
1700
- `paintCached` reads, and it should be derived from the same values
1701
- `applyProps` compares so the two cannot drift. A key that misses something
1702
- shows stale pixels, which is the hardest kind of bug to see — so develop with
1703
- `REACT_X11_PAINT_CACHE=verify`, which re-renders every hit and complains
1704
- loudly when the drawing changed while the key did not. Return `null` for
1705
- anything animated, hovered, focused or blinking.
1706
-
1707
- Two footguns worth naming. `paintCached` draws in **surface-local**
1708
- coordinates: reaching for `this.abs.x` inside it is the first thing to check
1709
- when a cached element paints in the wrong place. And the cache is per X
1710
- connection and keyed only on your string, so make the key specific enough
1711
- that two different drawings cannot collide — prefix it with your element
1712
- name, as above.
1713
-
1714
- `globalAlpha`, the clip and any ancestor translation are applied when the
1715
- cached copy is composited, so an ancestor animating opacity is a cache hit
1716
- rather than a reason to opt out.
1264
+ `paintCached` reads, derived from what `applyProps` compares; a missed one
1265
+ shows stale pixels. Develop with `REACT_X11_PAINT_CACHE=verify`, which
1266
+ re-renders every hit and complains when the drawing changed under the same
1267
+ key. Return `null` while animated, hovered, focused or blinking.
1268
+
1269
+ `paintCached` draws in **surface-local** coordinates. The cache is per X
1270
+ connection, keyed only on your string: prefix keys with your element name.
1271
+ `globalAlpha`, the clip and ancestor translation apply at composite time, so
1272
+ an ancestor's opacity animation still hits.
1717
1273
 
1718
1274
  ### Parts of your drawing on layers of their own
1719
1275
 
1720
- An element that draws a scene has animations inside it — a document's CSS
1721
- animation on one element, a cursor's blink, an edge's dash, a tooltip's
1722
- fade — and each is a frame of JavaScript: restyle, claim, repaint. On macOS
1723
- the render server can run such an animation itself, with no frame at all, if
1724
- what moves is on a layer of its own. Layer promotion does that for a plain
1725
- `<box>` ([macos.md](macos.md#layer-promotion-the-animated-few-on-their-own-layers-above-the-surface-presenter));
1726
- an element whose content is its own `paint` answers **sprites**: the parts
1727
- of its drawing it would like lifted, each with what the layer shows and what
1728
- runs on it.
1276
+ On macOS the render server can run an animation — a cursor's blink, an
1277
+ edge's dash, a tooltip's fade — with no JavaScript frame, if what moves is
1278
+ on a layer of its own. Layer promotion does that for a `<box>`
1279
+ ([macos.md](macos.md#layer-promotion-the-animated-few-on-their-own-layers-above-the-surface-presenter));
1280
+ an element that paints its own content answers **sprites**: parts of its
1281
+ drawing to lift, each with what its layer shows and runs.
1729
1282
 
1730
1283
  ```js
1731
1284
  sprites() {
@@ -1761,91 +1314,63 @@ spriteAnimationEnded(key, id, finished) {
1761
1314
  }
1762
1315
  ```
1763
1316
 
1764
- | method | |
1765
- | ----------------------------------------- | --------------------------------------------------------------------------- |
1766
- | `sprites()` | the parts on offer, or `null`; asked every frame, after layout |
1767
- | `spritesLifted(keys)` | which of them are on layers now — before the frame paints |
1768
- | `spriteAnimationEnded(key, id, finished)` | the render server is done with one: it ran out, or its layer went |
1769
- | `spritesChanged()` | ask for that frame when the parts changed and nothing the element draws did |
1770
-
1771
- A part is its `key`, its `rect` (and a `reach`, if it draws past it, and a
1772
- `clip`, if you cut it to one, with a `clipRadius` where the clip's corners
1773
- are round), a `paint` that draws it **where it is in the
1774
- window, at opacity 1 and untransformed** — the layer carries both — and a
1775
- `version` whose change paints it again. Its `opacity` and its `transform`, CSS's `matrix(a, b, c,
1776
- d, e, f)` as an array with `e` and `f` in device pixels, about an `origin`
1777
- that defaults to the rect's centre, are what the layer shows at rest. A
1778
- part turned out of the plane hands over `matrix3d()`'s sixteen numbers
1779
- instead, column by column, its translation and the distance its perspective
1780
- divides by in device pixels: Core Animation draws the layer through the
1781
- whole matrix, perspective and all, on the GPU. The presenter lifts such a
1782
- part only where none of its corners is behind the viewer, at rest or in
1783
- any keyframe, since past that plane its picture has no bounds, and asks
1784
- about it where its corners are seen rather than where it was laid out. Its
1785
- `animations` are keyframes for one of those two properties each: `values`,
1786
- `keyTimes`, a `timings` curve per segment, a `duration`, a `repeat`
1787
- (`Infinity` loops), `autoreverse`, and `hold` to stay on the last value. A
1788
- CSS animation goes over as it plays: an opacity's keyframes with their
1789
- curves, and a transform **sampled** into matrices from the element's own
1790
- interpolation, which is what makes a full turn turn and a mixed function
1791
- list interpolate as CSS says — Core Animation's own interpolation would do
1792
- neither.
1793
-
1794
- The rules the presenter decides by, every frame, are promotion's, asked of
1795
- the element at **everywhere the part can be** — its reach through every
1796
- transform it is animated through:
1317
+ | method | |
1318
+ | ----------------------------------------- | ----------------------------------------------------------------- |
1319
+ | `sprites()` | the parts on offer, or `null`; asked every frame, after layout |
1320
+ | `spritesLifted(keys)` | which are on layers now, before the frame paints |
1321
+ | `spriteAnimationEnded(key, id, finished)` | the render server is done with one: it ran out, or its layer went |
1322
+ | `spritesChanged()` | ask for a frame when the parts changed and nothing you draw did |
1323
+
1324
+ A part has:
1325
+
1326
+ - `key`; `rect` (device pixels, window coordinates); `reach` if it draws
1327
+ past `rect`; a `clip`, with `clipRadius` for round corners.
1328
+ - `paint(ctx)`, drawing it **where it is, at opacity 1, untransformed** (the
1329
+ layer applies both), and a `version` whose change paints it again.
1330
+ - At rest, `opacity` and `transform`: CSS's `matrix(a, b, c, d, e, f)` as an
1331
+ array, `e`/`f` in device pixels, about `origin` (default the rect's
1332
+ centre); out of the plane, `matrix3d()`'s sixteen numbers column by
1333
+ column, translation and perspective distance in device pixels — lifted
1334
+ only if no corner is ever behind the viewer.
1335
+ - `animations` of `opacity` or `transform`: `id`, `property`, `values`,
1336
+ `keyTimes`, `timings` (a curve per segment), `duration`, `delay`, `repeat`
1337
+ (`Infinity` loops), `autoreverse`, `hold` (stay on the last value). Hand a
1338
+ CSS transform animation over **sampled** into matrices from your own
1339
+ interpolation; Core Animation's would not turn a full turn or mix
1340
+ functions as CSS does.
1341
+
1342
+ Every frame the presenter applies promotion's rules **everywhere the part
1343
+ can be** through its animations:
1797
1344
 
1798
1345
  - **Nothing painted after the element reaches into what shows of it** — no
1799
- later sibling, no ancestor's border, ring or scrollbar. What shows is
1800
- what is inside its `clip` and inside every clipping ancestor with square
1801
- corners: the layer goes in a box that masks it to them, so a carousel's
1802
- slide under `overflow: hidden`, or a part scrolled half out of its pane,
1803
- is cut there as the bitmap is. A rounded ancestor has to hold all of it,
1804
- since one box cannot take its corners. A `clip` of your own may have
1805
- round corners — a card with a radius, cutting what it holds — and the
1806
- box takes them; an ancestor with square corners that cuts it again puts
1807
- that box in a box of its own. A part none of which shows — scrolled out
1808
- of its pane, or outside its own clip — stays on its layer, cut to show
1809
- nothing, so that its animation runs where it would and none of yours is
1810
- spent on it; past a budget of raster for such parts (32 MB a window), it
1811
- is yours to draw.
1812
- - **Neither the element nor any ancestor fades**: their group on the bitmap
1813
- would leave the layer out of it.
1814
- - **Your inside is yours.** Offer only a part that nothing you draw after it
1815
- overlaps — but for another part you offer after it, since that one's layer
1816
- stands above: list your parts in the order you paint them, and they stack
1817
- in it. The presenter cannot see inside your `paint`, so a part it turns
1818
- down is yours to draw, in the bitmap under every layer, and every part
1819
- listed before it that it overlaps is turned down with it, in that frame;
1820
- one it does not reach keeps its layer.
1821
-
1822
- A part can be inside another of yours — a spinner in a card that fades —
1823
- by naming it as its `parent`, earlier in the list. Its layer goes in the
1824
- parent's layer, so it fades, turns and is cut with it, and it is asked
1825
- about nothing more than staying inside the parent's raster everywhere it
1826
- can be: the parent was asked about all of that. Its `rect`, `origin` and
1827
- `clip` are where it is in the window with its parent untransformed. The
1828
- parent's `paint` is handed the keys of the parts lifted inside it, and
1829
- leaves them out; the presenter paints it again whenever they change, in
1830
- the frame they do. A part whose parent is not lifted, or that reaches out
1831
- of it, is yours to draw — in the parent's raster, where the parent is
1832
- lifted.
1346
+ later sibling, no ancestor's border, ring or scrollbar. The layer is
1347
+ masked to its `clip` and square-cornered clipping ancestors (a carousel
1348
+ slide under `overflow: hidden`); a rounded ancestor must hold all of it.
1349
+ A part wholly clipped out stays lifted showing nothing, its animation
1350
+ running, up to 32 MB of such raster per window; past that, yours to draw.
1351
+ - **Neither the element nor any ancestor fades.**
1352
+ - **Your inside is yours.** Offer only parts nothing you draw later
1353
+ overlaps, except later parts; list them in paint order. A part turned
1354
+ down is yours to draw, and turns down every earlier part it overlaps.
1355
+
1356
+ A part may name an earlier one as `parent` (a spinner in a fading card):
1357
+ its layer goes in the parent's, fading, turning and clipping with it, and
1358
+ must stay inside the parent's raster. Its `rect`, `origin` and `clip` are
1359
+ with the parent untransformed. The parent's `paint` gets the keys lifted
1360
+ inside it and leaves them out, and is repainted in the frame they change.
1361
+ A child whose parent is not lifted, or that reaches out of it, is yours to
1362
+ draw, in the parent's raster where that is lifted.
1833
1363
 
1834
1364
  A lifted part is a hole in your drawing from the frame `spritesLifted` says
1835
- so, and a part the set no longer holds is yours to draw again in that same
1836
- frame — the bitmap under it is claimed as it comes back. Declining is always
1837
- safe, and it is what happens everywhere but on macOS: X11, Wayland, the
1838
- layer presenter and a scene that refuses never ask, and your `paint` draws
1839
- everything as it always did, so the same element runs on every backend and
1840
- the animation runs on your own clock there.
1365
+ so; one dropped from the set is yours again that frame. Declining is always
1366
+ safe: X11, Wayland, the layer presenter and a scene that refuses never ask,
1367
+ `paint` draws everything, and animations run on your clock.
1841
1368
 
1842
1369
  #### A part that shows a source
1843
1370
 
1844
- A part may show something the presenter does not paint at all: its
1845
- `contents`, in place of `paint` and `version`. Today that is a
1846
- `VideoFrames` sink ([elements.md](elements.md#video)) — a camera feed in a
1847
- chart's corner, a preview inside a document — whose every `push` reaches the
1848
- part's layer by itself, with no frame of the window's and no `paint`:
1371
+ `contents`, in place of `paint` and `version`, is today a `VideoFrames` sink
1372
+ ([elements.md](elements.md#video)) — a camera feed in a chart, a preview in
1373
+ a document — whose every `push` reaches the layer with no window frame:
1849
1374
 
1850
1375
  ```js
1851
1376
  sprites() {
@@ -1861,40 +1386,29 @@ paint(ctx) {
1861
1386
  }
1862
1387
  ```
1863
1388
 
1864
- The layer is exactly `rect` — the picture scaled into it — so put its edges
1865
- on whole pixels, or the hole you leave and the layer that fills it meet with
1866
- a seam. Everything else is a part's: the rules above, a `clip` if you cut it,
1867
- `spritesChanged()` when the part comes or goes. A presenter that cannot show
1868
- the source — any bridge without video surfaces, and every backend but macOS —
1869
- declines the part, and drawing the newest frame yourself is the same code
1870
- that runs in every frame something is painted over it. `<video>` is built
1871
- exactly this way (src/nodes/video.js).
1872
-
1873
- Three things to know about ids and time. An animation is attached **once per
1874
- `id`**: one that changed is a new id, and one the render server finished is
1875
- not started again while you still list it. Its `delay` counts from the frame
1876
- that asks, so it is a fresh number every frame and read only on the first.
1877
- And a part is painted once unless its `version` changes — a move by whole
1878
- pixels moves the layer, but a move to another fraction of a pixel, or a new
1879
- size, paints it again.
1880
-
1881
- Sprites need `@windowkit/appkit` 0.19 or later, which takes a transform as
1882
- a matrix or a matrix3d and a negative delay as one begun in the past: on an
1883
- older bridge nothing is asked, and the element draws everything. A bridge
1884
- that lists `matrix` and not `matrix3d` among its `transformForms()` lifts
1885
- the parts in the plane and leaves those out of it to their elements.
1389
+ The layer is exactly `rect`, the picture scaled in, so keep it on whole
1390
+ pixels or a seam shows; `spritesChanged()` when it comes or goes. A
1391
+ presenter without video surfaces — every backend but macOS — declines, and
1392
+ you draw the newest frame. `<video>` is built this
1393
+ way (`src/nodes/video.js`).
1394
+
1395
+ For every part: an animation is attached **once per `id`**; change the id
1396
+ to restart it, and a finished one is not restarted while listed. `delay`
1397
+ counts from the asking frame and is read only on the first. A part repaints
1398
+ only on a new `version`, a fractional move or a resize; a whole-pixel move
1399
+ just moves the layer.
1400
+
1401
+ Sprites need `@windowkit/appkit` 0.19 or later; on an older bridge nothing
1402
+ is asked. A bridge whose `transformForms()` lacks `matrix3d` leaves parts
1403
+ out of the plane to their elements.
1886
1404
 
1887
1405
  #### A part that is its shadows
1888
1406
 
1889
- A part may be **shadows** alone: CSS's outer `box-shadow`, which the
1890
- render server draws on the GPU where your `paint` would blur a glow a pixel
1891
- at a time. It has `shadows` in place of `paint`, its `rect` is the box that
1892
- casts them — none of them falls inside it, with `rectRadius` corners, as an
1893
- outer shadow is never drawn under its box — and each shadow is the shape it
1894
- is the shadow of, with its corners' `radius`, an offset `x` and `y`, CSS's
1895
- `blur` radius and a `color`, `[r, g, b, a]` in straight sRGB from 0 to 1.
1896
- The first is on top, as CSS lists them; a spread is yours to fold into
1897
- the shape.
1407
+ `shadows` in place of `paint` makes a part CSS outer `box-shadow`s, blurred
1408
+ on the GPU. `rect` is the casting box, with `rectRadius`; nothing is drawn
1409
+ inside it. Each shadow: a `rect` and its corners' `radius`, offsets `x` and
1410
+ `y`, CSS's `blur` and `color` as straight-sRGB `[r, g, b, a]` in 0–1. The
1411
+ first is on top; fold a spread into the shape yourself.
1898
1412
 
1899
1413
  ```js
1900
1414
  sprites() {
@@ -1923,59 +1437,41 @@ sprites() {
1923
1437
  }
1924
1438
  ```
1925
1439
 
1926
- Their `animations` are of `'shadowBlur'`, `'shadowColor'` and
1927
- `'shadowOffset'`, with `shadow` saying which: blurs and `[x, y]` offsets in
1928
- device pixels, colours as above, and an `opacity` and a `transform` on the
1929
- whole part as any part has. A transition between two lists of shadows is
1930
- yours to pad, as CSS pads it, and to sample where Core Animation's straight
1931
- interpolation of a colour would not be CSS's. Where they can reach is
1932
- worked out from the shadows through every keyframe, and that is everywhere
1933
- the part is asked about: a part that is its shadows stands over what is
1934
- under its glow, so nothing painted after your element may reach into it.
1935
- Leave the box itself in your bitmap and draw everything but its shadows
1936
- there while the part is lifted. A part that is its shadows is never inside
1937
- another's.
1440
+ Animations: `'shadowBlur'`, `'shadowColor'`, `'shadowOffset'` (`shadow`
1441
+ the index; blurs and `[x, y]` in device pixels), plus `opacity` and
1442
+ `transform` on the whole part. Pad between different shadow lists as CSS
1443
+ does, and sample colours where Core Animation's straight interpolation is
1444
+ not CSS's. Nothing painted after your element may reach into the glow,
1445
+ through any keyframe. While lifted, draw the box minus its shadows. Such a
1446
+ part is never inside another.
1938
1447
 
1939
1448
  ### Elements that own a real X window
1940
1449
 
1941
- `drawn: false` is for a node backed by its own child X window rather than
1942
- painted into its parent — a GL surface, an XEMBED socket, a video overlay.
1943
- There are two worked examples, and they are worth reading in this order.
1944
-
1945
- `GlAreaNode` (`src/glnodes.js`) is the simple one: it holds no paint code,
1946
- and the owning `WindowNode` realizes it in the commit phase
1947
- (`_realizeChildWindows`) so the child window names its real parent from the
1948
- start. The ordering constraint — no X calls in the render phase, because the
1949
- render phase is discardable — is the part that is easy to get wrong.
1950
-
1951
- Input is the other thing a child window decides without being asked. X
1952
- delivers a device event to the first window up the hierarchy that selected
1953
- it, so a window that selects nothing hands the pointer over it to the window
1954
- the tree lives in — which is how a `<glarea>`'s presses reach the tree — and
1955
- one that selects `ButtonPress` keeps every press for itself. Select only
1956
- what the element needs from the server, and remember that ntk selects
1957
- whatever a window is listened to for. The panes a `<glarea>`'s children are
1958
- drawn on (`src/gloverlay.js`) select nothing for the same reason. The owning
1959
- window's hit test knows about `<glarea>` surfaces and their children and
1960
- asks them before the tree (`GlAreaNode.hitSurface`); it does not know about
1961
- a registered element's window, so a point over one lands on whatever the
1962
- tree has behind it.
1963
-
1964
- `ForeignNode` (`src/foreignnodes.js`) is the same shape with the stakes
1965
- raised, and it is the one to read if your element touches a resource you did
1966
- not create. Three things it has to do that a GL surface does not:
1967
-
1968
- - **Nothing in the render phase, for a sharper reason.** A `CreateWindow`
1969
- from a discarded render leaks a server resource; a `ReparentWindow` from
1970
- one has moved another application's window for real.
1971
- - **Teardown is synchronous.** `WindowNode.destroySubtree` destroys its own
1972
- X window in the same turn your node's does, and `DestroyWindow` takes every
1973
- inferior with it — so a release that waits for a round trip releases a
1974
- window that no longer exists. Compute what you would have asked for; the
1975
- requests are ordered on the connection, which is what makes "hand it back,
1976
- _then_ destroy the container" a guarantee.
1977
- - **Give it back, do not destroy it.** The element's own test file asserts
1978
- that first, because it is the failure this shape is most able to cause.
1450
+ `drawn: false` is for a node with its own child X window — a GL surface, an
1451
+ XEMBED socket, a video overlay. Read the two examples in order.
1452
+
1453
+ `GlAreaNode` (`src/glnodes.js`) has no paint code; the owning `WindowNode`
1454
+ realizes it in the commit phase, so the child names its real parent from
1455
+ the start. **No X calls in the render phase**, which can be discarded.
1456
+
1457
+ X delivers a device event to the first window up the hierarchy that
1458
+ selected it: a child that selects nothing passes the pointer to the tree's
1459
+ window (as `<glarea>` does), one that selects `ButtonPress` keeps every
1460
+ press. Select only what you need; ntk selects whatever a window is listened
1461
+ to for. The owning window's hit test knows `<glarea>` surfaces, not your
1462
+ window, so a point over yours lands on what the tree has behind it.
1463
+
1464
+ `ForeignNode` (`src/foreignnodes.js`), for a resource you did not create:
1465
+
1466
+ - **Nothing in the render phase.** A `CreateWindow` from a discarded render
1467
+ leaks a server resource; a `ReparentWindow` has moved another program's
1468
+ window for real.
1469
+ - **Teardown is synchronous.** `WindowNode.destroySubtree` destroys its X
1470
+ window in the same turn as your node, and `DestroyWindow` takes every
1471
+ inferior, so a release awaiting a round trip is too late. Requests are
1472
+ ordered on the connection, which makes "hand it back, _then_ destroy the
1473
+ container" a guarantee.
1474
+ - **Give it back, do not destroy it.**
1979
1475
 
1980
1476
  ## A layout algorithm of your own
1981
1477
 
@@ -2014,63 +1510,54 @@ registerLayout('radial', {
2014
1510
  ```
2015
1511
 
2016
1512
  **One function, two questions.** `layout(children, constraints, options,
2017
- info)` is asked how big the box's content is for `constraints` —
2018
- `{ width, height, widthMode, heightMode }`, the vocabulary `measureContent`
2019
- speaks, with `Infinity` for no bound — several times a pass, at sizes
2020
- nothing is drawn at. When both modes are `'exactly'` the box has its size,
2021
- and the answer places too: `children`, one rect per child, `{ x, y }` from
2022
- the content box's corner, with a `width` and a `height` to lay the child out
2023
- at — either one left out is the child's own. So it is a question about a
2024
- hypothetical, under the rules `measureContent` has
2025
- ([above](#what-it-must-not-do)): no reading `abs`, no state, finite numbers.
1513
+ info)` is asked how big the content is for `constraints` (as
1514
+ `measureContent` gets them, `Infinity` for no bound), several times a pass,
1515
+ at sizes nothing is drawn at. When both modes are `'exactly'` the answer
1516
+ places too: `children`, one rect per child — `{ x, y }` from the content
1517
+ box's corner, plus a `width` and `height` to lay it out at, either left out
1518
+ being the child's own. `measureContent`'s rules apply
1519
+ ([above](#what-it-must-not-do)): no `abs`, no state, finite numbers.
2026
1520
 
2027
1521
  **What it gets of a child** is a `LayoutChild`, never a node:
2028
1522
 
2029
- | member | |
2030
- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
2031
- | `measure(constraints?)` | the child's size under the constraints. A number with no mode is `'exactly'`, an axis left out `'unconstrained'`, `'at-most'` fit-content |
2032
- | `intrinsicSizes()` | `{ minContentWidth, maxContentWidth }`: the narrowest it can be drawn at — its content floor — and the width it takes with no bound |
2033
- | `options` | its `layoutItem`, against your `childOptions` |
2034
- | `style` | its own resolved style, read-only, in device pixels — what `grid` places a child by (`gridColumn`) and aligns it with (`alignSelf`) |
2035
- | `index` | its place in the list |
2036
-
2037
- Every size is the child's **margin box** — a margin is part of the room a
2038
- child takes — so an algorithm that lines children up is right about margins
2039
- without knowing they exist; and every size is in device pixels, like
2040
- `measureContent`'s.
2041
-
2042
- **Options are declared, and typed.** `options` and `childOptions` map each
2043
- name to `{ type, default }`: `'length'`, `'number'`, `'integer'`,
2044
- `'boolean'`, `'string'`, `'any'`, or an array of the values it may take. A
2045
- `length` is written in logical pixels in the style and arrives in device
2046
- pixels, so an algorithm never multiplies by the scale. A value of the wrong
2047
- type, or an option you did not declare, is reported naming it, and takes
2048
- the default. What the box's own style already says — `gap`,
2049
- `justifyContent`, `alignItems` — is `info.style`: read it rather than
2050
- declaring options that mean the same thing. `info.scale` is the box's device
2051
- pixels per logical pixel, for a length the schema cannot see: one inside a
2052
- string, like a grid's track list. `info.report(message, consequence)` says
2053
- that something in a style is wrong but can be laid out around — a grid area
2054
- nobody named — once, the way a bad style value is said; a throw is for what
2055
- cannot be laid out at all.
2056
-
2057
- **What core does, so the algorithm does not have to.** It lays each child
2058
- out at its rect, mirrors the slots when the box reads right to left and
2059
- rounds them to whole pixels, places absolutely positioned children by their
2060
- insets, measures the children's content floors, carries a child's change
2061
- into the next pass, and — when the algorithm throws, or answers something
2062
- that is not a size — reports it and lays the box out as flexbox, in the same
2063
- frame.
2064
-
2065
- Register before you render, as for an element. A name registered twice with
2066
- a different function throws unless `override: true`; under hot reload a
2067
- module re-registering its own replaces it, and a mounted box keeps the
2068
- definition it started with until its style names a layout again. The
2069
- built-in `masonry` and `equal-row` are worked examples (`src/layouts.js`),
2070
- and so is `grid` (`src/grid.js`), which takes no options: it reads the box's
2071
- tracks from `info.style` and each child's place from the child's `style`.
2072
-
2073
- In TypeScript, augment `CustomLayouts` with your layout's options and
1523
+ | member | |
1524
+ | ----------------------- | -------------------------------------------------------------------------------------------------- |
1525
+ | `measure(constraints?)` | its size. A bare number is `'exactly'`, an omitted axis `'unconstrained'`, `'at-most'` fit-content |
1526
+ | `intrinsicSizes()` | `{ minContentWidth, maxContentWidth }`: its content floor, and its width with no bound |
1527
+ | `options` | its `layoutItem`, against your `childOptions` |
1528
+ | `style` | its resolved style, read-only, device pixels (`gridColumn`, `alignSelf`…) |
1529
+ | `index` | its place in the list |
1530
+
1531
+ Every size is the child's **margin box**, in device pixels, so lining
1532
+ children up is right about margins without knowing they exist.
1533
+
1534
+ **Options are declared, and typed.** `options` and `childOptions` map names
1535
+ to `{ type, default }`: `'length'`, `'number'`, `'integer'`, `'boolean'`,
1536
+ `'string'`, `'any'`, or an array of allowed values. A `length` is written in
1537
+ logical pixels and arrives in device pixels. A wrong-typed value or an
1538
+ undeclared option is reported by name and takes the default. The box's own
1539
+ style (`gap`, `justifyContent`, `alignItems`) is `info.style`, so do not
1540
+ declare look-alikes. `info.scale` is for a length the schema cannot see (in
1541
+ a string, like a grid's tracks). `info.report(message, consequence)`
1542
+ reports once something wrong that can be laid out around (an unnamed grid
1543
+ area); throw only for what cannot be laid out at all.
1544
+
1545
+ **What core does.** It lays each child out at its rect, mirrors the slots
1546
+ for right-to-left and rounds them to whole pixels, places absolutely
1547
+ positioned children by their insets, measures content floors, carries a
1548
+ child's change into the next pass, and — when the algorithm throws or
1549
+ answers something that is not a size — reports it and lays the box out as
1550
+ flexbox in the same frame.
1551
+
1552
+ Register before you render. Registering a name twice with a different
1553
+ function throws unless `override: true`; under hot reload a module
1554
+ re-registering its own replaces it, and a mounted box keeps its definition
1555
+ until its style names a layout again. Worked examples: `masonry` and
1556
+ `equal-row` (`src/layouts.js`), and `grid` (`src/grid.js`), which takes no
1557
+ options and reads tracks from `info.style` and places from each child's
1558
+ `style`.
1559
+
1560
+ In TypeScript, augment `CustomLayouts` and
2074
1561
  `layout: { name: 'radial', radius: 40 }` type-checks, typos included;
2075
1562
  `CustomLayoutItem` is what a child may say:
2076
1563
 
@@ -2087,8 +1574,8 @@ declare module 'react-x11/host' {
2087
1574
  `registerPosition` adds a positioning scheme
2088
1575
  ([styling.md](styling.md#custom-positions)): the box is laid out in flow, as
2089
1576
  `relative` is, and after every layout pass `place(node, context)` says how
2090
- far to move it from there. A drop's flight home — from where the pointer let
2091
- go to where layout put the item:
1577
+ far to move it. A drop's flight home, from where the pointer let go to where
1578
+ layout put the item:
2092
1579
 
2093
1580
  ```js
2094
1581
  import { registerPosition } from 'react-x11/host';
@@ -2112,86 +1599,64 @@ registerPosition('landing', {
2112
1599
  });
2113
1600
  ```
2114
1601
 
2115
- `place` returns `{ x, y }` — how far to move the node, in device pixels, which
2116
- are rounded to whole ones for you — or null to leave it where layout put it;
2117
- `again: true` asks for another frame, which runs the placement pass with
2118
- nothing to lay out. It resizes nothing and sets no state: it runs on every
2119
- layout pass, a scroll's included, which is what lets what it moves land in
2120
- the frame that scrolled.
2121
-
2122
- What it is handed, in window coordinates and device pixels, the space `abs`
2123
- is in:
2124
-
2125
- | field | |
2126
- | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
2127
- | `laidOut` | `{ x, y, width, height }`: where layout put the node, before any placement moved it |
2128
- | `pane` | the nearest scroll pane: its `scrollport` — inside its border, over its padding — and `scrollX`/`scrollY`; or null |
2129
- | `container` | the box it is contained by: its parent's content box, or the whole scrolled content for a direct child of a pane |
2130
- | `margin` | its margins |
2131
- | `direction`/`scale` | the node's |
2132
- | `now` | the frame clock, in ms |
2133
- | `options` | its options, resolved as a layout's are |
2134
-
2135
- `node` is the node itself, for its `style`: the insets are the scheme's to
2136
- read — sticky reads them as thresholds — and yoga never sees them as
2137
- offsets.
2138
-
2139
- **What core does.** It re-derives where layout put the node on every pass
2140
- (nothing may remember an offset: the scroll fast path moves a node with its
2141
- pane, offset and all), claims where it was and where it went as damage —
2142
- into the scroll blit's ledger, so a pane of placed nodes keeps the blit —
2143
- lifts it over its siblings of the same `zIndex` for paint and for hit
2144
- testing, and when `place` throws, reports it once and leaves the node where
2145
- layout put it until its style names another position. `position: 'sticky'`
2146
- is written against exactly this context (`placeSticky` in `src/layouts.js`).
1602
+ `place` returns `{ x, y }`, how far to move the node in device pixels
1603
+ (rounded for you), or null to leave it; `again: true` asks for another
1604
+ frame, which runs placement with nothing to lay out. It resizes nothing and
1605
+ sets no state: it runs on every layout pass, a scroll's included, so what
1606
+ it moves lands in the frame that scrolled.
1607
+
1608
+ Its context, in window coordinates and device pixels:
1609
+
1610
+ | field | |
1611
+ | ------------------- | --------------------------------------------------------------------------------------------- |
1612
+ | `laidOut` | `{ x, y, width, height }`: where layout put the node, before placement |
1613
+ | `pane` | nearest scroll pane: `scrollport` (inside border, over padding), `scrollX`/`scrollY`; or null |
1614
+ | `container` | the parent's content box, or for a pane's direct child its scrolled content |
1615
+ | `margin` | its margins |
1616
+ | `direction`/`scale` | the node's |
1617
+ | `now` | the frame clock, in ms |
1618
+ | `options` | its options, resolved as a layout's are |
1619
+
1620
+ `node` is the node, for its `style`: the insets are the scheme's to read
1621
+ (sticky reads them as thresholds); yoga never sees them as offsets.
1622
+
1623
+ **What core does.** It re-derives the laid-out position every pass, so
1624
+ nothing remembers an offset; claims where the node was and went as damage,
1625
+ keeping a pane's scroll blit; lifts it over siblings of the same `zIndex`
1626
+ for paint and hit testing; and when `place` throws, reports it once and
1627
+ leaves the node where layout put it until its style names another position.
1628
+ `position: 'sticky'` is written against exactly this (`placeSticky` in
1629
+ `src/layouts.js`).
2147
1630
 
2148
1631
  In TypeScript, augment `CustomPositions` the way `CustomLayouts` is.
2149
1632
 
2150
1633
  ## The subpath exports
2151
1634
 
2152
- | subpath | |
2153
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
2154
- | `react-x11/host` | `registerElement`, `registerLayout`, `registerPosition` and their `unregister…`/`registered…` pairs, `hostTypes`, `knownElements`, `drawnKinds` |
2155
- | `react-x11/node` | `Node`, the built-in node classes, `Scrollable`, `intrinsicSize` |
2156
- | `react-x11/style` | `createStyles`, `flattenStyle`, `isStyleProp`, `resolveTokens`, the rest of the vocabulary |
2157
- | `react-x11/yoga` | the layout engine — `Yoga`, `loadLayout`, `layoutLoaded`. Rarely needed; see below |
2158
- | `react-x11/ntk` | ntk itself, re-exported — `Path2D`, `Image`, `Pixmap`, the font sources, `createClient` — `Surface`, on whichever backend the app is, and `decodeImageBytes` |
2159
- | `react-x11/keysyms` | the `XK_*` constants, `keysymOf`, `charOf`, `MOD`, `ctrlChordLetter` |
2160
-
2161
- **Reach ntk through `react-x11/ntk`, not a second dependency.** Two copies
2162
- of ntk in one process means two font caches and two glyph atlases, and a
2163
- node built against one cannot be painted by the other — a failure that
2164
- looks like a drawing bug rather than a dependency bug. It is also where a
2165
- drawing-adjacent name gets its backend-neutral answer: `Surface` from here
2166
- is a pixmap on X11, a CG bitmap on the Cocoa backend, a GL render target on
2167
- Wayland and a Direct2D bitmap on win32, where `Surface` from `ntk` is the
2168
- pixmap only. And it is where an element that draws images of its own
2169
- decodes them: `decodeImageBytes` is the ladder `<image>` reads
2170
- bytes with — WebP everywhere, and under Bun every format Bun reads, off the
2171
- JavaScript thread — where ntk's `decodeImage` is PNG and JPEG, synchronously
2172
- ([formats](elements.md#formats-and-what-decodes-them)).
2173
-
2174
- **An element does not need the layout engine, and that is deliberate.**
2175
- `measureContent` is handed its constraints in words (`'exactly'`,
2176
- `'at-most'`, `'unconstrained'`) rather than yoga's integers, so an element
2177
- never links against it and yoga's ABI never becomes part of this seam. If you
2178
- are writing an element and reaching for `react-x11/yoga`, the answer is
2179
- almost certainly `measureContent`.
2180
-
2181
- `react-x11/yoga` is for the case that is genuinely different. A layout that
2182
- arranges a box's children is `registerLayout`
2183
- ([above](#a-layout-algorithm-of-your-own)) and never needs it; what does is
2184
- an element laying out content it draws itself that wants to delegate part
2185
- of that to flexbox. `@react-x11/components`'s `<Html>` is the worked example
2186
- — its `display: flex` builds a small yoga tree, asks it, and reads the
2187
- answer back rather than re-deriving flexbox, which is long, subtle, and
2188
- silently wrong when it is wrong.
2189
-
2190
- Such a package must use **this** engine rather than its own `yoga-layout`
2191
- dependency, for the same reason it must reach ntk through `react-x11/ntk`:
2192
- two instances are two WebAssembly modules, and a node created by one cannot
2193
- be inserted into a tree owned by the other. That failure surfaces as a crash
2194
- inside the engine, naming nothing you wrote.
1635
+ | subpath | |
1636
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------- |
1637
+ | `react-x11/host` | `registerElement`/`Layout`/`Position`, their `unregister…` and `registered…s`, `hostTypes`, `knownElements`, `drawnKinds` |
1638
+ | `react-x11/node` | `Node`, the built-in node classes, `Scrollable`, `intrinsicSize` |
1639
+ | `react-x11/style` | `createStyles`, `flattenStyle`, `isStyleProp`, `resolveTokens`, the rest of the vocabulary |
1640
+ | `react-x11/yoga` | the layout engine: `Yoga`, `loadLayout`, `layoutLoaded`. Rarely needed (below) |
1641
+ | `react-x11/ntk` | ntk (`Path2D`, `Image`, `Pixmap`, font sources, `createClient`), the backend's `Surface`, `decodeImageBytes` |
1642
+ | `react-x11/keysyms` | the `XK_*` constants, `keysymOf`, `charOf`, `MOD`, `ctrlChordLetter` |
1643
+
1644
+ **Reach ntk through `react-x11/ntk`, never a second dependency.** Two copies
1645
+ mean two font caches and glyph atlases, and a node built against one cannot
1646
+ be painted by the other — it looks like a drawing bug. Its `Surface` is the
1647
+ backend's own (above), where ntk's is the X11 pixmap; its
1648
+ `decodeImageBytes` is what `<image>` uses — WebP everywhere, under Bun every
1649
+ format Bun reads, off the JavaScript thread — where ntk's `decodeImage` is
1650
+ PNG and JPEG, synchronously ([formats](elements.md#formats-and-what-decodes-them)).
1651
+
1652
+ **An element does not need the layout engine**: `measureContent` gets its
1653
+ constraints in words, not yoga's integers, and arranging a box's children
1654
+ is `registerLayout` ([above](#a-layout-algorithm-of-your-own)).
1655
+ `react-x11/yoga` is for an element laying out content it draws that wants
1656
+ flexbox for part of it (`@react-x11/components`' `<Html>` does, for
1657
+ `display: flex`). Use **this** engine, never your own `yoga-layout`: two
1658
+ WebAssembly instances cannot share nodes, and mixing them crashes inside
1659
+ the engine, naming nothing you wrote.
2195
1660
 
2196
1661
  ```ts
2197
1662
  import { Yoga, loadLayout } from 'react-x11/yoga';
@@ -2201,15 +1666,14 @@ const root = Yoga.Node.create();
2201
1666
  root.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
2202
1667
  ```
2203
1668
 
2204
- The enum constants are readable from the first tick; `Node` and `Config`
2205
- throw until the assembly is loaded, which `createRoot()` awaits before it
2206
- builds anything (docs/packaging.md explains why it is loaded rather than
2207
- imported).
1669
+ The enum constants are readable at once; `Node` and `Config` throw until
1670
+ the assembly is loaded, which `createRoot()` awaits
1671
+ ([packaging.md](packaging.md)).
2208
1672
 
2209
1673
  ## Typing
2210
1674
 
2211
- The element also has to be declared to JSX, which is module augmentation —
2212
- the same mechanism [typescript.md](typescript.md) describes:
1675
+ Declare the element to JSX by module augmentation, as
1676
+ [typescript.md](typescript.md) describes:
2213
1677
 
2214
1678
  ```ts
2215
1679
  import type { Style } from 'react-x11/style';
@@ -2228,15 +1692,9 @@ declare module 'react-x11/jsx-runtime' {
2228
1692
  }
2229
1693
  ```
2230
1694
 
2231
- `test/types/extend.tsx` in this repo compiles exactly that, so the story
2232
- stays true as the declarations move.
2233
-
2234
- The drawing side is declared too. `Context2D` from `react-x11/node` is the
2235
- canvas-shaped subset the backends implement — ntk's context over
2236
- XRender, and the one `src/backend/context2d.js` wraps over CoreGraphics on
2237
- Cocoa and Direct2D on win32 — so `paintContent(ctx: Context2D)`
2238
- type-checks every call an element makes, and a member not every backend
2239
- has is optional there (`globalCompositeOperation`, which a native context
2240
- takes only for the modes its bridge draws — assign it and read it back) or
2241
- absent. `paintCachePlan`, `paintCached`, `opaqueRect` and the rest of the
2242
- contract above are on the class, with `PaintCachePlan` for the plan.
1695
+ `Context2D` from `react-x11/node` is the canvas-shaped subset every backend
1696
+ implements, so `paintContent(ctx: Context2D)` type-checks every call; a
1697
+ member not every backend has is optional (`globalCompositeOperation`, which
1698
+ a native context takes only for modes its bridge draws — assign and read
1699
+ back) or absent. `paintCachePlan`, `paintCached`, `opaqueRect` and the rest
1700
+ of the contract are on the class, with `PaintCachePlan` for the plan.