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.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1091 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- 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
|
|
4
|
-
|
|
5
|
-
`<
|
|
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
|
|
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
|
|
55
|
-
| `drawn` | default `true`:
|
|
56
|
-
| `semanticNames` | prop names this element owns even though they are also style names
|
|
57
|
-
| `selfDamagedProps` |
|
|
58
|
-
| `childrenAllowed` | default `true`; `false` rejects children by name
|
|
59
|
-
| `override` | replace an existing registration. Off by default
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
[`react-x11/refresh`](ecosystem/dev-tooling.md#react-refresh) hot-reloads
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
| method | when
|
|
111
|
-
| ------------------------------ |
|
|
112
|
-
| `paint(ctx)` | draw. Call `super.paint(ctx)` first
|
|
113
|
-
| `paintContent(ctx)` | draw _between_
|
|
114
|
-
| `opaqueRect()` | the rect you cover
|
|
115
|
-
| `viewportFixedRects()` | what you draw fixed to your scroll pane's viewport, so its
|
|
116
|
-
| `paintOverhang()` | how far you draw past your box, so claims and culls cover it
|
|
117
|
-
| `applyProps(next, prev)` | props changed. Call `super
|
|
118
|
-
| `paintChanged(next, prev)` | did anything you draw change? Only
|
|
119
|
-
| `measureContent(constraints)` | your content has a size of its own
|
|
120
|
-
| `default*(ev)` |
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
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`
|
|
129
|
-
`:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
- `this.
|
|
133
|
-
|
|
134
|
-
- `this.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- `this.
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
glyph
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
**
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
|
401
|
-
|
|
|
402
|
-
| `
|
|
403
|
-
| `at-most`
|
|
404
|
-
| `
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
427
|
-
|
|
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
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
440
|
-
|
|
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
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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
|
|
469
|
-
width from a style height when only that
|
|
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
|
|
475
|
-
editing, a tree with type-ahead — implements
|
|
476
|
-
|
|
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
|
|
480
|
-
|
|
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
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
|
490
|
-
|
|
|
491
|
-
| `
|
|
492
|
-
| `
|
|
493
|
-
| `
|
|
494
|
-
| `
|
|
495
|
-
| `
|
|
496
|
-
| `
|
|
497
|
-
| `
|
|
498
|
-
| `
|
|
499
|
-
| `
|
|
500
|
-
| `
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
**
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
**
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
**
|
|
588
|
-
(
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
Tab
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
([events.md](events.md#
|
|
624
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
`
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
**
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
`
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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()` |
|
|
806
|
-
| `notifyA11ySceneChanged()` | the scene moved between commits
|
|
807
|
-
| `a11ySceneAction(id, action)` | an AT acted on one
|
|
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
|
|
814
|
-
| `rect` | required.
|
|
815
|
-
| `role` | an ARIA role
|
|
816
|
-
| `name` / `description` | what a screen reader says
|
|
817
|
-
| `states` | `selected`, `checked`, `expanded`, `disabled`, `busy
|
|
818
|
-
| `focusable` | default true
|
|
819
|
-
| `props` |
|
|
820
|
-
| `children` |
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
**
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
-
|
|
921
|
-
|
|
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
|
-
|
|
736
|
+
### Scrolling content you painted
|
|
926
737
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
`
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
**
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
`
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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;
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
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}`,
|
|
1116
|
-
coordinates) in place by
|
|
1117
|
-
survived
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
`
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
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
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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`.**
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
1349
|
-
**`paintChanged(next, prev)
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
**
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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`
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
|
|
1479
|
-
the
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
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
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
the
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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
|
|
1641
|
-
|
|
1642
|
-
|
|
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
|
|
1658
|
-
|
|
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
|
|
1665
|
-
|
|
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
|
|
1696
|
-
|
|
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,
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
`
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
the
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
|
1768
|
-
| `spriteAnimationEnded(key, id, finished)` | the render server is done with one: it ran out, or its layer went
|
|
1769
|
-
| `spritesChanged()` | ask for
|
|
1770
|
-
|
|
1771
|
-
A part
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
`
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
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.
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
|
1836
|
-
|
|
1837
|
-
|
|
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
|
-
|
|
1845
|
-
|
|
1846
|
-
`
|
|
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
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
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
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
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
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
window
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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
|
|
2018
|
-
`
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
the
|
|
2023
|
-
|
|
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?)` |
|
|
2032
|
-
| `intrinsicSizes()` | `{ minContentWidth, maxContentWidth }`:
|
|
2033
|
-
| `options` | its `layoutItem`, against your `childOptions`
|
|
2034
|
-
| `style` | its
|
|
2035
|
-
| `index` | its place in the list
|
|
2036
|
-
|
|
2037
|
-
Every size is the child's **margin box
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
`
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
`
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
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
|
|
2091
|
-
|
|
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 }
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
|
2126
|
-
|
|
|
2127
|
-
| `
|
|
2128
|
-
| `
|
|
2129
|
-
| `
|
|
2130
|
-
| `
|
|
2131
|
-
| `
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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`,
|
|
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
|
|
2158
|
-
| `react-x11/ntk` | ntk
|
|
2159
|
-
| `react-x11/keysyms` | the `XK_*` constants, `keysymOf`, `charOf`, `MOD`, `ctrlChordLetter`
|
|
2160
|
-
|
|
2161
|
-
**Reach ntk through `react-x11/ntk`,
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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
|
|
2205
|
-
|
|
2206
|
-
|
|
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
|
-
|
|
2212
|
-
|
|
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
|
-
`
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
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.
|