react-x11 2.51.1 → 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 +94 -61
- package/docs/README.md +256 -0
- package/docs/accessibility.md +575 -0
- package/docs/appearance.md +317 -0
- package/docs/click-to-component.md +92 -0
- package/docs/clipboard.md +262 -0
- package/docs/components.md +1231 -0
- package/docs/dbus.md +205 -0
- package/docs/debugging.md +371 -0
- package/docs/desktop-calendar.md +247 -0
- package/docs/desktop.md +523 -0
- package/docs/devtools.md +133 -0
- package/docs/drag-and-drop.md +585 -0
- package/docs/ecosystem/animation.md +360 -0
- package/docs/ecosystem/data-fetching.md +186 -0
- package/docs/ecosystem/dev-tooling.md +393 -0
- package/docs/ecosystem/forms.md +277 -0
- package/docs/ecosystem/headless.md +194 -0
- package/docs/ecosystem/i18n.md +86 -0
- package/docs/ecosystem/icons.md +377 -0
- package/docs/ecosystem/layout.md +206 -0
- package/docs/ecosystem/routing.md +135 -0
- package/docs/ecosystem/state.md +344 -0
- package/docs/ecosystem/testing.md +474 -0
- package/docs/ecosystem/theming.md +494 -0
- package/docs/ecosystem.md +439 -0
- package/docs/elements.md +2079 -0
- package/docs/embedding.md +239 -0
- package/docs/events.md +742 -0
- package/docs/extending.md +1700 -0
- package/docs/eyedropper.md +164 -0
- package/docs/filedialog.md +222 -0
- package/docs/frame.md +436 -0
- package/docs/gl.md +173 -0
- package/docs/globalmenu.md +195 -0
- package/docs/macos.md +592 -0
- package/docs/notifications.md +150 -0
- package/docs/packaging.md +468 -0
- package/docs/permissions.md +143 -0
- package/docs/react-features.md +405 -0
- package/docs/remote.md +179 -0
- package/docs/scale.md +225 -0
- package/docs/security.md +132 -0
- package/docs/styling.md +1710 -0
- package/docs/system.md +519 -0
- package/docs/testing.md +494 -0
- package/docs/typescript.md +75 -0
- package/docs/uri-schemes.md +340 -0
- package/docs/wayland-backend.md +332 -0
- package/docs/windows-integrations.md +177 -0
- package/docs/windows.md +380 -0
- package/package.json +11 -3
- 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/bus.js +39 -26
- package/src/cli/create.d.ts +43 -0
- package/src/cli/create.js +305 -0
- package/src/cli/env.js +484 -0
- package/src/cli/index.js +29 -2
- package/src/cli/templates/AGENTS.template.md +195 -0
- package/src/cli/templates/DESIGN.template.md +269 -0
- 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/desktopintegration.js +2 -1
- package/src/desktopsettings.js +193 -24
- package/src/dnd.js +2 -2
- package/src/filedialog.js +3 -3
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/keyboardstate.js +28 -0
- package/src/layouts.js +1 -1
- package/src/node.d.ts +10 -10
- 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/portal.js +31 -0
- package/src/refresh/index.js +2 -1
- package/src/screencolor.js +5 -10
- package/src/types/system.d.ts +8 -6
- 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 +86 -0
- package/src/wayland/clipboard.js +14 -7
- package/src/wayland/connection.js +3 -0
- package/src/wayland/context2d.js +129 -10
- package/src/wayland/protocols/index.json +6 -0
- package/src/wayland/protocols/xdg-foreign-unstable-v2.json +1 -0
- package/src/wayland/seat.js +28 -1
- package/src/wayland/swapchain.js +23 -0
- package/src/wayland/video.js +376 -0
- package/src/wayland/xkb.js +137 -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/elements.md
ADDED
|
@@ -0,0 +1,2079 @@
|
|
|
1
|
+
# Elements
|
|
2
|
+
|
|
3
|
+
Only `<window>`, `<popup>`, `<glarea>` and `<foreign>` are backed by real
|
|
4
|
+
windows of the display system. Everything else is a retained lightweight
|
|
5
|
+
node — one [yoga-layout](https://www.yogalayout.dev/) node each — painted
|
|
6
|
+
into the owning window's double-buffered 2d context on ntk's frame clock.
|
|
7
|
+
|
|
8
|
+
## `style` and props
|
|
9
|
+
|
|
10
|
+
Everything CSS has a concept for goes in **`style`**; everything else is a
|
|
11
|
+
prop. No name means both, which is why `<window width>` is unambiguously the
|
|
12
|
+
real window's geometry. See [styling.md](styling.md) for arrays, state
|
|
13
|
+
blocks and `createStyles`.
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
<box style={{ flexDirection: 'row', gap: 8 }} onClick={pick}>
|
|
17
|
+
<text style={{ fontSize: 14, color: '#2d3436' }}>hello</text>
|
|
18
|
+
</box>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Layout properties (all drawn elements + windows)
|
|
22
|
+
|
|
23
|
+
Lengths are logical pixels — a number, or `'12px'` — and `'50%'` / `'auto'`
|
|
24
|
+
pass through to yoga where it takes them. `em`, `rem`,
|
|
25
|
+
`vw`/`vh`/`vmin`/`vmax` and `calc()`/`min()`/`max()`/`clamp()` resolve to
|
|
26
|
+
pixels for their node, and again when what they are relative to changes; a
|
|
27
|
+
CSS unit react-x11 does not read (`ex`, `cqw`, `pt`) is an error naming it.
|
|
28
|
+
CSS's shorthands are read too — `padding: '8px 16px'`,
|
|
29
|
+
`border: '1px solid $border'`, `inset: 0`
|
|
30
|
+
([styling.md](styling.md#lengths-and-shorthands)).
|
|
31
|
+
|
|
32
|
+
- **Size**: `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
|
|
33
|
+
`maxHeight`, `aspectRatio`. `<image>` and `<svg>` keep their aspect ratio
|
|
34
|
+
when only one of `width`/`height` is given
|
|
35
|
+
- **Flex**: `flexDirection` (`row`, `column`, `row-reverse`,
|
|
36
|
+
`column-reverse`), `justifyContent` (`flex-start`, `center`, `flex-end`,
|
|
37
|
+
`space-between`, `space-around`, `space-evenly`), `alignItems`,
|
|
38
|
+
`alignSelf`, `alignContent`, `flexWrap`, `flex`, `flexGrow`, `flexShrink`,
|
|
39
|
+
`flexBasis`, `gap`, `rowGap`, `columnGap`
|
|
40
|
+
- **Position**: `position` (`relative`, `absolute`, `static`, `sticky`, or
|
|
41
|
+
one registered with `registerPosition`), `top`, `right`, `bottom`,
|
|
42
|
+
`left`, `start`, `end`, and `inset` for all four. Under `sticky` the
|
|
43
|
+
insets are how close to the nearest scroll pane's edges the box may come,
|
|
44
|
+
not offsets — see [styling.md](styling.md#sticky-positioning) and
|
|
45
|
+
[custom positions](styling.md#custom-positions)
|
|
46
|
+
- **Grid**: `display: 'grid'` arranges the children as a CSS grid —
|
|
47
|
+
`gridTemplateColumns`, `gridTemplateRows`, `gridTemplateAreas`,
|
|
48
|
+
`gridAutoColumns`, `gridAutoRows`, `gridAutoFlow` and `justifyItems` on
|
|
49
|
+
the box, `gridColumn`, `gridRow`, `gridArea` and `justifySelf` on a child
|
|
50
|
+
— see [styling.md](styling.md#grid)
|
|
51
|
+
- **Arrangement**: `layout` (`grid`, `masonry`, `equal-row`, or one
|
|
52
|
+
registered with `registerLayout`) arranges the children in place of
|
|
53
|
+
flexbox, and a child's `layoutItem` is what it tells that layout — see
|
|
54
|
+
[styling.md](styling.md#custom-layouts)
|
|
55
|
+
- **Spacing**: `margin`, `marginTop/Right/Bottom/Left`, `padding`,
|
|
56
|
+
`paddingTop/Right/Bottom/Left`, and the logical pairs `paddingInline`,
|
|
57
|
+
`paddingBlock`, `marginInline`, `marginBlock`. `margin` and `padding` take
|
|
58
|
+
CSS's one-to-four values as one string: `'0 auto'`, `'8px 16px'`
|
|
59
|
+
- **Visibility**: `display` (`flex`, `grid`, `none`), `overflow` (`visible`,
|
|
60
|
+
`hidden`, `scroll`)
|
|
61
|
+
|
|
62
|
+
### Everything shrinks, nothing shrinks to nothing
|
|
63
|
+
|
|
64
|
+
`flexShrink` is `1`, as in CSS, but **an item never shrinks below what is
|
|
65
|
+
inside it**: every item has a floor of its own min-content size (CSS's
|
|
66
|
+
`min-width: auto` on a flex item). A row of chips squeezes until it wraps; a
|
|
67
|
+
label squeezes to its longest word; a `height: 40` row stays 40 tall. To say
|
|
68
|
+
otherwise:
|
|
69
|
+
|
|
70
|
+
| write | to mean |
|
|
71
|
+
| --------------------------------- | ----------------------------------------- |
|
|
72
|
+
| `flexShrink: 0` | never give up any space at all |
|
|
73
|
+
| `minWidth: 0` / `minHeight: 0` | this may shrink past its content, to zero |
|
|
74
|
+
| `overflow: 'hidden'` / `'scroll'` | the same, and clip what no longer fits |
|
|
75
|
+
|
|
76
|
+
The last two are CSS's own rule, and why a `<box overflow="scroll">` — a
|
|
77
|
+
viewport smaller than its content — gets `minWidth: 0` and `minHeight: 0`
|
|
78
|
+
for free.
|
|
79
|
+
|
|
80
|
+
One deliberate difference from CSS: **a size that was named is kept**. CSS
|
|
81
|
+
floors an item at `min(the size it was given, its content)`, so an empty
|
|
82
|
+
`height: 40` box squashes to nothing in a column too short for it; here it
|
|
83
|
+
stays 40. Say `minHeight: 0` to get CSS's answer.
|
|
84
|
+
|
|
85
|
+
### `flex`, the shorthand
|
|
86
|
+
|
|
87
|
+
`flex: 1` is `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }` — "take this
|
|
88
|
+
share of what is left, from a base size of nothing" — the right thing for a
|
|
89
|
+
pane that should fill its window rather than its content. `flex: 'auto'`
|
|
90
|
+
grows and shrinks from the content's own size, and `flex: 'none'` does
|
|
91
|
+
neither. CSS's long form reads too — `'1 1 0'`, `'0 0 240px'`,
|
|
92
|
+
`'2 200px'`. A longhand written beside it wins, so
|
|
93
|
+
`{ flex: 1, flexBasis: 'auto' }` reads the way it does in CSS.
|
|
94
|
+
|
|
95
|
+
## Paint properties
|
|
96
|
+
|
|
97
|
+
- `backgroundColor` — any CSS color string (`'#2980b9'`,
|
|
98
|
+
`'rgba(0,0,0,.5)'`, `'red'`); `background` sets it and a gradient
|
|
99
|
+
(`backgroundImage`) in one
|
|
100
|
+
- any colour may be `'currentColor'`, the node's ink — its own `color` or
|
|
101
|
+
the one it inherits, followed when it moves
|
|
102
|
+
([styling.md](styling.md#currentcolor))
|
|
103
|
+
- `borderWidth`, `borderColor`, `borderRadius`, `borderStyle`
|
|
104
|
+
(`'solid'` default, `'dashed'`), or `border: '1px solid $border'` for the
|
|
105
|
+
first three — `border: '1px solid'` is drawn in `currentColor`
|
|
106
|
+
- per-side borders: `borderTopWidth`/`Right`/`Bottom`/`Left` override
|
|
107
|
+
`borderWidth`, `borderTopColor`/… fall back to `borderColor`, and
|
|
108
|
+
`borderBottom: '1px solid $border'` sets one side's pair — a blockquote's
|
|
109
|
+
bar is `borderLeftWidth: 3` on the quote box, a table rule
|
|
110
|
+
`borderBottomWidth: 1` on the row. `borderRadius` requires uniform
|
|
111
|
+
borders; a non-uniform border paints square
|
|
112
|
+
([styling.md](styling.md#per-side-borders))
|
|
113
|
+
- `zIndex` — paint/hit order among siblings (stable sort)
|
|
114
|
+
- `objectFit` — where an `<image>`'s or a `<video>`'s picture goes in its
|
|
115
|
+
content box, CSS's `object-fit`, centred: `'fill'` (`<image>`'s default),
|
|
116
|
+
`'contain'` (`<video>`'s), `'cover'`, `'none'`, `'scale-down'`
|
|
117
|
+
- `outlineWidth`, `outlineColor`, `outlineOffset` — the focus ring, painted
|
|
118
|
+
outside the border box and invisible to yoga, so it never moves what it
|
|
119
|
+
surrounds. A focusable node draws one on `:focus-visible` unasked;
|
|
120
|
+
`outlineWidth: 0` — or `outline: 'none'` — opts out
|
|
121
|
+
([styling.md](styling.md#the-focus-ring))
|
|
122
|
+
- `opacity` — how opaque the node is **with everything inside it**, 0 to 1:
|
|
123
|
+
the subtree is drawn once and composited at that alpha, so a card, its
|
|
124
|
+
icon and its text fade as one. `0` draws nothing and is still hit
|
|
125
|
+
([styling.md](styling.md#opacity))
|
|
126
|
+
- `transition` — `120`, or `{ backgroundColor: 120, left: 200 }`: how long a
|
|
127
|
+
change to that property takes ([styling.md](styling.md#transitions))
|
|
128
|
+
- any value may be a **theme token**: `'$panel'` resolves against the nearest
|
|
129
|
+
`theme` prop above the node ([styling.md](styling.md#theme-tokens))
|
|
130
|
+
- `'@width >= 600'` blocks restyle for the window's size, layout included
|
|
131
|
+
([styling.md](styling.md#window-size-queries))
|
|
132
|
+
|
|
133
|
+
`cursor` (`'pointer'`, `'text'`, `'wait'`, `'move'`, `'crosshair'`, resize
|
|
134
|
+
arrows, … — ntk's cursor names) and `pointerEvents` are style too.
|
|
135
|
+
`pointerEvents: 'none'` takes a node and its subtree out of hit testing;
|
|
136
|
+
`'box-none'` (React Native's) takes out only the node itself — its children
|
|
137
|
+
still take the pointer, and a press on its own area goes to what is behind:
|
|
138
|
+
the value for an overlay of controls, a `<glarea>`'s children included, that
|
|
139
|
+
must not swallow the clicks between them. So is `hitSlop`:
|
|
140
|
+
|
|
141
|
+
```jsx
|
|
142
|
+
<box style={{ height: 16, hitSlop: { top: 4, bottom: 4 } }} /> // 24px target
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`hitSlop: 4` grows every side, the object form only the sides it names. It
|
|
146
|
+
affects **hit testing only** — not paint, not layout — so a 16px control
|
|
147
|
+
can take the 24px target WCAG 2.2 SC 2.5.8 asks for without misaligning its
|
|
148
|
+
row. Where slop overlaps a sibling, the one painted on top wins.
|
|
149
|
+
|
|
150
|
+
## Interaction props
|
|
151
|
+
|
|
152
|
+
`focusable`, `tabIndex` (sequential focus order; `-1` is focusable but not
|
|
153
|
+
tabbable), `autoFocus`, `trapFocus` (own a focus scope — Tab and presses
|
|
154
|
+
stay inside it, focus is restored when it unmounts), `disabled` (never
|
|
155
|
+
focusable, and the trigger for a `:disabled` style block), and the event
|
|
156
|
+
handlers listed in [events.md](events.md).
|
|
157
|
+
|
|
158
|
+
`onLayout` reports layout: `{x, y, width, height}` in logical pixels, the
|
|
159
|
+
position within the parent _as laid out_ (a scroll does not fire it), after
|
|
160
|
+
the first layout and then on each change. It arrives after the pass, so
|
|
161
|
+
setting state from it is safe. Use it when the reaction to a size is not a
|
|
162
|
+
style ([react-features.md](react-features.md#measuring-a-node)); when it
|
|
163
|
+
is, a `'@container'` block answers in the same frame
|
|
164
|
+
([styling.md](styling.md#container-queries)).
|
|
165
|
+
|
|
166
|
+
`role` and the `aria-*` props are read by the built-in AT-SPI bridge so
|
|
167
|
+
screen readers see the tree; inert where no assistive technology listens,
|
|
168
|
+
with sensible defaults per element ([accessibility.md](accessibility.md)).
|
|
169
|
+
|
|
170
|
+
`dropAccept` + `onDrop` accept a drop and `draggable` + `dragData` start
|
|
171
|
+
one, with other applications as well as inside the app; the props alone
|
|
172
|
+
register the node ([drag-and-drop.md](drag-and-drop.md)).
|
|
173
|
+
|
|
174
|
+
`selectable` makes an element a **selection surface** — see
|
|
175
|
+
[Selecting text](#selecting-text).
|
|
176
|
+
|
|
177
|
+
`scale` zooms a subtree like CSS `zoom`, not a transform: every length under
|
|
178
|
+
the element, and its own style, is multiplied, and text is shaped at the
|
|
179
|
+
drawn size — what an element owning a viewport (a graph pane, a map) mounts
|
|
180
|
+
its content inside. Windows take no `scale`
|
|
181
|
+
([scale.md](scale.md#a-subtree-of-its-own)).
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## `<window>`
|
|
186
|
+
|
|
187
|
+
A real toplevel window — an X11 window, or an `NSWindow` on the Cocoa
|
|
188
|
+
backend; the flex, paint and event root for its subtree.
|
|
189
|
+
|
|
190
|
+
| prop | |
|
|
191
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
192
|
+
| `title` | window title (UTF-8, `WM_NAME` + `_NET_WM_NAME`) |
|
|
193
|
+
| `width`, `height`, `x`, `y` | geometry, not yoga style; a size is pixels or `'auto'` (default, [below](#natural-size)) |
|
|
194
|
+
| `backgroundColor` | full-window clear color (default white) |
|
|
195
|
+
| `onResize(ev)` | the geometry changed; the tree reflows itself. Fires for **moves** too |
|
|
196
|
+
| `onExpose(ev)` | after a repaint was required |
|
|
197
|
+
| `onCloseRequest(ev)` | WM close button; replaces the default answer |
|
|
198
|
+
| `states` | EWMH `_NET_WM_STATE`, controlled |
|
|
199
|
+
| `fullscreen`, `alwaysOnTop` | boolean sugar for two of those states |
|
|
200
|
+
| `decorations` | `false` asks the WM for no titlebar or border |
|
|
201
|
+
| `transparent` | 32-bit ARGB visual: rounded, translucent windows |
|
|
202
|
+
| `transientFor` | ICCCM `WM_TRANSIENT_FOR`: the window this one belongs to |
|
|
203
|
+
| `onStatesChange(states)` | what the window manager actually did |
|
|
204
|
+
| `onClientMessage(ev)` | a ClientMessage addressed to this window — EWMH, XEmbed, the tray |
|
|
205
|
+
| `theme` | palette `$token` style values resolve against, for this subtree |
|
|
206
|
+
| `embeddable` | never maps itself; an embedder ([`<foreign>`](embedding.md)) does. Set by a [`<Frame>`](frame.md) pane's root |
|
|
207
|
+
| `hidden` | realized and laid out, never mapped while true |
|
|
208
|
+
| `frameRate` | frame pacing: `'display'` (default), `'adaptive'`, `'throughput'`, fps, `{ budget, minFps, maxFps }` |
|
|
209
|
+
|
|
210
|
+
Each is described below. A geometry change sends only what changed, so a
|
|
211
|
+
new size leaves the window where the window manager or the user put it.
|
|
212
|
+
Windows may be nested (real X11 child windows). **Ref**: the live window —
|
|
213
|
+
on X11 the ntk `Window`, with `getContext('2d')`, `requestAnimationFrame`,
|
|
214
|
+
`setCursor` and the rest of the ntk API.
|
|
215
|
+
|
|
216
|
+
#### On the Cocoa backend
|
|
217
|
+
|
|
218
|
+
Everything about _drawing_ works — geometry, background, resize, close,
|
|
219
|
+
`transparent`, `theme`, `frameRate`, `hidden`. What a **window manager**
|
|
220
|
+
does differs, since macOS has none:
|
|
221
|
+
|
|
222
|
+
| prop | on Cocoa |
|
|
223
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `minWidth`/`minHeight`/`maxWidth`/`maxHeight` | honoured |
|
|
225
|
+
| `resizable`, `decorations` | honoured, but read **once, at creation** |
|
|
226
|
+
| `transparent`, `borderRadius` | always available: every window composites |
|
|
227
|
+
| `states: ['demands_attention']` | the Dock bounce, held until the state is removed |
|
|
228
|
+
| `fullscreen`, `states: ['maximized' \| 'hidden']` | the full-screen Space, the zoom, the Dock |
|
|
229
|
+
| `onStatesChange`, `useWindowState()` | fire for full screen (from the transition's start), zoom, Dock, key window; `desktop`/`obscured` never change |
|
|
230
|
+
| `embeddable` | a `<Frame>` pane, through shared surfaces ([frame.md](frame.md)) |
|
|
231
|
+
|
|
232
|
+
**Inert** — accepted and, today, silently ignored, so shared code stays
|
|
233
|
+
branch-free: every other `_NET_WM_STATE` (`sticky`, `below`,
|
|
234
|
+
`skip_taskbar`, `shaded`, …), `appId`, `windowType`, `gravity`, the
|
|
235
|
+
increment and aspect hints, and `transientFor` (a managed `<Dialog>` already
|
|
236
|
+
floats on its own window). `onClientMessage` never fires. Why:
|
|
237
|
+
[macos.md](macos.md#windowing-semantics-what-maps-what-bends-what-breaks).
|
|
238
|
+
|
|
239
|
+
### Natural size
|
|
240
|
+
|
|
241
|
+
A window with no `width`/`height` is **sized from its content and capped at
|
|
242
|
+
the screen**. `'auto'` says so explicitly, per axis:
|
|
243
|
+
|
|
244
|
+
```jsx
|
|
245
|
+
<window title="prefs" /> {/* natural, both ways */}
|
|
246
|
+
<window width={600} height="auto" /> {/* height follows the content */}
|
|
247
|
+
<window width="auto" maxWidth={720} minHeight={200} /> {/* auto within your own bounds */}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
This is CSS's shrink-to-fit — a top-level window has nothing to stretch
|
|
251
|
+
into; stretching into the screen is `fullscreen`. The size is worked out
|
|
252
|
+
before the window is created, so it never appears at the wrong size.
|
|
253
|
+
|
|
254
|
+
**How it is measured** — the second step is where the surprises are:
|
|
255
|
+
|
|
256
|
+
1. The tree is laid out with no available width, so nothing wraps and the
|
|
257
|
+
root reports its **max-content** width.
|
|
258
|
+
2. That is clamped into `[minWidth, min(maxWidth, the screen)]` — the size
|
|
259
|
+
hints bound the auto size as `min-width`/`max-width` bound `width: auto`;
|
|
260
|
+
`minWidth` wins a conflict.
|
|
261
|
+
3. The tree is laid out **again at that width**, and the height comes from
|
|
262
|
+
there: a paragraph that had to wrap is taller than step 1 said.
|
|
263
|
+
|
|
264
|
+
Unlike in CSS, the screen cap beats min-content: what does not fit is cut.
|
|
265
|
+
|
|
266
|
+
**The cap** is the usable area of the monitor the window opens on — its
|
|
267
|
+
Xinerama rect less the panels in `_NET_WORKAREA` — read once while
|
|
268
|
+
`createRoot()` connects; the screen on a server with neither; none in the
|
|
269
|
+
headless mock. The window manager's frame is not counted, so a window at
|
|
270
|
+
the cap is a titlebar too tall and the WM trims it.
|
|
271
|
+
|
|
272
|
+
**Afterwards, auto follows the content**, growing and shrinking with it,
|
|
273
|
+
until the user resizes the window or the window manager imposes a size;
|
|
274
|
+
then the window is theirs. A numeric `width` does the same, and `'auto'`
|
|
275
|
+
again hands it back. The window's own resize requests coming back are not a
|
|
276
|
+
takeover.
|
|
277
|
+
|
|
278
|
+
On X11 a re-fit is laid out once, at the new size: the frame waits for the
|
|
279
|
+
server to apply it (up to 150 ms, if the window manager refuses), so grown
|
|
280
|
+
content is never drawn cut off; the old pixels stay up meanwhile.
|
|
281
|
+
`REACT_X11_NO_REFIT_HOLD=1` turns the wait off
|
|
282
|
+
([debugging.md](debugging.md#react_x11_no_refit_hold1)). On Cocoa a window
|
|
283
|
+
is a whole number of **points**, rounded up (100.5 makes 101), and still
|
|
284
|
+
follows its content.
|
|
285
|
+
|
|
286
|
+
- A scroll container reports its **content** height, so an auto window
|
|
287
|
+
around a long list opens screen-tall. Give the window a `height`, or the
|
|
288
|
+
scroller a `maxHeight`.
|
|
289
|
+
- An auto width is the **unwrapped** row, so it depends on the UI font.
|
|
290
|
+
- A window tracking its content costs an extra layout pass on each frame
|
|
291
|
+
that lays out; one with both sizes given pays nothing.
|
|
292
|
+
|
|
293
|
+
### A floor the content decides
|
|
294
|
+
|
|
295
|
+
`minWidth`/`minHeight` also take `'auto'`: **the smallest size the content
|
|
296
|
+
can be drawn at**, measured and handed to the window manager so the user
|
|
297
|
+
cannot drag the window below it. Opt-in — Qt and GTK do it by default —
|
|
298
|
+
since CSS lets content overflow.
|
|
299
|
+
|
|
300
|
+
```jsx
|
|
301
|
+
<window minWidth="auto" minHeight="auto"> {/* never smaller than my content */}
|
|
302
|
+
<window width={900} minWidth="auto" /> {/* my opening size, the content's floor */}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**How the floor is measured**: the tree is laid out with _no room at all_,
|
|
306
|
+
and the floor is how far it still reached. Each node contributes what its
|
|
307
|
+
own style lets it shrink to:
|
|
308
|
+
|
|
309
|
+
| the node | contributes |
|
|
310
|
+
| --------------------------------- | ---------------- |
|
|
311
|
+
| a box that named a size | that size |
|
|
312
|
+
| a wrapping row | its widest item |
|
|
313
|
+
| text | its longest word |
|
|
314
|
+
| `overflow: 'scroll'` / `'hidden'` | nothing |
|
|
315
|
+
| a `minWidth: 0` of its own | nothing |
|
|
316
|
+
| a `minWidth: 240` of its own | 240 |
|
|
317
|
+
|
|
318
|
+
A registered element contributes its `measureContent` answer for the
|
|
319
|
+
smallest size ([extending.md](extending.md#a-size-of-your-own)). This is
|
|
320
|
+
every node's own floor
|
|
321
|
+
([above](#everything-shrinks-nothing-shrinks-to-nothing)) read at the root,
|
|
322
|
+
with the same escape hatch: a node that names a floor of its own stops its
|
|
323
|
+
contents counting, so a window around a scrolling pane is floored by
|
|
324
|
+
everything _except_ that pane.
|
|
325
|
+
|
|
326
|
+
`maxWidth`/`maxHeight="auto"` mean **the size the content wanted** — the
|
|
327
|
+
natural size, not the floor. Where nothing can shrink they are equal, so
|
|
328
|
+
`minWidth="auto" maxWidth="auto"` is a fixed-size dialog, as is
|
|
329
|
+
`resizable={false}`.
|
|
330
|
+
|
|
331
|
+
- **A floor is a request.** mutter, kwin and xfwm enforce it at the drag; a
|
|
332
|
+
tiling window manager may not, and the renderer will not fight it — clip
|
|
333
|
+
gracefully anyway.
|
|
334
|
+
- **`minHeight="auto"` is for the current width**, re-sent as that changes:
|
|
335
|
+
a paragraph is tallest at its narrowest.
|
|
336
|
+
- **The floor outlives the size.** An `'auto'` size stops tracking once the
|
|
337
|
+
user takes over; an `'auto'` bound does not. It costs a layout pass per
|
|
338
|
+
axis on frames that lay out, and is sent only when it moves.
|
|
339
|
+
|
|
340
|
+
### `hidden` — realized, laid out, not on screen
|
|
341
|
+
|
|
342
|
+
```jsx
|
|
343
|
+
<window hidden={minimized} title="scratch" />
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The window exists at its (measured) size with a live tree, but is not
|
|
347
|
+
mapped while `hidden`; clearing it maps the window where it stands. Nothing
|
|
348
|
+
unmounts, so state, subscriptions and the window survive — declarative
|
|
349
|
+
"minimize by unmapping", and how [`Tooltip`](components.md#tooltip)
|
|
350
|
+
measures a label before placing it.
|
|
351
|
+
|
|
352
|
+
`<Activity mode="hidden">` or a suspended `<Suspense>` compose with it: the
|
|
353
|
+
window shows only while **neither** hides it. Hiding releases the keyboard
|
|
354
|
+
focus inside ([focus-follows-visibility](events.md)); the reveal restores it
|
|
355
|
+
unless something else took it. On a managed `<window>` hiding is ICCCM's
|
|
356
|
+
_withdraw_, so on reveal the window manager may frame or place it anew, and
|
|
357
|
+
`states` are re-applied rather than remembered; a `<popup>` just unmaps and
|
|
358
|
+
maps.
|
|
359
|
+
|
|
360
|
+
### `frameRate` — pacing the frames under a flood
|
|
361
|
+
|
|
362
|
+
```jsx
|
|
363
|
+
<window frameRate="adaptive">…</window>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
By default a window paints every frame the clock gives it — on X11 as often
|
|
367
|
+
as the server keeps up, on macOS at the display's rate — right for a UI
|
|
368
|
+
answering input. For content that changes faster than anyone reads — a
|
|
369
|
+
terminal under `cat`, a socket-fed chart, a simulation — most of the JS
|
|
370
|
+
thread then goes to frames nobody sees, starving the producer.
|
|
371
|
+
|
|
372
|
+
`frameRate` prices frames in CPU time. Under `'adaptive'` painting may take
|
|
373
|
+
a quarter of the wall time while busy: credit accrues at that rate, each
|
|
374
|
+
frame spends what it cost (the flush, and on macOS the present), and a frame
|
|
375
|
+
claimed in debt waits until the credit is back to zero. So:
|
|
376
|
+
|
|
377
|
+
- **Idle is immediate**: the first frame after a pause paints on the next
|
|
378
|
+
tick, whatever the last cost.
|
|
379
|
+
- **Cheap is unthrottled**: a frame cheaper than its credit is never held,
|
|
380
|
+
so a blit-scrolled list keeps the display's rate.
|
|
381
|
+
- **A rare expensive frame is free**: the bucket holds a 50ms burst. A
|
|
382
|
+
_stream_ of them waits `cost × (1/budget − 1)` each — 18ms after a 6ms
|
|
383
|
+
frame.
|
|
384
|
+
- **A flood that ends is unthrottled at once**: the debt never exceeds one
|
|
385
|
+
frame's cost.
|
|
386
|
+
|
|
387
|
+
`minFps` bounds staleness: the screen is never more than `1000 / minFps` ms
|
|
388
|
+
behind the last paint. `maxFps` caps the rate, cheap frames included.
|
|
389
|
+
|
|
390
|
+
| value | `budget` | `minFps` | `maxFps` | when |
|
|
391
|
+
| ---------------------------- | -------: | -------: | -------: | --------------------------------------------------------- |
|
|
392
|
+
| `'display'` (the default) | 1 | none | none | a UI answering input: every frame, after React's batching |
|
|
393
|
+
| `'adaptive'` | 0.25 | 20 | none | fed off an input path: a terminal, a live chart |
|
|
394
|
+
| `'throughput'` | 0.1 | 10 | 30 | output over display: may lag up to 100ms during a flood |
|
|
395
|
+
| a number, `60` | 1 | none | 60 | a ceiling and nothing else |
|
|
396
|
+
| `{ budget, minFps, maxFps }` | | | | any subset; the rest are `'display'`'s |
|
|
397
|
+
|
|
398
|
+
A budget below 1 with no `minFps` warns in development: nothing would bound
|
|
399
|
+
the wait after an expensive frame.
|
|
400
|
+
|
|
401
|
+
Precedence: **`REACT_X11_FRAME_RATE`** (`adaptive`, `display`,
|
|
402
|
+
`throughput` or a number) overrides everything, so an A/B run needs no code
|
|
403
|
+
change; then **the prop**; then **`createRoot({ frameRate })`**, the
|
|
404
|
+
default for every window of that root; then `'display'`. A `<popup>` takes
|
|
405
|
+
its own prop the same way; a [`<glarea>`](#glarea) takes its window's unless
|
|
406
|
+
it names one.
|
|
407
|
+
|
|
408
|
+
The pacer never changes what is painted (held damage accumulates into the
|
|
409
|
+
next frame) and never delays the answer to input: a click or key paints at
|
|
410
|
+
once ([events.md](events.md)). It prices JS-thread time only, so on X11 it
|
|
411
|
+
rarely holds a frame, and it is inert where a backend has backpressure of
|
|
412
|
+
its own. `REACT_X11_TRACE=requests` prints `waited=` on each held frame
|
|
413
|
+
([debugging.md](debugging.md)). Design notes:
|
|
414
|
+
[frame pacing](../design/architecture/frame-pacing.md).
|
|
415
|
+
|
|
416
|
+
### `onResize` fires for moves
|
|
417
|
+
|
|
418
|
+
`onResize` reports _any_ geometry change: under a window manager that moves
|
|
419
|
+
windows opaquely, a title-bar drag fires it per pointer step at the same
|
|
420
|
+
size. The renderer spends nothing on a move; a handler should check which it
|
|
421
|
+
got:
|
|
422
|
+
|
|
423
|
+
```jsx
|
|
424
|
+
<window
|
|
425
|
+
onResize={(ev) => {
|
|
426
|
+
if (ev.resized) refit(ev.width, ev.height);
|
|
427
|
+
if (ev.moved) rememberPosition(ev.x, ev.y);
|
|
428
|
+
}}
|
|
429
|
+
/>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
`ev.previous` is the geometry these compare against. Once a reparenting
|
|
433
|
+
window manager has framed the window, `ev.x`/`ev.y` are frame-relative, not
|
|
434
|
+
screen coordinates.
|
|
435
|
+
|
|
436
|
+
### Stacking
|
|
437
|
+
|
|
438
|
+
Nested `<window>`s stack like drawn siblings — later on top, `zIndex` over
|
|
439
|
+
document order — and reordering them in JSX restacks the real windows.
|
|
440
|
+
Top-level windows are the window manager's to stack, so their tree order
|
|
441
|
+
means nothing; use `alwaysOnTop`.
|
|
442
|
+
|
|
443
|
+
```jsx
|
|
444
|
+
<window>
|
|
445
|
+
<window key="back" /> {/* bottom */}
|
|
446
|
+
<window key="front" /> {/* on top */}
|
|
447
|
+
<window key="always" zIndex={1} /> {/* above both, wherever it sits here */}
|
|
448
|
+
</window>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Window manager hints
|
|
452
|
+
|
|
453
|
+
Read by the window manager, applied at mount and on update (unchanged
|
|
454
|
+
values are not re-sent):
|
|
455
|
+
|
|
456
|
+
- `resizable={false}` pins min and max size to the current size.
|
|
457
|
+
- Size hints: `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `widthInc`,
|
|
458
|
+
`heightInc`, `baseWidth`, `baseHeight`, `minAspect`, `maxAspect`,
|
|
459
|
+
`gravity`.
|
|
460
|
+
- `appId`: `'instance'`, `['instance', 'Class']` or `{instance, class}`.
|
|
461
|
+
- `windowType`: `'dialog'`, `'utility'`, `'tooltip'`… or an array of
|
|
462
|
+
fallbacks.
|
|
463
|
+
- `decorations={false}` asks for no titlebar or border.
|
|
464
|
+
|
|
465
|
+
```jsx
|
|
466
|
+
<window width={400} height={300} resizable={false} windowType="dialog" />
|
|
467
|
+
<window width={400} height={300} minWidth={320} minHeight={200} />
|
|
468
|
+
<window width={400} height={300} minWidth="auto" /> {/* measured — see above */}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
On a `<window>` these names are the window's, not yoga's: `width`/`height`
|
|
472
|
+
are the real geometry the user can drag, `minWidth`/`maxHeight` what the WM
|
|
473
|
+
enforces, and the _contents_ are laid out by `style`. The four bounds also
|
|
474
|
+
take `'auto'` ([a floor the content decides](#a-floor-the-content-decides)),
|
|
475
|
+
and on an auto-sized window they bound the natural size, so
|
|
476
|
+
`<window resizable={false} windowType="dialog" />` is pinned at its measured
|
|
477
|
+
size — the fixed-size dialog.
|
|
478
|
+
|
|
479
|
+
### `transientFor` — a window that belongs to another
|
|
480
|
+
|
|
481
|
+
Without it, a secondary window is an unrelated application to the window
|
|
482
|
+
manager: its own taskbar and alt-tab entry, placed anywhere, stacked
|
|
483
|
+
independently, not minimised with its owner.
|
|
484
|
+
|
|
485
|
+
```jsx
|
|
486
|
+
const main = useRef(null);
|
|
487
|
+
|
|
488
|
+
<window ref={main} title="editor">…</window>
|
|
489
|
+
<window transientFor={main} windowType="dialog" title="Preferences">…</window>
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Takes a ref to a `<window>`/`<popup>` or to **any drawn node** (resolved to
|
|
493
|
+
its window, so an out-of-flow `<box>` in the owner is a good handle), a raw
|
|
494
|
+
XID, `'root'` (this client's whole window group), or `null` to clear. A ref
|
|
495
|
+
still null at mount — an owner mounted as a sibling in the same commit — is
|
|
496
|
+
retried next frame.
|
|
497
|
+
|
|
498
|
+
Set it **with** `windowType="dialog"`. Under EWMH a managed transient with
|
|
499
|
+
no type already is a dialog (kept out of taskbar and pager), but a `dialog`
|
|
500
|
+
with no owner has nothing to stack above or iconify with. Stacking above
|
|
501
|
+
the owner and iconifying with it are window-manager policy (quartz-wm does
|
|
502
|
+
neither). **Inert on override-redirect windows** — every `<popup>` by
|
|
503
|
+
default — which the WM never manages; ntk warns if asked.
|
|
504
|
+
|
|
505
|
+
`windowIdOf(refOrInstance)` is the same resolution: the XID, or null. An
|
|
506
|
+
xdg-desktop-portal `parent_window` is `` `x11:${id.toString(16)}` `` —
|
|
507
|
+
lowercase hex, **no** `0x`. Today's portal backends tolerate a prefix, so
|
|
508
|
+
the mistake goes unnoticed; Qt's parser returns 0 on failure, which would
|
|
509
|
+
mean a silently unparented, non-modal dialog. `useWindowId(ref)` is the
|
|
510
|
+
hook form, returning a getter like `useAnchor`.
|
|
511
|
+
|
|
512
|
+
`decorations={false}` writes `_MOTIF_WM_HINTS` (Mutter, KWin, Xfwm,
|
|
513
|
+
Openbox, i3); a window manager that ignores it decorates anyway.
|
|
514
|
+
|
|
515
|
+
### Closing a window
|
|
516
|
+
|
|
517
|
+
Every window the window manager manages advertises `WM_DELETE_WINDOW`, with
|
|
518
|
+
or without `onCloseRequest` — otherwise the close button would kill the
|
|
519
|
+
connection mid-frame, skipping effect cleanup. Windows the WM does not
|
|
520
|
+
frame (a child `<window>`, an override-redirect `<popup>`) do not; a
|
|
521
|
+
[managed `<popup>`](#a-managed-popup-is-a-dialog) does.
|
|
522
|
+
|
|
523
|
+
- **No `onCloseRequest`**: the **primary window** unmounts the tree and
|
|
524
|
+
closes the connection — effects clean up and the process ends on a drained
|
|
525
|
+
loop. Any other window refuses, with a dev warning.
|
|
526
|
+
- **`onCloseRequest`**: only your handler runs — `setOpen(false)` for a
|
|
527
|
+
dialog, a "save your work?" prompt, `root.unmount()` to quit.
|
|
528
|
+
|
|
529
|
+
The primary window is the first top-level `<window>` that is not
|
|
530
|
+
`transientFor` another and has no `windowType`; a lone window is primary
|
|
531
|
+
whatever its type. Secondary windows refuse because closing a
|
|
532
|
+
`{open && <window/>}` behind React's back would leave a window the app
|
|
533
|
+
believes open and can never reopen. Give a dialog a handler:
|
|
534
|
+
|
|
535
|
+
```jsx
|
|
536
|
+
{
|
|
537
|
+
showSettings && (
|
|
538
|
+
<window
|
|
539
|
+
title="Settings"
|
|
540
|
+
transientFor={main}
|
|
541
|
+
onCloseRequest={() => setShowSettings(false)}
|
|
542
|
+
/>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Window state
|
|
548
|
+
|
|
549
|
+
`states` is EWMH `_NET_WM_STATE`: `modal`, `sticky`, `maximized` (or
|
|
550
|
+
`maximized_vert`/`maximized_horz`), `shaded`, `skip_taskbar`, `skip_pager`,
|
|
551
|
+
`hidden`, `fullscreen`, `above`, `below`, `demands_attention`, `focused`.
|
|
552
|
+
`fullscreen` and `alwaysOnTop` are sugar for `fullscreen` and `above`,
|
|
553
|
+
unioned with `states`. On the [Cocoa backend](macos.md) `fullscreen` is the
|
|
554
|
+
full-screen Space, `maximized` the zoom, `hidden` the Dock, and
|
|
555
|
+
`demands_attention` bounces the Dock icon until the app is activated or the
|
|
556
|
+
state removed ([desktop.md](desktop.md)); the rest are inert.
|
|
557
|
+
|
|
558
|
+
```jsx
|
|
559
|
+
<window
|
|
560
|
+
states={['skip_taskbar']}
|
|
561
|
+
fullscreen={isFullscreen}
|
|
562
|
+
onStatesChange={(states) => setFullscreen(states.includes('fullscreen'))}
|
|
563
|
+
/>
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
**Controlled, and diffed against the previous props** — not the window's
|
|
567
|
+
current state, which the window manager changes behind the app's back
|
|
568
|
+
(maximize, a hotkey leaving fullscreen, a tiling WM). A state is re-sent
|
|
569
|
+
only when the app changes it, so a WM that disagreed is not fought.
|
|
570
|
+
`onStatesChange` reports what the WM did; without it the property is not
|
|
571
|
+
watched. States are applied **before mapping**, so a window opens fullscreen
|
|
572
|
+
without flashing at normal size. On XQuartz, whose quartz-wm lacks
|
|
573
|
+
`_NET_WM_STATE_ABOVE`, `alwaysOnTop` uses Apple-WM window levels.
|
|
574
|
+
|
|
575
|
+
### `onClientMessage` — speaking a protocol of your own
|
|
576
|
+
|
|
577
|
+
ClientMessage carries nearly every X11 convention — EWMH requests, XEmbed,
|
|
578
|
+
the system tray, protocols between copies of one app. `onClientMessage`
|
|
579
|
+
gets every one addressed to this window:
|
|
580
|
+
|
|
581
|
+
```jsx
|
|
582
|
+
<window
|
|
583
|
+
onClientMessage={(ev) => {
|
|
584
|
+
if (ev.messageType !== '_NET_SYSTEM_TRAY_OPCODE') return;
|
|
585
|
+
if (ev.data[1] === SYSTEM_TRAY_REQUEST_DOCK) dock(ev.data[2]);
|
|
586
|
+
}}
|
|
587
|
+
/>
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`ev.messageType` is the atom's **name** (no interning needed), `ev.atom` its
|
|
591
|
+
id, `ev.format` 8, 16 or 32, `ev.data` the payload at that width — 5, 10 or
|
|
592
|
+
20 values. No setup; a window without the prop pays nothing.
|
|
593
|
+
|
|
594
|
+
- **Order is preserved**, as chunked protocols need
|
|
595
|
+
(`_NET_SYSTEM_TRAY_BEGIN_MESSAGE` then `_NET_SYSTEM_TRAY_MESSAGE_DATA`).
|
|
596
|
+
`messageType` is `null` only for an atom the **server** does not know — a
|
|
597
|
+
broken sender.
|
|
598
|
+
- **Scoped to the window the message names**, unlike
|
|
599
|
+
`useApp().X.on('event')`, which sees every window's events and outlives
|
|
600
|
+
the element. Messages to other windows, the EWMH ones sent to the root
|
|
601
|
+
included, do not arrive; a window manager wants the raw stream
|
|
602
|
+
(`examples/wm-core.js`).
|
|
603
|
+
- **`preventDefault()` stops react-x11's own handling** — today XDND, for a
|
|
604
|
+
window speaking the drag protocol itself. The close button is
|
|
605
|
+
`onCloseRequest`'s.
|
|
606
|
+
|
|
607
|
+
Send with `useApp().X.SendClientMessage(destination, aboutWindow, atom,
|
|
608
|
+
format, data, eventMask)`, or ntk's `window.sendClientMessage(name, data)`,
|
|
609
|
+
which interns the atom. EWMH messages _about_ a window go to the **root**;
|
|
610
|
+
to another application's window pass event mask `0`. Taking a selection
|
|
611
|
+
needs a timestamp, not `CurrentTime`:
|
|
612
|
+
[`lastInputTime` and `serverTime`](clipboard.md#owning-a-selection-of-your-own).
|
|
613
|
+
|
|
614
|
+
### Kiosk
|
|
615
|
+
|
|
616
|
+
No `kiosk` prop — it is a combination:
|
|
617
|
+
|
|
618
|
+
```jsx
|
|
619
|
+
<window
|
|
620
|
+
fullscreen
|
|
621
|
+
decorations={false}
|
|
622
|
+
states={['above', 'skip_taskbar', 'skip_pager']}
|
|
623
|
+
style={{ cursor: 'none', flexGrow: 1 }}
|
|
624
|
+
/>
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
`cursor: 'none'` hides the pointer; an unset `cursor` inherits the root
|
|
628
|
+
window's ([styling.md](styling.md)).
|
|
629
|
+
|
|
630
|
+
### `transparent` — rounded corners and translucency
|
|
631
|
+
|
|
632
|
+
`transparent` creates the window on a 32-bit ARGB visual: what the tree
|
|
633
|
+
leaves unpainted stays empty, and a compositor blends it with what is
|
|
634
|
+
behind. With `borderRadius` in the window's own `style` that gives rounded
|
|
635
|
+
corners with an **antialiased** edge (alpha, not a 1-bit Shape mask):
|
|
636
|
+
|
|
637
|
+
```jsx
|
|
638
|
+
<popup
|
|
639
|
+
transparent
|
|
640
|
+
grab
|
|
641
|
+
style={{ backgroundColor: 'rgba(24, 24, 30, 0.86)', borderRadius: 14 }}
|
|
642
|
+
>
|
|
643
|
+
…
|
|
644
|
+
</popup>
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
`backgroundColor` may be translucent; unset, the window is empty but for
|
|
648
|
+
what the tree paints. A window's `borderRadius` works **only** with
|
|
649
|
+
`transparent`; on an opaque window it is ignored (the corners would show
|
|
650
|
+
the server's white).
|
|
651
|
+
|
|
652
|
+
### When transparency is not available
|
|
653
|
+
|
|
654
|
+
**On the Cocoa backend it always is.** On X11 it needs a **depth-32
|
|
655
|
+
TrueColor visual** (XQuartz has none) and a **running compositor** (Mutter,
|
|
656
|
+
KWin, picom, …); without one, painted-away corners show **black**.
|
|
657
|
+
react-x11 prevents that: where transparency would not be seen, the window
|
|
658
|
+
is filled edge to edge, its `borderRadius` ignored and a translucent
|
|
659
|
+
`backgroundColor` flattened — square and opaque, with nothing asked of the
|
|
660
|
+
app. Design the other side with `'@supports transparency'`:
|
|
661
|
+
|
|
662
|
+
```jsx
|
|
663
|
+
<popup
|
|
664
|
+
transparent
|
|
665
|
+
style={{
|
|
666
|
+
backgroundColor: '#1c1c22', // square and opaque, works everywhere
|
|
667
|
+
'@supports transparency': {
|
|
668
|
+
backgroundColor: 'rgba(24, 24, 30, 0.86)',
|
|
669
|
+
borderRadius: 14,
|
|
670
|
+
},
|
|
671
|
+
}}
|
|
672
|
+
/>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
The block ([Capability queries](styling.md#capability-queries)) is answered
|
|
676
|
+
per window and re-resolved live: a compositor started mid-session rounds the
|
|
677
|
+
popup without a remount, which is why `transparent` takes the 32-bit visual
|
|
678
|
+
even when nothing composites yet. `useSupports('transparency')` asks the
|
|
679
|
+
same of the display, before any window exists:
|
|
680
|
+
|
|
681
|
+
```jsx
|
|
682
|
+
const canBlend = useSupports('transparency');
|
|
683
|
+
const margin = canBlend ? 26 : 0; // room for a client-drawn shadow
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
`REACT_X11_NO_TRANSPARENCY=1` shows the fallback on a compositing display:
|
|
687
|
+
`transparent` is ignored and both answer false
|
|
688
|
+
([debugging.md](debugging.md)). Toggling `transparent` on a mounted window
|
|
689
|
+
does nothing until it remounts (change its `key`): the visual is fixed at
|
|
690
|
+
creation.
|
|
691
|
+
|
|
692
|
+
Input still hits the full rectangle — corners are invisible, not
|
|
693
|
+
click-through (the Shape input region is reachable through the ntk ref).
|
|
694
|
+
Child `<window>`s are composited by the X server, not the compositor, so a
|
|
695
|
+
transparent window nested in another blends against nothing.
|
|
696
|
+
|
|
697
|
+
## `<popup>`
|
|
698
|
+
|
|
699
|
+
An override-redirect top-level window at **screen coordinates** — menus,
|
|
700
|
+
tooltips, dropdowns — that the window manager ignores (no decorations, no
|
|
701
|
+
focus stealing). Its place in the JSX tree does not affect its place on
|
|
702
|
+
screen; it is its own paint and event root, and conditional rendering
|
|
703
|
+
controls its lifetime. `anchor` places it against a node
|
|
704
|
+
([below](#anchor--a-popup-that-places-itself)); `x`/`y` place it in screen
|
|
705
|
+
logical pixels, e.g. at `ev.screenX`/`screenY`. Same props as `<window>`,
|
|
706
|
+
**including [natural size](#natural-size)** — a menu sized by its rows.
|
|
707
|
+
|
|
708
|
+
| prop | |
|
|
709
|
+
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
|
710
|
+
| `anchor` | place against a node, a rect inside one, or a screen rect, at the content's size |
|
|
711
|
+
| `grab` | hold a pointer grab while up — how menus behave on X (below) |
|
|
712
|
+
| `grabKeyboard` | take the keyboard while up — for a popover a tray click opened (below) |
|
|
713
|
+
| `transparent` | rounded corners and translucency ([above](#transparent--rounded-corners-and-translucency)) |
|
|
714
|
+
| `onDismiss` | a press landed outside the popup: close it |
|
|
715
|
+
| `trapFocus` | own a focus scope: a modal ([events.md](events.md#focus-scopes-modals)) |
|
|
716
|
+
| `overrideRedirect` | `false` makes it a WM-managed window — a real dialog (below) |
|
|
717
|
+
| `dragPreview` | a drag preview following the pointer, never a drop target ([drag-and-drop.md](drag-and-drop.md)) |
|
|
718
|
+
|
|
719
|
+
### `anchor` — a popup that places itself
|
|
720
|
+
|
|
721
|
+
```jsx
|
|
722
|
+
function Completions({ editorRef, caret, matches, onPick }) {
|
|
723
|
+
if (!matches.length) return null;
|
|
724
|
+
return (
|
|
725
|
+
<popup
|
|
726
|
+
anchor={{ to: editorRef, at: caret, placement: 'bottom' }}
|
|
727
|
+
maxHeight={220}
|
|
728
|
+
>
|
|
729
|
+
<box
|
|
730
|
+
style={{ overflow: 'scroll', padding: 4, backgroundColor: '$surface' }}
|
|
731
|
+
>
|
|
732
|
+
{matches.map((m) => (
|
|
733
|
+
<box key={m.label} onClick={() => onPick(m)} style={{ padding: 6 }}>
|
|
734
|
+
<text>{m.label}</text>
|
|
735
|
+
</box>
|
|
736
|
+
))}
|
|
737
|
+
</box>
|
|
738
|
+
</popup>
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
That is the whole completion list: **at the caret**, as wide as its widest
|
|
744
|
+
label, at most 220 tall and scrolling past that, flipped above the line near
|
|
745
|
+
the screen's bottom — no font measured, size stated or position named.
|
|
746
|
+
|
|
747
|
+
`anchor` takes [`anchorRect`'s options](components.md#useanchorref--anchorrectnode-options)
|
|
748
|
+
— `placement`, `align`, `offset`, `alignOffset`, `alignTo`, `at` — plus `to`,
|
|
749
|
+
the node (or a ref to one) it hangs off; `x`/`y` are ignored while it is
|
|
750
|
+
set. The popup is measured, placed (flip and screen-edge clamp depend on its
|
|
751
|
+
size) and only then created, so it appears at the right size in the right
|
|
752
|
+
place. It then follows everything that can move either half: the anchor's
|
|
753
|
+
layout, an ancestor scrolling, the owner window moving, its own content
|
|
754
|
+
growing.
|
|
755
|
+
|
|
756
|
+
`at` is a rect **inside** the node, in its coordinates —
|
|
757
|
+
`{ x: caretX, y: lineTop, width: 1, height: lineHeight }` — read by the
|
|
758
|
+
flip, the alignment and `offset` as a small widget there would be; `{x, y}`
|
|
759
|
+
alone is a point.
|
|
760
|
+
|
|
761
|
+
An anchor ref not attached yet (written _above_ its trigger) is waited for,
|
|
762
|
+
one frame, rather than opening at the screen corner. **When the anchor
|
|
763
|
+
scrolls out of view the popup unmaps**, and returns — re-taking its
|
|
764
|
+
`grab` — with the anchor; a real window cannot be clipped by the anchor's
|
|
765
|
+
ancestors. To _close_ it instead, place it yourself with
|
|
766
|
+
[`useAnchorTracking`](components.md#useanchorref--anchorrectnode-options)'s
|
|
767
|
+
`onOutOfView`.
|
|
768
|
+
|
|
769
|
+
A popup never takes the X input focus, but nodes in it can hold the **owner
|
|
770
|
+
window's** focus — with `trapFocus` and `autoFocus` that is a modal
|
|
771
|
+
([Focus inside a `<popup>`](events.md#focus-inside-a-popup)). One with no
|
|
772
|
+
owner window takes keys with `grabKeyboard`
|
|
773
|
+
([below](#a-popover-under-a-tray-item)).
|
|
774
|
+
|
|
775
|
+
**`grab` makes a menu dismissable.** Without it, a press elsewhere —
|
|
776
|
+
another app, the root window, even this app's own window frame (the window
|
|
777
|
+
manager's) — never reaches the client, and the menu stays open. With it the
|
|
778
|
+
press is redirected to the popup, lands outside it, and `onDismiss` fires.
|
|
779
|
+
The app's own windows still get their presses, so submenus and the owner
|
|
780
|
+
keep working; only a menu's root popup needs the grab. `Select`,
|
|
781
|
+
`ContextMenu` and `MenuBar` do this.
|
|
782
|
+
|
|
783
|
+
### A popover under a tray item
|
|
784
|
+
|
|
785
|
+
A tray click has no node: [`useTray`](desktop.md#the-tray)'s `onClick`
|
|
786
|
+
reports the item's screen frame. `anchor` takes it as `rect` instead of
|
|
787
|
+
`to`, with the same flip, clamp and natural size, kept on that monitor and
|
|
788
|
+
below the menu bar:
|
|
789
|
+
|
|
790
|
+
```jsx
|
|
791
|
+
function MenuBarApp() {
|
|
792
|
+
const [open, setOpen] = useState(null);
|
|
793
|
+
const close = () => setOpen(null);
|
|
794
|
+
useTray({ icon: 'waveform', onClick: setOpen });
|
|
795
|
+
if (!open) return null;
|
|
796
|
+
return (
|
|
797
|
+
<popup
|
|
798
|
+
anchor={{ rect: open, placement: 'bottom', align: 'center', offset: 6 }}
|
|
799
|
+
grab
|
|
800
|
+
grabKeyboard
|
|
801
|
+
onDismiss={close}
|
|
802
|
+
onBlur={close}
|
|
803
|
+
>
|
|
804
|
+
<Controls onClose={close} />
|
|
805
|
+
</popup>
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
`rect` is logical screen pixels; `{ x, y }` alone is a point — all a
|
|
811
|
+
freedesktop tray reports. It moves only when replaced, and is never out of
|
|
812
|
+
view. Its monitor is the one the **whole rect** overlaps most (a menu-bar
|
|
813
|
+
item's frame starts above its display's top), else the nearest.
|
|
814
|
+
|
|
815
|
+
**`grabKeyboard`**: a menu-bar app has no window for keys to arrive
|
|
816
|
+
through, so the popup takes the keyboard while up:
|
|
817
|
+
|
|
818
|
+
| backend | what it is |
|
|
819
|
+
| ------- | ------------------------------------------------------------------------------- |
|
|
820
|
+
| X11 | a keyboard grab, taken and dropped with the map, as `grab` is for the pointer |
|
|
821
|
+
| macOS | a key-capable window at the popup level, activating the app — `NSPopover`'s way |
|
|
822
|
+
| Wayland | nothing more: a grabbing popup has the keyboard already |
|
|
823
|
+
|
|
824
|
+
On macOS the window's kind is fixed at creation, so the prop does not change
|
|
825
|
+
on a mounted popup. The popover closes on `onBlur` there (another app takes
|
|
826
|
+
the key window) and on `onDismiss` on X; pass both.
|
|
827
|
+
|
|
828
|
+
**Shortcuts work with nothing passed**: a binding belongs to the top-level
|
|
829
|
+
`<window>`, or in an app without one, to the root-level `<popup>` holding
|
|
830
|
+
the keyboard. So do `useFileDialog`'s parent, the global menu and
|
|
831
|
+
`useWindowState`.
|
|
832
|
+
|
|
833
|
+
```jsx
|
|
834
|
+
function Controls({ onClose, onToggle }) {
|
|
835
|
+
useAccelerator([['space']], onToggle);
|
|
836
|
+
useAccelerator([['Escape']], onClose);
|
|
837
|
+
return <box style={{ padding: 12, gap: 8 }}>…</box>;
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
A binding with neither a window nor a popup above it — above the
|
|
842
|
+
`if (!open)`, say — never fires; development says so once, and
|
|
843
|
+
[`scope`](events.md#accelerators) anchors one at a node you name.
|
|
844
|
+
|
|
845
|
+
### Centred over something
|
|
846
|
+
|
|
847
|
+
`placement: 'center'` puts the popup **over** its anchor, middle on middle,
|
|
848
|
+
clamped to the monitor. Over a window's rect it is a dialog —
|
|
849
|
+
[`Dialog`](components.md#dialog) passes its owner's screen rect as `rect`:
|
|
850
|
+
|
|
851
|
+
```jsx
|
|
852
|
+
<popup
|
|
853
|
+
anchor={{ rect: ownerRect, placement: 'center' }}
|
|
854
|
+
width={360}
|
|
855
|
+
height="auto"
|
|
856
|
+
>
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
It is centred **once**: when its size changes later it keeps its top edge
|
|
860
|
+
and horizontal middle, moving only to stay on the monitor, so what the user
|
|
861
|
+
is reading does not jump. A managed one grows from wherever it was moved. A
|
|
862
|
+
new `rect`, or the `to` node moving, centres it again.
|
|
863
|
+
|
|
864
|
+
### A managed `<popup>` is a dialog
|
|
865
|
+
|
|
866
|
+
`overrideRedirect` defaults to `true` — what makes a menu a menu. `false`
|
|
867
|
+
hands the popup to the window manager: framed, titled, movable, with a
|
|
868
|
+
close button (`onCloseRequest`). With `transientFor` it is a real dialog,
|
|
869
|
+
above its owner, out of the taskbar and alt-tab, minimising with the owner.
|
|
870
|
+
|
|
871
|
+
```jsx
|
|
872
|
+
<popup overrideRedirect={false} transientFor={anchor} windowType="dialog"
|
|
873
|
+
title="Preferences" trapFocus>
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
Turn `grab` off with it — a grab swallows the press that starts a WM drag —
|
|
877
|
+
so a press outside no longer dismisses it, as befits a dialog.
|
|
878
|
+
[`Dialog`](components.md#dialog) does this; `managed={false}` returns to
|
|
879
|
+
override-redirect.
|
|
880
|
+
|
|
881
|
+
`windowType` defaults to `"popup_menu"`; name yours (`"tooltip"`,
|
|
882
|
+
`"dropdown_menu"`, … — `Select`'s sheet and `Tooltip` do). It is
|
|
883
|
+
**additive**: the popup stays override-redirect, and compositors use the
|
|
884
|
+
type for consistent shadows and animations.
|
|
885
|
+
|
|
886
|
+
## `<box>`
|
|
887
|
+
|
|
888
|
+
The flex container. All layout + paint + interaction props above.
|
|
889
|
+
**Ref**: the retained node — `abs` (`{x, y, width, height}` within the
|
|
890
|
+
window, valid after layout). With `overflow: 'scroll'` it is also the
|
|
891
|
+
**scroll container**.
|
|
892
|
+
|
|
893
|
+
## Scrolling: `overflow: 'scroll'` {#scrolling}
|
|
894
|
+
|
|
895
|
+
There is no scrolling element: a `<box>` or `<window>` with
|
|
896
|
+
`overflow: 'scroll'` is a clipped viewport over its overflowing children, on
|
|
897
|
+
**both axes**:
|
|
898
|
+
|
|
899
|
+
```jsx
|
|
900
|
+
<box style={{ overflow: 'scroll', flexGrow: 1 }}>{rows}</box>
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
The wheel scrolls it; a draggable thumb is drawn on each overflowing axis.
|
|
904
|
+
`overflow: 'hidden'` clips without scrolling. Being a style, it toggles
|
|
905
|
+
without a remount (`overflow={dense ? 'scroll' : 'visible'}` keeps the node,
|
|
906
|
+
children and state); a box that stops scrolling resets its offset to 0, as
|
|
907
|
+
in CSS.
|
|
908
|
+
|
|
909
|
+
**It is a tab stop and takes keys** whenever it has somewhere to scroll, so
|
|
910
|
+
unfocusable content (a log, a long `<text>`) is readable without a pointer:
|
|
911
|
+
|
|
912
|
+
| key | |
|
|
913
|
+
| ------------------- | ------------------------------------ |
|
|
914
|
+
| arrows | one wheel notch (48px) on that axis |
|
|
915
|
+
| PageUp / PageDown | a viewport, keeping a sliver of it |
|
|
916
|
+
| Space / Shift+Space | the same, for the hand already there |
|
|
917
|
+
| Home / End | the top and the bottom |
|
|
918
|
+
|
|
919
|
+
One that fits its content is no tab stop, takes no keys and no wheel: **the
|
|
920
|
+
wheel chains past it** to the next scroll container out. The keys are a
|
|
921
|
+
default action — your `onKeyDown` runs first, and `preventDefault()` cancels
|
|
922
|
+
them. A pane with an `onClick` pages with Space and activates with Enter
|
|
923
|
+
([events.md](events.md#space-and-enter-are-a-click)); a focusable row
|
|
924
|
+
_inside_ a pane takes both keys and pages with neither.
|
|
925
|
+
|
|
926
|
+
A press on the bar never reaches the row behind it. A thumb drag keeps its
|
|
927
|
+
grip; a track press pages towards it. Drags move by whole device pixels, as
|
|
928
|
+
the wheel does ([extending.md](extending.md#scrolling-content-you-painted)),
|
|
929
|
+
so nothing is drawn off the pixel grid. `<textarea>` behaves the same, and a
|
|
930
|
+
bar press there never moves the caret.
|
|
931
|
+
|
|
932
|
+
### The layout defaults it brings
|
|
933
|
+
|
|
934
|
+
| default | when |
|
|
935
|
+
| ------------------------------ | ---------------------------------------- |
|
|
936
|
+
| `flexBasis: 0` | `flexGrow > 0` and no `width` / `height` |
|
|
937
|
+
| `minWidth: 0` / `minHeight: 0` | always |
|
|
938
|
+
|
|
939
|
+
`min-*: 0` lets a viewport be smaller than its content; the zero basis is
|
|
940
|
+
half of `flex: 1`. Without them a header/scroll/footer window grows past its
|
|
941
|
+
bounds and pushes the footer out of view. An explicit value always wins.
|
|
942
|
+
|
|
943
|
+
### Props
|
|
944
|
+
|
|
945
|
+
| prop | |
|
|
946
|
+
| ------------------- | -------------------------------------------------------------------------------- |
|
|
947
|
+
| `onScroll(ev)` | `{scrollX, scrollY, contentWidth, contentHeight, viewportWidth, viewportHeight}` |
|
|
948
|
+
| `onViewport(ev)` | `{width, height, contentWidth, contentHeight}` whenever they change |
|
|
949
|
+
| `scrollbar={false}` | hide the drawn scrollbars |
|
|
950
|
+
| `scrollbarColor` | thumb color |
|
|
951
|
+
|
|
952
|
+
**Ref**: the node, plus `scrollTo` / `scrollBy` / `scrollIntoView(node)` /
|
|
953
|
+
`canScroll(dx, dy)` and `scrollX` / `scrollY` / `contentWidth` /
|
|
954
|
+
`contentHeight`. In TypeScript, `useRef<ScrollableNode>(null)` types them;
|
|
955
|
+
`DrawnNode` still works for a box you do not scroll.
|
|
956
|
+
|
|
957
|
+
`scrollTo(y)` scrolls vertically; `scrollTo({x, y})` either axis, leaving an
|
|
958
|
+
omitted one; `scrollBy` likewise. `canScroll(dx, dy)` says whether there is
|
|
959
|
+
room on the axis a delta names — what the wheel asks each node on its way
|
|
960
|
+
out, and what your own element implements
|
|
961
|
+
([extending.md](extending.md#scrolling-content-you-painted)).
|
|
962
|
+
|
|
963
|
+
Horizontal content comes from children that will not shrink (a row of
|
|
964
|
+
fixed-width cells). Its extent is measured **through the subtree**, like
|
|
965
|
+
`scrollWidth`: a row stretched to the viewport whose cells overflow it
|
|
966
|
+
still scrolls; a node clipping its own children ends the measurement. A
|
|
967
|
+
horizontal wheel axis scrolls sideways, as does **Shift + vertical
|
|
968
|
+
wheel**. With both bars, each stops short of the other's corner. Distances
|
|
969
|
+
are the device's: with XI2 a touchpad scrolls by the fraction of a notch it
|
|
970
|
+
measured. A wheel notch and an arrow key are 48 pixels
|
|
971
|
+
([events.md](events.md#wheel)).
|
|
972
|
+
|
|
973
|
+
`onViewport` fires from **layout**, so it arrives before anyone scrolls —
|
|
974
|
+
what a virtualized list (`Table`) needs to decide how many rows to build,
|
|
975
|
+
since an effect cannot read the size off the ref yet. `onLayout`
|
|
976
|
+
([Interaction props](#interaction-props)) is the same signal on any element,
|
|
977
|
+
without the content size.
|
|
978
|
+
|
|
979
|
+
`scrollIntoView(node)` scrolls the minimum on both axes to show a
|
|
980
|
+
descendant; one larger than the viewport gets CSSOM's `block: nearest`
|
|
981
|
+
(left alone if it covers the viewport, else brought in by the nearer edge).
|
|
982
|
+
It is safe right after the node mounts — it resolves on the next layout
|
|
983
|
+
pass — and a node already in view leaves the pane alone.
|
|
984
|
+
|
|
985
|
+
`scrollTo` answers inside the call from the last pass's extent: the offset
|
|
986
|
+
moves and `onScroll` fires before it returns. If a pass is owed, whatever
|
|
987
|
+
that extent could not satisfy is re-applied against the new content before
|
|
988
|
+
the frame paints, so restoring a position from a mount-time
|
|
989
|
+
`useLayoutEffect` shows it in the first frame, and `scrollTo(Infinity)` from
|
|
990
|
+
a layout effect in the commit that added rows shows the end in the first
|
|
991
|
+
frame that has them. `Infinity` means the end wherever the pass finds it
|
|
992
|
+
(as the End key does) — prefer it to `contentHeight`, the last pass's
|
|
993
|
+
measurement, which falls short when a commit adds more than a viewport. A
|
|
994
|
+
`scrollBy` before the pass continues from where the pass puts the pane.
|
|
995
|
+
Until then `scrollX`/`scrollY` read the call's answer; `onScroll` reports
|
|
996
|
+
the pass's afterwards.
|
|
997
|
+
|
|
998
|
+
`onScroll` fires for **every** offset change. Wheel, keys, bars and calls on
|
|
999
|
+
a laid-out pane report inside the call. Layout's own moves — a held
|
|
1000
|
+
`scrollTo` landing, `scrollIntoView` resolving (focus on a row below the
|
|
1001
|
+
fold), the clamp when content shrinks or the viewport grows — report after
|
|
1002
|
+
the frame that shows them, only if the offset moved. The payload is read at
|
|
1003
|
+
delivery, so a handler mirroring the offset into state ends where the pane
|
|
1004
|
+
did.
|
|
1005
|
+
|
|
1006
|
+
### On a `<window>`
|
|
1007
|
+
|
|
1008
|
+
`<window style={{ overflow: 'scroll' }}>` scrolls the window's own content
|
|
1009
|
+
with the same wheel, keys and bars; it stays a `frame` to a screen reader.
|
|
1010
|
+
Its ref is still ntk's `Window`, so reach the scroll API through a ref on a
|
|
1011
|
+
box inside it, or the node in `onViewport`.
|
|
1012
|
+
|
|
1013
|
+
## `<text>`
|
|
1014
|
+
|
|
1015
|
+
Shaped, wrapped text (bidi, ligatures, kerning, font fallback). Strings and
|
|
1016
|
+
numbers are legal only inside `<text>`; nested `<text>`s are **style spans**
|
|
1017
|
+
of one paragraph, wrapped as one.
|
|
1018
|
+
|
|
1019
|
+
| prop | |
|
|
1020
|
+
| --------------------------------------------------- | ------------------------------------------------------ |
|
|
1021
|
+
| `color` | text color — inherited (see below) |
|
|
1022
|
+
| `fontSize`, `fontFamily`, `fontWeight`, `fontStyle` | ntk font style (fontconfig lookup + fallback) |
|
|
1023
|
+
| `fontVariationSettings` | a variable font's axes — see below |
|
|
1024
|
+
| `textRendering` | which glyph path to draw with — see below |
|
|
1025
|
+
| `letterSpacing` | space after every character, in px — see below |
|
|
1026
|
+
| `fontVariantNumeric`, `fontFeatureSettings` | figures and OpenType features — see below |
|
|
1027
|
+
| `textAlign` | `left`, `right`, `center`, `start`, `end` (bidi-aware) |
|
|
1028
|
+
| `lineHeight` | multiplier over the natural font line height |
|
|
1029
|
+
| `textWrap` | `wrap` (default) or `nowrap` |
|
|
1030
|
+
| `maxLines` | how many lines are kept — unlimited by default |
|
|
1031
|
+
| `textOverflow` | `clip` (default) or `ellipsis`, for what was cut |
|
|
1032
|
+
|
|
1033
|
+
The first four rows, `fontVariationSettings`, `textRendering`,
|
|
1034
|
+
`letterSpacing` and the two feature properties **inherit**, into spans and
|
|
1035
|
+
across `<box>`es alike
|
|
1036
|
+
([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)); the
|
|
1037
|
+
rest belong to the `<text>` that owns the box.
|
|
1038
|
+
|
|
1039
|
+
`{ textWrap: 'nowrap', textOverflow: 'ellipsis' }` is one line ending in
|
|
1040
|
+
`…`; an ellipsis with no `maxLines` means one line. Only the pixels are cut
|
|
1041
|
+
— the accessible name, caret indices and `textContent()` keep the whole
|
|
1042
|
+
string
|
|
1043
|
+
([styling.md](styling.md#keeping-text-on-one-line-and-saying-when-it-did-not-fit)).
|
|
1044
|
+
A font file of the app's own opens with `loadFont`/`useFont`, which return
|
|
1045
|
+
the family name for `fontFamily`
|
|
1046
|
+
([styling.md](styling.md#a-font-file-of-your-own)).
|
|
1047
|
+
|
|
1048
|
+
`start`/`end` follow the box's **direction**, not the string's first strong
|
|
1049
|
+
character: the paragraph is laid out at that base level, so `"(12) files"`
|
|
1050
|
+
in an RTL panel punctuates as RTL, and text with no strong characters still
|
|
1051
|
+
lands on the right side
|
|
1052
|
+
([styling.md](styling.md#direction-and-the-logical-edges)).
|
|
1053
|
+
|
|
1054
|
+
### Variable fonts
|
|
1055
|
+
|
|
1056
|
+
`fontWeight` drives a variable font's `wght` axis, so one file serves every
|
|
1057
|
+
weight, those between named instances included; `fontVariationSettings`
|
|
1058
|
+
sets other axes by tag:
|
|
1059
|
+
|
|
1060
|
+
```jsx
|
|
1061
|
+
<text style={{ fontFamily: 'Inter', fontWeight: 460 }}>
|
|
1062
|
+
Serious by default.
|
|
1063
|
+
</text>
|
|
1064
|
+
<text style={{ fontVariationSettings: { wdth: 87.5, slnt: -8 } }}>
|
|
1065
|
+
condensed
|
|
1066
|
+
</text>
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
It inherits, and spans may override it. Unknown axes are ignored and values
|
|
1070
|
+
clamped, so it is safe on any face; it compares **by value**, so a literal
|
|
1071
|
+
is fine. Faces are instantiated on demand but never quantized: a slider
|
|
1072
|
+
bound to an axis with `step={1}` asks for hundreds of faces as it drags —
|
|
1073
|
+
step the control. Use the `.ttf`; a variable `.woff2` cannot be
|
|
1074
|
+
instantiated (ntk's `docs/fonts.md`).
|
|
1075
|
+
|
|
1076
|
+
### `textRendering`
|
|
1077
|
+
|
|
1078
|
+
CSS's property: which glyph path draws the text.
|
|
1079
|
+
|
|
1080
|
+
| value | |
|
|
1081
|
+
| -------------------- | --------------------------------------------------------- |
|
|
1082
|
+
| `auto` (default) | ntk decides from the size — right for UI text |
|
|
1083
|
+
| `geometricPrecision` | glyph origins exactly where shaping asked; nothing cached |
|
|
1084
|
+
| `optimizeSpeed` | ntk's cached glyph bitmaps, at any size |
|
|
1085
|
+
| `optimizeLegibility` | accepted, means `auto` — there is no hinting to turn on |
|
|
1086
|
+
|
|
1087
|
+
```jsx
|
|
1088
|
+
<text style={{ fontSize: 96, textRendering: 'geometricPrecision' }}>
|
|
1089
|
+
Serious by default.
|
|
1090
|
+
</text>
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
The cached path puts each glyph on a whole pixel, so animating `wght` makes
|
|
1094
|
+
single glyphs jump while neighbours stand still; `geometricPrecision`
|
|
1095
|
+
places them where shaping asked, and the line slides. It inherits (a span
|
|
1096
|
+
may override it) and **never reflows** — only draw-time rounding differs —
|
|
1097
|
+
so it is safe to flip on a state change. It rasterizes outlines on every
|
|
1098
|
+
draw and caches nothing: right for animated display text, wrong for a
|
|
1099
|
+
static paragraph, hence `auto`.
|
|
1100
|
+
|
|
1101
|
+
### Letter spacing and figures
|
|
1102
|
+
|
|
1103
|
+
`fontVariantNumeric: 'tabular-nums'` gives every digit one width, so a
|
|
1104
|
+
changing number (a readout beside a slider) stops shuffling its row; most
|
|
1105
|
+
UI faces (SF Pro, Inter) default to proportional figures.
|
|
1106
|
+
|
|
1107
|
+
```jsx
|
|
1108
|
+
<text style={{ fontSize: 30, fontVariantNumeric: 'tabular-nums' }}>
|
|
1109
|
+
{hz} Hz
|
|
1110
|
+
</text>
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
Keywords are CSS's: `lining-nums`/`oldstyle-nums`,
|
|
1114
|
+
`proportional-nums`/`tabular-nums`, `diagonal-fractions`/`stacked-fractions`,
|
|
1115
|
+
`ordinal`, `slashed-zero` — several at once, one per pair
|
|
1116
|
+
(`'tabular-nums slashed-zero'`); `'normal'` restores the face's own.
|
|
1117
|
+
|
|
1118
|
+
`fontFeatureSettings` takes any OpenType feature by tag: an array to turn
|
|
1119
|
+
on, or an object of tag to on, off or alternate index. It wins where both
|
|
1120
|
+
name a feature; missing features are ignored; it compares by value.
|
|
1121
|
+
|
|
1122
|
+
```jsx
|
|
1123
|
+
<text style={{ fontFeatureSettings: ['tnum', 'zero'] }}>0.10</text>
|
|
1124
|
+
<text style={{ fontFeatureSettings: { ss01: true, salt: 2 } }}>Rag</text>
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
`letterSpacing` is CSS's, in logical pixels, after every character (the
|
|
1128
|
+
last on a line too); negative tightens. Spaced text drops the optional
|
|
1129
|
+
ligatures (`liga`, `clig`, `dlig`, `hlig`) — name one in
|
|
1130
|
+
`fontFeatureSettings` to keep it.
|
|
1131
|
+
|
|
1132
|
+
```jsx
|
|
1133
|
+
<text style={{ fontSize: 11, fontWeight: 600, letterSpacing: 1.26 }}>
|
|
1134
|
+
NOISE TYPE
|
|
1135
|
+
</text>
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
All three inherit, the two feature properties independently as in CSS (a
|
|
1139
|
+
`fontVariantNumeric` below replaces the one above, leaving an inherited
|
|
1140
|
+
`fontFeatureSettings` alone). All three re-measure on change, and none may
|
|
1141
|
+
go in a state block.
|
|
1142
|
+
|
|
1143
|
+
### Tiny text
|
|
1144
|
+
|
|
1145
|
+
Text under **6 logical pixels** is painted as one strip of its ink per line
|
|
1146
|
+
instead of glyphs — in a minimap, a zoomed-out graph or a grid of thousands
|
|
1147
|
+
of cells, it is an illegible smudge anyway, and the strip costs one fill.
|
|
1148
|
+
Layout is unchanged, and the size is logical, so it holds on a 2x panel.
|
|
1149
|
+
`createRoot({ textStripBelow })` moves the threshold (`0` keeps glyphs at
|
|
1150
|
+
every size); `REACT_X11_TEXT_STRIP_BELOW` sets it without `createRoot`.
|
|
1151
|
+
|
|
1152
|
+
## Selecting text
|
|
1153
|
+
|
|
1154
|
+
Labels are not selectable, as `Gtk.Label` is not: most desktop text is
|
|
1155
|
+
chrome, and a stray drag lighting up a caption is noise. An element opts in:
|
|
1156
|
+
|
|
1157
|
+
```jsx
|
|
1158
|
+
<box selectable style={{ padding: 12 }}>
|
|
1159
|
+
<text style={{ fontSize: 20, fontWeight: 'bold' }}>Release notes</text>
|
|
1160
|
+
<text>Everything below this heading can be dragged over and copied.</text>
|
|
1161
|
+
</box>
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
It becomes a **surface**: a drag selects across all text under it, a double
|
|
1165
|
+
click takes a word, a triple click a block, Ctrl+A the surface, Ctrl+C
|
|
1166
|
+
copies. Releasing the button hands the text to **PRIMARY**, so a middle
|
|
1167
|
+
click in a terminal pastes it ([clipboard.md](clipboard.md)); macOS has no
|
|
1168
|
+
PRIMARY, so only a copy reaches the pasteboard
|
|
1169
|
+
([clipboard.md](clipboard.md#primary-is-x11-only)).
|
|
1170
|
+
|
|
1171
|
+
### What is in a surface
|
|
1172
|
+
|
|
1173
|
+
Everything under it that answers for its own text, in document order:
|
|
1174
|
+
`<text>`, and any element implementing the accessors in
|
|
1175
|
+
[extending.md](extending.md#answering-for-your-own-text) — a terminal, log
|
|
1176
|
+
view or code editor from outside this package joins the same drag, with no
|
|
1177
|
+
registration. **Not** in it:
|
|
1178
|
+
|
|
1179
|
+
- anything under `selectable={false}` (CSS's `user-select: none`) — a
|
|
1180
|
+
list's bullets, a table's chrome, a caption inside a document;
|
|
1181
|
+
- `<textinput>`, `<textarea>` and any element declaring `hasOwnSelection` —
|
|
1182
|
+
they keep their own selection, and a press in one is theirs;
|
|
1183
|
+
- `<svg>`, and registered elements that draw text without exposing
|
|
1184
|
+
character geometry.
|
|
1185
|
+
|
|
1186
|
+
### What a copy assembles
|
|
1187
|
+
|
|
1188
|
+
Separators come from **layout**, not markup: text sharing a band of pixels
|
|
1189
|
+
is joined with a **tab**, text starting below with a **newline** — cells and
|
|
1190
|
+
rows for a table, a paragraph per line for prose. An item excluded with
|
|
1191
|
+
`selectable={false}` is absent from the copy as from the highlight.
|
|
1192
|
+
|
|
1193
|
+
```jsx
|
|
1194
|
+
// copies as "name\tsize\nnotes\t4 kB"
|
|
1195
|
+
<box selectable>
|
|
1196
|
+
<box style={{ flexDirection: 'row' }}>
|
|
1197
|
+
<text style={{ width: 120 }}>name</text>
|
|
1198
|
+
<text>size</text>
|
|
1199
|
+
</box>
|
|
1200
|
+
<box style={{ flexDirection: 'row' }}>
|
|
1201
|
+
<text style={{ width: 120 }}>notes</text>
|
|
1202
|
+
<text>4 kB</text>
|
|
1203
|
+
</box>
|
|
1204
|
+
</box>
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
### The props
|
|
1208
|
+
|
|
1209
|
+
| prop | |
|
|
1210
|
+
| ----------------------- | ----------------------------------------------------------------------------------- |
|
|
1211
|
+
| `selectable` | `true` starts a surface; `false` opts a subtree out of the one above it |
|
|
1212
|
+
| `selectionColor` | highlight behind selected text (default: theme's `selection`, a tint of its accent) |
|
|
1213
|
+
| `onSelectionChange(ev)` | `ev.text` is what a copy would take; `ev.isCollapsed` whether it is empty |
|
|
1214
|
+
|
|
1215
|
+
On the surface's node, through a ref: `selectAll()`, `clearSelection()`,
|
|
1216
|
+
`selectedText()`, `setSelection(anchor, focus)` — each end
|
|
1217
|
+
`{ node, index }`, indices in code points — and `textSelection`, a snapshot
|
|
1218
|
+
of `{ isCollapsed, text, ranges }`.
|
|
1219
|
+
|
|
1220
|
+
### One selection, and where the keys go
|
|
1221
|
+
|
|
1222
|
+
**One selection is visible per application**: selecting in a document
|
|
1223
|
+
collapses a field's highlight and vice versa — which keeps PRIMARY honest.
|
|
1224
|
+
|
|
1225
|
+
A surface is a **focus target** (Ctrl+A and Ctrl+C must arrive somewhere),
|
|
1226
|
+
so it is in the Tab cycle; `tabIndex={-1}` removes it but keeps it
|
|
1227
|
+
press-focusable — right for a document in a larger UI, wrong for a reader
|
|
1228
|
+
whose window is the document. The pointer is an I-beam over it unless
|
|
1229
|
+
`cursor` says otherwise. The selection reaches assistive technology through
|
|
1230
|
+
`<text>`'s AT-SPI text interface
|
|
1231
|
+
([accessibility.md](accessibility.md#text-controls)).
|
|
1232
|
+
|
|
1233
|
+
### The geometry underneath
|
|
1234
|
+
|
|
1235
|
+
Four accessors on every drawn node build the selection, and are public — a
|
|
1236
|
+
caret rect is what an autocomplete popup anchors to
|
|
1237
|
+
(`<popup anchor={{ to, at }}>`), a hit test turns a click into an offset:
|
|
1238
|
+
|
|
1239
|
+
```js
|
|
1240
|
+
const node = paragraphRef.current;
|
|
1241
|
+
node.textContent(); // 'Everything below this heading…'
|
|
1242
|
+
node.textIndexAt(ev.x, ev.y); // 14 — window coordinates in, a code point out
|
|
1243
|
+
node.textCaretRect(14); // { x, y, width: 0, height }
|
|
1244
|
+
node.textRangeRects(4, 14); // the bands a highlight over [4, 14) fills
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
Indices are **code points**, not UTF-16 units. Rects are in the owning
|
|
1248
|
+
window's device pixels, like `abs` and `contentBox()`; mouse `x`/`y` are
|
|
1249
|
+
logical ([scale.md](scale.md)). `textRangeRects` returns a band per line
|
|
1250
|
+
_and_ per direction run, since a range crossing from Latin into Arabic
|
|
1251
|
+
covers separate stretches of pixels.
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
## `<textinput>`
|
|
1256
|
+
|
|
1257
|
+

|
|
1258
|
+
|
|
1259
|
+
Single-line editable text; caret and selection positions are exact across
|
|
1260
|
+
kerning, shaping and trailing whitespace.
|
|
1261
|
+
|
|
1262
|
+
| prop | |
|
|
1263
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
1264
|
+
| `value` + `onChange(ev)` | controlled mode (display follows the prop) |
|
|
1265
|
+
| `defaultValue` | uncontrolled mode |
|
|
1266
|
+
| `onSubmit(ev)` | Enter/Shift+Enter; with Ctrl, Alt or Super, a shortcut's ([events.md](events.md#accelerators)) |
|
|
1267
|
+
| `name` | field name, echoed on the event |
|
|
1268
|
+
| `placeholder`, `placeholderColor` | shown when empty |
|
|
1269
|
+
| `maxLength` | code-point limit |
|
|
1270
|
+
| `selectionColor`, `caretColor` | selection/caret paint |
|
|
1271
|
+
| `inputMode` | IME hint: `numeric`, `decimal`, `tel`, `email`, `url` ([events.md](events.md#input-methods)) |
|
|
1272
|
+
| text style props | as `<text>` |
|
|
1273
|
+
|
|
1274
|
+
The field follows its box's **direction** as `<text>` does: value,
|
|
1275
|
+
placeholder and caret start at the base direction's start edge (the right
|
|
1276
|
+
under `direction: 'rtl'`), the value is shaped at that base level, and
|
|
1277
|
+
clicks, caret, selection and horizontal scroll mirror together
|
|
1278
|
+
([styling.md](styling.md#inside-an-editable-field)).
|
|
1279
|
+
|
|
1280
|
+
### The change event
|
|
1281
|
+
|
|
1282
|
+
`onChange` and `onSubmit` get a synthetic event, not a bare string:
|
|
1283
|
+
|
|
1284
|
+
```jsx
|
|
1285
|
+
<textinput
|
|
1286
|
+
name="email"
|
|
1287
|
+
value={email}
|
|
1288
|
+
onChange={(ev) => setEmail(ev.target.value)}
|
|
1289
|
+
/>
|
|
1290
|
+
```
|
|
1291
|
+
|
|
1292
|
+
| on the event | |
|
|
1293
|
+
| ----------------------------------- | --------------------------------------------------- |
|
|
1294
|
+
| `ev.value`, `ev.target.value` | the text **after** the edit |
|
|
1295
|
+
| `ev.name`, `ev.target.name` | the `name` prop |
|
|
1296
|
+
| `ev.type` | `'change'` or `'submit'` |
|
|
1297
|
+
| `ev.target`, `ev.currentTarget` | the node (they are the same — this does not bubble) |
|
|
1298
|
+
| `ev.nativeEvent` | the X key event, or **null** — see below |
|
|
1299
|
+
| `preventDefault`, `stopPropagation` | present for uniformity; nothing reads them |
|
|
1300
|
+
|
|
1301
|
+
- **`ev.target.value` is the new value even when controlled**, before the
|
|
1302
|
+
parent re-renders — as in the DOM, which is why react-hook-form and formik
|
|
1303
|
+
work ([ecosystem/forms.md](ecosystem/forms.md)). `ev.target` is the live
|
|
1304
|
+
node, so read later it shows the current value; `ev.value` is a snapshot.
|
|
1305
|
+
- **`ev.nativeEvent` can be null** — a paste resolving, an undo or a
|
|
1306
|
+
pushed-back value has no X event. Guard it (downshift reads
|
|
1307
|
+
`nativeEvent.preventDownshiftDefault` unguarded).
|
|
1308
|
+
|
|
1309
|
+
`node.value = ''` sets the text without firing `onChange`, as on a DOM
|
|
1310
|
+
input — how react-hook-form's `register()` resets a field.
|
|
1311
|
+
|
|
1312
|
+
Keys: click/drag selection, double-click word, triple-click all, **dead
|
|
1313
|
+
keys and Compose** ([events.md](events.md#composition)), Backspace/Delete,
|
|
1314
|
+
arrows (+Shift extends), Ctrl+arrows and Ctrl+Backspace/Delete by word,
|
|
1315
|
+
Home/End, Ctrl+A, Ctrl+C/X/V on CLIPBOARD, middle-click paste from PRIMARY,
|
|
1316
|
+
selections (select-all included) owning PRIMARY
|
|
1317
|
+
([X11 only](clipboard.md#primary-is-x11-only)),
|
|
1318
|
+
**Ctrl+Z / Ctrl+Shift+Z** (and Ctrl+Y) to undo and redo, and a
|
|
1319
|
+
**right-click menu**. On Cocoa the keys are the Mac's: ⌘A, ⌘C/X/V,
|
|
1320
|
+
⌘Z/⇧⌘Z; ⌥ moves and deletes by word; ⌘←/→ and ⌘↑/↓ go to the ends of the
|
|
1321
|
+
line and the value; ⌘⌫ deletes to line start; ⌃A, ⌃E, ⌃K, ⌃Y and AppKit's
|
|
1322
|
+
other Emacs keys work while other ⌃ chords type nothing; ⌥ types what the
|
|
1323
|
+
layout puts on a key (`ß` for ⌥S) ([events.md](events.md#primary-modifier)).
|
|
1324
|
+
Focusable by default; shows the text cursor. `ev.preventDefault()` in
|
|
1325
|
+
`onKeyDown`/`onMouseDown` suppresses the built-in editing. Copying and
|
|
1326
|
+
pasting from elsewhere: [clipboard.md](clipboard.md).
|
|
1327
|
+
|
|
1328
|
+
### The right-click menu
|
|
1329
|
+
|
|
1330
|
+

|
|
1331
|
+
|
|
1332
|
+
Undo / Redo / Cut / Copy / Paste / Select All, with no wiring — each row
|
|
1333
|
+
live only when it would act, each running its shortcut's code.
|
|
1334
|
+
Right-clicking **inside** a selection keeps it; outside, the caret moves
|
|
1335
|
+
there first. Arrows walk enabled rows, Enter chooses, Escape or a press
|
|
1336
|
+
outside closes; the selection stays highlighted meanwhile.
|
|
1337
|
+
|
|
1338
|
+
**Paste** is greyed when nothing owns CLIPBOARD — from a session's second
|
|
1339
|
+
menu on, since the field starts watching when its menu first opens; without
|
|
1340
|
+
XFixes it stays enabled. On macOS it tracks the pasteboard's change count, a
|
|
1341
|
+
quarter second behind another app's copy
|
|
1342
|
+
([clipboard.md](clipboard.md#knowing-when-it-changes)).
|
|
1343
|
+
|
|
1344
|
+
`contextMenu={false}` plus a `ContextMenu` of your own replaces it
|
|
1345
|
+
(`onContextMenu` still fires); `ev.preventDefault()` in `onContextMenu`
|
|
1346
|
+
suppresses it once. `openEditMenu(node, at, actions)` opens the same menu
|
|
1347
|
+
for anything else that edits or selects text
|
|
1348
|
+
([extending.md](extending.md#the-standard-edit-menu)).
|
|
1349
|
+
|
|
1350
|
+
### `sensitive`
|
|
1351
|
+
|
|
1352
|
+
`<textinput sensitive>`'s text **never reaches a selection**: Ctrl+C and the
|
|
1353
|
+
copy half of Ctrl+X do nothing, the menu has no Cut or Copy (absent, not
|
|
1354
|
+
greyed — greyed over a password looks like a bug), and selecting does not
|
|
1355
|
+
take PRIMARY. The caret, selection, arrows, undo and pasting _in_ are
|
|
1356
|
+
untouched. The clipboard, unlike the screen, is readable by every client on
|
|
1357
|
+
the display, and clipboard managers record it. `PasswordInput` sets it while
|
|
1358
|
+
the secret is revealed ([components.md](components.md#passwordinput)). On
|
|
1359
|
+
Wayland the field is also declared `sensitive_data` to the input method,
|
|
1360
|
+
with no surrounding text offered ([events.md](events.md#input-methods)).
|
|
1361
|
+
|
|
1362
|
+
**Ref**: the node, plus `value`, `undo()` / `redo()` and `canUndo` /
|
|
1363
|
+
`canRedo` — enough for a toolbar button beside the field.
|
|
1364
|
+
|
|
1365
|
+
### Undo
|
|
1366
|
+
|
|
1367
|
+
Consecutive typing is one undo step, so Ctrl+Z takes back a word, not a
|
|
1368
|
+
keystroke; runs of Backspace coalesce too. A run ends at whitespace, at
|
|
1369
|
+
anything moving the caret (arrows, Home/End, a click, focus leaving), and
|
|
1370
|
+
around edits that are their own step: a paste, a cut, a replaced selection,
|
|
1371
|
+
Ctrl+Backspace (⌥⌫, ⌘⌫ and ⌃K on a Mac), a `<textarea>` newline. Undo
|
|
1372
|
+
restores the selection and puts the caret where the undone edit started. A
|
|
1373
|
+
composed character is one entry: Ctrl+Z after `dead_acute` `e` steps over
|
|
1374
|
+
`é`.
|
|
1375
|
+
|
|
1376
|
+
The stack holds up to 200 snapshots. A **controlled** `value` reports the
|
|
1377
|
+
restored text through `onChange` and waits for the prop, as typing does, so
|
|
1378
|
+
a parent filtering values has the same say over an undo. A value changed
|
|
1379
|
+
from outside (a form reset) is its own entry.
|
|
1380
|
+
|
|
1381
|
+
## `<textarea>`
|
|
1382
|
+
|
|
1383
|
+
Multi-line editable text on `<textinput>`'s editing core. It word-wraps at
|
|
1384
|
+
the content width; Enter and Shift+Enter insert a newline; Ctrl+Enter (⌘↩
|
|
1385
|
+
on Cocoa) fires `onSubmit`, and with no `onSubmit` is left to a shortcut, as
|
|
1386
|
+
Alt+Enter and Super+Enter are except on Cocoa
|
|
1387
|
+
([events.md](events.md#accelerators)). Up/Down move between visual lines
|
|
1388
|
+
keeping a goal column, Home/End go to the ends of the visual line, selection
|
|
1389
|
+
spans lines, and the view scrolls to follow the caret (and with the wheel).
|
|
1390
|
+
On Cocoa ⌘←/→ are Home/End, ⌘↑/↓ go to the ends of the value and ⌥↑/↓ to
|
|
1391
|
+
the ends of a paragraph ([events.md](events.md#primary-modifier)). A field
|
|
1392
|
+
whose text fits passes the wheel out to the pane or window behind it, as a
|
|
1393
|
+
`<box overflow="scroll">` does.
|
|
1394
|
+
|
|
1395
|
+
| prop | |
|
|
1396
|
+
| --------------- | -------------------------------------------------------------- |
|
|
1397
|
+
| `rows` | preferred height in text lines (default 3) |
|
|
1398
|
+
| everything else | as `<textinput>` (`onSubmit` fires on Ctrl+Enter, ⌘↩ on Cocoa) |
|
|
1399
|
+
|
|
1400
|
+
## `<image>`
|
|
1401
|
+
|
|
1402
|
+
| prop | |
|
|
1403
|
+
| ---------- | -------------------------------------------------------------------------------------- |
|
|
1404
|
+
| `src` | client-side pixels, in any of the four forms below, or a symbol by name |
|
|
1405
|
+
| `picture` | `{ id, width, height }` — an existing server-side Picture, composited as-is |
|
|
1406
|
+
| `drawable` | `{ id, width, height, depth? }` — an existing Pixmap/Window, composited as-is |
|
|
1407
|
+
| `cacheKey` | the source's identity, when `src` is re-derived per render |
|
|
1408
|
+
| `alt` | the accessible name — what a screen reader says ([accessibility.md](accessibility.md)) |
|
|
1409
|
+
|
|
1410
|
+
`src`, `picture` and `drawable` are mutually exclusive; passing two throws.
|
|
1411
|
+
Size by `style`: with one dimension set the other follows the aspect ratio;
|
|
1412
|
+
with neither, the natural size (a server-side source's descriptor size),
|
|
1413
|
+
shrunk to the width on offer. `objectFit` places the picture, centred:
|
|
1414
|
+
`'fill'` (default) stretches, `'contain'` fits inside, `'cover'` fills and
|
|
1415
|
+
crops, `'none'` keeps its size, `'scale-down'` is the smaller of `'none'`
|
|
1416
|
+
and `'contain'`.
|
|
1417
|
+
|
|
1418
|
+
```jsx
|
|
1419
|
+
<image src={photo} style={{ height: 64 }} /> // width follows the aspect ratio
|
|
1420
|
+
<image src={photo} style={{ width: 96, height: 96, objectFit: 'cover' }} />
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
### `src` — pixels the client has
|
|
1424
|
+
|
|
1425
|
+
```jsx
|
|
1426
|
+
<image src="./logo.png" /> // file path or file URL
|
|
1427
|
+
<image src={bytes} /> // encoded PNG, JPEG, WebP, …
|
|
1428
|
+
<image src={{ width, height, data }} /> // raw straight RGBA
|
|
1429
|
+
<image src={ntkImage} /> // an ntk Image (or Surface)
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
A path (or `new URL('./logo.png', import.meta.url)`) decodes asynchronously:
|
|
1433
|
+
the element is 0×0 until it lands, then reflows — style a `width` and
|
|
1434
|
+
`height` to avoid the jump. Raw RGBA and an ntk `Image` are synchronous;
|
|
1435
|
+
encoded bytes are synchronous where the decoder is (PNG and JPEG under Node)
|
|
1436
|
+
and land a moment later otherwise (any format under Bun, WebP elsewhere).
|
|
1437
|
+
Raw `data` is `width × height × 4` bytes of straight (non-premultiplied)
|
|
1438
|
+
RGBA, as `getImageData` returns. Sources are treated as immutable: pass a
|
|
1439
|
+
new object when the pixels change — encoded bytes too, which under Bun are
|
|
1440
|
+
read on another thread after the commit.
|
|
1441
|
+
|
|
1442
|
+
#### Formats, and what decodes them
|
|
1443
|
+
|
|
1444
|
+
The format comes from the bytes, never a file name; the decoder from the
|
|
1445
|
+
runtime:
|
|
1446
|
+
|
|
1447
|
+
| format | under Bun | under Node |
|
|
1448
|
+
| ---------------- | -------------------------------------------------- | ------------------------------------------ |
|
|
1449
|
+
| PNG, JPEG | `Bun.Image`, off the JavaScript thread | pngjs, jpeg-js — synchronous |
|
|
1450
|
+
| WebP | `Bun.Image`, off the JavaScript thread | image-in-browser, loaded on the first WebP |
|
|
1451
|
+
| GIF, BMP | `Bun.Image`, off the JavaScript thread | not decoded — the error says to convert |
|
|
1452
|
+
| TIFF, HEIC, AVIF | `Bun.Image` through ImageIO (macOS), WIC (Windows) | not decoded; nor under Bun on Linux |
|
|
1453
|
+
|
|
1454
|
+
Under Bun every format decodes on a worker thread (a 1080p JPEG costs the
|
|
1455
|
+
JavaScript thread ~110ms in jpeg-js), so bytes land a frame or so late, like
|
|
1456
|
+
a file. Where that matters — a test measuring the first frame — decode
|
|
1457
|
+
yourself; the result is used as-is everywhere:
|
|
1458
|
+
|
|
1459
|
+
```jsx
|
|
1460
|
+
import { decodeImage } from 'ntk/image'; // PNG and JPEG, synchronous
|
|
1461
|
+
|
|
1462
|
+
const logo = <image src={decodeImage(bytes)} />;
|
|
1463
|
+
```
|
|
1464
|
+
|
|
1465
|
+
`decodeImageBytes` from `react-x11/ntk` is the same ladder for an element
|
|
1466
|
+
that draws its own images (a document's `<img>`, a CSS background): an
|
|
1467
|
+
`Image`, or a promise of one where decoding is async; it throws or rejects,
|
|
1468
|
+
naming what to convert to, where `<image>` would show nothing.
|
|
1469
|
+
|
|
1470
|
+
Between the runtimes:
|
|
1471
|
+
|
|
1472
|
+
- **Animations show their first frame** — animated WebP anywhere, GIF under
|
|
1473
|
+
Bun (Bun 1.4's libwebp cannot decode animated WebP; those go to the
|
|
1474
|
+
JavaScript decoder).
|
|
1475
|
+
- **JPEG EXIF orientation is applied under Bun, not Node**: rotate portrait
|
|
1476
|
+
phone photos first, or use Bun.
|
|
1477
|
+
- **No decoder, no picture**: GIF under Node or HEIC under Bun on Linux
|
|
1478
|
+
logs what to convert to and shows nothing, like a missing file.
|
|
1479
|
+
|
|
1480
|
+
Pixels are uploaded once and composited every repaint. An ntk `Image` is
|
|
1481
|
+
used as-is and never destroyed by the element, so one `Image` in many
|
|
1482
|
+
`<image>`s is one decode and one upload. On Cocoa the element frees its
|
|
1483
|
+
upload of an `Image` it decoded; the upload of yours is freed when your
|
|
1484
|
+
`Image` is garbage-collected (ntk's `destroy()` frees only X copies).
|
|
1485
|
+
|
|
1486
|
+
### `src={{ symbol }}` — the platform's own icons
|
|
1487
|
+
|
|
1488
|
+
```jsx
|
|
1489
|
+
<box style={{ flexDirection: 'row', gap: 6, color: theme.textMuted }}>
|
|
1490
|
+
<image src={{ symbol: 'speaker.wave.3.fill', variableValue: volume }} />
|
|
1491
|
+
<text>Volume</text>
|
|
1492
|
+
</box>
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
A name instead of pixels: the platform's icon in the **text colour** (what
|
|
1496
|
+
`currentColor` is to `<svg>`). The names are those `useTray`'s `icon` and a
|
|
1497
|
+
menu item's `iconName` take:
|
|
1498
|
+
|
|
1499
|
+
| backend | names |
|
|
1500
|
+
| ------------- | ---------------------------------------------------------------------------------------------------- |
|
|
1501
|
+
| macOS | SF Symbols, like `speaker.wave.3.fill`, through `@windowkit/appkit` |
|
|
1502
|
+
| X11 / Wayland | freedesktop icon theme (the settings daemon's, else Adwaita, then hicolor), like `audio-volume-high` |
|
|
1503
|
+
|
|
1504
|
+
**Sized like the surrounding text**: an SF Symbol takes the inherited
|
|
1505
|
+
`fontSize` and `fontWeight` as point size and weight; an icon theme is asked
|
|
1506
|
+
for the matching size (16 beside 14px text), square. A styled
|
|
1507
|
+
`width`/`height` fits it into that box, centred. Source keys:
|
|
1508
|
+
|
|
1509
|
+
| key | |
|
|
1510
|
+
| --------------- | ------------------------------------------------------------------------------ |
|
|
1511
|
+
| `symbol` | the name |
|
|
1512
|
+
| `weight` | what `fontWeight` takes; defaults to the text's. SF Symbols only |
|
|
1513
|
+
| `scale` | `'small'`, `'medium'` (default) or `'large'`, beside the text. SF Symbols only |
|
|
1514
|
+
| `variableValue` | 0 to 1, how much of a variable symbol shows. SF Symbols, macOS 13 and later |
|
|
1515
|
+
|
|
1516
|
+
On Linux a `-symbolic` icon is preferred and takes the text colour; a
|
|
1517
|
+
full-colour one shows as it is. Lookup follows the Icon Theme
|
|
1518
|
+
Specification, read at the first symbol. **An unknown name takes no room
|
|
1519
|
+
and draws nothing** (one dev warning). The two icon sets do not share
|
|
1520
|
+
names, so pick by platform.
|
|
1521
|
+
|
|
1522
|
+
### `cacheKey` — when the buffer is new but the picture is not
|
|
1523
|
+
|
|
1524
|
+
A component re-deriving its bytes per render (decoding a stream, slicing a
|
|
1525
|
+
capture) passes a new `src` each time — a fresh decode and upload. A
|
|
1526
|
+
`cacheKey`, the `<canvas cacheKey>` contract for sources, vouches that the
|
|
1527
|
+
buffer is the same picture: nothing is redone, and `<image>`s sharing a key
|
|
1528
|
+
share one decoded copy, freed with the last.
|
|
1529
|
+
|
|
1530
|
+
```jsx
|
|
1531
|
+
<image src={frame.rgba} cacheKey={`frame:${frame.serial}`} />
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
The key must name the content, or stale pixels show (development warns
|
|
1535
|
+
when two sizes collide under one key). Ignored for an ntk `Image`; refused
|
|
1536
|
+
with `picture`/`drawable`.
|
|
1537
|
+
|
|
1538
|
+
### `picture` / `drawable` — pixels the server already has
|
|
1539
|
+
|
|
1540
|
+
Content already on the X server — an offscreen pixmap, `NameWindowPixmap`
|
|
1541
|
+
from Composite, a cached tile — composites straight from that resource,
|
|
1542
|
+
with no readback, upload or round trip:
|
|
1543
|
+
|
|
1544
|
+
```jsx
|
|
1545
|
+
<image picture={{ id: pic, width: 64, height: 64 }} />
|
|
1546
|
+
<image drawable={{ id: pixmap, width: 128, height: 96, depth: 32 }} />
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
You state the size (asking would be a round trip); descriptors compare by
|
|
1550
|
+
value. Scaling is server-side and bilinear; drawing a `picture` scaled sets
|
|
1551
|
+
its transform and filter for the composite and resets them to
|
|
1552
|
+
identity/nearest after, so a picture with a transform of its own should be
|
|
1553
|
+
drawn 1:1. `drawable` wraps the pixmap or window in a Picture the element
|
|
1554
|
+
owns (freed on unmount; the drawable stays yours); `depth` is 24 (default,
|
|
1555
|
+
the screen's and a Composite window pixmap's), 32 for argb, or 8, which
|
|
1556
|
+
composites as ink through its alpha (a mask preview); a `picture`'s format
|
|
1557
|
+
was fixed when it was created. Both accept richer objects with those
|
|
1558
|
+
fields — an ntk `Pixmap` goes straight in as `drawable`. Both are
|
|
1559
|
+
**X11-only**: on Cocoa they draw nothing (one dev warning); pass pixels to
|
|
1560
|
+
`src`.
|
|
1561
|
+
|
|
1562
|
+
## `<video>` {#video}
|
|
1563
|
+
|
|
1564
|
+
```jsx
|
|
1565
|
+
// frames the application decodes — every backend
|
|
1566
|
+
const frames = useVideoFrames({ width: 1280, height: 720, format: 'NV12' });
|
|
1567
|
+
useEffect(() => startDecoder(file, frames), [frames, file]);
|
|
1568
|
+
<video frames={frames} style={{ width: '100%', backgroundColor: '#000' }} />;
|
|
1569
|
+
```
|
|
1570
|
+
|
|
1571
|
+
| prop | |
|
|
1572
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
1573
|
+
| `frames` | a `VideoFrames` sink you push decoded frames into — every backend |
|
|
1574
|
+
| `src` | a path or URL the platform plays, where `useSupports('mediaPlayback')` (below) |
|
|
1575
|
+
| `poster` | an `<image src>` value, shown while there is no frame |
|
|
1576
|
+
| `autoPlay`, `loop`, `muted`, `volume`, `playbackRate` | HTML's names and defaults — nothing plays until asked; for `src` |
|
|
1577
|
+
| `paused` | controlled when present; for `src` |
|
|
1578
|
+
| `onLoadedMetadata` | `{ width, height, duration }` once the size is known (a sink: on mount, `duration: Infinity`) |
|
|
1579
|
+
| `onPlay`, `onPause`, `onEnded`, `onTimeUpdate` | for `src` |
|
|
1580
|
+
| `onError` | the source failed, or `src` has no player; unhandled, one console warning |
|
|
1581
|
+
|
|
1582
|
+
`frames` and `src` are exclusive; passing both throws. It is a drawn node like
|
|
1583
|
+
`<image>`, so a video in a form sits under its popups and inside its scroll
|
|
1584
|
+
panes, which a child window or `<glarea>` cannot. **Size** is HTML's: the
|
|
1585
|
+
stream's size in logical pixels (1280x720 is 1280 wide at any scale), else
|
|
1586
|
+
the poster's, else 300x150; one styled axis derives the other from the
|
|
1587
|
+
ratio. `objectFit` defaults to `'contain'`; `backgroundColor` fills the
|
|
1588
|
+
bars.
|
|
1589
|
+
|
|
1590
|
+
### `VideoFrames`, the sink
|
|
1591
|
+
|
|
1592
|
+
react-x11 decodes nothing; `useVideoFrames` / `createVideoFrames` make a
|
|
1593
|
+
sink, and every source ends in `push`:
|
|
1594
|
+
|
|
1595
|
+
```js
|
|
1596
|
+
import { spawn } from 'node:child_process';
|
|
1597
|
+
|
|
1598
|
+
// ffmpeg decoding a file into NV12 frames on a pipe, paced to the clip's rate
|
|
1599
|
+
function startDecoder(file, frames) {
|
|
1600
|
+
const ff = spawn('ffmpeg', [
|
|
1601
|
+
'-re',
|
|
1602
|
+
'-i',
|
|
1603
|
+
file,
|
|
1604
|
+
'-f',
|
|
1605
|
+
'rawvideo',
|
|
1606
|
+
'-pix_fmt',
|
|
1607
|
+
frames.format.toLowerCase(),
|
|
1608
|
+
'-s',
|
|
1609
|
+
`${frames.width}x${frames.height}`,
|
|
1610
|
+
'-',
|
|
1611
|
+
]);
|
|
1612
|
+
const size = frames.width * frames.height * 1.5; // NV12
|
|
1613
|
+
let pending = Buffer.alloc(0);
|
|
1614
|
+
ff.stdout.on('data', (chunk) => {
|
|
1615
|
+
pending = Buffer.concat([pending, chunk]);
|
|
1616
|
+
while (pending.length >= size) {
|
|
1617
|
+
frames.push(pending.subarray(0, size)); // one buffer, planes back to back
|
|
1618
|
+
pending = pending.subarray(size);
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
return () => ff.kill();
|
|
1622
|
+
}
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
- **Formats**: `'NV12'` (Y, then Cb/Cr pairs — hardware decoders),
|
|
1626
|
+
`'I420'` (Y, Cb, Cr — `yuv420p`, most software decoders) and `'BGRA'`
|
|
1627
|
+
(fourth byte ignored: frames are opaque). `colorSpace` (`'bt709'`
|
|
1628
|
+
default, `'bt601'`, `'bt2020'`) and `range` (`'video'` default, `'full'`)
|
|
1629
|
+
say what YCbCr numbers mean.
|
|
1630
|
+
- **`preferredFormats`** is the display's order, cheapest first:
|
|
1631
|
+
`NV12, I420, BGRA` where something native takes YCbCr as it is — a layer
|
|
1632
|
+
on macOS, a shader on Wayland, an Xv port on X11 — and `BGRA` where the
|
|
1633
|
+
frame is converted in JavaScript. On X11 it is `BGRA`, not kept, for the
|
|
1634
|
+
few round trips until the port answers after the first sink is made.
|
|
1635
|
+
Every format works everywhere.
|
|
1636
|
+
- **The newest frame is the frame**: a push replaces the last, shown or not;
|
|
1637
|
+
nothing queues, and the decoder's clock is the pacing. A push hands its
|
|
1638
|
+
planes over until the next push — they are read then, not copied at the
|
|
1639
|
+
call — so a decoder reusing one buffer pushes it again after writing.
|
|
1640
|
+
- **`push(planes, { strides, time })`** takes a Buffer per plane or one
|
|
1641
|
+
Buffer back to back, with per-plane row bytes when padded; a plane too
|
|
1642
|
+
small is a `TypeError` naming it and the bytes needed. `close()` drops the
|
|
1643
|
+
frame and ignores later pushes, so a decoder racing an unmount loses
|
|
1644
|
+
quietly.
|
|
1645
|
+
- **Nothing to free**: a sink owns nothing native.
|
|
1646
|
+
|
|
1647
|
+
### Lifted, drawn, and what each costs
|
|
1648
|
+
|
|
1649
|
+
On **macOS** a video goes on its own layer above the window's bitmap when
|
|
1650
|
+
nothing is painted over it, it is not fading and no rounded clipping
|
|
1651
|
+
ancestor holds it ([macos.md](macos.md)); then a `push` costs well under a
|
|
1652
|
+
millisecond and **no window frame**. Otherwise — a tooltip over it, a
|
|
1653
|
+
dialog's dim, a rounded card — it is **drawn**: a push claims the picture,
|
|
1654
|
+
and the next frame composites it in paint order, in the same colours.
|
|
1655
|
+
|
|
1656
|
+
On **X11, Wayland and Windows** it is always drawn, and what differs is how
|
|
1657
|
+
the frame reaches the element's surface:
|
|
1658
|
+
|
|
1659
|
+
- **Wayland**: planes as textures, the matrix in a shader — about 0.2ms of
|
|
1660
|
+
CPU for a 1080p NV12 frame, against 7.6ms converted in JavaScript.
|
|
1661
|
+
- **X11 with an Xv image adaptor** (glamor, under Xorg or Xwayland): the
|
|
1662
|
+
frame goes to the server as decoded, through shared memory, and its GPU
|
|
1663
|
+
converts it into a hidden window copied into the surface — 1.5ms a 1080p
|
|
1664
|
+
NV12 frame against 15ms. Full range, BT.2020, the first frame of a size
|
|
1665
|
+
and a window off screen go the JavaScript way.
|
|
1666
|
+
- **X11 without one, and Windows**: converted to RGBA in JavaScript unless
|
|
1667
|
+
BGRA (hence their preference); X11 uploads through shared memory where
|
|
1668
|
+
available.
|
|
1669
|
+
|
|
1670
|
+
No colour management is applied on any of them. `examples/video.jsx` plays
|
|
1671
|
+
a file through whichever path the display has.
|
|
1672
|
+
|
|
1673
|
+
### `src` — the platform's player
|
|
1674
|
+
|
|
1675
|
+
Where `useSupports('mediaPlayback')` is true — macOS (AVFoundation), and a
|
|
1676
|
+
`<Frame>` pane whose host plays it ([frame.md](frame.md#sizing-stacking-input))
|
|
1677
|
+
— `src` is a path, `file://` or `http(s)` URL (HLS included), played with
|
|
1678
|
+
the platform's decoder, audio, seeking and streaming.
|
|
1679
|
+
|
|
1680
|
+
```jsx
|
|
1681
|
+
const ref = useRef(null);
|
|
1682
|
+
<video
|
|
1683
|
+
ref={ref}
|
|
1684
|
+
src="/Users/me/clip.mp4"
|
|
1685
|
+
autoPlay
|
|
1686
|
+
muted
|
|
1687
|
+
onLoadedMetadata={({ duration }) => setDuration(duration)}
|
|
1688
|
+
onTimeUpdate={({ currentTime }) => setAt(currentTime)}
|
|
1689
|
+
onEnded={() => setDone(true)}
|
|
1690
|
+
style={{ width: 640 }}
|
|
1691
|
+
/>;
|
|
1692
|
+
// a transport bar of your own
|
|
1693
|
+
<button onPress={() => ref.current.seek(30)} />;
|
|
1694
|
+
```
|
|
1695
|
+
|
|
1696
|
+
Laid out at 300x150 (or the poster's size) until ready, then at the clip's
|
|
1697
|
+
size from `onLoadedMetadata`. Prop changes go to the same player. The `ref`
|
|
1698
|
+
is HTMLMediaElement's imperative half: `play()` (restarts an ended item),
|
|
1699
|
+
`pause()`, `seek(seconds)` (exact, not keyframe), `currentTime`,
|
|
1700
|
+
`duration`, `paused`. `onPlay`/`onPause` fire as it starts and stops,
|
|
1701
|
+
`onTimeUpdate` four times a second while playing and after a seek,
|
|
1702
|
+
`onEnded` at the end of a non-looping item, `onError` when the item fails.
|
|
1703
|
+
Drawn, the picture is copied at the clip's frame rate; lifted, it costs the
|
|
1704
|
+
window nothing. There is no `controls`: the transport is your React.
|
|
1705
|
+
|
|
1706
|
+
Where it is false — X11 and Wayland for good, Windows today, macOS on an
|
|
1707
|
+
older bridge — the element reports one `NoMediaPlaybackError`
|
|
1708
|
+
(`code: 'ENOMEDIAPLAYBACK'`) through `onError` on a microtask and shows its
|
|
1709
|
+
poster, never a black box. Ask first:
|
|
1710
|
+
|
|
1711
|
+
```jsx
|
|
1712
|
+
const playback = useSupports('mediaPlayback');
|
|
1713
|
+
{
|
|
1714
|
+
playback ? <video src={file} autoPlay /> : <video frames={decoded} />;
|
|
1715
|
+
}
|
|
1716
|
+
```
|
|
1717
|
+
|
|
1718
|
+
## `<canvas>`
|
|
1719
|
+
|
|
1720
|
+
The escape hatch: a retained node you paint.
|
|
1721
|
+
|
|
1722
|
+
```jsx
|
|
1723
|
+
<canvas
|
|
1724
|
+
style={{ flexGrow: 1 }}
|
|
1725
|
+
onDraw={(ctx, { width, height, node }) => {
|
|
1726
|
+
ctx.fillStyle = 'tomato';
|
|
1727
|
+
ctx.fillRect(0, 0, width / 2, height);
|
|
1728
|
+
}}
|
|
1729
|
+
/>
|
|
1730
|
+
```
|
|
1731
|
+
|
|
1732
|
+
`ctx` is ntk's canvas-like 2d context (paths, transforms, gradients incl.
|
|
1733
|
+
conical, `setLineDash`, round caps/joins, images, text), translated to the
|
|
1734
|
+
node's origin and clipped to it. `onDraw` runs on every repaint of the
|
|
1735
|
+
window. `fillRects` sends many same-colour rectangles (a sparkline, a heat
|
|
1736
|
+
map, terminal cells) as one request, with `fillRect`'s fill style, alpha,
|
|
1737
|
+
composite op and clip:
|
|
1738
|
+
|
|
1739
|
+
```jsx
|
|
1740
|
+
ctx.fillStyle = theme.accent;
|
|
1741
|
+
ctx.fillRects(bars); // [[x, y, w, h], …] or one flat [x, y, w, h, x, …]
|
|
1742
|
+
```
|
|
1743
|
+
|
|
1744
|
+
### Raw pixels — the one call that is not translated
|
|
1745
|
+
|
|
1746
|
+
`putImageData` and `getImageData` ignore the transform _and_ the clip (the
|
|
1747
|
+
HTML canvas rule), so `ctx.putImageData(data, 0, 0)` lands at the
|
|
1748
|
+
**window** corner, silently. `onDraw`'s second argument carries the origin:
|
|
1749
|
+
|
|
1750
|
+
```jsx
|
|
1751
|
+
<canvas
|
|
1752
|
+
onDraw={(ctx, { width, height, x, y }) => {
|
|
1753
|
+
ctx.putImageData({ width, height, data: rgba }, x, y); // in the node
|
|
1754
|
+
}}
|
|
1755
|
+
style={{ width, height }}
|
|
1756
|
+
/>
|
|
1757
|
+
```
|
|
1758
|
+
|
|
1759
|
+
`x`/`y` are the node's place in the window in a live paint and the cache
|
|
1760
|
+
surface's origin under a [`cacheKey`](#cachekey--draw-it-once), so
|
|
1761
|
+
offsetting by them is right in both. In development an out-of-box
|
|
1762
|
+
`putImageData` warns once.
|
|
1763
|
+
|
|
1764
|
+
For pixels that last, draw an `Image` instead (ntk's, from
|
|
1765
|
+
[`react-x11/ntk`](extending.md#the-subpath-exports)): `drawImage` honours
|
|
1766
|
+
the transform and clip (a half-scrolled canvas stays cut), and the image
|
|
1767
|
+
keeps its server-side pixmap rather than re-uploading every repaint.
|
|
1768
|
+
|
|
1769
|
+
```jsx
|
|
1770
|
+
import { Image } from 'react-x11/ntk';
|
|
1771
|
+
|
|
1772
|
+
const img = new Image({ width, height, data: rgba }); // keep it memoized
|
|
1773
|
+
<canvas onDraw={(ctx) => ctx.drawImage(img, 0, 0, width, height)} />;
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1776
|
+
### `cacheKey` — draw it once
|
|
1777
|
+
|
|
1778
|
+
With a `cacheKey` the canvas is rendered once and composited on later
|
|
1779
|
+
repaints; canvases sharing a key share the copy.
|
|
1780
|
+
|
|
1781
|
+
```jsx
|
|
1782
|
+
<canvas cacheKey={`spark:${series.id}:${w}x${h}`} onDraw={drawSparkline} />
|
|
1783
|
+
```
|
|
1784
|
+
|
|
1785
|
+
Opt-in, since `onDraw` is an opaque closure only you can read. **The key
|
|
1786
|
+
must name every input the drawing reads**, or stale pixels show; develop
|
|
1787
|
+
with `REACT_X11_PAINT_CACHE=verify`, which reports exactly that
|
|
1788
|
+
([Runtime diagnostics](debugging.md#the-paint-cache--react_x11_no_paint_cache-react_x11_paint_cacheverify)).
|
|
1789
|
+
Leave it unset for anything animated or driven by state outside the props.
|
|
1790
|
+
`<svg>` builds its key itself.
|
|
1791
|
+
|
|
1792
|
+
### `mono` — one colour, and the colour out of the key
|
|
1793
|
+
|
|
1794
|
+
`mono` promises **the drawing is one colour, and not its own to choose**:
|
|
1795
|
+
`fillStyle` and `strokeStyle` arrive preset from the node's resolved
|
|
1796
|
+
`color` — its own, else inherited
|
|
1797
|
+
([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)) — so
|
|
1798
|
+
`:hover`, `:disabled` and a theme flip all reach the same `onDraw`:
|
|
1799
|
+
|
|
1800
|
+
```jsx
|
|
1801
|
+
const chevron = (ctx, { width: w, height: h }) => {
|
|
1802
|
+
ctx.lineWidth = 1.5;
|
|
1803
|
+
ctx.beginPath();
|
|
1804
|
+
ctx.moveTo(w * 0.25, h * 0.375);
|
|
1805
|
+
ctx.lineTo(w * 0.5, h * 0.65);
|
|
1806
|
+
ctx.lineTo(w * 0.75, h * 0.375);
|
|
1807
|
+
ctx.stroke(); // no strokeStyle: the ink is style.color
|
|
1808
|
+
};
|
|
1809
|
+
|
|
1810
|
+
<canvas
|
|
1811
|
+
mono
|
|
1812
|
+
cacheKey="chevron"
|
|
1813
|
+
onDraw={chevron}
|
|
1814
|
+
style={{
|
|
1815
|
+
width: 12,
|
|
1816
|
+
height: 12,
|
|
1817
|
+
color: '$textMuted',
|
|
1818
|
+
':disabled': { color: '$border' },
|
|
1819
|
+
}}
|
|
1820
|
+
/>;
|
|
1821
|
+
```
|
|
1822
|
+
|
|
1823
|
+
With a `cacheKey`, a `mono` drawing is cached as **coverage** and coloured
|
|
1824
|
+
at composite time, so one copy serves every ink (as `<svg>` does for
|
|
1825
|
+
`currentColor`); without `mono`, each colour is its own rasterization. It
|
|
1826
|
+
works without `cacheKey`, but together is the point. The
|
|
1827
|
+
[system icon set](components.md#system-icons) is twelve of these.
|
|
1828
|
+
|
|
1829
|
+
---
|
|
1830
|
+
|
|
1831
|
+
## `<glarea>`
|
|
1832
|
+
|
|
1833
|
+
An OpenGL surface in the layout — on X11 the only drawn element owning a
|
|
1834
|
+
real X window; other backends give it a surface of their own
|
|
1835
|
+
([gl.md](gl.md)). On X11 under the default `glPolicy` it needs a server with
|
|
1836
|
+
**indirect GLX** (`+iglx` / `AllowIndirectGLX`, off by default on many).
|
|
1837
|
+
|
|
1838
|
+
```jsx
|
|
1839
|
+
<glarea
|
|
1840
|
+
style={{ flexGrow: 1 }}
|
|
1841
|
+
clearColor="#0b1021"
|
|
1842
|
+
frameLoop="always"
|
|
1843
|
+
onCreated={(gl) => gl.Enable(gl.DEPTH_TEST)}
|
|
1844
|
+
onDraw={(gl, { width, height }) => {
|
|
1845
|
+
gl.MatrixMode(gl.PROJECTION);
|
|
1846
|
+
gl.LoadIdentity();
|
|
1847
|
+
gl.Frustum(-1, 1, -height / width, height / width, 2, 20);
|
|
1848
|
+
// ... immediate-mode or display-list drawing
|
|
1849
|
+
}}
|
|
1850
|
+
/>
|
|
1851
|
+
```
|
|
1852
|
+
|
|
1853
|
+
- `onDraw(gl, { width, height, node })` — draw one frame; the viewport is
|
|
1854
|
+
set and buffers cleared before it, `SwapBuffers` follows.
|
|
1855
|
+
- `onCreated(gl, info)` — once, when the context is current: one-time GL
|
|
1856
|
+
state, texture uploads, display-list compilation.
|
|
1857
|
+
- `clearColor` — CSS colour or `[r, g, b, a]` floats (default black).
|
|
1858
|
+
- `frameLoop` — `'demand'` (default) redraws on prop, size and expose
|
|
1859
|
+
changes; `'always'` renders continuously on ntk's frame clock.
|
|
1860
|
+
- `frameRate` — [`<window frameRate>`](#framerate--pacing-the-frames-under-a-flood)'s
|
|
1861
|
+
values, priced by what `onDraw` and the swap cost; defaults to the owning
|
|
1862
|
+
window's. Name `'display'` for a scene wanting every frame while its
|
|
1863
|
+
window streams; `'always'` under `'adaptive'` loops at the budget.
|
|
1864
|
+
- `glx` — a visual spec for ntk's `chooseGLXConfig`, e.g.
|
|
1865
|
+
`{ DEPTH_SIZE: 24 }`; one query per app, shared by every `<glarea>`.
|
|
1866
|
+
- `onError(err)` — no GL surface (no GLX, no matching visual); unhandled, a
|
|
1867
|
+
console warning.
|
|
1868
|
+
|
|
1869
|
+
On X11's default indirect path `gl` is fixed-function OpenGL 1.4 (immediate
|
|
1870
|
+
mode, matrices, lighting, textures, display lists) — no shaders or vertex
|
|
1871
|
+
arrays, which GLX does not encode. **Put geometry in display lists**: each
|
|
1872
|
+
immediate-mode vertex crosses the wire every frame; a compiled list is one
|
|
1873
|
+
`CallList`. The direct path and Cocoa give `onDraw` a WebGL-shaped table
|
|
1874
|
+
with shaders ([gl.md](gl.md)).
|
|
1875
|
+
|
|
1876
|
+
The surface sits above everything its window paints. **Its own children are
|
|
1877
|
+
drawn above it**: laid out like a `<box>`'s, cut to its box — a legend, a
|
|
1878
|
+
toolbar, a label — and painted on **panes** over the surface in the same
|
|
1879
|
+
frame.
|
|
1880
|
+
|
|
1881
|
+
```jsx
|
|
1882
|
+
<glarea style={{ flexGrow: 1 }} clearColor="#0e1320" onDraw={drawMap}>
|
|
1883
|
+
<box
|
|
1884
|
+
style={{
|
|
1885
|
+
position: 'absolute',
|
|
1886
|
+
left: 12,
|
|
1887
|
+
top: 12,
|
|
1888
|
+
padding: 8,
|
|
1889
|
+
backgroundColor: '#f4f5f8',
|
|
1890
|
+
}}
|
|
1891
|
+
>
|
|
1892
|
+
<text>Legend</text>
|
|
1893
|
+
</box>
|
|
1894
|
+
</glarea>
|
|
1895
|
+
```
|
|
1896
|
+
|
|
1897
|
+
- **Cocoa**: one transparent layer over the whole surface, composited by
|
|
1898
|
+
Core Animation — a translucent fill, antialiased edge or shadow blends
|
|
1899
|
+
with the GL frame.
|
|
1900
|
+
- **X11**: one child window per child, stacked above the surface in the
|
|
1901
|
+
children's order and cut (SHAPE) to where the child painted half its
|
|
1902
|
+
alpha or more; the GL frame shows everywhere else. A translucent pixel
|
|
1903
|
+
inside the cut blends with `clearColor`, not the frame — so a translucent
|
|
1904
|
+
fill is tinted by `clearColor`, and no compositor helps. Without SHAPE
|
|
1905
|
+
(`react-x11/test`'s server) a pane is the child's whole rectangle,
|
|
1906
|
+
`clearColor` where unpainted.
|
|
1907
|
+
|
|
1908
|
+
A child that only moves (its `left` or `top`) has its pixels copied, not
|
|
1909
|
+
repainted — only uncovered strips are drawn, and panes it passes over are
|
|
1910
|
+
untouched — so content panned over a scene is cheap; scroll panes among the
|
|
1911
|
+
children scroll the same way.
|
|
1912
|
+
[`REACT_X11_NO_SCROLL_BLIT=1`](debugging.md#react_x11_no_scroll_blit1)
|
|
1913
|
+
turns the copies off.
|
|
1914
|
+
|
|
1915
|
+
`useSupports('glOverlay')` says whether children are drawn: true everywhere
|
|
1916
|
+
but **XQuartz**, where the macOS window server composites every GL surface
|
|
1917
|
+
above all X drawing — children are laid out but not drawn, the pointer over
|
|
1918
|
+
them is the surface's, and development says so once. Draw the overlay
|
|
1919
|
+
beside the surface or in GL there, or use the Cocoa backend.
|
|
1920
|
+
|
|
1921
|
+
Limits worth knowing:
|
|
1922
|
+
|
|
1923
|
+
- **Overlapping surfaces:** both overlays are above both frames on Cocoa;
|
|
1924
|
+
on X11 they stack by creation order.
|
|
1925
|
+
- **Scroll panes:** a surface scrolled partly out of a pane is not clipped
|
|
1926
|
+
by it — its window and panes are real windows at its rect.
|
|
1927
|
+
- **Promotion:** on Cocoa a child is never lifted onto its own layer for an
|
|
1928
|
+
animation (it would sit under the surface); its transitions run on the
|
|
1929
|
+
frame clock.
|
|
1930
|
+
|
|
1931
|
+
**The pointer over the surface is the tree's**, on every backend:
|
|
1932
|
+
`onMouseDown`, `onMouseMove`, `onMouseUp`, `onClick` (double click:
|
|
1933
|
+
`detail: 2`), `onContextMenu`, `onMouseEnter`/`onMouseLeave`,
|
|
1934
|
+
`:hover`/`:active` and `onWheel` fire at the child under the pointer, or the
|
|
1935
|
+
`<glarea>` between children, and bubble. A press focuses the nearest
|
|
1936
|
+
focusable one; `ev.capturePointer()` keeps a drag that leaves the surface.
|
|
1937
|
+
The surface and its children win hit tests over what the tree has behind;
|
|
1938
|
+
`pointerEvents: 'none'` on the surface lets the pointer through.
|
|
1939
|
+
|
|
1940
|
+
```jsx
|
|
1941
|
+
<glarea
|
|
1942
|
+
onMouseDown={(ev) => {
|
|
1943
|
+
ev.capturePointer();
|
|
1944
|
+
orbit.start(ev.x, ev.y);
|
|
1945
|
+
}}
|
|
1946
|
+
onMouseMove={(ev) => orbit.drag(ev.x, ev.y)}
|
|
1947
|
+
onWheel={(ev) => zoom(ev.deltaY)}
|
|
1948
|
+
/>
|
|
1949
|
+
```
|
|
1950
|
+
|
|
1951
|
+
An element built on `<glarea>` must **not listen for the pointer on
|
|
1952
|
+
`node.window`**: on X11 that diverts every press and wheel from the tree.
|
|
1953
|
+
`node.forwardsPointer` (`GlAreaNode.prototype.forwardsPointer` from
|
|
1954
|
+
`react-x11/node`, to ask without rendering) is `true` where core delivers
|
|
1955
|
+
the pointer — the switch for dropping such a listener.
|
|
1956
|
+
|
|
1957
|
+
A scene graph over `onDraw` is `@react-x11/components/three`. See
|
|
1958
|
+
`examples/viewer3d.jsx` for the raw form, and `examples/labs/gl-overlay.jsx`
|
|
1959
|
+
for a HUD over a surface on both backends, and beside it on XQuartz.
|
|
1960
|
+
|
|
1961
|
+
---
|
|
1962
|
+
|
|
1963
|
+
## `<foreign>`
|
|
1964
|
+
|
|
1965
|
+
**X11 only**; no other backend has cross-process window embedding, and
|
|
1966
|
+
none is planned. On Cocoa, Wayland, Windows and the headless mock it lays
|
|
1967
|
+
out as an empty box, draws nothing, never calls `onReady`, and calls
|
|
1968
|
+
`onError` once. Ask first with `useSupports('embedding')`. (`<Frame>`, which
|
|
1969
|
+
uses it on X11 to host a pane of your _own_ app, works on Cocoa and Windows
|
|
1970
|
+
another way — [frame.md](frame.md).)
|
|
1971
|
+
|
|
1972
|
+
Another process's top-level X window, laid out as an element — a terminal
|
|
1973
|
+
pane, a video surface, a docked tray icon:
|
|
1974
|
+
|
|
1975
|
+
```jsx
|
|
1976
|
+
<foreign
|
|
1977
|
+
windowId={terminal.windowId}
|
|
1978
|
+
style={{ flexGrow: 1, backgroundColor: '#101014' }}
|
|
1979
|
+
onEmbedded={({ xembed }) => setMode(xembed ? 'xembed' : 'reparented')}
|
|
1980
|
+
onClientGone={() => respawn()}
|
|
1981
|
+
/>
|
|
1982
|
+
```
|
|
1983
|
+
|
|
1984
|
+
- `windowId` — the window to embed; changing it hands the old client back
|
|
1985
|
+
first. **Omit it** to adopt whatever is put inside this node, as
|
|
1986
|
+
`xterm -into WID` and `mpv --wid=WID` need.
|
|
1987
|
+
- `onReady({ windowId })` — this node's container id, offered before
|
|
1988
|
+
anything is embedded, so a program can be spawned into it.
|
|
1989
|
+
- `onEmbedded({ id, xembed, version })` — a client is in; `xembed: false`
|
|
1990
|
+
(no `_XEMBED_INFO`, plain reparenting) is the common case.
|
|
1991
|
+
- `onClientGone()` — destroyed, or reparented away by someone else.
|
|
1992
|
+
- `onRequestFocus()` — the client asked for focus; it is given through the
|
|
1993
|
+
focus manager, so the tree observes it normally.
|
|
1994
|
+
- `onError(err)` — the embed failed, or (once, at mount) the backend has no
|
|
1995
|
+
embedding; unhandled, a console warning (for the latter, one per app).
|
|
1996
|
+
- `focusable` — default `true`, like `<textinput>`.
|
|
1997
|
+
- `backgroundColor` in `style` shows while there is no client.
|
|
1998
|
+
|
|
1999
|
+
**Unmount reparents the client back to the root and drops it from the save
|
|
2000
|
+
set; it never destroys it.** Layout treats it as a leaf; its X window
|
|
2001
|
+
follows that rect, and the client gets the synthetic `ConfigureNotify`
|
|
2002
|
+
ICCCM requires, in root coordinates. It stacks above everything drawn in
|
|
2003
|
+
the parent and takes no children, so an overlay belongs in a sibling
|
|
2004
|
+
`<popup>`. Pointer events over it are the client's.
|
|
2005
|
+
|
|
2006
|
+
**While it holds focus, the app's handlers see every key first, and
|
|
2007
|
+
anything they do not `preventDefault()` is forwarded to the client.** That,
|
|
2008
|
+
the XEmbed focus messages, the tab chain crossing the boundary and the one
|
|
2009
|
+
case the rule cannot cover are in [embedding.md](embedding.md).
|
|
2010
|
+
|
|
2011
|
+
---
|
|
2012
|
+
|
|
2013
|
+
## 3D scene: `<mesh>`, `<group>`, geometries, materials
|
|
2014
|
+
|
|
2015
|
+
These live in [`@react-x11/components/three`](ecosystem.md), a scene graph
|
|
2016
|
+
with its own reconciler over `<glarea>`, rendering on **both** GL backends
|
|
2017
|
+
from the same JSX. Core keeps the surface and `onDraw` ([gl.md](gl.md)).
|
|
2018
|
+
|
|
2019
|
+
## Vector drawings
|
|
2020
|
+
|
|
2021
|
+
`<svg>` is a drawing, laid out like an `<image>`: it reports an intrinsic
|
|
2022
|
+
size and scales into the content box it is given. For documents use
|
|
2023
|
+
[`@react-x11/components`](https://github.com/sidorares/react-x11-components)'
|
|
2024
|
+
`<Markdown>` (GFM) and `<Formula>` (KaTeX), with `<RichText>` and
|
|
2025
|
+
`<CodeBlock>` beneath: built from host elements, they select across blocks
|
|
2026
|
+
and re-render only the changed block while content streams in. (The
|
|
2027
|
+
`<markdown>`, `<html>` and `<tex>` elements were removed in 2.0.)
|
|
2028
|
+
|
|
2029
|
+
### `<svg>`
|
|
2030
|
+
|
|
2031
|
+
ntk `SvgView` (static SVG via Path2D — paths, shapes, gradients,
|
|
2032
|
+
transforms, basic text). Sized like `<image>`: natural `viewBox` size,
|
|
2033
|
+
aspect-preserving when the style sets one of `width`/`height`, scaled into
|
|
2034
|
+
the content box when it sets both. Content is **JSX children, like SVG in
|
|
2035
|
+
React DOM** — camelCase props (`strokeWidth`, `fillRule`; `viewBox` as-is),
|
|
2036
|
+
re-rendered on any prop change:
|
|
2037
|
+
|
|
2038
|
+
```jsx
|
|
2039
|
+
<svg viewBox="0 0 24 24" style={{ width: 40, height: 40 }}>
|
|
2040
|
+
<circle cx={12} cy={12} r={10} fill={active ? '#2980b9' : '#ccc'} />
|
|
2041
|
+
<path d="M8 12l3 3 5-6" stroke="white" strokeWidth={2} fill="none" />
|
|
2042
|
+
</svg>
|
|
2043
|
+
```
|
|
2044
|
+
|
|
2045
|
+
| prop | |
|
|
2046
|
+
| --------- | --------------------------------- |
|
|
2047
|
+
| children | declarative SVG elements |
|
|
2048
|
+
| `source` | SVG markup string |
|
|
2049
|
+
| `viewBox` | coordinate system (children form) |
|
|
2050
|
+
|
|
2051
|
+
Children win over `source` when both are given. Unsupported tags are
|
|
2052
|
+
skipped. Per-child event handlers are not dispatched — put them on the
|
|
2053
|
+
`<svg>`.
|
|
2054
|
+
|
|
2055
|
+
**`currentColor`** in `fill`/`stroke` resolves as in CSS: the node's own
|
|
2056
|
+
`color`, else the inherited ink, else the palette's `text`
|
|
2057
|
+
([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)) — so
|
|
2058
|
+
one drawing serves every state, and an unstyled one is readable on a dark
|
|
2059
|
+
desktop:
|
|
2060
|
+
|
|
2061
|
+
```jsx
|
|
2062
|
+
<svg
|
|
2063
|
+
source={icons.gauge}
|
|
2064
|
+
style={{
|
|
2065
|
+
width: 20,
|
|
2066
|
+
height: 20,
|
|
2067
|
+
color: '$fg',
|
|
2068
|
+
':hover': { color: '$accent' },
|
|
2069
|
+
}}
|
|
2070
|
+
/>
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
A drawing painted entirely in `currentColor`, or in one colour, is cached
|
|
2074
|
+
as coverage: recolouring is a composite, not a re-render, and every colour
|
|
2075
|
+
shares one copy. Two colours or a gradient are baked in.
|
|
2076
|
+
`REACT_X11_DEBUG_PAINT_CACHE=1` shows what is kept.
|
|
2077
|
+
|
|
2078
|
+
Design notes behind this page — mechanism, history, measurements:
|
|
2079
|
+
[design/elements.md](../design/elements.md).
|