react-x11 2.52.0 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1091 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
package/docs/elements.md
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
# Elements
|
|
2
2
|
|
|
3
3
|
Only `<window>`, `<popup>`, `<glarea>` and `<foreign>` are backed by real
|
|
4
|
-
windows of the display system
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
React might discard.
|
|
8
|
-
Everything else is a retained lightweight node — one
|
|
9
|
-
[yoga-layout](https://www.yogalayout.dev/) node each — painted into the
|
|
10
|
-
owning window's double-buffered 2d context on ntk's frame clock.
|
|
4
|
+
windows of the display system. Everything else is a retained lightweight
|
|
5
|
+
node — one [yoga-layout](https://www.yogalayout.dev/) node each — painted
|
|
6
|
+
into the owning window's double-buffered 2d context on ntk's frame clock.
|
|
11
7
|
|
|
12
8
|
## `style` and props
|
|
13
9
|
|
|
@@ -24,14 +20,14 @@ blocks and `createStyles`.
|
|
|
24
20
|
|
|
25
21
|
## Layout properties (all drawn elements + windows)
|
|
26
22
|
|
|
27
|
-
Lengths are logical pixels — a number, or
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
`
|
|
34
|
-
[styling.md](styling.md#lengths-and-shorthands).
|
|
23
|
+
Lengths are logical pixels — a number, or `'12px'` — and `'50%'` / `'auto'`
|
|
24
|
+
pass through to yoga where it takes them. `em`, `rem`,
|
|
25
|
+
`vw`/`vh`/`vmin`/`vmax` and `calc()`/`min()`/`max()`/`clamp()` resolve to
|
|
26
|
+
pixels for their node, and again when what they are relative to changes; a
|
|
27
|
+
CSS unit react-x11 does not read (`ex`, `cqw`, `pt`) is an error naming it.
|
|
28
|
+
CSS's shorthands are read too — `padding: '8px 16px'`,
|
|
29
|
+
`border: '1px solid $border'`, `inset: 0`
|
|
30
|
+
([styling.md](styling.md#lengths-and-shorthands)).
|
|
35
31
|
|
|
36
32
|
- **Size**: `width`, `height`, `minWidth`, `minHeight`, `maxWidth`,
|
|
37
33
|
`maxHeight`, `aspectRatio`. `<image>` and `<svg>` keep their aspect ratio
|
|
@@ -43,9 +39,9 @@ they are relative to moves; a unit CSS has and react-x11 does not read (`ex`,
|
|
|
43
39
|
`flexBasis`, `gap`, `rowGap`, `columnGap`
|
|
44
40
|
- **Position**: `position` (`relative`, `absolute`, `static`, `sticky`, or
|
|
45
41
|
one registered with `registerPosition`), `top`, `right`, `bottom`,
|
|
46
|
-
`left`, `start`, `end`, and `inset` for all four. Under `sticky` the
|
|
47
|
-
nearest scroll pane's edges the box may come,
|
|
48
|
-
[styling.md](styling.md#sticky-positioning) and
|
|
42
|
+
`left`, `start`, `end`, and `inset` for all four. Under `sticky` the
|
|
43
|
+
insets are how close to the nearest scroll pane's edges the box may come,
|
|
44
|
+
not offsets — see [styling.md](styling.md#sticky-positioning) and
|
|
49
45
|
[custom positions](styling.md#custom-positions)
|
|
50
46
|
- **Grid**: `display: 'grid'` arranges the children as a CSS grid —
|
|
51
47
|
`gridTemplateColumns`, `gridTemplateRows`, `gridTemplateAreas`,
|
|
@@ -65,16 +61,11 @@ they are relative to moves; a unit CSS has and react-x11 does not read (`ex`,
|
|
|
65
61
|
|
|
66
62
|
### Everything shrinks, nothing shrinks to nothing
|
|
67
63
|
|
|
68
|
-
`flexShrink` is `1`, as in CSS
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
implements the shrinking half and not the floor half. A row of chips squeezes
|
|
74
|
-
until it wraps; a label squeezes to its longest word; a `height: 40` row
|
|
75
|
-
stays 40 tall.
|
|
76
|
-
|
|
77
|
-
Three ways a style says otherwise:
|
|
64
|
+
`flexShrink` is `1`, as in CSS, but **an item never shrinks below what is
|
|
65
|
+
inside it**: every item has a floor of its own min-content size (CSS's
|
|
66
|
+
`min-width: auto` on a flex item). A row of chips squeezes until it wraps; a
|
|
67
|
+
label squeezes to its longest word; a `height: 40` row stays 40 tall. To say
|
|
68
|
+
otherwise:
|
|
78
69
|
|
|
79
70
|
| write | to mean |
|
|
80
71
|
| --------------------------------- | ----------------------------------------- |
|
|
@@ -82,32 +73,23 @@ Three ways a style says otherwise:
|
|
|
82
73
|
| `minWidth: 0` / `minHeight: 0` | this may shrink past its content, to zero |
|
|
83
74
|
| `overflow: 'hidden'` / `'scroll'` | the same, and clip what no longer fits |
|
|
84
75
|
|
|
85
|
-
The last two are CSS's own rule
|
|
86
|
-
|
|
87
|
-
possible: a viewport is a box that is _allowed_ to be smaller than what is
|
|
88
|
-
inside it. A `<box overflow="scroll">` gets `minWidth: 0` and `minHeight: 0`
|
|
76
|
+
The last two are CSS's own rule, and why a `<box overflow="scroll">` — a
|
|
77
|
+
viewport smaller than its content — gets `minWidth: 0` and `minHeight: 0`
|
|
89
78
|
for free.
|
|
90
79
|
|
|
91
80
|
One deliberate difference from CSS: **a size that was named is kept**. CSS
|
|
92
81
|
floors an item at `min(the size it was given, its content)`, so an empty
|
|
93
|
-
`height: 40` box squashes to nothing in a column too short for it;
|
|
94
|
-
|
|
95
|
-
children are not flex items at all, and it is not survivable here, where
|
|
96
|
-
every box lays its children out with flex. Say `minHeight: 0` to get CSS's
|
|
97
|
-
answer for one.
|
|
98
|
-
|
|
99
|
-
The measurement costs the tree an extra layout pass per axis, on frames that
|
|
100
|
-
change the layout — a scroll, which moves no box, keeps the single pass it
|
|
101
|
-
always had.
|
|
82
|
+
`height: 40` box squashes to nothing in a column too short for it; here it
|
|
83
|
+
stays 40. Say `minHeight: 0` to get CSS's answer.
|
|
102
84
|
|
|
103
85
|
### `flex`, the shorthand
|
|
104
86
|
|
|
105
87
|
`flex: 1` is `{ flexGrow: 1, flexShrink: 1, flexBasis: 0 }` — "take this
|
|
106
|
-
share of what is left, from a base size of nothing" —
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
`'
|
|
88
|
+
share of what is left, from a base size of nothing" — the right thing for a
|
|
89
|
+
pane that should fill its window rather than its content. `flex: 'auto'`
|
|
90
|
+
grows and shrinks from the content's own size, and `flex: 'none'` does
|
|
91
|
+
neither. CSS's long form reads too — `'1 1 0'`, `'0 0 240px'`,
|
|
92
|
+
`'2 200px'`. A longhand written beside it wins, so
|
|
111
93
|
`{ flex: 1, flexBasis: 'auto' }` reads the way it does in CSS.
|
|
112
94
|
|
|
113
95
|
## Paint properties
|
|
@@ -122,12 +104,11 @@ content. `flex: 'auto'` grows and shrinks from the content's own size, and
|
|
|
122
104
|
(`'solid'` default, `'dashed'`), or `border: '1px solid $border'` for the
|
|
123
105
|
first three — `border: '1px solid'` is drawn in `currentColor`
|
|
124
106
|
- per-side borders: `borderTopWidth`/`Right`/`Bottom`/`Left` override
|
|
125
|
-
`borderWidth`
|
|
126
|
-
`
|
|
127
|
-
|
|
128
|
-
`borderLeftWidth: 3` on the quote box, not a 3px sibling; a table rule is
|
|
107
|
+
`borderWidth`, `borderTopColor`/… fall back to `borderColor`, and
|
|
108
|
+
`borderBottom: '1px solid $border'` sets one side's pair — a blockquote's
|
|
109
|
+
bar is `borderLeftWidth: 3` on the quote box, a table rule
|
|
129
110
|
`borderBottomWidth: 1` on the row. `borderRadius` requires uniform
|
|
130
|
-
borders
|
|
111
|
+
borders; a non-uniform border paints square
|
|
131
112
|
([styling.md](styling.md#per-side-borders))
|
|
132
113
|
- `zIndex` — paint/hit order among siblings (stable sort)
|
|
133
114
|
- `objectFit` — where an `<image>`'s or a `<video>`'s picture goes in its
|
|
@@ -135,11 +116,11 @@ $border'` sets one side's pair. A blockquote's bar is
|
|
|
135
116
|
`'contain'` (`<video>`'s), `'cover'`, `'none'`, `'scale-down'`
|
|
136
117
|
- `outlineWidth`, `outlineColor`, `outlineOffset` — the focus ring, painted
|
|
137
118
|
outside the border box and invisible to yoga, so it never moves what it
|
|
138
|
-
surrounds. A focusable node draws one on `:focus-visible`
|
|
139
|
-
|
|
119
|
+
surrounds. A focusable node draws one on `:focus-visible` unasked;
|
|
120
|
+
`outlineWidth: 0` — or `outline: 'none'` — opts out
|
|
140
121
|
([styling.md](styling.md#the-focus-ring))
|
|
141
|
-
- `opacity` — how opaque the node is **with everything inside it**,
|
|
142
|
-
|
|
122
|
+
- `opacity` — how opaque the node is **with everything inside it**, 0 to 1:
|
|
123
|
+
the subtree is drawn once and composited at that alpha, so a card, its
|
|
143
124
|
icon and its text fade as one. `0` draws nothing and is still hit
|
|
144
125
|
([styling.md](styling.md#opacity))
|
|
145
126
|
- `transition` — `120`, or `{ backgroundColor: 120, left: 200 }`: how long a
|
|
@@ -150,25 +131,21 @@ $border'` sets one side's pair. A blockquote's bar is
|
|
|
150
131
|
([styling.md](styling.md#window-size-queries))
|
|
151
132
|
|
|
152
133
|
`cursor` (`'pointer'`, `'text'`, `'wait'`, `'move'`, `'crosshair'`, resize
|
|
153
|
-
arrows, … —
|
|
154
|
-
has both, and React Native has been moving `pointerEvents` the same way.
|
|
134
|
+
arrows, … — ntk's cursor names) and `pointerEvents` are style too.
|
|
155
135
|
`pointerEvents: 'none'` takes a node and its subtree out of hit testing;
|
|
156
|
-
`'box-none'
|
|
157
|
-
still take the pointer, and a press on its own area goes to what is behind
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
on the element behind the surface included. So is `hitSlop`:
|
|
136
|
+
`'box-none'` (React Native's) takes out only the node itself — its children
|
|
137
|
+
still take the pointer, and a press on its own area goes to what is behind:
|
|
138
|
+
the value for an overlay of controls, a `<glarea>`'s children included, that
|
|
139
|
+
must not swallow the clicks between them. So is `hitSlop`:
|
|
161
140
|
|
|
162
141
|
```jsx
|
|
163
142
|
<box style={{ height: 16, hitSlop: { top: 4, bottom: 4 } }} /> // 24px target
|
|
164
143
|
```
|
|
165
144
|
|
|
166
145
|
`hitSlop: 4` grows every side, the object form only the sides it names. It
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
sibling's box; hit testing runs front to back over paint order, so the
|
|
171
|
-
sibling on top keeps its own pixels either way.
|
|
146
|
+
affects **hit testing only** — not paint, not layout — so a 16px control
|
|
147
|
+
can take the 24px target WCAG 2.2 SC 2.5.8 asks for without misaligning its
|
|
148
|
+
row. Where slop overlaps a sibling, the one painted on top wins.
|
|
172
149
|
|
|
173
150
|
## Interaction props
|
|
174
151
|
|
|
@@ -178,37 +155,30 @@ stay inside it, focus is restored when it unmounts), `disabled` (never
|
|
|
178
155
|
focusable, and the trigger for a `:disabled` style block), and the event
|
|
179
156
|
handlers listed in [events.md](events.md).
|
|
180
157
|
|
|
181
|
-
`onLayout`
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
a
|
|
187
|
-
([react-features.md](react-features.md#measuring-a-node)); where it is one,
|
|
188
|
-
a `'@container'` block answers in the same frame
|
|
158
|
+
`onLayout` reports layout: `{x, y, width, height}` in logical pixels, the
|
|
159
|
+
position within the parent _as laid out_ (a scroll does not fire it), after
|
|
160
|
+
the first layout and then on each change. It arrives after the pass, so
|
|
161
|
+
setting state from it is safe. Use it when the reaction to a size is not a
|
|
162
|
+
style ([react-features.md](react-features.md#measuring-a-node)); when it
|
|
163
|
+
is, a `'@container'` block answers in the same frame
|
|
189
164
|
([styling.md](styling.md#container-queries)).
|
|
190
165
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
listening, and the defaults already say something sensible for every
|
|
195
|
-
element: [accessibility.md](accessibility.md).
|
|
166
|
+
`role` and the `aria-*` props are read by the built-in AT-SPI bridge so
|
|
167
|
+
screen readers see the tree; inert where no assistive technology listens,
|
|
168
|
+
with sensible defaults per element ([accessibility.md](accessibility.md)).
|
|
196
169
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
Their presence is what registers the node, so there is nothing else to
|
|
201
|
-
mount: [drag-and-drop.md](drag-and-drop.md).
|
|
170
|
+
`dropAccept` + `onDrop` accept a drop and `draggable` + `dragData` start
|
|
171
|
+
one, with other applications as well as inside the app; the props alone
|
|
172
|
+
register the node ([drag-and-drop.md](drag-and-drop.md)).
|
|
202
173
|
|
|
203
|
-
`selectable` makes an element a **selection surface
|
|
204
|
-
|
|
205
|
-
[Selecting text](#selecting-text) below.
|
|
174
|
+
`selectable` makes an element a **selection surface** — see
|
|
175
|
+
[Selecting text](#selecting-text).
|
|
206
176
|
|
|
207
|
-
`scale` zooms a subtree
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
177
|
+
`scale` zooms a subtree like CSS `zoom`, not a transform: every length under
|
|
178
|
+
the element, and its own style, is multiplied, and text is shaped at the
|
|
179
|
+
drawn size — what an element owning a viewport (a graph pane, a map) mounts
|
|
180
|
+
its content inside. Windows take no `scale`
|
|
181
|
+
([scale.md](scale.md#a-subtree-of-its-own)).
|
|
212
182
|
|
|
213
183
|
---
|
|
214
184
|
|
|
@@ -217,62 +187,59 @@ a viewport (a graph pane, a map) mounts its content inside. Windows take no
|
|
|
217
187
|
A real toplevel window — an X11 window, or an `NSWindow` on the Cocoa
|
|
218
188
|
backend; the flex, paint and event root for its subtree.
|
|
219
189
|
|
|
220
|
-
| prop |
|
|
221
|
-
| --------------------------- |
|
|
222
|
-
| `title` | window title (UTF-8,
|
|
223
|
-
| `width`, `height`, `x`, `y` |
|
|
224
|
-
| `backgroundColor` | full-window clear color (default white)
|
|
225
|
-
| `onResize(ev)` |
|
|
226
|
-
| `onExpose(ev)` | after a repaint was required
|
|
227
|
-
| `onCloseRequest(ev)` | WM close button
|
|
228
|
-
| `states` | EWMH `_NET_WM_STATE
|
|
229
|
-
| `fullscreen`, `alwaysOnTop` | boolean sugar for two of those states
|
|
230
|
-
| `decorations` | `false` asks the WM for no titlebar or border
|
|
231
|
-
| `transparent` | 32-bit ARGB visual
|
|
232
|
-
| `transientFor` | ICCCM `WM_TRANSIENT_FOR
|
|
233
|
-
| `onStatesChange(states)` | what the window manager actually did
|
|
234
|
-
| `onClientMessage(ev)` | a ClientMessage addressed to this window — EWMH, XEmbed, the tray
|
|
235
|
-
| `theme` | palette
|
|
236
|
-
| `embeddable` |
|
|
237
|
-
| `hidden` | realized and laid out, never mapped while true
|
|
238
|
-
| `frameRate` |
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
190
|
+
| prop | |
|
|
191
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
192
|
+
| `title` | window title (UTF-8, `WM_NAME` + `_NET_WM_NAME`) |
|
|
193
|
+
| `width`, `height`, `x`, `y` | geometry, not yoga style; a size is pixels or `'auto'` (default, [below](#natural-size)) |
|
|
194
|
+
| `backgroundColor` | full-window clear color (default white) |
|
|
195
|
+
| `onResize(ev)` | the geometry changed; the tree reflows itself. Fires for **moves** too |
|
|
196
|
+
| `onExpose(ev)` | after a repaint was required |
|
|
197
|
+
| `onCloseRequest(ev)` | WM close button; replaces the default answer |
|
|
198
|
+
| `states` | EWMH `_NET_WM_STATE`, controlled |
|
|
199
|
+
| `fullscreen`, `alwaysOnTop` | boolean sugar for two of those states |
|
|
200
|
+
| `decorations` | `false` asks the WM for no titlebar or border |
|
|
201
|
+
| `transparent` | 32-bit ARGB visual: rounded, translucent windows |
|
|
202
|
+
| `transientFor` | ICCCM `WM_TRANSIENT_FOR`: the window this one belongs to |
|
|
203
|
+
| `onStatesChange(states)` | what the window manager actually did |
|
|
204
|
+
| `onClientMessage(ev)` | a ClientMessage addressed to this window — EWMH, XEmbed, the tray |
|
|
205
|
+
| `theme` | palette `$token` style values resolve against, for this subtree |
|
|
206
|
+
| `embeddable` | never maps itself; an embedder ([`<foreign>`](embedding.md)) does. Set by a [`<Frame>`](frame.md) pane's root |
|
|
207
|
+
| `hidden` | realized and laid out, never mapped while true |
|
|
208
|
+
| `frameRate` | frame pacing: `'display'` (default), `'adaptive'`, `'throughput'`, fps, `{ budget, minFps, maxFps }` |
|
|
209
|
+
|
|
210
|
+
Each is described below. A geometry change sends only what changed, so a
|
|
211
|
+
new size leaves the window where the window manager or the user put it.
|
|
212
|
+
Windows may be nested (real X11 child windows). **Ref**: the live window —
|
|
213
|
+
on X11 the ntk `Window`, with `getContext('2d')`, `requestAnimationFrame`,
|
|
214
|
+
`setCursor` and the rest of the ntk API.
|
|
244
215
|
|
|
245
216
|
#### On the Cocoa backend
|
|
246
217
|
|
|
247
|
-
Everything
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
| `
|
|
255
|
-
| `
|
|
256
|
-
| `
|
|
257
|
-
| `states: ['
|
|
258
|
-
| `
|
|
259
|
-
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
accepted and does nothing, so shared application code stays branch-free. See
|
|
268
|
-
[macos.md](macos.md#windowing-semantics-what-maps-what-bends-what-breaks)
|
|
269
|
-
for why that is the policy and what it costs.
|
|
218
|
+
Everything about _drawing_ works — geometry, background, resize, close,
|
|
219
|
+
`transparent`, `theme`, `frameRate`, `hidden`. What a **window manager**
|
|
220
|
+
does differs, since macOS has none:
|
|
221
|
+
|
|
222
|
+
| prop | on Cocoa |
|
|
223
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
224
|
+
| `minWidth`/`minHeight`/`maxWidth`/`maxHeight` | honoured |
|
|
225
|
+
| `resizable`, `decorations` | honoured, but read **once, at creation** |
|
|
226
|
+
| `transparent`, `borderRadius` | always available: every window composites |
|
|
227
|
+
| `states: ['demands_attention']` | the Dock bounce, held until the state is removed |
|
|
228
|
+
| `fullscreen`, `states: ['maximized' \| 'hidden']` | the full-screen Space, the zoom, the Dock |
|
|
229
|
+
| `onStatesChange`, `useWindowState()` | fire for full screen (from the transition's start), zoom, Dock, key window; `desktop`/`obscured` never change |
|
|
230
|
+
| `embeddable` | a `<Frame>` pane, through shared surfaces ([frame.md](frame.md)) |
|
|
231
|
+
|
|
232
|
+
**Inert** — accepted and, today, silently ignored, so shared code stays
|
|
233
|
+
branch-free: every other `_NET_WM_STATE` (`sticky`, `below`,
|
|
234
|
+
`skip_taskbar`, `shaded`, …), `appId`, `windowType`, `gravity`, the
|
|
235
|
+
increment and aspect hints, and `transientFor` (a managed `<Dialog>` already
|
|
236
|
+
floats on its own window). `onClientMessage` never fires. Why:
|
|
237
|
+
[macos.md](macos.md#windowing-semantics-what-maps-what-bends-what-breaks).
|
|
270
238
|
|
|
271
239
|
### Natural size
|
|
272
240
|
|
|
273
241
|
A window with no `width`/`height` is **sized from its content and capped at
|
|
274
|
-
the screen**. `'auto'` says
|
|
275
|
-
independent:
|
|
242
|
+
the screen**. `'auto'` says so explicitly, per axis:
|
|
276
243
|
|
|
277
244
|
```jsx
|
|
278
245
|
<window title="prefs" /> {/* natural, both ways */}
|
|
@@ -280,91 +247,64 @@ independent:
|
|
|
280
247
|
<window width="auto" maxWidth={720} minHeight={200} /> {/* auto within your own bounds */}
|
|
281
248
|
```
|
|
282
249
|
|
|
283
|
-
This is CSS's
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
container to stretch into, and stretching into the screen is what
|
|
287
|
-
`fullscreen` means.
|
|
250
|
+
This is CSS's shrink-to-fit — a top-level window has nothing to stretch
|
|
251
|
+
into; stretching into the screen is `fullscreen`. The size is worked out
|
|
252
|
+
before the window is created, so it never appears at the wrong size.
|
|
288
253
|
|
|
289
|
-
|
|
290
|
-
the right size rather than resized into it after mapping. Nothing is ever on
|
|
291
|
-
screen at the wrong size, and there is no jump to watch for.
|
|
292
|
-
|
|
293
|
-
**How it is measured**, which is worth knowing because the second step is
|
|
294
|
-
where the surprises are:
|
|
254
|
+
**How it is measured** — the second step is where the surprises are:
|
|
295
255
|
|
|
296
256
|
1. The tree is laid out with no available width, so nothing wraps and the
|
|
297
257
|
root reports its **max-content** width.
|
|
298
|
-
2. That is clamped into `[minWidth, min(maxWidth, the screen)]` — the
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
two disagree, `minWidth` wins, which is CSS's order too.
|
|
258
|
+
2. That is clamped into `[minWidth, min(maxWidth, the screen)]` — the size
|
|
259
|
+
hints bound the auto size as `min-width`/`max-width` bound `width: auto`;
|
|
260
|
+
`minWidth` wins a conflict.
|
|
302
261
|
3. The tree is laid out **again at that width**, and the height comes from
|
|
303
|
-
there
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
back; the moment the user drags an edge, or a window manager answers with a
|
|
323
|
-
size of its own, the window is theirs and stops re-fitting. Setting `width` to
|
|
324
|
-
a number is the app doing the same thing, and setting it back to `'auto'`
|
|
325
|
-
hands it back. Only a size the window did not ask for counts: its own
|
|
326
|
-
requests come back from the server as echoes, sometimes late and sometimes
|
|
327
|
-
behind a move that still carries the old size, and none of those is anybody
|
|
328
|
-
taking the window over.
|
|
329
|
-
|
|
330
|
-
**A re-fit is laid out once, at the new size.** On X11 a window takes a size
|
|
331
|
-
when the server answers the request, a round trip later, so the frame that
|
|
332
|
-
re-fits waits for that answer before it lays out, and the old pixels stay
|
|
333
|
-
up meanwhile. Laid out at the size it still had, content that grew would be
|
|
334
|
-
drawn for a round trip wrapped and cut off at the bottom of a window too
|
|
335
|
-
small for it — a tooltip whose value gained a word. A request answered with
|
|
336
|
-
no new size (a window manager refusing it) gives up the wait after 150 ms.
|
|
337
|
-
Cocoa takes a size on the call, so there is nothing to wait for there.
|
|
262
|
+
there: a paragraph that had to wrap is taller than step 1 said.
|
|
263
|
+
|
|
264
|
+
Unlike in CSS, the screen cap beats min-content: what does not fit is cut.
|
|
265
|
+
|
|
266
|
+
**The cap** is the usable area of the monitor the window opens on — its
|
|
267
|
+
Xinerama rect less the panels in `_NET_WORKAREA` — read once while
|
|
268
|
+
`createRoot()` connects; the screen on a server with neither; none in the
|
|
269
|
+
headless mock. The window manager's frame is not counted, so a window at
|
|
270
|
+
the cap is a titlebar too tall and the WM trims it.
|
|
271
|
+
|
|
272
|
+
**Afterwards, auto follows the content**, growing and shrinking with it,
|
|
273
|
+
until the user resizes the window or the window manager imposes a size;
|
|
274
|
+
then the window is theirs. A numeric `width` does the same, and `'auto'`
|
|
275
|
+
again hands it back. The window's own resize requests coming back are not a
|
|
276
|
+
takeover.
|
|
277
|
+
|
|
278
|
+
On X11 a re-fit is laid out once, at the new size: the frame waits for the
|
|
279
|
+
server to apply it (up to 150 ms, if the window manager refuses), so grown
|
|
280
|
+
content is never drawn cut off; the old pixels stay up meanwhile.
|
|
338
281
|
`REACT_X11_NO_REFIT_HOLD=1` turns the wait off
|
|
339
|
-
([debugging.md](debugging.md#react_x11_no_refit_hold1)).
|
|
282
|
+
([debugging.md](debugging.md#react_x11_no_refit_hold1)). On Cocoa a window
|
|
283
|
+
is a whole number of **points**, rounded up (100.5 makes 101), and still
|
|
284
|
+
follows its content.
|
|
340
285
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
286
|
+
- A scroll container reports its **content** height, so an auto window
|
|
287
|
+
around a long list opens screen-tall. Give the window a `height`, or the
|
|
288
|
+
scroller a `maxHeight`.
|
|
289
|
+
- An auto width is the **unwrapped** row, so it depends on the UI font.
|
|
290
|
+
- A window tracking its content costs an extra layout pass on each frame
|
|
291
|
+
that lays out; one with both sizes given pays nothing.
|
|
345
292
|
|
|
346
293
|
### A floor the content decides
|
|
347
294
|
|
|
348
295
|
`minWidth`/`minHeight` also take `'auto'`: **the smallest size the content
|
|
349
|
-
can be drawn at**, measured
|
|
350
|
-
|
|
296
|
+
can be drawn at**, measured and handed to the window manager so the user
|
|
297
|
+
cannot drag the window below it. Opt-in — Qt and GTK do it by default —
|
|
298
|
+
since CSS lets content overflow.
|
|
351
299
|
|
|
352
300
|
```jsx
|
|
353
301
|
<window minWidth="auto" minHeight="auto"> {/* never smaller than my content */}
|
|
354
302
|
<window width={900} minWidth="auto" /> {/* my opening size, the content's floor */}
|
|
355
303
|
```
|
|
356
304
|
|
|
357
|
-
This is what Qt and GTK both do, and in both it is the default rather than
|
|
358
|
-
something you ask for: a Qt layout's `minimumSize()` becomes the window's
|
|
359
|
-
via `QLayout::SetDefaultConstraint`, and GTK writes the widget tree's
|
|
360
|
-
minimum into the WM geometry hints. Here it is opt-in, because a renderer
|
|
361
|
-
whose layout is CSS has no floor unless one is asked for — CSS lets content
|
|
362
|
-
overflow.
|
|
363
|
-
|
|
364
305
|
**How the floor is measured**: the tree is laid out with _no room at all_,
|
|
365
|
-
and the floor is how far it still reached.
|
|
366
|
-
|
|
367
|
-
what _doesn't_ count:
|
|
306
|
+
and the floor is how far it still reached. Each node contributes what its
|
|
307
|
+
own style lets it shrink to:
|
|
368
308
|
|
|
369
309
|
| the node | contributes |
|
|
370
310
|
| --------------------------------- | ---------------- |
|
|
@@ -375,55 +315,27 @@ what _doesn't_ count:
|
|
|
375
315
|
| a `minWidth: 0` of its own | nothing |
|
|
376
316
|
| a `minWidth: 240` of its own | 240 |
|
|
377
317
|
|
|
378
|
-
A registered element contributes
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`min-content-width`. A node that named a floor of its own is taken at its
|
|
386
|
-
word and its contents stop counting, so a window around a scrolling pane is
|
|
387
|
-
floored by everything _except_ that pane.
|
|
318
|
+
A registered element contributes its `measureContent` answer for the
|
|
319
|
+
smallest size ([extending.md](extending.md#a-size-of-your-own)). This is
|
|
320
|
+
every node's own floor
|
|
321
|
+
([above](#everything-shrinks-nothing-shrinks-to-nothing)) read at the root,
|
|
322
|
+
with the same escape hatch: a node that names a floor of its own stops its
|
|
323
|
+
contents counting, so a window around a scrolling pane is floored by
|
|
324
|
+
everything _except_ that pane.
|
|
388
325
|
|
|
389
|
-
`maxWidth`/`maxHeight`
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
the two are the same number, which is what makes `minWidth="auto"
|
|
394
|
-
maxWidth="auto"` the fixed-size dialog — the same thing `resizable={false}`
|
|
395
|
-
says more briefly.
|
|
396
|
-
|
|
397
|
-
Three things worth knowing:
|
|
326
|
+
`maxWidth`/`maxHeight="auto"` mean **the size the content wanted** — the
|
|
327
|
+
natural size, not the floor. Where nothing can shrink they are equal, so
|
|
328
|
+
`minWidth="auto" maxWidth="auto"` is a fixed-size dialog, as is
|
|
329
|
+
`resizable={false}`.
|
|
398
330
|
|
|
399
331
|
- **A floor is a request.** mutter, kwin and xfwm enforce it at the drag; a
|
|
400
|
-
tiling window manager may
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
re-sent as that changes. A minimum height is a height _for a width_ — a
|
|
405
|
-
paragraph is tallest at its narrowest — and `WM_NORMAL_HINTS` holds two
|
|
406
|
-
independent numbers with no way to say so. (GTK answers the same question
|
|
407
|
-
at its minimum _width_, which is a taller floor than a wide window needs.)
|
|
332
|
+
tiling window manager may not, and the renderer will not fight it — clip
|
|
333
|
+
gracefully anyway.
|
|
334
|
+
- **`minHeight="auto"` is for the current width**, re-sent as that changes:
|
|
335
|
+
a paragraph is tallest at its narrowest.
|
|
408
336
|
- **The floor outlives the size.** An `'auto'` size stops tracking once the
|
|
409
|
-
user takes
|
|
410
|
-
|
|
411
|
-
lays out — one extra layout pass per axis asked for — and written only on
|
|
412
|
-
the frames it actually moves.
|
|
413
|
-
|
|
414
|
-
Three things worth knowing before reaching for it:
|
|
415
|
-
|
|
416
|
-
- A scroll container reports its **content** height, not a viewport height,
|
|
417
|
-
so an auto window around a long list opens as tall as the screen. Give the
|
|
418
|
-
window a `height`, or the scrolling box a `maxHeight`, if the scrolling is
|
|
419
|
-
meant to happen inside a smaller window.
|
|
420
|
-
- The natural size is measured with **no available width**, where nothing
|
|
421
|
-
wraps and nothing is short of room to shrink into, so an auto width is the
|
|
422
|
-
**unwrapped** row. That makes the width font-dependent: the same window is
|
|
423
|
-
wider under a wide UI face than under a narrow one.
|
|
424
|
-
- Measuring costs an extra layout pass per frame that lays out, for as long
|
|
425
|
-
as the window is still tracking. A window with both sizes given pays
|
|
426
|
-
nothing.
|
|
337
|
+
user takes over; an `'auto'` bound does not. It costs a layout pass per
|
|
338
|
+
axis on frames that lay out, and is sent only when it moves.
|
|
427
339
|
|
|
428
340
|
### `hidden` — realized, laid out, not on screen
|
|
429
341
|
|
|
@@ -431,25 +343,19 @@ Three things worth knowing before reaching for it:
|
|
|
431
343
|
<window hidden={minimized} title="scratch" />
|
|
432
344
|
```
|
|
433
345
|
|
|
434
|
-
The
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
reveal hands it back unless something else took it meanwhile.
|
|
448
|
-
|
|
449
|
-
On a `<popup>` the pair of unmap/map is the whole story. On a managed
|
|
450
|
-
`<window>` an unmap is ICCCM's _withdraw_ — the window manager forgets the
|
|
451
|
-
window, and the re-map is a fresh `MapRequest`, so a WM may frame or place
|
|
452
|
-
it anew and `states` are re-declared rather than remembered.
|
|
346
|
+
The window exists at its (measured) size with a live tree, but is not
|
|
347
|
+
mapped while `hidden`; clearing it maps the window where it stands. Nothing
|
|
348
|
+
unmounts, so state, subscriptions and the window survive — declarative
|
|
349
|
+
"minimize by unmapping", and how [`Tooltip`](components.md#tooltip)
|
|
350
|
+
measures a label before placing it.
|
|
351
|
+
|
|
352
|
+
`<Activity mode="hidden">` or a suspended `<Suspense>` compose with it: the
|
|
353
|
+
window shows only while **neither** hides it. Hiding releases the keyboard
|
|
354
|
+
focus inside ([focus-follows-visibility](events.md)); the reveal restores it
|
|
355
|
+
unless something else took it. On a managed `<window>` hiding is ICCCM's
|
|
356
|
+
_withdraw_, so on reveal the window manager may frame or place it anew, and
|
|
357
|
+
`states` are re-applied rather than remembered; a `<popup>` just unmaps and
|
|
358
|
+
maps.
|
|
453
359
|
|
|
454
360
|
### `frameRate` — pacing the frames under a flood
|
|
455
361
|
|
|
@@ -457,89 +363,62 @@ it anew and `states` are re-declared rather than remembered.
|
|
|
457
363
|
<window frameRate="adaptive">…</window>
|
|
458
364
|
```
|
|
459
365
|
|
|
460
|
-
By default a window paints every frame the clock gives it
|
|
461
|
-
as the server keeps up, on macOS at the display's
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
| value | `budget` | `minFps` | `maxFps` | when |
|
|
502
|
-
| ---------------------------- | -------: | -------: | -------: | -------------------------------------------------------------------------------------- |
|
|
503
|
-
| `'display'` (the default) | 1 | none | none | a UI answering input: every frame the clock gives, after React's own batching |
|
|
504
|
-
| `'adaptive'` | 0.25 | 20 | none | a window fed off an input path: a terminal, a live chart, a scene that streams |
|
|
505
|
-
| `'throughput'` | 0.1 | 10 | 30 | output matters more than the display: the screen may lag by up to 100ms during a flood |
|
|
506
|
-
| a number, `60` | 1 | none | 60 | a ceiling and nothing else — the fixed cap |
|
|
507
|
-
| `{ budget, minFps, maxFps }` | | | | the three numbers, any subset; the ones left out are `'display'`'s |
|
|
508
|
-
|
|
509
|
-
A budget below 1 with no `minFps` warns in development: after an expensive
|
|
510
|
-
frame the next may wait three times its cost with nothing to bound it, and
|
|
511
|
-
`{ budget, minFps: 20 }` is what `'adaptive'` does.
|
|
512
|
-
|
|
513
|
-
Three sources, in order of precedence: **`REACT_X11_FRAME_RATE`** in the
|
|
514
|
-
environment (`adaptive`, `display`, `throughput` or a number) overrides
|
|
515
|
-
everything, the way `REACT_X11_BACKEND` does, so an A/B run and a field
|
|
516
|
-
diagnosis need no code change; then **the prop**; then
|
|
517
|
-
**`createRoot({ frameRate })`**, the default for every window of that root;
|
|
518
|
-
then `'display'`. A `<popup>` paces itself the same way, from its own prop.
|
|
519
|
-
A [`<glarea>`](#glarea) takes the window's policy for its own frames unless
|
|
366
|
+
By default a window paints every frame the clock gives it — on X11 as often
|
|
367
|
+
as the server keeps up, on macOS at the display's rate — right for a UI
|
|
368
|
+
answering input. For content that changes faster than anyone reads — a
|
|
369
|
+
terminal under `cat`, a socket-fed chart, a simulation — most of the JS
|
|
370
|
+
thread then goes to frames nobody sees, starving the producer.
|
|
371
|
+
|
|
372
|
+
`frameRate` prices frames in CPU time. Under `'adaptive'` painting may take
|
|
373
|
+
a quarter of the wall time while busy: credit accrues at that rate, each
|
|
374
|
+
frame spends what it cost (the flush, and on macOS the present), and a frame
|
|
375
|
+
claimed in debt waits until the credit is back to zero. So:
|
|
376
|
+
|
|
377
|
+
- **Idle is immediate**: the first frame after a pause paints on the next
|
|
378
|
+
tick, whatever the last cost.
|
|
379
|
+
- **Cheap is unthrottled**: a frame cheaper than its credit is never held,
|
|
380
|
+
so a blit-scrolled list keeps the display's rate.
|
|
381
|
+
- **A rare expensive frame is free**: the bucket holds a 50ms burst. A
|
|
382
|
+
_stream_ of them waits `cost × (1/budget − 1)` each — 18ms after a 6ms
|
|
383
|
+
frame.
|
|
384
|
+
- **A flood that ends is unthrottled at once**: the debt never exceeds one
|
|
385
|
+
frame's cost.
|
|
386
|
+
|
|
387
|
+
`minFps` bounds staleness: the screen is never more than `1000 / minFps` ms
|
|
388
|
+
behind the last paint. `maxFps` caps the rate, cheap frames included.
|
|
389
|
+
|
|
390
|
+
| value | `budget` | `minFps` | `maxFps` | when |
|
|
391
|
+
| ---------------------------- | -------: | -------: | -------: | --------------------------------------------------------- |
|
|
392
|
+
| `'display'` (the default) | 1 | none | none | a UI answering input: every frame, after React's batching |
|
|
393
|
+
| `'adaptive'` | 0.25 | 20 | none | fed off an input path: a terminal, a live chart |
|
|
394
|
+
| `'throughput'` | 0.1 | 10 | 30 | output over display: may lag up to 100ms during a flood |
|
|
395
|
+
| a number, `60` | 1 | none | 60 | a ceiling and nothing else |
|
|
396
|
+
| `{ budget, minFps, maxFps }` | | | | any subset; the rest are `'display'`'s |
|
|
397
|
+
|
|
398
|
+
A budget below 1 with no `minFps` warns in development: nothing would bound
|
|
399
|
+
the wait after an expensive frame.
|
|
400
|
+
|
|
401
|
+
Precedence: **`REACT_X11_FRAME_RATE`** (`adaptive`, `display`,
|
|
402
|
+
`throughput` or a number) overrides everything, so an A/B run needs no code
|
|
403
|
+
change; then **the prop**; then **`createRoot({ frameRate })`**, the
|
|
404
|
+
default for every window of that root; then `'display'`. A `<popup>` takes
|
|
405
|
+
its own prop the same way; a [`<glarea>`](#glarea) takes its window's unless
|
|
520
406
|
it names one.
|
|
521
407
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
holds
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
holds a frame there; where a backend has backpressure of its own, the
|
|
530
|
-
pacer is inert. `REACT_X11_TRACE=requests` prints `waited=` on every frame
|
|
531
|
-
the pacer held ([debugging.md](debugging.md)), and
|
|
532
|
-
[architecture/frame-pacing.md](architecture/frame-pacing.md) is the design
|
|
533
|
-
record, with the measurements.
|
|
408
|
+
The pacer never changes what is painted (held damage accumulates into the
|
|
409
|
+
next frame) and never delays the answer to input: a click or key paints at
|
|
410
|
+
once ([events.md](events.md)). It prices JS-thread time only, so on X11 it
|
|
411
|
+
rarely holds a frame, and it is inert where a backend has backpressure of
|
|
412
|
+
its own. `REACT_X11_TRACE=requests` prints `waited=` on each held frame
|
|
413
|
+
([debugging.md](debugging.md)). Design notes:
|
|
414
|
+
[frame pacing](../design/architecture/frame-pacing.md).
|
|
534
415
|
|
|
535
416
|
### `onResize` fires for moves
|
|
536
417
|
|
|
537
|
-
`onResize`
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
handler of your own has to, or it does its work per step of a drag. The
|
|
542
|
-
event says which it was:
|
|
418
|
+
`onResize` reports _any_ geometry change: under a window manager that moves
|
|
419
|
+
windows opaquely, a title-bar drag fires it per pointer step at the same
|
|
420
|
+
size. The renderer spends nothing on a move; a handler should check which it
|
|
421
|
+
got:
|
|
543
422
|
|
|
544
423
|
```jsx
|
|
545
424
|
<window
|
|
@@ -550,17 +429,16 @@ event says which it was:
|
|
|
550
429
|
/>
|
|
551
430
|
```
|
|
552
431
|
|
|
553
|
-
`ev.previous`
|
|
554
|
-
`ev.x`/`ev.y` are frame-relative
|
|
555
|
-
|
|
556
|
-
resolves the real origin itself for popup anchoring.
|
|
432
|
+
`ev.previous` is the geometry these compare against. Once a reparenting
|
|
433
|
+
window manager has framed the window, `ev.x`/`ev.y` are frame-relative, not
|
|
434
|
+
screen coordinates.
|
|
557
435
|
|
|
558
436
|
### Stacking
|
|
559
437
|
|
|
560
|
-
Nested `<window>`s stack
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
nothing
|
|
438
|
+
Nested `<window>`s stack like drawn siblings — later on top, `zIndex` over
|
|
439
|
+
document order — and reordering them in JSX restacks the real windows.
|
|
440
|
+
Top-level windows are the window manager's to stack, so their tree order
|
|
441
|
+
means nothing; use `alwaysOnTop`.
|
|
564
442
|
|
|
565
443
|
```jsx
|
|
566
444
|
<window>
|
|
@@ -570,24 +448,19 @@ nothing at all when mount order already agrees.
|
|
|
570
448
|
</window>
|
|
571
449
|
```
|
|
572
450
|
|
|
573
|
-
Top-level windows are the window manager's to stack — it redirects the
|
|
574
|
-
request — so their order in the tree carries no stacking meaning. Use
|
|
575
|
-
`alwaysOnTop` (below) for those.
|
|
576
|
-
|
|
577
451
|
### Window manager hints
|
|
578
452
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
the geometry they constrain — with style in its own channel the yoga names
|
|
582
|
-
are free, so no `sizeHints` object is needed.
|
|
453
|
+
Read by the window manager, applied at mount and on update (unchanged
|
|
454
|
+
values are not re-sent):
|
|
583
455
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
456
|
+
- `resizable={false}` pins min and max size to the current size.
|
|
457
|
+
- Size hints: `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `widthInc`,
|
|
458
|
+
`heightInc`, `baseWidth`, `baseHeight`, `minAspect`, `maxAspect`,
|
|
459
|
+
`gravity`.
|
|
460
|
+
- `appId`: `'instance'`, `['instance', 'Class']` or `{instance, class}`.
|
|
461
|
+
- `windowType`: `'dialog'`, `'utility'`, `'tooltip'`… or an array of
|
|
462
|
+
fallbacks.
|
|
463
|
+
- `decorations={false}` asks for no titlebar or border.
|
|
591
464
|
|
|
592
465
|
```jsx
|
|
593
466
|
<window width={400} height={300} resizable={false} windowType="dialog" />
|
|
@@ -595,26 +468,19 @@ are free, so no `sizeHints` object is needed.
|
|
|
595
468
|
<window width={400} height={300} minWidth="auto" /> {/* measured — see above */}
|
|
596
469
|
```
|
|
597
470
|
|
|
598
|
-
The four bounds also take `'auto'`, which asks the content instead of naming
|
|
599
|
-
a number: [a floor the content decides](#a-floor-the-content-decides).
|
|
600
|
-
|
|
601
471
|
On a `<window>` these names are the window's, not yoga's: `width`/`height`
|
|
602
|
-
are the real geometry the user can drag,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
```jsx
|
|
609
|
-
<window resizable={false} windowType="dialog" />
|
|
610
|
-
```
|
|
472
|
+
are the real geometry the user can drag, `minWidth`/`maxHeight` what the WM
|
|
473
|
+
enforces, and the _contents_ are laid out by `style`. The four bounds also
|
|
474
|
+
take `'auto'` ([a floor the content decides](#a-floor-the-content-decides)),
|
|
475
|
+
and on an auto-sized window they bound the natural size, so
|
|
476
|
+
`<window resizable={false} windowType="dialog" />` is pinned at its measured
|
|
477
|
+
size — the fixed-size dialog.
|
|
611
478
|
|
|
612
479
|
### `transientFor` — a window that belongs to another
|
|
613
480
|
|
|
614
|
-
Without it,
|
|
615
|
-
manager
|
|
616
|
-
|
|
617
|
-
minimise with the window it belongs to.
|
|
481
|
+
Without it, a secondary window is an unrelated application to the window
|
|
482
|
+
manager: its own taskbar and alt-tab entry, placed anywhere, stacked
|
|
483
|
+
independently, not minimised with its owner.
|
|
618
484
|
|
|
619
485
|
```jsx
|
|
620
486
|
const main = useRef(null);
|
|
@@ -623,84 +489,48 @@ const main = useRef(null);
|
|
|
623
489
|
<window transientFor={main} windowType="dialog" title="Preferences">…</window>
|
|
624
490
|
```
|
|
625
491
|
|
|
626
|
-
Takes a ref to a `<window>`/`<popup
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
**It is inert on an override-redirect window**, which is every `<popup>` by
|
|
649
|
-
default: the WM never manages those, so nothing reads the property. ICCCM
|
|
650
|
-
4.1.2.6 draws exactly that distinction — `WM_TRANSIENT_FOR` for windows the
|
|
651
|
-
WM manages, override-redirect plus a pointer grab for menus. ntk warns if
|
|
652
|
-
you ask it to write the property on one.
|
|
653
|
-
|
|
654
|
-
Resolution happens in the **commit phase**, not at element creation: a React
|
|
655
|
-
ref is not something ntk should be asked to understand. Refs attach in the
|
|
656
|
-
layout phase, after every mutation, so on the commit that mounts two sibling
|
|
657
|
-
`<window>`s the second realizes while the first one's ref is still null —
|
|
658
|
-
that owner is retried on the frame the mount schedules rather than dropped.
|
|
659
|
-
|
|
660
|
-
`windowIdOf(refOrInstance)` is the same resolution, exported: it returns the
|
|
661
|
-
XID a ref points at, or null. It is also what an xdg-desktop-portal
|
|
662
|
-
`parent_window` handle needs — `` `x11:${id.toString(16)}` ``, lowercase hex
|
|
663
|
-
with **no** `0x` prefix. Both shipping portal backends happen to tolerate a
|
|
664
|
-
prefix, which is exactly why it is easy to get wrong and never notice; Qt's
|
|
665
|
-
parser returns 0 on failure with no error path, so the symptom would be a
|
|
666
|
-
silently unparented, non-modal dialog. `useWindowId(ref)` is the hook form
|
|
667
|
-
and returns a getter, like `useAnchor`.
|
|
668
|
-
|
|
669
|
-
`decorations={false}` writes `_MOTIF_WM_HINTS` — honoured by Mutter, KWin,
|
|
670
|
-
Xfwm, Openbox and i3. A window manager that ignores it simply decorates the
|
|
671
|
-
window; there is no way to force the matter.
|
|
492
|
+
Takes a ref to a `<window>`/`<popup>` or to **any drawn node** (resolved to
|
|
493
|
+
its window, so an out-of-flow `<box>` in the owner is a good handle), a raw
|
|
494
|
+
XID, `'root'` (this client's whole window group), or `null` to clear. A ref
|
|
495
|
+
still null at mount — an owner mounted as a sibling in the same commit — is
|
|
496
|
+
retried next frame.
|
|
497
|
+
|
|
498
|
+
Set it **with** `windowType="dialog"`. Under EWMH a managed transient with
|
|
499
|
+
no type already is a dialog (kept out of taskbar and pager), but a `dialog`
|
|
500
|
+
with no owner has nothing to stack above or iconify with. Stacking above
|
|
501
|
+
the owner and iconifying with it are window-manager policy (quartz-wm does
|
|
502
|
+
neither). **Inert on override-redirect windows** — every `<popup>` by
|
|
503
|
+
default — which the WM never manages; ntk warns if asked.
|
|
504
|
+
|
|
505
|
+
`windowIdOf(refOrInstance)` is the same resolution: the XID, or null. An
|
|
506
|
+
xdg-desktop-portal `parent_window` is `` `x11:${id.toString(16)}` `` —
|
|
507
|
+
lowercase hex, **no** `0x`. Today's portal backends tolerate a prefix, so
|
|
508
|
+
the mistake goes unnoticed; Qt's parser returns 0 on failure, which would
|
|
509
|
+
mean a silently unparented, non-modal dialog. `useWindowId(ref)` is the
|
|
510
|
+
hook form, returning a getter like `useAnchor`.
|
|
511
|
+
|
|
512
|
+
`decorations={false}` writes `_MOTIF_WM_HINTS` (Mutter, KWin, Xfwm,
|
|
513
|
+
Openbox, i3); a window manager that ignores it decorates anyway.
|
|
672
514
|
|
|
673
515
|
### Closing a window
|
|
674
516
|
|
|
675
|
-
Every window the window manager manages advertises `WM_DELETE_WINDOW
|
|
676
|
-
`
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
kill first. Advertising it is not an opt-in feature, it is the difference
|
|
681
|
-
between closing and crashing, so react-x11 does it for you.
|
|
682
|
-
|
|
683
|
-
Windows the WM does not frame never advertise it, because nothing would read
|
|
684
|
-
it: a child `<window>` (a region inside another window) and an
|
|
685
|
-
override-redirect `<popup>`. A [managed `<popup>`](#a-managed-popup-is-a-dialog)
|
|
686
|
-
is a real dialog and does.
|
|
687
|
-
|
|
688
|
-
What a close request _does_ is the part you can change:
|
|
517
|
+
Every window the window manager manages advertises `WM_DELETE_WINDOW`, with
|
|
518
|
+
or without `onCloseRequest` — otherwise the close button would kill the
|
|
519
|
+
connection mid-frame, skipping effect cleanup. Windows the WM does not
|
|
520
|
+
frame (a child `<window>`, an override-redirect `<popup>`) do not; a
|
|
521
|
+
[managed `<popup>`](#a-managed-popup-is-a-dialog) does.
|
|
689
522
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
523
|
+
- **No `onCloseRequest`**: the **primary window** unmounts the tree and
|
|
524
|
+
closes the connection — effects clean up and the process ends on a drained
|
|
525
|
+
loop. Any other window refuses, with a dev warning.
|
|
526
|
+
- **`onCloseRequest`**: only your handler runs — `setOpen(false)` for a
|
|
527
|
+
dialog, a "save your work?" prompt, `root.unmount()` to quit.
|
|
694
528
|
|
|
695
|
-
The primary window is
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
The refusal for secondary windows is deliberate. A `{open && <window/>}` was
|
|
701
|
-
opened by a `setOpen(true)` somewhere, and closing it behind React's back
|
|
702
|
-
would leave a window the app still believes is open and can never reopen. So
|
|
703
|
-
a dialog wants a handler:
|
|
529
|
+
The primary window is the first top-level `<window>` that is not
|
|
530
|
+
`transientFor` another and has no `windowType`; a lone window is primary
|
|
531
|
+
whatever its type. Secondary windows refuse because closing a
|
|
532
|
+
`{open && <window/>}` behind React's back would leave a window the app
|
|
533
|
+
believes open and can never reopen. Give a dialog a handler:
|
|
704
534
|
|
|
705
535
|
```jsx
|
|
706
536
|
{
|
|
@@ -717,15 +547,13 @@ a dialog wants a handler:
|
|
|
717
547
|
### Window state
|
|
718
548
|
|
|
719
549
|
`states` is EWMH `_NET_WM_STATE`: `modal`, `sticky`, `maximized` (or
|
|
720
|
-
`maximized_vert`/`maximized_horz`
|
|
721
|
-
`
|
|
722
|
-
`
|
|
723
|
-
|
|
724
|
-
`
|
|
725
|
-
`
|
|
726
|
-
|
|
727
|
-
is activated, stopping when the state is removed ([desktop.md](desktop.md));
|
|
728
|
-
the rest have no AppKit equivalent and are inert there.
|
|
550
|
+
`maximized_vert`/`maximized_horz`), `shaded`, `skip_taskbar`, `skip_pager`,
|
|
551
|
+
`hidden`, `fullscreen`, `above`, `below`, `demands_attention`, `focused`.
|
|
552
|
+
`fullscreen` and `alwaysOnTop` are sugar for `fullscreen` and `above`,
|
|
553
|
+
unioned with `states`. On the [Cocoa backend](macos.md) `fullscreen` is the
|
|
554
|
+
full-screen Space, `maximized` the zoom, `hidden` the Dock, and
|
|
555
|
+
`demands_attention` bounces the Dock icon until the app is activated or the
|
|
556
|
+
state removed ([desktop.md](desktop.md)); the rest are inert.
|
|
729
557
|
|
|
730
558
|
```jsx
|
|
731
559
|
<window
|
|
@@ -735,29 +563,20 @@ the rest have no AppKit equivalent and are inert there.
|
|
|
735
563
|
/>
|
|
736
564
|
```
|
|
737
565
|
|
|
738
|
-
**
|
|
739
|
-
the window manager changes
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
window without a handler costs nothing.
|
|
747
|
-
|
|
748
|
-
States are applied **before the window is mapped**, which is what EWMH 7.7
|
|
749
|
-
requires and the only way to open already fullscreen instead of flashing at
|
|
750
|
-
the normal size first.
|
|
751
|
-
|
|
752
|
-
`alwaysOnTop` falls back to Apple-WM window levels on XQuartz, where
|
|
753
|
-
quartz-wm does not support `_NET_WM_STATE_ABOVE`.
|
|
566
|
+
**Controlled, and diffed against the previous props** — not the window's
|
|
567
|
+
current state, which the window manager changes behind the app's back
|
|
568
|
+
(maximize, a hotkey leaving fullscreen, a tiling WM). A state is re-sent
|
|
569
|
+
only when the app changes it, so a WM that disagreed is not fought.
|
|
570
|
+
`onStatesChange` reports what the WM did; without it the property is not
|
|
571
|
+
watched. States are applied **before mapping**, so a window opens fullscreen
|
|
572
|
+
without flashing at normal size. On XQuartz, whose quartz-wm lacks
|
|
573
|
+
`_NET_WM_STATE_ABOVE`, `alwaysOnTop` uses Apple-WM window levels.
|
|
754
574
|
|
|
755
575
|
### `onClientMessage` — speaking a protocol of your own
|
|
756
576
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
every one of them that was addressed to this window.
|
|
577
|
+
ClientMessage carries nearly every X11 convention — EWMH requests, XEmbed,
|
|
578
|
+
the system tray, protocols between copies of one app. `onClientMessage`
|
|
579
|
+
gets every one addressed to this window:
|
|
761
580
|
|
|
762
581
|
```jsx
|
|
763
582
|
<window
|
|
@@ -768,50 +587,33 @@ every one of them that was addressed to this window.
|
|
|
768
587
|
/>
|
|
769
588
|
```
|
|
770
589
|
|
|
771
|
-
`ev.messageType` is the atom's **name
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
- **Messages arrive in the order they were sent.** The chunked protocols
|
|
784
|
-
depend on it — `_NET_SYSTEM_TRAY_BEGIN_MESSAGE` and the
|
|
785
|
-
`_NET_SYSTEM_TRAY_MESSAGE_DATA` pieces after it reassemble by arrival order
|
|
786
|
-
and nothing else. Naming a type this connection has never seen costs one
|
|
787
|
-
`GetAtomName`, and everything behind it waits rather than overtaking it.
|
|
788
|
-
`messageType` is therefore `null` only for an atom the **server** does not
|
|
789
|
-
know, which is a broken sender.
|
|
790
|
-
- **It is scoped to the window the message names**, not to the connection —
|
|
791
|
-
that is the whole difference from `useApp().X.on('event')`, which sees
|
|
792
|
-
every event for every window and outlives the element. A message aimed at
|
|
793
|
-
another window, including the EWMH ones sent to the root, does not arrive
|
|
794
|
-
here; a window manager wants the raw stream for those
|
|
590
|
+
`ev.messageType` is the atom's **name** (no interning needed), `ev.atom` its
|
|
591
|
+
id, `ev.format` 8, 16 or 32, `ev.data` the payload at that width — 5, 10 or
|
|
592
|
+
20 values. No setup; a window without the prop pays nothing.
|
|
593
|
+
|
|
594
|
+
- **Order is preserved**, as chunked protocols need
|
|
595
|
+
(`_NET_SYSTEM_TRAY_BEGIN_MESSAGE` then `_NET_SYSTEM_TRAY_MESSAGE_DATA`).
|
|
596
|
+
`messageType` is `null` only for an atom the **server** does not know — a
|
|
597
|
+
broken sender.
|
|
598
|
+
- **Scoped to the window the message names**, unlike
|
|
599
|
+
`useApp().X.on('event')`, which sees every window's events and outlives
|
|
600
|
+
the element. Messages to other windows, the EWMH ones sent to the root
|
|
601
|
+
included, do not arrive; a window manager wants the raw stream
|
|
795
602
|
(`examples/wm-core.js`).
|
|
796
|
-
- **`preventDefault()` stops react-x11
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
application's window pass an event mask of `0`.
|
|
806
|
-
|
|
807
|
-
Selections are the other half of most of these protocols, and taking one
|
|
808
|
-
needs a timestamp rather than `CurrentTime`:
|
|
603
|
+
- **`preventDefault()` stops react-x11's own handling** — today XDND, for a
|
|
604
|
+
window speaking the drag protocol itself. The close button is
|
|
605
|
+
`onCloseRequest`'s.
|
|
606
|
+
|
|
607
|
+
Send with `useApp().X.SendClientMessage(destination, aboutWindow, atom,
|
|
608
|
+
format, data, eventMask)`, or ntk's `window.sendClientMessage(name, data)`,
|
|
609
|
+
which interns the atom. EWMH messages _about_ a window go to the **root**;
|
|
610
|
+
to another application's window pass event mask `0`. Taking a selection
|
|
611
|
+
needs a timestamp, not `CurrentTime`:
|
|
809
612
|
[`lastInputTime` and `serverTime`](clipboard.md#owning-a-selection-of-your-own).
|
|
810
613
|
|
|
811
614
|
### Kiosk
|
|
812
615
|
|
|
813
|
-
|
|
814
|
-
no decoration, no taskbar entry, nothing above it, and no pointer:
|
|
616
|
+
No `kiosk` prop — it is a combination:
|
|
815
617
|
|
|
816
618
|
```jsx
|
|
817
619
|
<window
|
|
@@ -822,16 +624,15 @@ no decoration, no taskbar entry, nothing above it, and no pointer:
|
|
|
822
624
|
/>
|
|
823
625
|
```
|
|
824
626
|
|
|
825
|
-
`cursor: 'none'` hides the pointer
|
|
826
|
-
|
|
827
|
-
cursor is — see [styling.md](styling.md).
|
|
627
|
+
`cursor: 'none'` hides the pointer; an unset `cursor` inherits the root
|
|
628
|
+
window's ([styling.md](styling.md)).
|
|
828
629
|
|
|
829
630
|
### `transparent` — rounded corners and translucency
|
|
830
631
|
|
|
831
|
-
`transparent` creates the window on a 32-bit ARGB visual
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
632
|
+
`transparent` creates the window on a 32-bit ARGB visual: what the tree
|
|
633
|
+
leaves unpainted stays empty, and a compositor blends it with what is
|
|
634
|
+
behind. With `borderRadius` in the window's own `style` that gives rounded
|
|
635
|
+
corners with an **antialiased** edge (alpha, not a 1-bit Shape mask):
|
|
835
636
|
|
|
836
637
|
```jsx
|
|
837
638
|
<popup
|
|
@@ -843,40 +644,20 @@ possible, and it is the main reason to reach for it:
|
|
|
843
644
|
</popup>
|
|
844
645
|
```
|
|
845
646
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
rather than the Shape extension's 1-bit mask — no `XShapeCombineMask`, no
|
|
851
|
-
jagged diagonal.
|
|
852
|
-
|
|
853
|
-
`backgroundColor` may be translucent, and leaving it unset gives a window
|
|
854
|
-
that is empty except for what the tree paints.
|
|
855
|
-
|
|
856
|
-
`borderRadius` on a window is meaningful **only** with `transparent`. On an
|
|
857
|
-
opaque window it is ignored: giving up the corners there would only expose
|
|
858
|
-
the server's white, which is worse than square.
|
|
647
|
+
`backgroundColor` may be translucent; unset, the window is empty but for
|
|
648
|
+
what the tree paints. A window's `borderRadius` works **only** with
|
|
649
|
+
`transparent`; on an opaque window it is ignored (the corners would show
|
|
650
|
+
the server's white).
|
|
859
651
|
|
|
860
652
|
### When transparency is not available
|
|
861
653
|
|
|
862
|
-
**On the Cocoa backend it always is
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
compositor the X server shows the raw pixels, and a corner you painted away
|
|
870
|
-
is not transparent — it is **black**.
|
|
871
|
-
|
|
872
|
-
react-x11 never lets that happen. When transparency would not actually be
|
|
873
|
-
seen, the window is filled edge to edge and `borderRadius` on it is ignored:
|
|
874
|
-
you get the square opaque popup, not a black-cornered one. A translucent
|
|
875
|
-
`backgroundColor` is flattened rather than composited onto the last frame.
|
|
876
|
-
Nothing is required of the application for that floor to hold.
|
|
877
|
-
|
|
878
|
-
What the application _does_ control is the design on the other side of it,
|
|
879
|
-
through the `'@supports transparency'` style block:
|
|
654
|
+
**On the Cocoa backend it always is.** On X11 it needs a **depth-32
|
|
655
|
+
TrueColor visual** (XQuartz has none) and a **running compositor** (Mutter,
|
|
656
|
+
KWin, picom, …); without one, painted-away corners show **black**.
|
|
657
|
+
react-x11 prevents that: where transparency would not be seen, the window
|
|
658
|
+
is filled edge to edge, its `borderRadius` ignored and a translucent
|
|
659
|
+
`backgroundColor` flattened — square and opaque, with nothing asked of the
|
|
660
|
+
app. Design the other side with `'@supports transparency'`:
|
|
880
661
|
|
|
881
662
|
```jsx
|
|
882
663
|
<popup
|
|
@@ -891,62 +672,49 @@ through the `'@supports transparency'` style block:
|
|
|
891
672
|
/>
|
|
892
673
|
```
|
|
893
674
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
the window paints can.
|
|
900
|
-
|
|
901
|
-
For decisions that are not styling, `useSupports('transparency')` answers
|
|
902
|
-
the same question about the display, and can be asked before any window
|
|
903
|
-
exists:
|
|
675
|
+
The block ([Capability queries](styling.md#capability-queries)) is answered
|
|
676
|
+
per window and re-resolved live: a compositor started mid-session rounds the
|
|
677
|
+
popup without a remount, which is why `transparent` takes the 32-bit visual
|
|
678
|
+
even when nothing composites yet. `useSupports('transparency')` asks the
|
|
679
|
+
same of the display, before any window exists:
|
|
904
680
|
|
|
905
681
|
```jsx
|
|
906
682
|
const canBlend = useSupports('transparency');
|
|
907
683
|
const margin = canBlend ? 26 : 0; // room for a client-drawn shadow
|
|
908
684
|
```
|
|
909
685
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
desktop with it. See [debugging.md](debugging.md).
|
|
686
|
+
`REACT_X11_NO_TRANSPARENCY=1` shows the fallback on a compositing display:
|
|
687
|
+
`transparent` is ignored and both answer false
|
|
688
|
+
([debugging.md](debugging.md)). Toggling `transparent` on a mounted window
|
|
689
|
+
does nothing until it remounts (change its `key`): the visual is fixed at
|
|
690
|
+
creation.
|
|
916
691
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
Two things a transparent window does **not** change. Input still hits the
|
|
922
|
-
full rectangle — the corners are invisible, not click-through; use the Shape
|
|
923
|
-
extension's input region through the ntk ref if that matters. And child
|
|
924
|
-
`<window>`s are composited by the X server, not the compositor, so nesting a
|
|
925
|
-
transparent window inside another window blends against nothing.
|
|
692
|
+
Input still hits the full rectangle — corners are invisible, not
|
|
693
|
+
click-through (the Shape input region is reachable through the ntk ref).
|
|
694
|
+
Child `<window>`s are composited by the X server, not the compositor, so a
|
|
695
|
+
transparent window nested in another blends against nothing.
|
|
926
696
|
|
|
927
697
|
## `<popup>`
|
|
928
698
|
|
|
929
|
-
An override-redirect top-level window at **screen coordinates** —
|
|
930
|
-
window manager ignores
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
([below](#anchor--a-popup-that-places-itself)); `x`/`y`
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
|
941
|
-
|
|
|
942
|
-
| `
|
|
943
|
-
| `
|
|
944
|
-
| `
|
|
945
|
-
| `
|
|
946
|
-
| `
|
|
947
|
-
| `
|
|
948
|
-
| `overrideRedirect` | `false` makes it a WM-managed window instead — a real dialog (below) |
|
|
949
|
-
| `dragPreview` | this popup is a drag preview following the pointer, never a drop target — [drag-and-drop.md](drag-and-drop.md) |
|
|
699
|
+
An override-redirect top-level window at **screen coordinates** — menus,
|
|
700
|
+
tooltips, dropdowns — that the window manager ignores (no decorations, no
|
|
701
|
+
focus stealing). Its place in the JSX tree does not affect its place on
|
|
702
|
+
screen; it is its own paint and event root, and conditional rendering
|
|
703
|
+
controls its lifetime. `anchor` places it against a node
|
|
704
|
+
([below](#anchor--a-popup-that-places-itself)); `x`/`y` place it in screen
|
|
705
|
+
logical pixels, e.g. at `ev.screenX`/`screenY`. Same props as `<window>`,
|
|
706
|
+
**including [natural size](#natural-size)** — a menu sized by its rows.
|
|
707
|
+
|
|
708
|
+
| prop | |
|
|
709
|
+
| ------------------ | ------------------------------------------------------------------------------------------------ |
|
|
710
|
+
| `anchor` | place against a node, a rect inside one, or a screen rect, at the content's size |
|
|
711
|
+
| `grab` | hold a pointer grab while up — how menus behave on X (below) |
|
|
712
|
+
| `grabKeyboard` | take the keyboard while up — for a popover a tray click opened (below) |
|
|
713
|
+
| `transparent` | rounded corners and translucency ([above](#transparent--rounded-corners-and-translucency)) |
|
|
714
|
+
| `onDismiss` | a press landed outside the popup: close it |
|
|
715
|
+
| `trapFocus` | own a focus scope: a modal ([events.md](events.md#focus-scopes-modals)) |
|
|
716
|
+
| `overrideRedirect` | `false` makes it a WM-managed window — a real dialog (below) |
|
|
717
|
+
| `dragPreview` | a drag preview following the pointer, never a drop target ([drag-and-drop.md](drag-and-drop.md)) |
|
|
950
718
|
|
|
951
719
|
### `anchor` — a popup that places itself
|
|
952
720
|
|
|
@@ -973,82 +741,51 @@ function Completions({ editorRef, caret, matches, onPick }) {
|
|
|
973
741
|
```
|
|
974
742
|
|
|
975
743
|
That is the whole completion list: **at the caret**, as wide as its widest
|
|
976
|
-
label,
|
|
977
|
-
|
|
978
|
-
states a size, or names a position — `caret` is a rect the editor already
|
|
979
|
-
knows, in its own coordinates.
|
|
744
|
+
label, at most 220 tall and scrolling past that, flipped above the line near
|
|
745
|
+
the screen's bottom — no font measured, size stated or position named.
|
|
980
746
|
|
|
981
747
|
`anchor` takes [`anchorRect`'s options](components.md#useanchorref--anchorrectnode-options)
|
|
982
748
|
— `placement`, `align`, `offset`, `alignOffset`, `alignTo`, `at` — plus `to`,
|
|
983
|
-
the node (or a ref to one) it hangs off
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
**When the anchor scrolls out of view the popup unmaps** and comes back,
|
|
1017
|
-
where it belongs — and holding its `grab` again, since X drops a pointer
|
|
1018
|
-
grab whose window stops being viewable — when the anchor does. A popup is a real X window rather
|
|
1019
|
-
than an element its ancestors clip, so a caret that has scrolled out of the
|
|
1020
|
-
editor leaves a list floating over a document it no longer points into —
|
|
1021
|
-
and there is no position that fixes that. Whether it should _close_ instead
|
|
1022
|
-
is React state and therefore the application's: do the placement yourself
|
|
1023
|
-
with [`useAnchorTracking`](components.md#useanchorref--anchorrectnode-options)
|
|
1024
|
-
and its `onOutOfView`.
|
|
1025
|
-
|
|
1026
|
-
A popup never receives the X input focus, but nodes inside it can hold the
|
|
1027
|
-
**owner window's** focus and receive keys — with `trapFocus` and `autoFocus`
|
|
1028
|
-
that is a modal dialog. See
|
|
1029
|
-
[Focus inside a `<popup>`](events.md#focus-inside-a-popup). A popup that has
|
|
1030
|
-
no owner window to receive keys for it takes them itself with
|
|
1031
|
-
`grabKeyboard` ([below](#a-popover-under-a-tray-item)).
|
|
1032
|
-
|
|
1033
|
-
**`grab` is what makes a menu dismissable.** Without it, a press that lands
|
|
1034
|
-
anywhere else — another application, the root window, or even this app's own
|
|
1035
|
-
**window frame**, which belongs to the window manager — never reaches the
|
|
1036
|
-
client, so the menu stays open behind whatever was clicked. With the grab,
|
|
1037
|
-
that press is redirected to the popup instead, arrives outside its bounds,
|
|
1038
|
-
and `onDismiss` fires. The client's own windows still receive their own
|
|
1039
|
-
presses (X owner-events), so submenus and the owner window keep working
|
|
1040
|
-
normally, and only the root popup of a menu needs the grab. Needs
|
|
1041
|
-
ntk ≥ 3.7.0; on older ntk the popup behaves as it did before.
|
|
1042
|
-
|
|
1043
|
-
`Select`, `ContextMenu` and `MenuBar` already do this.
|
|
749
|
+
the node (or a ref to one) it hangs off; `x`/`y` are ignored while it is
|
|
750
|
+
set. The popup is measured, placed (flip and screen-edge clamp depend on its
|
|
751
|
+
size) and only then created, so it appears at the right size in the right
|
|
752
|
+
place. It then follows everything that can move either half: the anchor's
|
|
753
|
+
layout, an ancestor scrolling, the owner window moving, its own content
|
|
754
|
+
growing.
|
|
755
|
+
|
|
756
|
+
`at` is a rect **inside** the node, in its coordinates —
|
|
757
|
+
`{ x: caretX, y: lineTop, width: 1, height: lineHeight }` — read by the
|
|
758
|
+
flip, the alignment and `offset` as a small widget there would be; `{x, y}`
|
|
759
|
+
alone is a point.
|
|
760
|
+
|
|
761
|
+
An anchor ref not attached yet (written _above_ its trigger) is waited for,
|
|
762
|
+
one frame, rather than opening at the screen corner. **When the anchor
|
|
763
|
+
scrolls out of view the popup unmaps**, and returns — re-taking its
|
|
764
|
+
`grab` — with the anchor; a real window cannot be clipped by the anchor's
|
|
765
|
+
ancestors. To _close_ it instead, place it yourself with
|
|
766
|
+
[`useAnchorTracking`](components.md#useanchorref--anchorrectnode-options)'s
|
|
767
|
+
`onOutOfView`.
|
|
768
|
+
|
|
769
|
+
A popup never takes the X input focus, but nodes in it can hold the **owner
|
|
770
|
+
window's** focus — with `trapFocus` and `autoFocus` that is a modal
|
|
771
|
+
([Focus inside a `<popup>`](events.md#focus-inside-a-popup)). One with no
|
|
772
|
+
owner window takes keys with `grabKeyboard`
|
|
773
|
+
([below](#a-popover-under-a-tray-item)).
|
|
774
|
+
|
|
775
|
+
**`grab` makes a menu dismissable.** Without it, a press elsewhere —
|
|
776
|
+
another app, the root window, even this app's own window frame (the window
|
|
777
|
+
manager's) — never reaches the client, and the menu stays open. With it the
|
|
778
|
+
press is redirected to the popup, lands outside it, and `onDismiss` fires.
|
|
779
|
+
The app's own windows still get their presses, so submenus and the owner
|
|
780
|
+
keep working; only a menu's root popup needs the grab. `Select`,
|
|
781
|
+
`ContextMenu` and `MenuBar` do this.
|
|
1044
782
|
|
|
1045
783
|
### A popover under a tray item
|
|
1046
784
|
|
|
1047
|
-
A tray click has no node
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
on, and below the menu bar, since that is not usable area:
|
|
785
|
+
A tray click has no node: [`useTray`](desktop.md#the-tray)'s `onClick`
|
|
786
|
+
reports the item's screen frame. `anchor` takes it as `rect` instead of
|
|
787
|
+
`to`, with the same flip, clamp and natural size, kept on that monitor and
|
|
788
|
+
below the menu bar:
|
|
1052
789
|
|
|
1053
790
|
```jsx
|
|
1054
791
|
function MenuBarApp() {
|
|
@@ -1070,64 +807,28 @@ function MenuBarApp() {
|
|
|
1070
807
|
}
|
|
1071
808
|
```
|
|
1072
809
|
|
|
1073
|
-
`rect` is logical screen pixels,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
810
|
+
`rect` is logical screen pixels; `{ x, y }` alone is a point — all a
|
|
811
|
+
freedesktop tray reports. It moves only when replaced, and is never out of
|
|
812
|
+
view. Its monitor is the one the **whole rect** overlaps most (a menu-bar
|
|
813
|
+
item's frame starts above its display's top), else the nearest.
|
|
1077
814
|
|
|
1078
|
-
|
|
815
|
+
**`grabKeyboard`**: a menu-bar app has no window for keys to arrive
|
|
816
|
+
through, so the popup takes the keyboard while up:
|
|
1079
817
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
measured, centred at the size it measured and only then created.
|
|
818
|
+
| backend | what it is |
|
|
819
|
+
| ------- | ------------------------------------------------------------------------------- |
|
|
820
|
+
| X11 | a keyboard grab, taken and dropped with the map, as `grab` is for the pointer |
|
|
821
|
+
| macOS | a key-capable window at the popup level, activating the app — `NSPopover`'s way |
|
|
822
|
+
| Wayland | nothing more: a grabbing popup has the keyboard already |
|
|
1086
823
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
width={360}
|
|
1091
|
-
height="auto"
|
|
1092
|
-
>
|
|
1093
|
-
```
|
|
824
|
+
On macOS the window's kind is fixed at creation, so the prop does not change
|
|
825
|
+
on a mounted popup. The popover closes on `onBlur` there (another app takes
|
|
826
|
+
the key window) and on `onDismiss` on X; pass both.
|
|
1094
827
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
whatever the user was reading or about to press. A managed one grows from
|
|
1100
|
-
wherever the window manager or the user has put it since, and is moved only
|
|
1101
|
-
to keep it on the monitor. A new `rect` (or the `to` node moving) centres it
|
|
1102
|
-
again.
|
|
1103
|
-
|
|
1104
|
-
**The monitor comes from the whole rect**, not from a corner of it. A
|
|
1105
|
-
menu-bar item's frame starts a few points _above_ the top of the display it
|
|
1106
|
-
is on, so its top-left corner is outside that display — and on a desk where
|
|
1107
|
-
a taller neighbour reaches down past that edge, inside a different one. The
|
|
1108
|
-
monitor the rect overlaps most is the one it is on; a rect that overlaps
|
|
1109
|
-
none takes the nearest.
|
|
1110
|
-
|
|
1111
|
-
**`grabKeyboard` is the other half.** A menu-bar app has no window of its
|
|
1112
|
-
own, so the keys a popup's content would normally get through its owner
|
|
1113
|
-
window arrive nowhere. With it the popup takes the keyboard while it is up:
|
|
1114
|
-
|
|
1115
|
-
| backend | what it is |
|
|
1116
|
-
| ------- | ---------------------------------------------------------------------------------------------- |
|
|
1117
|
-
| X11 | a keyboard grab, taken and dropped with the map, as `grab` is for the pointer |
|
|
1118
|
-
| macOS | a window AppKit can make key, at the popup level, shown activating the app — `NSPopover`'s way |
|
|
1119
|
-
| Wayland | nothing more: a grabbing popup has the keyboard already |
|
|
1120
|
-
|
|
1121
|
-
On macOS the window's kind is decided when the popup is created, so the prop
|
|
1122
|
-
does not change on a mounted popup. There the popover closes on `onBlur` —
|
|
1123
|
-
clicking any other app takes the key window away — and on X on `onDismiss`,
|
|
1124
|
-
the press outside the pointer grab reports. Written with both, it closes the
|
|
1125
|
-
same way on either.
|
|
1126
|
-
|
|
1127
|
-
**Shortcuts in the popover work, with nothing passed.** A binding belongs to
|
|
1128
|
-
the tree's top-level `<window>`, and in an app that has none it belongs to
|
|
1129
|
-
the root-level `<popup>` that took the keyboard — this one, which is where
|
|
1130
|
-
the keys are arriving anyway:
|
|
828
|
+
**Shortcuts work with nothing passed**: a binding belongs to the top-level
|
|
829
|
+
`<window>`, or in an app without one, to the root-level `<popup>` holding
|
|
830
|
+
the keyboard. So do `useFileDialog`'s parent, the global menu and
|
|
831
|
+
`useWindowState`.
|
|
1131
832
|
|
|
1132
833
|
```jsx
|
|
1133
834
|
function Controls({ onClose, onToggle }) {
|
|
@@ -1137,75 +838,76 @@ function Controls({ onClose, onToggle }) {
|
|
|
1137
838
|
}
|
|
1138
839
|
```
|
|
1139
840
|
|
|
1140
|
-
|
|
1141
|
-
`
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
841
|
+
A binding with neither a window nor a popup above it — above the
|
|
842
|
+
`if (!open)`, say — never fires; development says so once, and
|
|
843
|
+
[`scope`](events.md#accelerators) anchors one at a node you name.
|
|
844
|
+
|
|
845
|
+
### Centred over something
|
|
846
|
+
|
|
847
|
+
`placement: 'center'` puts the popup **over** its anchor, middle on middle,
|
|
848
|
+
clamped to the monitor. Over a window's rect it is a dialog —
|
|
849
|
+
[`Dialog`](components.md#dialog) passes its owner's screen rect as `rect`:
|
|
850
|
+
|
|
851
|
+
```jsx
|
|
852
|
+
<popup
|
|
853
|
+
anchor={{ rect: ownerRect, placement: 'center' }}
|
|
854
|
+
width={360}
|
|
855
|
+
height="auto"
|
|
856
|
+
>
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
It is centred **once**: when its size changes later it keeps its top edge
|
|
860
|
+
and horizontal middle, moving only to stay on the monitor, so what the user
|
|
861
|
+
is reading does not jump. A managed one grows from wherever it was moved. A
|
|
862
|
+
new `rect`, or the `to` node moving, centres it again.
|
|
1145
863
|
|
|
1146
864
|
### A managed `<popup>` is a dialog
|
|
1147
865
|
|
|
1148
|
-
`overrideRedirect` defaults to `true`
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
`transientFor` that is a real dialog — above its owner, out of the taskbar
|
|
1153
|
-
and alt-tab list, minimising with the owner.
|
|
866
|
+
`overrideRedirect` defaults to `true` — what makes a menu a menu. `false`
|
|
867
|
+
hands the popup to the window manager: framed, titled, movable, with a
|
|
868
|
+
close button (`onCloseRequest`). With `transientFor` it is a real dialog,
|
|
869
|
+
above its owner, out of the taskbar and alt-tab, minimising with the owner.
|
|
1154
870
|
|
|
1155
871
|
```jsx
|
|
1156
872
|
<popup overrideRedirect={false} transientFor={anchor} windowType="dialog"
|
|
1157
873
|
title="Preferences" trapFocus>
|
|
1158
874
|
```
|
|
1159
875
|
|
|
1160
|
-
Turn `grab` off with it
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
The [`Dialog`](components.md#dialog) component does exactly this, and takes
|
|
1166
|
-
`managed={false}` to go back to the override-redirect shape.
|
|
876
|
+
Turn `grab` off with it — a grab swallows the press that starts a WM drag —
|
|
877
|
+
so a press outside no longer dismisses it, as befits a dialog.
|
|
878
|
+
[`Dialog`](components.md#dialog) does this; `managed={false}` returns to
|
|
879
|
+
override-redirect.
|
|
1167
880
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
`tooltip`. The hint is **additive** — override-
|
|
1173
|
-
redirect is what keeps the WM from moving or decorating the popup, and
|
|
1174
|
-
stays on. The EWMH spec asks for the type hint on override-redirect
|
|
1175
|
-
windows too, so compositing managers can give menus and tooltips
|
|
1176
|
-
consistent shadows and animations.
|
|
881
|
+
`windowType` defaults to `"popup_menu"`; name yours (`"tooltip"`,
|
|
882
|
+
`"dropdown_menu"`, … — `Select`'s sheet and `Tooltip` do). It is
|
|
883
|
+
**additive**: the popup stays override-redirect, and compositors use the
|
|
884
|
+
type for consistent shadows and animations.
|
|
1177
885
|
|
|
1178
886
|
## `<box>`
|
|
1179
887
|
|
|
1180
888
|
The flex container. All layout + paint + interaction props above.
|
|
1181
889
|
**Ref**: the retained node — `abs` (`{x, y, width, height}` within the
|
|
1182
|
-
window, valid after layout).
|
|
1183
|
-
|
|
1184
|
-
With `overflow: 'scroll'` it is also the **scroll container** — see below.
|
|
890
|
+
window, valid after layout). With `overflow: 'scroll'` it is also the
|
|
891
|
+
**scroll container**.
|
|
1185
892
|
|
|
1186
893
|
## Scrolling: `overflow: 'scroll'` {#scrolling}
|
|
1187
894
|
|
|
1188
|
-
There is no scrolling
|
|
1189
|
-
|
|
1190
|
-
|
|
895
|
+
There is no scrolling element: a `<box>` or `<window>` with
|
|
896
|
+
`overflow: 'scroll'` is a clipped viewport over its overflowing children, on
|
|
897
|
+
**both axes**:
|
|
1191
898
|
|
|
1192
899
|
```jsx
|
|
1193
900
|
<box style={{ overflow: 'scroll', flexGrow: 1 }}>{rows}</box>
|
|
1194
901
|
```
|
|
1195
902
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
scrolling without remounting, which is what makes `overflow={dense ?
|
|
1202
|
-
'scroll' : 'visible'}` cheap: the node, its children and their state all
|
|
1203
|
-
survive the change. When a box stops being a scroll container its offset
|
|
1204
|
-
resets to 0, as it does in CSS.
|
|
903
|
+
The wheel scrolls it; a draggable thumb is drawn on each overflowing axis.
|
|
904
|
+
`overflow: 'hidden'` clips without scrolling. Being a style, it toggles
|
|
905
|
+
without a remount (`overflow={dense ? 'scroll' : 'visible'}` keeps the node,
|
|
906
|
+
children and state); a box that stops scrolling resets its offset to 0, as
|
|
907
|
+
in CSS.
|
|
1205
908
|
|
|
1206
|
-
**It is
|
|
1207
|
-
|
|
1208
|
-
log, a long `<text>`, a rendered document) be read without a pointer at all:
|
|
909
|
+
**It is a tab stop and takes keys** whenever it has somewhere to scroll, so
|
|
910
|
+
unfocusable content (a log, a long `<text>`) is readable without a pointer:
|
|
1209
911
|
|
|
1210
912
|
| key | |
|
|
1211
913
|
| ------------------- | ------------------------------------ |
|
|
@@ -1214,46 +916,29 @@ log, a long `<text>`, a rendered document) be read without a pointer at all:
|
|
|
1214
916
|
| Space / Shift+Space | the same, for the hand already there |
|
|
1215
917
|
| Home / End | the top and the bottom |
|
|
1216
918
|
|
|
1217
|
-
One that fits its content is
|
|
1218
|
-
wheel
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
A pane that also has an `onClick` keeps Space for paging: paging is the
|
|
1225
|
-
pane's own default action and runs ahead of the keyboard activation Space
|
|
1226
|
-
would otherwise be, so such a pane pages with Space and activates with Enter
|
|
1227
|
-
([events.md](events.md#space-and-enter-are-a-click)). A focusable row _inside_
|
|
1228
|
-
a pane takes both keys and pages with neither — a default action runs on the
|
|
1229
|
-
focused node, and that is the row.
|
|
1230
|
-
|
|
1231
|
-
The bar belongs to the scroller, not to the content painted under it — the
|
|
1232
|
-
same rule a browser applies — so a press on the thumb never reaches the row
|
|
1233
|
-
behind it. Dragging keeps the grip where it was taken, so the thumb does not
|
|
1234
|
-
jump to the pointer, and a press on the track pages towards it, like
|
|
1235
|
-
PageUp/PageDown. A drag moves the pane by whole device pixels, as a wheel
|
|
1236
|
-
does ([extending.md](extending.md#scrolling-content-you-painted)), so the
|
|
1237
|
-
scroll stays a blit and nothing in the pane is drawn off the pixel grid.
|
|
1238
|
-
`<textarea>` behaves the same way, and there a bar press never moves the
|
|
1239
|
-
caret.
|
|
919
|
+
One that fits its content is no tab stop, takes no keys and no wheel: **the
|
|
920
|
+
wheel chains past it** to the next scroll container out. The keys are a
|
|
921
|
+
default action — your `onKeyDown` runs first, and `preventDefault()` cancels
|
|
922
|
+
them. A pane with an `onClick` pages with Space and activates with Enter
|
|
923
|
+
([events.md](events.md#space-and-enter-are-a-click)); a focusable row
|
|
924
|
+
_inside_ a pane takes both keys and pages with neither.
|
|
1240
925
|
|
|
1241
|
-
|
|
926
|
+
A press on the bar never reaches the row behind it. A thumb drag keeps its
|
|
927
|
+
grip; a track press pages towards it. Drags move by whole device pixels, as
|
|
928
|
+
the wheel does ([extending.md](extending.md#scrolling-content-you-painted)),
|
|
929
|
+
so nothing is drawn off the pixel grid. `<textarea>` behaves the same, and a
|
|
930
|
+
bar press there never moves the caret.
|
|
1242
931
|
|
|
1243
|
-
|
|
1244
|
-
scroll container does not have to restate them:
|
|
932
|
+
### The layout defaults it brings
|
|
1245
933
|
|
|
1246
934
|
| default | when |
|
|
1247
935
|
| ------------------------------ | ---------------------------------------- |
|
|
1248
936
|
| `flexBasis: 0` | `flexGrow > 0` and no `width` / `height` |
|
|
1249
937
|
| `minWidth: 0` / `minHeight: 0` | always |
|
|
1250
938
|
|
|
1251
|
-
`min-*: 0`
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
half of what `flex: 1` means. Without them a header/scroll/footer window
|
|
1255
|
-
grows past its own bounds as rows are added and the footer is pushed out of
|
|
1256
|
-
view. Set either yourself to opt out; an explicit value always wins.
|
|
939
|
+
`min-*: 0` lets a viewport be smaller than its content; the zero basis is
|
|
940
|
+
half of `flex: 1`. Without them a header/scroll/footer window grows past its
|
|
941
|
+
bounds and pushes the footer out of view. An explicit value always wins.
|
|
1257
942
|
|
|
1258
943
|
### Props
|
|
1259
944
|
|
|
@@ -1264,102 +949,72 @@ view. Set either yourself to opt out; an explicit value always wins.
|
|
|
1264
949
|
| `scrollbar={false}` | hide the drawn scrollbars |
|
|
1265
950
|
| `scrollbarColor` | thumb color |
|
|
1266
951
|
|
|
1267
|
-
**Ref**: the node, plus `scrollTo` / `scrollBy` / `scrollIntoView(node)`
|
|
1268
|
-
`scrollX` / `scrollY` / `contentWidth` /
|
|
1269
|
-
`useRef<ScrollableNode>(null)` types
|
|
1270
|
-
box you do not scroll.
|
|
1271
|
-
|
|
1272
|
-
`scrollTo(y)`
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
`canScroll(dx, dy)` answers whether there is room to move on the axis a delta
|
|
1277
|
-
names — what the wheel asks each node on its way out, and the one method an
|
|
1278
|
-
element of your own implements to be asked it too
|
|
952
|
+
**Ref**: the node, plus `scrollTo` / `scrollBy` / `scrollIntoView(node)` /
|
|
953
|
+
`canScroll(dx, dy)` and `scrollX` / `scrollY` / `contentWidth` /
|
|
954
|
+
`contentHeight`. In TypeScript, `useRef<ScrollableNode>(null)` types them;
|
|
955
|
+
`DrawnNode` still works for a box you do not scroll.
|
|
956
|
+
|
|
957
|
+
`scrollTo(y)` scrolls vertically; `scrollTo({x, y})` either axis, leaving an
|
|
958
|
+
omitted one; `scrollBy` likewise. `canScroll(dx, dy)` says whether there is
|
|
959
|
+
room on the axis a delta names — what the wheel asks each node on its way
|
|
960
|
+
out, and what your own element implements
|
|
1279
961
|
([extending.md](extending.md#scrolling-content-you-painted)).
|
|
1280
962
|
|
|
1281
|
-
Horizontal content comes from children that will not shrink
|
|
1282
|
-
fixed-width cells
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
A
|
|
1289
|
-
wheel
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
`
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
`
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
`
|
|
1308
|
-
in
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
- A pane no pass has laid out yet has no extent at all. Restoring a list's
|
|
1324
|
-
position from a mount-time `useLayoutEffect` shows the list at that
|
|
1325
|
-
position in its first frame.
|
|
1326
|
-
- Rows added in the commit the request comes from are not in the extent
|
|
1327
|
-
yet. A log that follows its new lines with `scrollTo(Infinity)` from a
|
|
1328
|
-
layout effect shows each one at the end, in the first frame that has it.
|
|
1329
|
-
|
|
1330
|
-
`Infinity` means the end, wherever the pass finds it, and the End key goes
|
|
1331
|
-
there the same way. Prefer it to `contentHeight`, which is the last pass's
|
|
1332
|
-
measurement too: a target computed from it falls short when a commit adds
|
|
1333
|
-
more than a viewport. A `scrollBy` made before that pass moves on from where
|
|
1334
|
-
the pass puts the pane. Until the pass, `scrollX`/`scrollY` read the call's
|
|
1335
|
-
answer, and `onScroll` reports the pass's once it is over.
|
|
1336
|
-
|
|
1337
|
-
`onScroll` fires for **every** move of the offsets, not only the ones a call
|
|
1338
|
-
made. The wheel, the keys, a bar and `scrollTo`/`scrollBy` on a laid-out
|
|
1339
|
-
pane report inside the call. Layout moves the offsets on its own, and a
|
|
1340
|
-
browser fires `scroll` for each: the held `scrollTo` above landing, a
|
|
1341
|
-
`scrollIntoView` resolving (focus landing on a row below the fold is one),
|
|
1342
|
-
and the clamp that pulls the offset back when the content shrinks or the
|
|
1343
|
-
viewport grows under it, as when rows are filtered out of a list scrolled
|
|
1344
|
-
to its end. Those report once the pass is over, like `onViewport`: after
|
|
1345
|
-
the frame that shows the new offset, and only if the pass moved it. The
|
|
1346
|
-
payload is read when the report is delivered, so a handler that mirrors the
|
|
1347
|
-
offset into state, as `Table` does, ends where the pane did.
|
|
963
|
+
Horizontal content comes from children that will not shrink (a row of
|
|
964
|
+
fixed-width cells). Its extent is measured **through the subtree**, like
|
|
965
|
+
`scrollWidth`: a row stretched to the viewport whose cells overflow it
|
|
966
|
+
still scrolls; a node clipping its own children ends the measurement. A
|
|
967
|
+
horizontal wheel axis scrolls sideways, as does **Shift + vertical
|
|
968
|
+
wheel**. With both bars, each stops short of the other's corner. Distances
|
|
969
|
+
are the device's: with XI2 a touchpad scrolls by the fraction of a notch it
|
|
970
|
+
measured. A wheel notch and an arrow key are 48 pixels
|
|
971
|
+
([events.md](events.md#wheel)).
|
|
972
|
+
|
|
973
|
+
`onViewport` fires from **layout**, so it arrives before anyone scrolls —
|
|
974
|
+
what a virtualized list (`Table`) needs to decide how many rows to build,
|
|
975
|
+
since an effect cannot read the size off the ref yet. `onLayout`
|
|
976
|
+
([Interaction props](#interaction-props)) is the same signal on any element,
|
|
977
|
+
without the content size.
|
|
978
|
+
|
|
979
|
+
`scrollIntoView(node)` scrolls the minimum on both axes to show a
|
|
980
|
+
descendant; one larger than the viewport gets CSSOM's `block: nearest`
|
|
981
|
+
(left alone if it covers the viewport, else brought in by the nearer edge).
|
|
982
|
+
It is safe right after the node mounts — it resolves on the next layout
|
|
983
|
+
pass — and a node already in view leaves the pane alone.
|
|
984
|
+
|
|
985
|
+
`scrollTo` answers inside the call from the last pass's extent: the offset
|
|
986
|
+
moves and `onScroll` fires before it returns. If a pass is owed, whatever
|
|
987
|
+
that extent could not satisfy is re-applied against the new content before
|
|
988
|
+
the frame paints, so restoring a position from a mount-time
|
|
989
|
+
`useLayoutEffect` shows it in the first frame, and `scrollTo(Infinity)` from
|
|
990
|
+
a layout effect in the commit that added rows shows the end in the first
|
|
991
|
+
frame that has them. `Infinity` means the end wherever the pass finds it
|
|
992
|
+
(as the End key does) — prefer it to `contentHeight`, the last pass's
|
|
993
|
+
measurement, which falls short when a commit adds more than a viewport. A
|
|
994
|
+
`scrollBy` before the pass continues from where the pass puts the pane.
|
|
995
|
+
Until then `scrollX`/`scrollY` read the call's answer; `onScroll` reports
|
|
996
|
+
the pass's afterwards.
|
|
997
|
+
|
|
998
|
+
`onScroll` fires for **every** offset change. Wheel, keys, bars and calls on
|
|
999
|
+
a laid-out pane report inside the call. Layout's own moves — a held
|
|
1000
|
+
`scrollTo` landing, `scrollIntoView` resolving (focus on a row below the
|
|
1001
|
+
fold), the clamp when content shrinks or the viewport grows — report after
|
|
1002
|
+
the frame that shows them, only if the offset moved. The payload is read at
|
|
1003
|
+
delivery, so a handler mirroring the offset into state ends where the pane
|
|
1004
|
+
did.
|
|
1348
1005
|
|
|
1349
1006
|
### On a `<window>`
|
|
1350
1007
|
|
|
1351
|
-
`<window style={{ overflow: 'scroll' }}>` scrolls the window's own content
|
|
1352
|
-
with
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
through the node in `onViewport`.
|
|
1008
|
+
`<window style={{ overflow: 'scroll' }}>` scrolls the window's own content
|
|
1009
|
+
with the same wheel, keys and bars; it stays a `frame` to a screen reader.
|
|
1010
|
+
Its ref is still ntk's `Window`, so reach the scroll API through a ref on a
|
|
1011
|
+
box inside it, or the node in `onViewport`.
|
|
1356
1012
|
|
|
1357
1013
|
## `<text>`
|
|
1358
1014
|
|
|
1359
|
-
Shaped, wrapped text
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
run list so wrapping spans the whole content.
|
|
1015
|
+
Shaped, wrapped text (bidi, ligatures, kerning, font fallback). Strings and
|
|
1016
|
+
numbers are legal only inside `<text>`; nested `<text>`s are **style spans**
|
|
1017
|
+
of one paragraph, wrapped as one.
|
|
1363
1018
|
|
|
1364
1019
|
| prop | |
|
|
1365
1020
|
| --------------------------------------------------- | ------------------------------------------------------ |
|
|
@@ -1376,72 +1031,51 @@ run list so wrapping spans the whole content.
|
|
|
1376
1031
|
| `textOverflow` | `clip` (default) or `ellipsis`, for what was cut |
|
|
1377
1032
|
|
|
1378
1033
|
The first four rows, `fontVariationSettings`, `textRendering`,
|
|
1379
|
-
`letterSpacing` and the two feature properties **inherit
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
[styling.md](styling.md#keeping-text-on-one-line-and-saying-when-it-did-not-fit).
|
|
1392
|
-
|
|
1393
|
-
A face the app supplies itself — one it ships, or one the user picked in a
|
|
1394
|
-
dialog — is opened with `loadFont`/`useFont`, which hand back the family
|
|
1395
|
-
name to put in `fontFamily`
|
|
1034
|
+
`letterSpacing` and the two feature properties **inherit**, into spans and
|
|
1035
|
+
across `<box>`es alike
|
|
1036
|
+
([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)); the
|
|
1037
|
+
rest belong to the `<text>` that owns the box.
|
|
1038
|
+
|
|
1039
|
+
`{ textWrap: 'nowrap', textOverflow: 'ellipsis' }` is one line ending in
|
|
1040
|
+
`…`; an ellipsis with no `maxLines` means one line. Only the pixels are cut
|
|
1041
|
+
— the accessible name, caret indices and `textContent()` keep the whole
|
|
1042
|
+
string
|
|
1043
|
+
([styling.md](styling.md#keeping-text-on-one-line-and-saying-when-it-did-not-fit)).
|
|
1044
|
+
A font file of the app's own opens with `loadFont`/`useFont`, which return
|
|
1045
|
+
the family name for `fontFamily`
|
|
1396
1046
|
([styling.md](styling.md#a-font-file-of-your-own)).
|
|
1397
1047
|
|
|
1398
|
-
`start
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
See [styling.md](styling.md#direction-and-the-logical-edges).
|
|
1048
|
+
`start`/`end` follow the box's **direction**, not the string's first strong
|
|
1049
|
+
character: the paragraph is laid out at that base level, so `"(12) files"`
|
|
1050
|
+
in an RTL panel punctuates as RTL, and text with no strong characters still
|
|
1051
|
+
lands on the right side
|
|
1052
|
+
([styling.md](styling.md#direction-and-the-logical-edges)).
|
|
1404
1053
|
|
|
1405
1054
|
### Variable fonts
|
|
1406
1055
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
`fontWeight` already drives its `wght` axis — so a family with one variable
|
|
1411
|
-
file behaves like a family with nine faces, and like nine hundred, since
|
|
1412
|
-
the weights between the named instances are the point of an axis:
|
|
1056
|
+
`fontWeight` drives a variable font's `wght` axis, so one file serves every
|
|
1057
|
+
weight, those between named instances included; `fontVariationSettings`
|
|
1058
|
+
sets other axes by tag:
|
|
1413
1059
|
|
|
1414
1060
|
```jsx
|
|
1415
1061
|
<text style={{ fontFamily: 'Inter', fontWeight: 460 }}>
|
|
1416
1062
|
Serious by default.
|
|
1417
1063
|
</text>
|
|
1418
|
-
```
|
|
1419
|
-
|
|
1420
|
-
`fontVariationSettings` is for the other axes, by OpenType tag:
|
|
1421
|
-
|
|
1422
|
-
```jsx
|
|
1423
1064
|
<text style={{ fontVariationSettings: { wdth: 87.5, slnt: -8 } }}>
|
|
1424
1065
|
condensed
|
|
1425
1066
|
</text>
|
|
1426
1067
|
```
|
|
1427
1068
|
|
|
1428
|
-
It inherits
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
ntk instantiates on demand and rasterizes only the glyphs actually drawn,
|
|
1435
|
-
at the size drawn; both its caches are bounded. What it does not do is
|
|
1436
|
-
quantize for you, so a slider bound straight to an axis with `step={1}`
|
|
1437
|
-
really will ask for hundreds of distinct faces as it is dragged — step the
|
|
1438
|
-
control, not the font. See ntk's `docs/fonts.md` for the whole picture,
|
|
1439
|
-
including why a variable `.woff2` cannot be instantiated and you want the
|
|
1440
|
-
`.ttf`.
|
|
1069
|
+
It inherits, and spans may override it. Unknown axes are ignored and values
|
|
1070
|
+
clamped, so it is safe on any face; it compares **by value**, so a literal
|
|
1071
|
+
is fine. Faces are instantiated on demand but never quantized: a slider
|
|
1072
|
+
bound to an axis with `step={1}` asks for hundreds of faces as it drags —
|
|
1073
|
+
step the control. Use the `.ttf`; a variable `.woff2` cannot be
|
|
1074
|
+
instantiated (ntk's `docs/fonts.md`).
|
|
1441
1075
|
|
|
1442
1076
|
### `textRendering`
|
|
1443
1077
|
|
|
1444
|
-
|
|
1078
|
+
CSS's property: which glyph path draws the text.
|
|
1445
1079
|
|
|
1446
1080
|
| value | |
|
|
1447
1081
|
| -------------------- | --------------------------------------------------------- |
|
|
@@ -1456,37 +1090,19 @@ _ntk ≥ 7.2.0._ CSS's property, picking which glyph path draws the text.
|
|
|
1456
1090
|
</text>
|
|
1457
1091
|
```
|
|
1458
1092
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
It inherits into spans and a span may override it, so a headline and the
|
|
1468
|
-
body text under it can take different paths in one paragraph.
|
|
1469
|
-
|
|
1470
|
-
**It never reflows.** Only draw-time rounding differs; ntk's layout measures
|
|
1471
|
-
byte-identically for every value, down to per-run offsets. Changing it drops
|
|
1472
|
-
the cached layout — the value rides on the spans inside it — and repaints,
|
|
1473
|
-
without marking anything dirty for yoga. So it is safe to flip on a state
|
|
1474
|
-
change without the tree moving.
|
|
1475
|
-
|
|
1476
|
-
The cost is real: the precise path rasterizes outlines on every draw and
|
|
1477
|
-
caches nothing. That is the right trade for text being animated and the
|
|
1478
|
-
wrong one for a paragraph that never changes, which is why `auto` is the
|
|
1479
|
-
default rather than the other way round.
|
|
1093
|
+
The cached path puts each glyph on a whole pixel, so animating `wght` makes
|
|
1094
|
+
single glyphs jump while neighbours stand still; `geometricPrecision`
|
|
1095
|
+
places them where shaping asked, and the line slides. It inherits (a span
|
|
1096
|
+
may override it) and **never reflows** — only draw-time rounding differs —
|
|
1097
|
+
so it is safe to flip on a state change. It rasterizes outlines on every
|
|
1098
|
+
draw and caches nothing: right for animated display text, wrong for a
|
|
1099
|
+
static paragraph, hence `auto`.
|
|
1480
1100
|
|
|
1481
1101
|
### Letter spacing and figures
|
|
1482
1102
|
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
faces — SF Pro, Inter — draw proportional figures by default, where a `1` is
|
|
1487
|
-
much narrower than an `8`, so a number that changes while you watch it
|
|
1488
|
-
changes width too, and a readout beside a slider shuffles its row on every
|
|
1489
|
-
value. Tabular figures hold it still:
|
|
1103
|
+
`fontVariantNumeric: 'tabular-nums'` gives every digit one width, so a
|
|
1104
|
+
changing number (a readout beside a slider) stops shuffling its row; most
|
|
1105
|
+
UI faces (SF Pro, Inter) default to proportional figures.
|
|
1490
1106
|
|
|
1491
1107
|
```jsx
|
|
1492
1108
|
<text style={{ fontSize: 30, fontVariantNumeric: 'tabular-nums' }}>
|
|
@@ -1494,26 +1110,24 @@ value. Tabular figures hold it still:
|
|
|
1494
1110
|
</text>
|
|
1495
1111
|
```
|
|
1496
1112
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
`slashed-zero` — several at once,
|
|
1500
|
-
`'tabular-nums slashed-zero'
|
|
1113
|
+
Keywords are CSS's: `lining-nums`/`oldstyle-nums`,
|
|
1114
|
+
`proportional-nums`/`tabular-nums`, `diagonal-fractions`/`stacked-fractions`,
|
|
1115
|
+
`ordinal`, `slashed-zero` — several at once, one per pair
|
|
1116
|
+
(`'tabular-nums slashed-zero'`); `'normal'` restores the face's own.
|
|
1501
1117
|
|
|
1502
|
-
`fontFeatureSettings`
|
|
1503
|
-
|
|
1504
|
-
|
|
1118
|
+
`fontFeatureSettings` takes any OpenType feature by tag: an array to turn
|
|
1119
|
+
on, or an object of tag to on, off or alternate index. It wins where both
|
|
1120
|
+
name a feature; missing features are ignored; it compares by value.
|
|
1505
1121
|
|
|
1506
1122
|
```jsx
|
|
1507
1123
|
<text style={{ fontFeatureSettings: ['tnum', 'zero'] }}>0.10</text>
|
|
1508
1124
|
<text style={{ fontFeatureSettings: { ss01: true, salt: 2 } }}>Rag</text>
|
|
1509
1125
|
```
|
|
1510
1126
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
`letterSpacing` is CSS's `letter-spacing`, in logical pixels: added after
|
|
1516
|
-
every character, the last on a line included, and negative to tighten.
|
|
1127
|
+
`letterSpacing` is CSS's, in logical pixels, after every character (the
|
|
1128
|
+
last on a line too); negative tightens. Spaced text drops the optional
|
|
1129
|
+
ligatures (`liga`, `clig`, `dlig`, `hlig`) — name one in
|
|
1130
|
+
`fontFeatureSettings` to keep it.
|
|
1517
1131
|
|
|
1518
1132
|
```jsx
|
|
1519
1133
|
<text style={{ fontSize: 11, fontWeight: 600, letterSpacing: 1.26 }}>
|
|
@@ -1521,37 +1135,24 @@ every character, the last on a line included, and negative to tighten.
|
|
|
1521
1135
|
</text>
|
|
1522
1136
|
```
|
|
1523
1137
|
|
|
1524
|
-
|
|
1525
|
-
`
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
All three inherit, and the two feature properties inherit apart, as CSS
|
|
1529
|
-
inherits them: a `fontVariantNumeric` below replaces the one above and
|
|
1530
|
-
leaves an inherited `fontFeatureSettings` alone. All three can move glyphs,
|
|
1531
|
-
so a change re-measures, and none of them may go in a state block.
|
|
1138
|
+
All three inherit, the two feature properties independently as in CSS (a
|
|
1139
|
+
`fontVariantNumeric` below replaces the one above, leaving an inherited
|
|
1140
|
+
`fontFeatureSettings` alone). All three re-measure on change, and none may
|
|
1141
|
+
go in a state block.
|
|
1532
1142
|
|
|
1533
1143
|
### Tiny text
|
|
1534
1144
|
|
|
1535
|
-
Text
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
untouched: the text is still shaped, the strip is as wide as the line. The
|
|
1542
|
-
size is logical because legibility is physical — a 5px label is the same
|
|
1543
|
-
size on a 2x panel as on a 1x monitor.
|
|
1544
|
-
|
|
1545
|
-
`createRoot({ textStripBelow })` moves the line, in logical pixels; `0`
|
|
1546
|
-
keeps glyphs at every size. `REACT_X11_TEXT_STRIP_BELOW` is the same line
|
|
1547
|
-
for a process that cannot reach `createRoot`.
|
|
1145
|
+
Text under **6 logical pixels** is painted as one strip of its ink per line
|
|
1146
|
+
instead of glyphs — in a minimap, a zoomed-out graph or a grid of thousands
|
|
1147
|
+
of cells, it is an illegible smudge anyway, and the strip costs one fill.
|
|
1148
|
+
Layout is unchanged, and the size is logical, so it holds on a 2x panel.
|
|
1149
|
+
`createRoot({ textStripBelow })` moves the threshold (`0` keeps glyphs at
|
|
1150
|
+
every size); `REACT_X11_TEXT_STRIP_BELOW` sets it without `createRoot`.
|
|
1548
1151
|
|
|
1549
1152
|
## Selecting text
|
|
1550
1153
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
a stray drag lighting up a button's caption is noise. Text becomes
|
|
1554
|
-
selectable when an element says so.
|
|
1154
|
+
Labels are not selectable, as `Gtk.Label` is not: most desktop text is
|
|
1155
|
+
chrome, and a stray drag lighting up a caption is noise. An element opts in:
|
|
1555
1156
|
|
|
1556
1157
|
```jsx
|
|
1557
1158
|
<box selectable style={{ padding: 12 }}>
|
|
@@ -1560,43 +1161,34 @@ selectable when an element says so.
|
|
|
1560
1161
|
</box>
|
|
1561
1162
|
```
|
|
1562
1163
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
on X11 ([clipboard.md](clipboard.md)). macOS has no PRIMARY, so there a
|
|
1569
|
-
release copies nothing and only a copy reaches the pasteboard
|
|
1164
|
+
It becomes a **surface**: a drag selects across all text under it, a double
|
|
1165
|
+
click takes a word, a triple click a block, Ctrl+A the surface, Ctrl+C
|
|
1166
|
+
copies. Releasing the button hands the text to **PRIMARY**, so a middle
|
|
1167
|
+
click in a terminal pastes it ([clipboard.md](clipboard.md)); macOS has no
|
|
1168
|
+
PRIMARY, so only a copy reaches the pasteboard
|
|
1570
1169
|
([clipboard.md](clipboard.md#primary-is-x11-only)).
|
|
1571
1170
|
|
|
1572
1171
|
### What is in a surface
|
|
1573
1172
|
|
|
1574
|
-
Everything under it that
|
|
1575
|
-
`<text
|
|
1576
|
-
[extending.md](extending.md#answering-for-your-own-text) — a terminal,
|
|
1577
|
-
view
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
What is **not** in it:
|
|
1173
|
+
Everything under it that answers for its own text, in document order:
|
|
1174
|
+
`<text>`, and any element implementing the accessors in
|
|
1175
|
+
[extending.md](extending.md#answering-for-your-own-text) — a terminal, log
|
|
1176
|
+
view or code editor from outside this package joins the same drag, with no
|
|
1177
|
+
registration. **Not** in it:
|
|
1581
1178
|
|
|
1582
|
-
- anything under
|
|
1583
|
-
|
|
1584
|
-
- `<textinput
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
their text through an ntk widget that exposes no character geometry to us.
|
|
1179
|
+
- anything under `selectable={false}` (CSS's `user-select: none`) — a
|
|
1180
|
+
list's bullets, a table's chrome, a caption inside a document;
|
|
1181
|
+
- `<textinput>`, `<textarea>` and any element declaring `hasOwnSelection` —
|
|
1182
|
+
they keep their own selection, and a press in one is theirs;
|
|
1183
|
+
- `<svg>`, and registered elements that draw text without exposing
|
|
1184
|
+
character geometry.
|
|
1589
1185
|
|
|
1590
1186
|
### What a copy assembles
|
|
1591
1187
|
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
pixels are joined with a **tab**; one that starts below the last is joined
|
|
1597
|
-
with a **newline**. For a table laid out as rows of cells that is exactly
|
|
1598
|
-
"cells with tabs, rows with newlines", and for ordinary prose it is one
|
|
1599
|
-
paragraph per line.
|
|
1188
|
+
Separators come from **layout**, not markup: text sharing a band of pixels
|
|
1189
|
+
is joined with a **tab**, text starting below with a **newline** — cells and
|
|
1190
|
+
rows for a table, a paragraph per line for prose. An item excluded with
|
|
1191
|
+
`selectable={false}` is absent from the copy as from the highlight.
|
|
1600
1192
|
|
|
1601
1193
|
```jsx
|
|
1602
1194
|
// copies as "name\tsize\nnotes\t4 kB"
|
|
@@ -1612,50 +1204,37 @@ paragraph per line.
|
|
|
1612
1204
|
</box>
|
|
1613
1205
|
```
|
|
1614
1206
|
|
|
1615
|
-
A list marker excluded with `selectable={false}` is absent from the copied
|
|
1616
|
-
text as well as from the highlight, which is the point of excluding it.
|
|
1617
|
-
|
|
1618
1207
|
### The props
|
|
1619
1208
|
|
|
1620
|
-
| prop |
|
|
1621
|
-
| ----------------------- |
|
|
1622
|
-
| `selectable` | `true` starts a surface; `false` opts a subtree out of the one above it
|
|
1623
|
-
| `selectionColor` |
|
|
1624
|
-
| `onSelectionChange(ev)` | `ev.text` is what a copy would
|
|
1209
|
+
| prop | |
|
|
1210
|
+
| ----------------------- | ----------------------------------------------------------------------------------- |
|
|
1211
|
+
| `selectable` | `true` starts a surface; `false` opts a subtree out of the one above it |
|
|
1212
|
+
| `selectionColor` | highlight behind selected text (default: theme's `selection`, a tint of its accent) |
|
|
1213
|
+
| `onSelectionChange(ev)` | `ev.text` is what a copy would take; `ev.isCollapsed` whether it is empty |
|
|
1625
1214
|
|
|
1626
|
-
|
|
1215
|
+
On the surface's node, through a ref: `selectAll()`, `clearSelection()`,
|
|
1627
1216
|
`selectedText()`, `setSelection(anchor, focus)` — each end
|
|
1628
|
-
`{ node, index }`, indices in code points — and `textSelection`, a snapshot
|
|
1629
|
-
`{ isCollapsed, text, ranges }`.
|
|
1217
|
+
`{ node, index }`, indices in code points — and `textSelection`, a snapshot
|
|
1218
|
+
of `{ isCollapsed, text, ranges }`.
|
|
1630
1219
|
|
|
1631
1220
|
### One selection, and where the keys go
|
|
1632
1221
|
|
|
1633
|
-
**
|
|
1634
|
-
a
|
|
1635
|
-
a field clears the document — the two never both claim to be showing the
|
|
1636
|
-
selection, which is a rule core holds rather than one every surface has to
|
|
1637
|
-
keep. That is also what makes PRIMARY honest: there is one selection on the
|
|
1638
|
-
display, so there is one here.
|
|
1639
|
-
|
|
1640
|
-
A surface is a **focus target**, because Ctrl+A and Ctrl+C are keystrokes
|
|
1641
|
-
and a keystroke has to arrive somewhere. That puts it in the Tab cycle;
|
|
1642
|
-
`tabIndex={-1}` takes it back out while leaving it focusable by a press —
|
|
1643
|
-
which is the right choice for a document inside a larger UI, and the wrong
|
|
1644
|
-
one for a reader whose window is the document.
|
|
1645
|
-
|
|
1646
|
-
The pointer shows an I-beam over a surface, the way it does over a field.
|
|
1647
|
-
`style={{ cursor: … }}` overrides it as usual.
|
|
1222
|
+
**One selection is visible per application**: selecting in a document
|
|
1223
|
+
collapses a field's highlight and vice versa — which keeps PRIMARY honest.
|
|
1648
1224
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1225
|
+
A surface is a **focus target** (Ctrl+A and Ctrl+C must arrive somewhere),
|
|
1226
|
+
so it is in the Tab cycle; `tabIndex={-1}` removes it but keeps it
|
|
1227
|
+
press-focusable — right for a document in a larger UI, wrong for a reader
|
|
1228
|
+
whose window is the document. The pointer is an I-beam over it unless
|
|
1229
|
+
`cursor` says otherwise. The selection reaches assistive technology through
|
|
1230
|
+
`<text>`'s AT-SPI text interface
|
|
1231
|
+
([accessibility.md](accessibility.md#text-controls)).
|
|
1652
1232
|
|
|
1653
1233
|
### The geometry underneath
|
|
1654
1234
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
test is how an app turns a click into a character offset:
|
|
1235
|
+
Four accessors on every drawn node build the selection, and are public — a
|
|
1236
|
+
caret rect is what an autocomplete popup anchors to
|
|
1237
|
+
(`<popup anchor={{ to, at }}>`), a hit test turns a click into an offset:
|
|
1659
1238
|
|
|
1660
1239
|
```js
|
|
1661
1240
|
const node = paragraphRef.current;
|
|
@@ -1665,14 +1244,11 @@ node.textCaretRect(14); // { x, y, width: 0, height }
|
|
|
1665
1244
|
node.textRangeRects(4, 14); // the bands a highlight over [4, 14) fills
|
|
1666
1245
|
```
|
|
1667
1246
|
|
|
1668
|
-
Indices are **code points**, not UTF-16 units
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
crosses from Latin into Arabic covers two separate stretches of pixels, and
|
|
1674
|
-
a single rectangle drawn between two caret positions would paint over text
|
|
1675
|
-
nobody selected.
|
|
1247
|
+
Indices are **code points**, not UTF-16 units. Rects are in the owning
|
|
1248
|
+
window's device pixels, like `abs` and `contentBox()`; mouse `x`/`y` are
|
|
1249
|
+
logical ([scale.md](scale.md)). `textRangeRects` returns a band per line
|
|
1250
|
+
_and_ per direction run, since a range crossing from Latin into Arabic
|
|
1251
|
+
covers separate stretches of pixels.
|
|
1676
1252
|
|
|
1677
1253
|
---
|
|
1678
1254
|
|
|
@@ -1680,36 +1256,30 @@ nobody selected.
|
|
|
1680
1256
|
|
|
1681
1257
|

|
|
1682
1258
|
|
|
1683
|
-
Single-line editable text
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
|
1688
|
-
|
|
|
1689
|
-
| `
|
|
1690
|
-
| `
|
|
1691
|
-
| `
|
|
1692
|
-
| `
|
|
1693
|
-
| `
|
|
1694
|
-
| `
|
|
1695
|
-
| `
|
|
1696
|
-
|
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
caret
|
|
1702
|
-
|
|
1703
|
-
at its own first strong character, which is what makes `"(1) 12:30 — نص"`
|
|
1704
|
-
punctuate the same way in a field and in the paragraph beside it. Clicking,
|
|
1705
|
-
the caret, the selection bands and the horizontal scroll are read from one
|
|
1706
|
-
placement, so they mirror together. See
|
|
1707
|
-
[styling.md](styling.md#inside-an-editable-field).
|
|
1259
|
+
Single-line editable text; caret and selection positions are exact across
|
|
1260
|
+
kerning, shaping and trailing whitespace.
|
|
1261
|
+
|
|
1262
|
+
| prop | |
|
|
1263
|
+
| --------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
1264
|
+
| `value` + `onChange(ev)` | controlled mode (display follows the prop) |
|
|
1265
|
+
| `defaultValue` | uncontrolled mode |
|
|
1266
|
+
| `onSubmit(ev)` | Enter/Shift+Enter; with Ctrl, Alt or Super, a shortcut's ([events.md](events.md#accelerators)) |
|
|
1267
|
+
| `name` | field name, echoed on the event |
|
|
1268
|
+
| `placeholder`, `placeholderColor` | shown when empty |
|
|
1269
|
+
| `maxLength` | code-point limit |
|
|
1270
|
+
| `selectionColor`, `caretColor` | selection/caret paint |
|
|
1271
|
+
| `inputMode` | IME hint: `numeric`, `decimal`, `tel`, `email`, `url` ([events.md](events.md#input-methods)) |
|
|
1272
|
+
| text style props | as `<text>` |
|
|
1273
|
+
|
|
1274
|
+
The field follows its box's **direction** as `<text>` does: value,
|
|
1275
|
+
placeholder and caret start at the base direction's start edge (the right
|
|
1276
|
+
under `direction: 'rtl'`), the value is shaped at that base level, and
|
|
1277
|
+
clicks, caret, selection and horizontal scroll mirror together
|
|
1278
|
+
([styling.md](styling.md#inside-an-editable-field)).
|
|
1708
1279
|
|
|
1709
1280
|
### The change event
|
|
1710
1281
|
|
|
1711
|
-
`onChange` and `onSubmit` get a synthetic event,
|
|
1712
|
-
handler in the library gets, rather than a bare string:
|
|
1282
|
+
`onChange` and `onSubmit` get a synthetic event, not a bare string:
|
|
1713
1283
|
|
|
1714
1284
|
```jsx
|
|
1715
1285
|
<textinput
|
|
@@ -1728,139 +1298,99 @@ handler in the library gets, rather than a bare string:
|
|
|
1728
1298
|
| `ev.nativeEvent` | the X key event, or **null** — see below |
|
|
1729
1299
|
| `preventDefault`, `stopPropagation` | present for uniformity; nothing reads them |
|
|
1730
1300
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
PRIMARY (X11 conventions, select-all included; [X11 only](clipboard.md#primary-is-x11-only)), **Ctrl+Z / Ctrl+Shift+Z** (Ctrl+Y too) to undo
|
|
1758
|
-
and redo, and a **right-click menu**. On the Cocoa backend the keys are the
|
|
1759
|
-
Mac's instead: the chords are ⌘ — ⌘A, ⌘C/X/V, ⌘Z / ⇧⌘Z — ⌥ moves and deletes
|
|
1760
|
-
by word, ⌘←/→ and ⌘↑/↓ go to the ends of the line and of the value, ⌘⌫
|
|
1761
|
-
deletes to the start of the line, ⌃A, ⌃E, ⌃K, ⌃Y and the rest of AppKit's
|
|
1762
|
-
Emacs keys work while any other ⌃ chord types nothing, and ⌥ types what the
|
|
1763
|
-
layout puts on a key, `ß` for ⌥S ([events.md](events.md#primary-modifier)).
|
|
1764
|
-
Focusable by default; shows the text cursor. `ev.preventDefault()` in your
|
|
1765
|
-
`onKeyDown`/`onMouseDown` suppresses the built-in editing behavior. To copy
|
|
1766
|
-
or paste from anywhere else — a canvas, a list, your own menu item — see
|
|
1767
|
-
[clipboard.md](clipboard.md).
|
|
1301
|
+
- **`ev.target.value` is the new value even when controlled**, before the
|
|
1302
|
+
parent re-renders — as in the DOM, which is why react-hook-form and formik
|
|
1303
|
+
work ([ecosystem/forms.md](ecosystem/forms.md)). `ev.target` is the live
|
|
1304
|
+
node, so read later it shows the current value; `ev.value` is a snapshot.
|
|
1305
|
+
- **`ev.nativeEvent` can be null** — a paste resolving, an undo or a
|
|
1306
|
+
pushed-back value has no X event. Guard it (downshift reads
|
|
1307
|
+
`nativeEvent.preventDownshiftDefault` unguarded).
|
|
1308
|
+
|
|
1309
|
+
`node.value = ''` sets the text without firing `onChange`, as on a DOM
|
|
1310
|
+
input — how react-hook-form's `register()` resets a field.
|
|
1311
|
+
|
|
1312
|
+
Keys: click/drag selection, double-click word, triple-click all, **dead
|
|
1313
|
+
keys and Compose** ([events.md](events.md#composition)), Backspace/Delete,
|
|
1314
|
+
arrows (+Shift extends), Ctrl+arrows and Ctrl+Backspace/Delete by word,
|
|
1315
|
+
Home/End, Ctrl+A, Ctrl+C/X/V on CLIPBOARD, middle-click paste from PRIMARY,
|
|
1316
|
+
selections (select-all included) owning PRIMARY
|
|
1317
|
+
([X11 only](clipboard.md#primary-is-x11-only)),
|
|
1318
|
+
**Ctrl+Z / Ctrl+Shift+Z** (and Ctrl+Y) to undo and redo, and a
|
|
1319
|
+
**right-click menu**. On Cocoa the keys are the Mac's: ⌘A, ⌘C/X/V,
|
|
1320
|
+
⌘Z/⇧⌘Z; ⌥ moves and deletes by word; ⌘←/→ and ⌘↑/↓ go to the ends of the
|
|
1321
|
+
line and the value; ⌘⌫ deletes to line start; ⌃A, ⌃E, ⌃K, ⌃Y and AppKit's
|
|
1322
|
+
other Emacs keys work while other ⌃ chords type nothing; ⌥ types what the
|
|
1323
|
+
layout puts on a key (`ß` for ⌥S) ([events.md](events.md#primary-modifier)).
|
|
1324
|
+
Focusable by default; shows the text cursor. `ev.preventDefault()` in
|
|
1325
|
+
`onKeyDown`/`onMouseDown` suppresses the built-in editing. Copying and
|
|
1326
|
+
pasting from elsewhere: [clipboard.md](clipboard.md).
|
|
1768
1327
|
|
|
1769
1328
|
### The right-click menu
|
|
1770
1329
|
|
|
1771
1330
|

|
|
1772
1331
|
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
**Paste** is greyed when nothing owns
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
a press anywhere outside closes. The selection stays visibly highlighted
|
|
1791
|
-
while the menu is up, even though the popup holds the keyboard.
|
|
1792
|
-
|
|
1793
|
-
To replace it with your own, set `contextMenu={false}` and render a
|
|
1794
|
-
`ContextMenu` — `onContextMenu` still fires. To suppress it for one event,
|
|
1795
|
-
call `ev.preventDefault()` in an `onContextMenu` handler.
|
|
1796
|
-
|
|
1797
|
-
The menu is not the field's private property: `openEditMenu(node, at,
|
|
1798
|
-
actions)` opens this one for anything that edits or selects text — a code
|
|
1799
|
-
editor, a rendered document with a selection — and `<textinput>`'s own menu
|
|
1800
|
-
is a caller of it, which is what keeps the two the same menu. See
|
|
1801
|
-
[extending.md](extending.md#the-standard-edit-menu).
|
|
1332
|
+
Undo / Redo / Cut / Copy / Paste / Select All, with no wiring — each row
|
|
1333
|
+
live only when it would act, each running its shortcut's code.
|
|
1334
|
+
Right-clicking **inside** a selection keeps it; outside, the caret moves
|
|
1335
|
+
there first. Arrows walk enabled rows, Enter chooses, Escape or a press
|
|
1336
|
+
outside closes; the selection stays highlighted meanwhile.
|
|
1337
|
+
|
|
1338
|
+
**Paste** is greyed when nothing owns CLIPBOARD — from a session's second
|
|
1339
|
+
menu on, since the field starts watching when its menu first opens; without
|
|
1340
|
+
XFixes it stays enabled. On macOS it tracks the pasteboard's change count, a
|
|
1341
|
+
quarter second behind another app's copy
|
|
1342
|
+
([clipboard.md](clipboard.md#knowing-when-it-changes)).
|
|
1343
|
+
|
|
1344
|
+
`contextMenu={false}` plus a `ContextMenu` of your own replaces it
|
|
1345
|
+
(`onContextMenu` still fires); `ev.preventDefault()` in `onContextMenu`
|
|
1346
|
+
suppresses it once. `openEditMenu(node, at, actions)` opens the same menu
|
|
1347
|
+
for anything else that edits or selects text
|
|
1348
|
+
([extending.md](extending.md#the-standard-edit-menu)).
|
|
1802
1349
|
|
|
1803
1350
|
### `sensitive`
|
|
1804
1351
|
|
|
1805
|
-
`<textinput sensitive>`
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
The first stops being visible when the field is hidden or the window closes;
|
|
1815
|
-
the second is readable by every client on the display until something else
|
|
1816
|
-
takes the selection, and a clipboard manager will have written it down.
|
|
1817
|
-
`PasswordInput` sets it on the input it shows while the secret is revealed —
|
|
1818
|
-
see [components.md](components.md#passwordinput). An input method is on the
|
|
1819
|
-
same side of the line: on Wayland a `sensitive` field is declared
|
|
1820
|
-
`sensitive_data` and none of its text is offered as surrounding text
|
|
1821
|
-
([events.md](events.md#input-methods)).
|
|
1352
|
+
`<textinput sensitive>`'s text **never reaches a selection**: Ctrl+C and the
|
|
1353
|
+
copy half of Ctrl+X do nothing, the menu has no Cut or Copy (absent, not
|
|
1354
|
+
greyed — greyed over a password looks like a bug), and selecting does not
|
|
1355
|
+
take PRIMARY. The caret, selection, arrows, undo and pasting _in_ are
|
|
1356
|
+
untouched. The clipboard, unlike the screen, is readable by every client on
|
|
1357
|
+
the display, and clipboard managers record it. `PasswordInput` sets it while
|
|
1358
|
+
the secret is revealed ([components.md](components.md#passwordinput)). On
|
|
1359
|
+
Wayland the field is also declared `sensitive_data` to the input method,
|
|
1360
|
+
with no surrounding text offered ([events.md](events.md#input-methods)).
|
|
1822
1361
|
|
|
1823
1362
|
**Ref**: the node, plus `value`, `undo()` / `redo()` and `canUndo` /
|
|
1824
1363
|
`canRedo` — enough for a toolbar button beside the field.
|
|
1825
1364
|
|
|
1826
1365
|
### Undo
|
|
1827
1366
|
|
|
1828
|
-
Consecutive typing
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
Undo is a stack of snapshots of the states the control has shown, capped
|
|
1842
|
-
at 200. A **controlled** `value` reports the restored text through
|
|
1843
|
-
`onChange` and waits for the prop to come back, exactly as typing does —
|
|
1844
|
-
so a parent that filters or rejects a value gets the same say over an undo
|
|
1845
|
-
that it has over an edit. A value changed from outside (a form reset)
|
|
1846
|
-
becomes its own entry, and undoing steps back through it.
|
|
1367
|
+
Consecutive typing is one undo step, so Ctrl+Z takes back a word, not a
|
|
1368
|
+
keystroke; runs of Backspace coalesce too. A run ends at whitespace, at
|
|
1369
|
+
anything moving the caret (arrows, Home/End, a click, focus leaving), and
|
|
1370
|
+
around edits that are their own step: a paste, a cut, a replaced selection,
|
|
1371
|
+
Ctrl+Backspace (⌥⌫, ⌘⌫ and ⌃K on a Mac), a `<textarea>` newline. Undo
|
|
1372
|
+
restores the selection and puts the caret where the undone edit started. A
|
|
1373
|
+
composed character is one entry: Ctrl+Z after `dead_acute` `e` steps over
|
|
1374
|
+
`é`.
|
|
1375
|
+
|
|
1376
|
+
The stack holds up to 200 snapshots. A **controlled** `value` reports the
|
|
1377
|
+
restored text through `onChange` and waits for the prop, as typing does, so
|
|
1378
|
+
a parent filtering values has the same say over an undo. A value changed
|
|
1379
|
+
from outside (a form reset) is its own entry.
|
|
1847
1380
|
|
|
1848
1381
|
## `<textarea>`
|
|
1849
1382
|
|
|
1850
|
-
Multi-line editable text on
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
[events.md](events.md#accelerators))
|
|
1855
|
-
Home/End go to the
|
|
1856
|
-
lines, and the view scrolls
|
|
1857
|
-
|
|
1858
|
-
the
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
The wheel reaches it through the same chain a `<box overflow="scroll">` is
|
|
1862
|
-
in, so a field whose text fits passes the gesture out to the pane or the
|
|
1863
|
-
window behind it rather than swallowing it.
|
|
1383
|
+
Multi-line editable text on `<textinput>`'s editing core. It word-wraps at
|
|
1384
|
+
the content width; Enter and Shift+Enter insert a newline; Ctrl+Enter (⌘↩
|
|
1385
|
+
on Cocoa) fires `onSubmit`, and with no `onSubmit` is left to a shortcut, as
|
|
1386
|
+
Alt+Enter and Super+Enter are except on Cocoa
|
|
1387
|
+
([events.md](events.md#accelerators)). Up/Down move between visual lines
|
|
1388
|
+
keeping a goal column, Home/End go to the ends of the visual line, selection
|
|
1389
|
+
spans lines, and the view scrolls to follow the caret (and with the wheel).
|
|
1390
|
+
On Cocoa ⌘←/→ are Home/End, ⌘↑/↓ go to the ends of the value and ⌥↑/↓ to
|
|
1391
|
+
the ends of a paragraph ([events.md](events.md#primary-modifier)). A field
|
|
1392
|
+
whose text fits passes the wheel out to the pane or window behind it, as a
|
|
1393
|
+
`<box overflow="scroll">` does.
|
|
1864
1394
|
|
|
1865
1395
|
| prop | |
|
|
1866
1396
|
| --------------- | -------------------------------------------------------------- |
|
|
@@ -1877,26 +1407,16 @@ window behind it rather than swallowing it.
|
|
|
1877
1407
|
| `cacheKey` | the source's identity, when `src` is re-derived per render |
|
|
1878
1408
|
| `alt` | the accessible name — what a screen reader says ([accessibility.md](accessibility.md)) |
|
|
1879
1409
|
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
`width`/`height` stated in the descriptor.
|
|
1410
|
+
`src`, `picture` and `drawable` are mutually exclusive; passing two throws.
|
|
1411
|
+
Size by `style`: with one dimension set the other follows the aspect ratio;
|
|
1412
|
+
with neither, the natural size (a server-side source's descriptor size),
|
|
1413
|
+
shrunk to the width on offer. `objectFit` places the picture, centred:
|
|
1414
|
+
`'fill'` (default) stretches, `'contain'` fits inside, `'cover'` fills and
|
|
1415
|
+
crops, `'none'` keeps its size, `'scale-down'` is the smaller of `'none'`
|
|
1416
|
+
and `'contain'`.
|
|
1888
1417
|
|
|
1889
1418
|
```jsx
|
|
1890
1419
|
<image src={photo} style={{ height: 64 }} /> // width follows the aspect ratio
|
|
1891
|
-
```
|
|
1892
|
-
|
|
1893
|
-
`objectFit` decides where the picture goes inside that box when the two
|
|
1894
|
-
shapes differ: `'fill'` — the default — stretches it to the box, `'contain'`
|
|
1895
|
-
fits it inside with the background around it, `'cover'` fills the box and
|
|
1896
|
-
cuts what overflows, `'none'` leaves it its own size and `'scale-down'` is
|
|
1897
|
-
the smaller of `'none'` and `'contain'`, all centred.
|
|
1898
|
-
|
|
1899
|
-
```jsx
|
|
1900
1420
|
<image src={photo} style={{ width: 96, height: 96, objectFit: 'cover' }} />
|
|
1901
1421
|
```
|
|
1902
1422
|
|
|
@@ -1909,24 +1429,20 @@ the smaller of `'none'` and `'contain'`, all centred.
|
|
|
1909
1429
|
<image src={ntkImage} /> // an ntk Image (or Surface)
|
|
1910
1430
|
```
|
|
1911
1431
|
|
|
1912
|
-
A
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
raw RGBA and an ntk `Image` are synchronous. Encoded bytes are synchronous
|
|
1918
|
-
where the decoder is (PNG and JPEG under Node) and land a moment later, the
|
|
1919
|
-
way a file does, where it is not: any format under Bun, and WebP elsewhere.
|
|
1432
|
+
A path (or `new URL('./logo.png', import.meta.url)`) decodes asynchronously:
|
|
1433
|
+
the element is 0×0 until it lands, then reflows — style a `width` and
|
|
1434
|
+
`height` to avoid the jump. Raw RGBA and an ntk `Image` are synchronous;
|
|
1435
|
+
encoded bytes are synchronous where the decoder is (PNG and JPEG under Node)
|
|
1436
|
+
and land a moment later otherwise (any format under Bun, WebP elsewhere).
|
|
1920
1437
|
Raw `data` is `width × height × 4` bytes of straight (non-premultiplied)
|
|
1921
|
-
RGBA
|
|
1922
|
-
|
|
1923
|
-
renderer cannot tell. That goes for encoded bytes too, which under Bun are
|
|
1438
|
+
RGBA, as `getImageData` returns. Sources are treated as immutable: pass a
|
|
1439
|
+
new object when the pixels change — encoded bytes too, which under Bun are
|
|
1924
1440
|
read on another thread after the commit.
|
|
1925
1441
|
|
|
1926
1442
|
#### Formats, and what decodes them
|
|
1927
1443
|
|
|
1928
|
-
The format comes from the bytes, never
|
|
1929
|
-
|
|
1444
|
+
The format comes from the bytes, never a file name; the decoder from the
|
|
1445
|
+
runtime:
|
|
1930
1446
|
|
|
1931
1447
|
| format | under Bun | under Node |
|
|
1932
1448
|
| ---------------- | -------------------------------------------------- | ------------------------------------------ |
|
|
@@ -1935,14 +1451,10 @@ reads them depends on the runtime:
|
|
|
1935
1451
|
| GIF, BMP | `Bun.Image`, off the JavaScript thread | not decoded — the error says to convert |
|
|
1936
1452
|
| TIFF, HEIC, AVIF | `Bun.Image` through ImageIO (macOS), WIC (Windows) | not decoded; nor under Bun on Linux |
|
|
1937
1453
|
|
|
1938
|
-
|
|
1939
|
-
JavaScript
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
between a stall the user feels and none. What it costs is that bytes under
|
|
1943
|
-
Bun land a frame or so late, like a file. Where that matters — a test that
|
|
1944
|
-
measures the element on its first frame — decode the bytes yourself and pass
|
|
1945
|
-
the result, which is used as-is on every runtime:
|
|
1454
|
+
Under Bun every format decodes on a worker thread (a 1080p JPEG costs the
|
|
1455
|
+
JavaScript thread ~110ms in jpeg-js), so bytes land a frame or so late, like
|
|
1456
|
+
a file. Where that matters — a test measuring the first frame — decode
|
|
1457
|
+
yourself; the result is used as-is everywhere:
|
|
1946
1458
|
|
|
1947
1459
|
```jsx
|
|
1948
1460
|
import { decodeImage } from 'ntk/image'; // PNG and JPEG, synchronous
|
|
@@ -1950,45 +1462,26 @@ import { decodeImage } from 'ntk/image'; // PNG and JPEG, synchronous
|
|
|
1950
1462
|
const logo = <image src={decodeImage(bytes)} />;
|
|
1951
1463
|
```
|
|
1952
1464
|
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
handing it over, or run under Bun.
|
|
1974
|
-
- **A format with no decoder says so**: GIF under Node, or HEIC under Bun on
|
|
1975
|
-
Linux, is logged with what to convert it to, and the element shows
|
|
1976
|
-
nothing, the same as a missing file.
|
|
1977
|
-
|
|
1978
|
-
Decoded pixels are uploaded to the server once and composited from there on
|
|
1979
|
-
every repaint (ntk's `Image` caches its upload per connection), so the cost
|
|
1980
|
-
of an image is one `PutImage` at first paint, not one per frame.
|
|
1981
|
-
|
|
1982
|
-
An ntk `Image` is used as-is and never destroyed by the element — identity
|
|
1983
|
-
is yours, which is also the sharing idiom: one `Image` shown by many
|
|
1984
|
-
`<image>`s is one decode and one upload, however many places composite it.
|
|
1985
|
-
|
|
1986
|
-
On the Cocoa backend (macOS's default) the upload is a CoreGraphics bitmap
|
|
1987
|
-
rather than a pixmap — made once per `Image` at its first paint and
|
|
1988
|
-
composited, scaled, from there — so the cost is the same one upload. The
|
|
1989
|
-
element frees it with an `Image` it decoded; for an `Image` of yours it is
|
|
1990
|
-
freed when the `Image` is garbage-collected, since ntk's `destroy()` frees
|
|
1991
|
-
only its X copies.
|
|
1465
|
+
`decodeImageBytes` from `react-x11/ntk` is the same ladder for an element
|
|
1466
|
+
that draws its own images (a document's `<img>`, a CSS background): an
|
|
1467
|
+
`Image`, or a promise of one where decoding is async; it throws or rejects,
|
|
1468
|
+
naming what to convert to, where `<image>` would show nothing.
|
|
1469
|
+
|
|
1470
|
+
Between the runtimes:
|
|
1471
|
+
|
|
1472
|
+
- **Animations show their first frame** — animated WebP anywhere, GIF under
|
|
1473
|
+
Bun (Bun 1.4's libwebp cannot decode animated WebP; those go to the
|
|
1474
|
+
JavaScript decoder).
|
|
1475
|
+
- **JPEG EXIF orientation is applied under Bun, not Node**: rotate portrait
|
|
1476
|
+
phone photos first, or use Bun.
|
|
1477
|
+
- **No decoder, no picture**: GIF under Node or HEIC under Bun on Linux
|
|
1478
|
+
logs what to convert to and shows nothing, like a missing file.
|
|
1479
|
+
|
|
1480
|
+
Pixels are uploaded once and composited every repaint. An ntk `Image` is
|
|
1481
|
+
used as-is and never destroyed by the element, so one `Image` in many
|
|
1482
|
+
`<image>`s is one decode and one upload. On Cocoa the element frees its
|
|
1483
|
+
upload of an `Image` it decoded; the upload of yours is freed when your
|
|
1484
|
+
`Image` is garbage-collected (ntk's `destroy()` frees only X copies).
|
|
1992
1485
|
|
|
1993
1486
|
### `src={{ symbol }}` — the platform's own icons
|
|
1994
1487
|
|
|
@@ -1999,22 +1492,19 @@ only its X copies.
|
|
|
1999
1492
|
</box>
|
|
2000
1493
|
```
|
|
2001
1494
|
|
|
2002
|
-
A name instead of pixels: the
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
and a menu item's `iconName` take:
|
|
1495
|
+
A name instead of pixels: the platform's icon in the **text colour** (what
|
|
1496
|
+
`currentColor` is to `<svg>`). The names are those `useTray`'s `icon` and a
|
|
1497
|
+
menu item's `iconName` take:
|
|
2006
1498
|
|
|
2007
|
-
| backend | names
|
|
2008
|
-
| ------------- |
|
|
2009
|
-
| macOS | SF Symbols, like `speaker.wave.3.fill`, through `@windowkit/appkit`
|
|
2010
|
-
| X11 / Wayland |
|
|
1499
|
+
| backend | names |
|
|
1500
|
+
| ------------- | ---------------------------------------------------------------------------------------------------- |
|
|
1501
|
+
| macOS | SF Symbols, like `speaker.wave.3.fill`, through `@windowkit/appkit` |
|
|
1502
|
+
| X11 / Wayland | freedesktop icon theme (the settings daemon's, else Adwaita, then hicolor), like `audio-volume-high` |
|
|
2011
1503
|
|
|
2012
|
-
|
|
2013
|
-
`fontSize` and `fontWeight`
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
square. Style the element a `width` and `height` of its own and the symbol is
|
|
2017
|
-
fitted into that box, centred. The source can say otherwise:
|
|
1504
|
+
**Sized like the surrounding text**: an SF Symbol takes the inherited
|
|
1505
|
+
`fontSize` and `fontWeight` as point size and weight; an icon theme is asked
|
|
1506
|
+
for the matching size (16 beside 14px text), square. A styled
|
|
1507
|
+
`width`/`height` fits it into that box, centred. Source keys:
|
|
2018
1508
|
|
|
2019
1509
|
| key | |
|
|
2020
1510
|
| --------------- | ------------------------------------------------------------------------------ |
|
|
@@ -2023,74 +1513,51 @@ fitted into that box, centred. The source can say otherwise:
|
|
|
2023
1513
|
| `scale` | `'small'`, `'medium'` (default) or `'large'`, beside the text. SF Symbols only |
|
|
2024
1514
|
| `variableValue` | 0 to 1, how much of a variable symbol shows. SF Symbols, macOS 13 and later |
|
|
2025
1515
|
|
|
2026
|
-
On Linux a `-symbolic` icon is preferred
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
**A name this desktop does not have takes no room and draws nothing**, and
|
|
2033
|
-
says so once in development. An app that runs on both systems picks its
|
|
2034
|
-
names by platform, as it does for the tray: the two icon sets do not share
|
|
2035
|
-
their names.
|
|
1516
|
+
On Linux a `-symbolic` icon is preferred and takes the text colour; a
|
|
1517
|
+
full-colour one shows as it is. Lookup follows the Icon Theme
|
|
1518
|
+
Specification, read at the first symbol. **An unknown name takes no room
|
|
1519
|
+
and draws nothing** (one dev warning). The two icon sets do not share
|
|
1520
|
+
names, so pick by platform.
|
|
2036
1521
|
|
|
2037
1522
|
### `cacheKey` — when the buffer is new but the picture is not
|
|
2038
1523
|
|
|
2039
|
-
A component
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
is re-decoded or re-uploaded — and two `<image>`s with one key share one
|
|
2045
|
-
decoded copy, freed when the last of them unmounts.
|
|
1524
|
+
A component re-deriving its bytes per render (decoding a stream, slicing a
|
|
1525
|
+
capture) passes a new `src` each time — a fresh decode and upload. A
|
|
1526
|
+
`cacheKey`, the `<canvas cacheKey>` contract for sources, vouches that the
|
|
1527
|
+
buffer is the same picture: nothing is redone, and `<image>`s sharing a key
|
|
1528
|
+
share one decoded copy, freed with the last.
|
|
2046
1529
|
|
|
2047
1530
|
```jsx
|
|
2048
1531
|
<image src={frame.rgba} cacheKey={`frame:${frame.serial}`} />
|
|
2049
1532
|
```
|
|
2050
1533
|
|
|
2051
|
-
The key must name the content
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
consulted for an ntk `Image` (the object is its own identity), and is
|
|
2055
|
-
refused with `picture`/`drawable` (there is nothing client-side to cache).
|
|
1534
|
+
The key must name the content, or stale pixels show (development warns
|
|
1535
|
+
when two sizes collide under one key). Ignored for an ntk `Image`; refused
|
|
1536
|
+
with `picture`/`drawable`.
|
|
2056
1537
|
|
|
2057
1538
|
### `picture` / `drawable` — pixels the server already has
|
|
2058
1539
|
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
composite straight from the existing resource: one `RenderComposite` into
|
|
2063
|
-
the window, no `PutImage`, no round trip.
|
|
1540
|
+
Content already on the X server — an offscreen pixmap, `NameWindowPixmap`
|
|
1541
|
+
from Composite, a cached tile — composites straight from that resource,
|
|
1542
|
+
with no readback, upload or round trip:
|
|
2064
1543
|
|
|
2065
1544
|
```jsx
|
|
2066
1545
|
<image picture={{ id: pic, width: 64, height: 64 }} />
|
|
2067
1546
|
<image drawable={{ id: pixmap, width: 128, height: 96, depth: 32 }} />
|
|
2068
1547
|
```
|
|
2069
1548
|
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
it is composited through: 24 — the screen's default depth, what a window
|
|
2083
|
-
pixmap from Composite is — is the default, 32 is argb, and 8 composites as
|
|
2084
|
-
ink through its alpha, which is what previewing a mask looks like. A
|
|
2085
|
-
`picture` needs no depth: its format was fixed when it was created.
|
|
2086
|
-
|
|
2087
|
-
Both name X server resources, so both are X11-only: the Cocoa backend has no
|
|
2088
|
-
X server to composite from, and there they draw nothing (development warns
|
|
2089
|
-
once). Hand `src` the pixels instead.
|
|
2090
|
-
|
|
2091
|
-
Both descriptors also accept the richer objects that already carry these
|
|
2092
|
-
fields — an ntk `Pixmap` is `{ id, width, height, depth }` and goes straight
|
|
2093
|
-
in as `drawable`.
|
|
1549
|
+
You state the size (asking would be a round trip); descriptors compare by
|
|
1550
|
+
value. Scaling is server-side and bilinear; drawing a `picture` scaled sets
|
|
1551
|
+
its transform and filter for the composite and resets them to
|
|
1552
|
+
identity/nearest after, so a picture with a transform of its own should be
|
|
1553
|
+
drawn 1:1. `drawable` wraps the pixmap or window in a Picture the element
|
|
1554
|
+
owns (freed on unmount; the drawable stays yours); `depth` is 24 (default,
|
|
1555
|
+
the screen's and a Composite window pixmap's), 32 for argb, or 8, which
|
|
1556
|
+
composites as ink through its alpha (a mask preview); a `picture`'s format
|
|
1557
|
+
was fixed when it was created. Both accept richer objects with those
|
|
1558
|
+
fields — an ntk `Pixmap` goes straight in as `drawable`. Both are
|
|
1559
|
+
**X11-only**: on Cocoa they draw nothing (one dev warning); pass pixels to
|
|
1560
|
+
`src`.
|
|
2094
1561
|
|
|
2095
1562
|
## `<video>` {#video}
|
|
2096
1563
|
|
|
@@ -2101,32 +1568,29 @@ useEffect(() => startDecoder(file, frames), [frames, file]);
|
|
|
2101
1568
|
<video frames={frames} style={{ width: '100%', backgroundColor: '#000' }} />;
|
|
2102
1569
|
```
|
|
2103
1570
|
|
|
2104
|
-
| prop |
|
|
2105
|
-
| ----------------------------------------------------- |
|
|
2106
|
-
| `frames` | a `VideoFrames` sink
|
|
2107
|
-
| `src` | a path or URL the platform plays
|
|
2108
|
-
| `poster` | an `<image src>` value, shown
|
|
2109
|
-
| `autoPlay`, `loop`, `muted`, `volume`, `playbackRate` | HTML's names and defaults — nothing plays until asked; for `src`
|
|
2110
|
-
| `paused` | controlled when present; for `src`
|
|
2111
|
-
| `onLoadedMetadata` | `{ width, height, duration }` once the
|
|
2112
|
-
| `onPlay`, `onPause`, `onEnded`, `onTimeUpdate` | for `src
|
|
2113
|
-
| `onError` | the source failed, or
|
|
2114
|
-
|
|
2115
|
-
`frames` and `src` are exclusive
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
style that names one axis gets the other from the picture's ratio, larger
|
|
2123
|
-
than the stream as well as smaller. `objectFit` is `'contain'` unless the
|
|
2124
|
-
style says otherwise, and `backgroundColor` is what shows in the bars.
|
|
1571
|
+
| prop | |
|
|
1572
|
+
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
1573
|
+
| `frames` | a `VideoFrames` sink you push decoded frames into — every backend |
|
|
1574
|
+
| `src` | a path or URL the platform plays, where `useSupports('mediaPlayback')` (below) |
|
|
1575
|
+
| `poster` | an `<image src>` value, shown while there is no frame |
|
|
1576
|
+
| `autoPlay`, `loop`, `muted`, `volume`, `playbackRate` | HTML's names and defaults — nothing plays until asked; for `src` |
|
|
1577
|
+
| `paused` | controlled when present; for `src` |
|
|
1578
|
+
| `onLoadedMetadata` | `{ width, height, duration }` once the size is known (a sink: on mount, `duration: Infinity`) |
|
|
1579
|
+
| `onPlay`, `onPause`, `onEnded`, `onTimeUpdate` | for `src` |
|
|
1580
|
+
| `onError` | the source failed, or `src` has no player; unhandled, one console warning |
|
|
1581
|
+
|
|
1582
|
+
`frames` and `src` are exclusive; passing both throws. It is a drawn node like
|
|
1583
|
+
`<image>`, so a video in a form sits under its popups and inside its scroll
|
|
1584
|
+
panes, which a child window or `<glarea>` cannot. **Size** is HTML's: the
|
|
1585
|
+
stream's size in logical pixels (1280x720 is 1280 wide at any scale), else
|
|
1586
|
+
the poster's, else 300x150; one styled axis derives the other from the
|
|
1587
|
+
ratio. `objectFit` defaults to `'contain'`; `backgroundColor` fills the
|
|
1588
|
+
bars.
|
|
2125
1589
|
|
|
2126
1590
|
### `VideoFrames`, the sink
|
|
2127
1591
|
|
|
2128
|
-
react-x11 decodes nothing;
|
|
2129
|
-
|
|
1592
|
+
react-x11 decodes nothing; `useVideoFrames` / `createVideoFrames` make a
|
|
1593
|
+
sink, and every source ends in `push`:
|
|
2130
1594
|
|
|
2131
1595
|
```js
|
|
2132
1596
|
import { spawn } from 'node:child_process';
|
|
@@ -2158,61 +1622,60 @@ function startDecoder(file, frames) {
|
|
|
2158
1622
|
}
|
|
2159
1623
|
```
|
|
2160
1624
|
|
|
2161
|
-
- **Formats
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
- **`preferredFormats`** is
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
1625
|
+
- **Formats**: `'NV12'` (Y, then Cb/Cr pairs — hardware decoders),
|
|
1626
|
+
`'I420'` (Y, Cb, Cr — `yuv420p`, most software decoders) and `'BGRA'`
|
|
1627
|
+
(fourth byte ignored: frames are opaque). `colorSpace` (`'bt709'`
|
|
1628
|
+
default, `'bt601'`, `'bt2020'`) and `range` (`'video'` default, `'full'`)
|
|
1629
|
+
say what YCbCr numbers mean.
|
|
1630
|
+
- **`preferredFormats`** is the display's order, cheapest first:
|
|
1631
|
+
`NV12, I420, BGRA` where something native takes YCbCr as it is — a layer
|
|
1632
|
+
on macOS, a shader on Wayland, an Xv port on X11 — and `BGRA` where the
|
|
1633
|
+
frame is converted in JavaScript. On X11 it is `BGRA`, not kept, for the
|
|
1634
|
+
few round trips until the port answers after the first sink is made.
|
|
1635
|
+
Every format works everywhere.
|
|
1636
|
+
- **The newest frame is the frame**: a push replaces the last, shown or not;
|
|
1637
|
+
nothing queues, and the decoder's clock is the pacing. A push hands its
|
|
1638
|
+
planes over until the next push — they are read then, not copied at the
|
|
1639
|
+
call — so a decoder reusing one buffer pushes it again after writing.
|
|
2175
1640
|
- **`push(planes, { strides, time })`** takes a Buffer per plane or one
|
|
2176
|
-
Buffer
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
- **Nothing to free
|
|
2181
|
-
surfaces and lets them go when it stops showing it.
|
|
1641
|
+
Buffer back to back, with per-plane row bytes when padded; a plane too
|
|
1642
|
+
small is a `TypeError` naming it and the bytes needed. `close()` drops the
|
|
1643
|
+
frame and ignores later pushes, so a decoder racing an unmount loses
|
|
1644
|
+
quietly.
|
|
1645
|
+
- **Nothing to free**: a sink owns nothing native.
|
|
2182
1646
|
|
|
2183
1647
|
### Lifted, drawn, and what each costs
|
|
2184
1648
|
|
|
2185
|
-
On **macOS** a video
|
|
2186
|
-
|
|
2187
|
-
([macos.md](macos.md))
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
1649
|
+
On **macOS** a video goes on its own layer above the window's bitmap when
|
|
1650
|
+
nothing is painted over it, it is not fading and no rounded clipping
|
|
1651
|
+
ancestor holds it ([macos.md](macos.md)); then a `push` costs well under a
|
|
1652
|
+
millisecond and **no window frame**. Otherwise — a tooltip over it, a
|
|
1653
|
+
dialog's dim, a rounded card — it is **drawn**: a push claims the picture,
|
|
1654
|
+
and the next frame composites it in paint order, in the same colours.
|
|
1655
|
+
|
|
1656
|
+
On **X11, Wayland and Windows** it is always drawn, and what differs is how
|
|
1657
|
+
the frame reaches the element's surface:
|
|
1658
|
+
|
|
1659
|
+
- **Wayland**: planes as textures, the matrix in a shader — about 0.2ms of
|
|
1660
|
+
CPU for a 1080p NV12 frame, against 7.6ms converted in JavaScript.
|
|
1661
|
+
- **X11 with an Xv image adaptor** (glamor, under Xorg or Xwayland): the
|
|
1662
|
+
frame goes to the server as decoded, through shared memory, and its GPU
|
|
1663
|
+
converts it into a hidden window copied into the surface — 1.5ms a 1080p
|
|
1664
|
+
NV12 frame against 15ms. Full range, BT.2020, the first frame of a size
|
|
1665
|
+
and a window off screen go the JavaScript way.
|
|
1666
|
+
- **X11 without one, and Windows**: converted to RGBA in JavaScript unless
|
|
1667
|
+
BGRA (hence their preference); X11 uploads through shared memory where
|
|
1668
|
+
available.
|
|
1669
|
+
|
|
1670
|
+
No colour management is applied on any of them. `examples/video.jsx` plays
|
|
1671
|
+
a file through whichever path the display has.
|
|
2207
1672
|
|
|
2208
1673
|
### `src` — the platform's player
|
|
2209
1674
|
|
|
2210
|
-
Where `useSupports('mediaPlayback')` is true
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
([frame.md](frame.md#sizing-stacking-input)). A path, a `file://` URL and an
|
|
2215
|
-
`http(s)` one, HLS included, all play.
|
|
1675
|
+
Where `useSupports('mediaPlayback')` is true — macOS (AVFoundation), and a
|
|
1676
|
+
`<Frame>` pane whose host plays it ([frame.md](frame.md#sizing-stacking-input))
|
|
1677
|
+
— `src` is a path, `file://` or `http(s)` URL (HLS included), played with
|
|
1678
|
+
the platform's decoder, audio, seeking and streaming.
|
|
2216
1679
|
|
|
2217
1680
|
```jsx
|
|
2218
1681
|
const ref = useRef(null);
|
|
@@ -2230,28 +1693,20 @@ const ref = useRef(null);
|
|
|
2230
1693
|
<button onPress={() => ref.current.seek(30)} />;
|
|
2231
1694
|
```
|
|
2232
1695
|
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
colours. There is no `controls` attribute: the transport is your React,
|
|
2248
|
-
over or beside the video.
|
|
2249
|
-
|
|
2250
|
-
Where `useSupports('mediaPlayback')` is false — X11 and Wayland for good,
|
|
2251
|
-
Windows today, and macOS over an older bridge — the element reports one
|
|
2252
|
-
`NoMediaPlaybackError` (`code: 'ENOMEDIAPLAYBACK'`) through `onError`, on a
|
|
2253
|
-
microtask, and shows its poster: never a black box that looks like a video
|
|
2254
|
-
that has not started. Ask before rendering one:
|
|
1696
|
+
Laid out at 300x150 (or the poster's size) until ready, then at the clip's
|
|
1697
|
+
size from `onLoadedMetadata`. Prop changes go to the same player. The `ref`
|
|
1698
|
+
is HTMLMediaElement's imperative half: `play()` (restarts an ended item),
|
|
1699
|
+
`pause()`, `seek(seconds)` (exact, not keyframe), `currentTime`,
|
|
1700
|
+
`duration`, `paused`. `onPlay`/`onPause` fire as it starts and stops,
|
|
1701
|
+
`onTimeUpdate` four times a second while playing and after a seek,
|
|
1702
|
+
`onEnded` at the end of a non-looping item, `onError` when the item fails.
|
|
1703
|
+
Drawn, the picture is copied at the clip's frame rate; lifted, it costs the
|
|
1704
|
+
window nothing. There is no `controls`: the transport is your React.
|
|
1705
|
+
|
|
1706
|
+
Where it is false — X11 and Wayland for good, Windows today, macOS on an
|
|
1707
|
+
older bridge — the element reports one `NoMediaPlaybackError`
|
|
1708
|
+
(`code: 'ENOMEDIAPLAYBACK'`) through `onError` on a microtask and shows its
|
|
1709
|
+
poster, never a black box. Ask first:
|
|
2255
1710
|
|
|
2256
1711
|
```jsx
|
|
2257
1712
|
const playback = useSupports('mediaPlayback');
|
|
@@ -2262,7 +1717,7 @@ const playback = useSupports('mediaPlayback');
|
|
|
2262
1717
|
|
|
2263
1718
|
## `<canvas>`
|
|
2264
1719
|
|
|
2265
|
-
The escape hatch: a retained node
|
|
1720
|
+
The escape hatch: a retained node you paint.
|
|
2266
1721
|
|
|
2267
1722
|
```jsx
|
|
2268
1723
|
<canvas
|
|
@@ -2275,31 +1730,22 @@ The escape hatch: a retained node whose content you paint.
|
|
|
2275
1730
|
```
|
|
2276
1731
|
|
|
2277
1732
|
`ctx` is ntk's canvas-like 2d context (paths, transforms, gradients incl.
|
|
2278
|
-
conical, `setLineDash`, round caps/joins, images, text
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
map, row striping, terminal cells — has a batch primitive, and the
|
|
2284
|
-
difference is a whole frame's worth of requests:
|
|
1733
|
+
conical, `setLineDash`, round caps/joins, images, text), translated to the
|
|
1734
|
+
node's origin and clipped to it. `onDraw` runs on every repaint of the
|
|
1735
|
+
window. `fillRects` sends many same-colour rectangles (a sparkline, a heat
|
|
1736
|
+
map, terminal cells) as one request, with `fillRect`'s fill style, alpha,
|
|
1737
|
+
composite op and clip:
|
|
2285
1738
|
|
|
2286
1739
|
```jsx
|
|
2287
1740
|
ctx.fillStyle = theme.accent;
|
|
2288
1741
|
ctx.fillRects(bars); // [[x, y, w, h], …] or one flat [x, y, w, h, x, …]
|
|
2289
1742
|
```
|
|
2290
1743
|
|
|
2291
|
-
`fillRects` is `fillRect` per rectangle — the same fill style, alpha,
|
|
2292
|
-
composite op and clip — sent as a single `Render.FillRectangles` where the
|
|
2293
|
-
loop would have sent one composite each.
|
|
2294
|
-
|
|
2295
1744
|
### Raw pixels — the one call that is not translated
|
|
2296
1745
|
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
coordinates are the drawable's own: `ctx.putImageData(data, 0, 0)` lands at
|
|
2301
|
-
the **window** corner, not in the node, and nothing errors on the way.
|
|
2302
|
-
`onDraw`'s second argument carries the node's origin for exactly this case:
|
|
1746
|
+
`putImageData` and `getImageData` ignore the transform _and_ the clip (the
|
|
1747
|
+
HTML canvas rule), so `ctx.putImageData(data, 0, 0)` lands at the
|
|
1748
|
+
**window** corner, silently. `onDraw`'s second argument carries the origin:
|
|
2303
1749
|
|
|
2304
1750
|
```jsx
|
|
2305
1751
|
<canvas
|
|
@@ -2310,19 +1756,15 @@ the **window** corner, not in the node, and nothing errors on the way.
|
|
|
2310
1756
|
/>
|
|
2311
1757
|
```
|
|
2312
1758
|
|
|
2313
|
-
`x`/`y` are
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
warns, once, naming this rule.
|
|
1759
|
+
`x`/`y` are the node's place in the window in a live paint and the cache
|
|
1760
|
+
surface's origin under a [`cacheKey`](#cachekey--draw-it-once), so
|
|
1761
|
+
offsetting by them is right in both. In development an out-of-box
|
|
1762
|
+
`putImageData` warns once.
|
|
2318
1763
|
|
|
2319
|
-
|
|
2320
|
-
`
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
half-scrolled-out canvas stays cut at the viewport, which no raw write is —
|
|
2324
|
-
and the image caches its server-side pixmap instead of re-uploading the
|
|
2325
|
-
bytes on every repaint:
|
|
1764
|
+
For pixels that last, draw an `Image` instead (ntk's, from
|
|
1765
|
+
[`react-x11/ntk`](extending.md#the-subpath-exports)): `drawImage` honours
|
|
1766
|
+
the transform and clip (a half-scrolled canvas stays cut), and the image
|
|
1767
|
+
keeps its server-side pixmap rather than re-uploading every repaint.
|
|
2326
1768
|
|
|
2327
1769
|
```jsx
|
|
2328
1770
|
import { Image } from 'react-x11/ntk';
|
|
@@ -2333,38 +1775,27 @@ const img = new Image({ width, height, data: rgba }); // keep it memoized
|
|
|
2333
1775
|
|
|
2334
1776
|
### `cacheKey` — draw it once
|
|
2335
1777
|
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
and composited on later repaints; two canvases with the same key share the one
|
|
2339
|
-
rendered copy.
|
|
1778
|
+
With a `cacheKey` the canvas is rendered once and composited on later
|
|
1779
|
+
repaints; canvases sharing a key share the copy.
|
|
2340
1780
|
|
|
2341
1781
|
```jsx
|
|
2342
1782
|
<canvas cacheKey={`spark:${series.id}:${w}x${h}`} onDraw={drawSparkline} />
|
|
2343
1783
|
```
|
|
2344
1784
|
|
|
2345
|
-
Opt-in,
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
out shows stale pixels, which is the hardest kind of bug to see. Develop with
|
|
2352
|
-
`REACT_X11_PAINT_CACHE=verify`, which turns exactly that mistake into a loud
|
|
2353
|
-
complaint — see [Runtime diagnostics](debugging.md#the-paint-cache--react_x11_no_paint_cache-react_x11_paint_cacheverify).
|
|
2354
|
-
Leave `cacheKey` unset for anything animated, or driven by state outside the
|
|
2355
|
-
props.
|
|
2356
|
-
|
|
2357
|
-
`<svg>` does this automatically: its content is fully described by
|
|
2358
|
-
its props, so the renderer can build the key itself.
|
|
1785
|
+
Opt-in, since `onDraw` is an opaque closure only you can read. **The key
|
|
1786
|
+
must name every input the drawing reads**, or stale pixels show; develop
|
|
1787
|
+
with `REACT_X11_PAINT_CACHE=verify`, which reports exactly that
|
|
1788
|
+
([Runtime diagnostics](debugging.md#the-paint-cache--react_x11_no_paint_cache-react_x11_paint_cacheverify)).
|
|
1789
|
+
Leave it unset for anything animated or driven by state outside the props.
|
|
1790
|
+
`<svg>` builds its key itself.
|
|
2359
1791
|
|
|
2360
1792
|
### `mono` — one colour, and the colour out of the key
|
|
2361
1793
|
|
|
2362
|
-
`mono`
|
|
2363
|
-
and
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
mark inside a dimmed or hovered row follows it with nothing said:
|
|
1794
|
+
`mono` promises **the drawing is one colour, and not its own to choose**:
|
|
1795
|
+
`fillStyle` and `strokeStyle` arrive preset from the node's resolved
|
|
1796
|
+
`color` — its own, else inherited
|
|
1797
|
+
([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)) — so
|
|
1798
|
+
`:hover`, `:disabled` and a theme flip all reach the same `onDraw`:
|
|
2368
1799
|
|
|
2369
1800
|
```jsx
|
|
2370
1801
|
const chevron = (ctx, { width: w, height: h }) => {
|
|
@@ -2389,38 +1820,20 @@ const chevron = (ctx, { width: w, height: h }) => {
|
|
|
2389
1820
|
/>;
|
|
2390
1821
|
```
|
|
2391
1822
|
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
**coverage** rather than as pixels, with the colour applied at composite time —
|
|
2398
|
-
so the colour leaves the key and one rendered copy serves every ink. That is
|
|
2399
|
-
the trick the glyph cache runs on text, and the one `<svg>` gets for a
|
|
2400
|
-
`fill="currentColor"` document; a closure cannot be scanned the way a document
|
|
2401
|
-
can, so here you say it. Without `mono`, each colour of the same shape is a
|
|
2402
|
-
separate rasterization and a separate pixmap.
|
|
2403
|
-
|
|
2404
|
-
Needs **ntk ≥ 7.3.3**, which `package.json` carries: earlier versions
|
|
2405
|
-
composite coverage as empty under a clip they cannot express as a rectangle
|
|
2406
|
-
([sidorares/ntk#243](https://github.com/sidorares/ntk/issues/243)), and a
|
|
2407
|
-
widget sits inside clips like that routinely.
|
|
2408
|
-
|
|
2409
|
-
`mono` works without `cacheKey` (the ink is preset either way), but the two
|
|
2410
|
-
together are the point. The [system icon set](components.md#system-icons) is
|
|
2411
|
-
twelve of these.
|
|
1823
|
+
With a `cacheKey`, a `mono` drawing is cached as **coverage** and coloured
|
|
1824
|
+
at composite time, so one copy serves every ink (as `<svg>` does for
|
|
1825
|
+
`currentColor`); without `mono`, each colour is its own rasterization. It
|
|
1826
|
+
works without `cacheKey`, but together is the point. The
|
|
1827
|
+
[system icon set](components.md#system-icons) is twelve of these.
|
|
2412
1828
|
|
|
2413
1829
|
---
|
|
2414
1830
|
|
|
2415
1831
|
## `<glarea>`
|
|
2416
1832
|
|
|
2417
|
-
An OpenGL surface in the layout — on X11 the only drawn element
|
|
2418
|
-
real X window
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
([gl.md](gl.md)). On X11 under the default `glPolicy` it needs ntk ≥ 3.6.0
|
|
2422
|
-
and a server with **indirect GLX** enabled (`+iglx` / `AllowIndirectGLX` —
|
|
2423
|
-
off by default on many).
|
|
1833
|
+
An OpenGL surface in the layout — on X11 the only drawn element owning a
|
|
1834
|
+
real X window; other backends give it a surface of their own
|
|
1835
|
+
([gl.md](gl.md)). On X11 under the default `glPolicy` it needs a server with
|
|
1836
|
+
**indirect GLX** (`+iglx` / `AllowIndirectGLX`, off by default on many).
|
|
2424
1837
|
|
|
2425
1838
|
```jsx
|
|
2426
1839
|
<glarea
|
|
@@ -2437,43 +1850,33 @@ off by default on many).
|
|
|
2437
1850
|
/>
|
|
2438
1851
|
```
|
|
2439
1852
|
|
|
2440
|
-
- `onDraw(gl, { width, height, node })` — draw one frame
|
|
2441
|
-
set and
|
|
2442
|
-
- `onCreated(gl, info)` —
|
|
2443
|
-
|
|
1853
|
+
- `onDraw(gl, { width, height, node })` — draw one frame; the viewport is
|
|
1854
|
+
set and buffers cleared before it, `SwapBuffers` follows.
|
|
1855
|
+
- `onCreated(gl, info)` — once, when the context is current: one-time GL
|
|
1856
|
+
state, texture uploads, display-list compilation.
|
|
2444
1857
|
- `clearColor` — CSS colour or `[r, g, b, a]` floats (default black).
|
|
2445
1858
|
- `frameLoop` — `'demand'` (default) redraws on prop, size and expose
|
|
2446
|
-
changes
|
|
2447
|
-
- `frameRate` —
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
it too; name `'display'` here for a scene that wants every frame while the
|
|
2452
|
-
window around it streams. A `frameLoop` of `'always'` under `'adaptive'`
|
|
2453
|
-
is a loop at the budget rather than at the display.
|
|
1859
|
+
changes; `'always'` renders continuously on ntk's frame clock.
|
|
1860
|
+
- `frameRate` — [`<window frameRate>`](#framerate--pacing-the-frames-under-a-flood)'s
|
|
1861
|
+
values, priced by what `onDraw` and the swap cost; defaults to the owning
|
|
1862
|
+
window's. Name `'display'` for a scene wanting every frame while its
|
|
1863
|
+
window streams; `'always'` under `'adaptive'` loops at the budget.
|
|
2454
1864
|
- `glx` — a visual spec for ntk's `chooseGLXConfig`, e.g.
|
|
2455
|
-
`{ DEPTH_SIZE: 24 }
|
|
2456
|
-
- `onError(err)` — no GL surface (no GLX, no matching visual)
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
On X11's indirect path
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
drawn above the surface.** Layout treats a `<glarea>` as a flex container
|
|
2471
|
-
like a `<box>` — it is sized and positioned like any node, its window follows
|
|
2472
|
-
that rect, and its children are laid out inside it and cut to its box: a
|
|
2473
|
-
legend in a corner, a toolbar along an edge, a label over the scene. They are
|
|
2474
|
-
ordinary nodes in every way but one. The window's own paint cannot reach
|
|
2475
|
-
above the surface, so they are painted on **panes** stacked over it, in the
|
|
2476
|
-
same frame and from the same damage as everything else.
|
|
1865
|
+
`{ DEPTH_SIZE: 24 }`; one query per app, shared by every `<glarea>`.
|
|
1866
|
+
- `onError(err)` — no GL surface (no GLX, no matching visual); unhandled, a
|
|
1867
|
+
console warning.
|
|
1868
|
+
|
|
1869
|
+
On X11's default indirect path `gl` is fixed-function OpenGL 1.4 (immediate
|
|
1870
|
+
mode, matrices, lighting, textures, display lists) — no shaders or vertex
|
|
1871
|
+
arrays, which GLX does not encode. **Put geometry in display lists**: each
|
|
1872
|
+
immediate-mode vertex crosses the wire every frame; a compiled list is one
|
|
1873
|
+
`CallList`. The direct path and Cocoa give `onDraw` a WebGL-shaped table
|
|
1874
|
+
with shaders ([gl.md](gl.md)).
|
|
1875
|
+
|
|
1876
|
+
The surface sits above everything its window paints. **Its own children are
|
|
1877
|
+
drawn above it**: laid out like a `<box>`'s, cut to its box — a legend, a
|
|
1878
|
+
toolbar, a label — and painted on **panes** over the surface in the same
|
|
1879
|
+
frame.
|
|
2477
1880
|
|
|
2478
1881
|
```jsx
|
|
2479
1882
|
<glarea style={{ flexGrow: 1 }} clearColor="#0e1320" onDraw={drawMap}>
|
|
@@ -2491,86 +1894,48 @@ same frame and from the same damage as everything else.
|
|
|
2491
1894
|
</glarea>
|
|
2492
1895
|
```
|
|
2493
1896
|
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
`
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
server keeps what each pane and the surface show while they are mapped
|
|
2518
|
-
(backing store), so what a moving pane uncovers is back at once rather than
|
|
2519
|
-
after an Expose and a repaint.
|
|
2520
|
-
|
|
2521
|
-
`useSupports('glOverlay')` says whether a connection draws a surface's
|
|
2522
|
-
children at all. Every backend does, on every display but one.
|
|
2523
|
-
|
|
2524
|
-
**XQuartz draws nothing over a GL surface.** There every surface, direct
|
|
2525
|
-
(Apple-DRI) or indirect, is composited by the macOS window server above
|
|
2526
|
-
everything the X server draws in the window. A pane stacked over the
|
|
2527
|
-
surface in X is drawn under it on screen, and nothing on the X side changes
|
|
2528
|
-
that: cutting the surface's window down with SHAPE still leaves the frame
|
|
2529
|
-
covering all of it. A pane over the _whole_ surface does show, but only
|
|
2530
|
-
because the frame then disappears. So on XQuartz the children are laid out
|
|
2531
|
-
and not drawn, the pointer over them is the surface's,
|
|
2532
|
-
`useSupports('glOverlay')` is false, and a development build says so once.
|
|
2533
|
-
Ask the hook and draw the overlay another way there, beside the surface or
|
|
2534
|
-
in GL, or run on the Cocoa backend (the default on macOS), which composites
|
|
2535
|
-
it.
|
|
2536
|
-
|
|
2537
|
-
**A child that only moves is not repainted.** A pane keeps what it drew
|
|
2538
|
-
between frames, so when all that changed about one of the surface's children
|
|
2539
|
-
is where it sits — its `left` or `top`, and nothing inside it — the frame
|
|
2540
|
-
moves that child's pixels on the pane and repaints only what the copy cannot
|
|
2541
|
-
supply: the strips the move uncovered, and the other children's pixels it
|
|
2542
|
-
passed over or under. A layer of mounted content panned across a scene,
|
|
2543
|
-
such as a graph's node bodies over its GL edges, then costs a copy and a few
|
|
2544
|
-
strips a frame rather than a repaint of everything on show. A scroll pane
|
|
2545
|
-
among the children scrolls the same way, moving the pane's pixels. A child
|
|
2546
|
-
that also changed size, or drew anything differently in the same commit, is
|
|
2547
|
-
repainted as before, and
|
|
2548
|
-
[`REACT_X11_NO_SCROLL_BLIT=1`](debugging.md#react_x11_no_scroll_blit1) turns
|
|
2549
|
-
every such copy off.
|
|
1897
|
+
- **Cocoa**: one transparent layer over the whole surface, composited by
|
|
1898
|
+
Core Animation — a translucent fill, antialiased edge or shadow blends
|
|
1899
|
+
with the GL frame.
|
|
1900
|
+
- **X11**: one child window per child, stacked above the surface in the
|
|
1901
|
+
children's order and cut (SHAPE) to where the child painted half its
|
|
1902
|
+
alpha or more; the GL frame shows everywhere else. A translucent pixel
|
|
1903
|
+
inside the cut blends with `clearColor`, not the frame — so a translucent
|
|
1904
|
+
fill is tinted by `clearColor`, and no compositor helps. Without SHAPE
|
|
1905
|
+
(`react-x11/test`'s server) a pane is the child's whole rectangle,
|
|
1906
|
+
`clearColor` where unpainted.
|
|
1907
|
+
|
|
1908
|
+
A child that only moves (its `left` or `top`) has its pixels copied, not
|
|
1909
|
+
repainted — only uncovered strips are drawn, and panes it passes over are
|
|
1910
|
+
untouched — so content panned over a scene is cheap; scroll panes among the
|
|
1911
|
+
children scroll the same way.
|
|
1912
|
+
[`REACT_X11_NO_SCROLL_BLIT=1`](debugging.md#react_x11_no_scroll_blit1)
|
|
1913
|
+
turns the copies off.
|
|
1914
|
+
|
|
1915
|
+
`useSupports('glOverlay')` says whether children are drawn: true everywhere
|
|
1916
|
+
but **XQuartz**, where the macOS window server composites every GL surface
|
|
1917
|
+
above all X drawing — children are laid out but not drawn, the pointer over
|
|
1918
|
+
them is the surface's, and development says so once. Draw the overlay
|
|
1919
|
+
beside the surface or in GL there, or use the Cocoa backend.
|
|
2550
1920
|
|
|
2551
1921
|
Limits worth knowing:
|
|
2552
1922
|
|
|
2553
|
-
- **Overlapping surfaces:**
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
`ev.capturePointer()` keeps a drag that wanders off the surface. The
|
|
2570
|
-
window's hit test knows the surface and its children are on top: a point
|
|
2571
|
-
inside the rect lands on them, not on whatever the tree has behind.
|
|
2572
|
-
`pointerEvents: 'none'` on the surface lets the pointer through to that
|
|
2573
|
-
instead.
|
|
1923
|
+
- **Overlapping surfaces:** both overlays are above both frames on Cocoa;
|
|
1924
|
+
on X11 they stack by creation order.
|
|
1925
|
+
- **Scroll panes:** a surface scrolled partly out of a pane is not clipped
|
|
1926
|
+
by it — its window and panes are real windows at its rect.
|
|
1927
|
+
- **Promotion:** on Cocoa a child is never lifted onto its own layer for an
|
|
1928
|
+
animation (it would sit under the surface); its transitions run on the
|
|
1929
|
+
frame clock.
|
|
1930
|
+
|
|
1931
|
+
**The pointer over the surface is the tree's**, on every backend:
|
|
1932
|
+
`onMouseDown`, `onMouseMove`, `onMouseUp`, `onClick` (double click:
|
|
1933
|
+
`detail: 2`), `onContextMenu`, `onMouseEnter`/`onMouseLeave`,
|
|
1934
|
+
`:hover`/`:active` and `onWheel` fire at the child under the pointer, or the
|
|
1935
|
+
`<glarea>` between children, and bubble. A press focuses the nearest
|
|
1936
|
+
focusable one; `ev.capturePointer()` keeps a drag that leaves the surface.
|
|
1937
|
+
The surface and its children win hit tests over what the tree has behind;
|
|
1938
|
+
`pointerEvents: 'none'` on the surface lets the pointer through.
|
|
2574
1939
|
|
|
2575
1940
|
```jsx
|
|
2576
1941
|
<glarea
|
|
@@ -2583,52 +1948,29 @@ instead.
|
|
|
2583
1948
|
/>
|
|
2584
1949
|
```
|
|
2585
1950
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
the pointer on `node.window`**. ntk selects whatever its window is listened
|
|
2596
|
-
to for, and X then delivers the event there instead of to the tree — every
|
|
2597
|
-
press, and the wheel with them, since both are ButtonPress. Listening there
|
|
2598
|
-
was the only way to hear a press over a surface before core delivered it;
|
|
2599
|
-
`node.forwardsPointer` is `true` where it does
|
|
2600
|
-
(`GlAreaNode.prototype.forwardsPointer` from `react-x11/node`, to ask
|
|
2601
|
-
without rendering), and is the switch for dropping such a listener.
|
|
2602
|
-
|
|
2603
|
-
`onDraw` is the raw escape hatch, and a scene graph over it is
|
|
2604
|
-
`@react-x11/components/three`. See `examples/viewer3d.jsx` for the raw
|
|
2605
|
-
form, and `examples/labs/gl-overlay.jsx` for a surface with a HUD over it on
|
|
2606
|
-
both backends, and beside it on XQuartz.
|
|
1951
|
+
An element built on `<glarea>` must **not listen for the pointer on
|
|
1952
|
+
`node.window`**: on X11 that diverts every press and wheel from the tree.
|
|
1953
|
+
`node.forwardsPointer` (`GlAreaNode.prototype.forwardsPointer` from
|
|
1954
|
+
`react-x11/node`, to ask without rendering) is `true` where core delivers
|
|
1955
|
+
the pointer — the switch for dropping such a listener.
|
|
1956
|
+
|
|
1957
|
+
A scene graph over `onDraw` is `@react-x11/components/three`. See
|
|
1958
|
+
`examples/viewer3d.jsx` for the raw form, and `examples/labs/gl-overlay.jsx`
|
|
1959
|
+
for a HUD over a surface on both backends, and beside it on XQuartz.
|
|
2607
1960
|
|
|
2608
1961
|
---
|
|
2609
1962
|
|
|
2610
1963
|
## `<foreign>`
|
|
2611
1964
|
|
|
2612
|
-
**X11 only
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
```jsx
|
|
2620
|
-
const embedding = useSupports('embedding');
|
|
2621
|
-
```
|
|
2622
|
-
|
|
2623
|
-
`<Frame>`, which uses it on X11 to host a pane of your _own_ application,
|
|
2624
|
-
does work on the Cocoa and Windows backends too ([frame.md](frame.md)) — it
|
|
2625
|
-
just gets there another way.
|
|
1965
|
+
**X11 only**; no other backend has cross-process window embedding, and
|
|
1966
|
+
none is planned. On Cocoa, Wayland, Windows and the headless mock it lays
|
|
1967
|
+
out as an empty box, draws nothing, never calls `onReady`, and calls
|
|
1968
|
+
`onError` once. Ask first with `useSupports('embedding')`. (`<Frame>`, which
|
|
1969
|
+
uses it on X11 to host a pane of your _own_ app, works on Cocoa and Windows
|
|
1970
|
+
another way — [frame.md](frame.md).)
|
|
2626
1971
|
|
|
2627
1972
|
Another process's top-level X window, laid out as an element — a terminal
|
|
2628
|
-
pane, a video surface, a docked tray icon
|
|
2629
|
-
real X window without painting into its parent, and the only one that makes
|
|
2630
|
-
a react-x11 app a _host_ rather than a drawer of its own pixels. Needs ntk
|
|
2631
|
-
≥ 7.4.0.
|
|
1973
|
+
pane, a video surface, a docked tray icon:
|
|
2632
1974
|
|
|
2633
1975
|
```jsx
|
|
2634
1976
|
<foreign
|
|
@@ -2639,82 +1981,59 @@ a react-x11 app a _host_ rather than a drawer of its own pixels. Needs ntk
|
|
|
2639
1981
|
/>
|
|
2640
1982
|
```
|
|
2641
1983
|
|
|
2642
|
-
- `windowId` — the window to embed
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
- `onEmbedded({ id, xembed, version })` — a client is in. `xembed: false`
|
|
2650
|
-
is a client that set no `_XEMBED_INFO` and got plain reparenting, which
|
|
2651
|
-
is the common case rather than the fallback.
|
|
1984
|
+
- `windowId` — the window to embed; changing it hands the old client back
|
|
1985
|
+
first. **Omit it** to adopt whatever is put inside this node, as
|
|
1986
|
+
`xterm -into WID` and `mpv --wid=WID` need.
|
|
1987
|
+
- `onReady({ windowId })` — this node's container id, offered before
|
|
1988
|
+
anything is embedded, so a program can be spawned into it.
|
|
1989
|
+
- `onEmbedded({ id, xembed, version })` — a client is in; `xembed: false`
|
|
1990
|
+
(no `_XEMBED_INFO`, plain reparenting) is the common case.
|
|
2652
1991
|
- `onClientGone()` — destroyed, or reparented away by someone else.
|
|
2653
|
-
- `onRequestFocus()` — the client asked for
|
|
2654
|
-
|
|
2655
|
-
- `onError(err)` — the embed failed, or
|
|
2656
|
-
|
|
2657
|
-
kind, one per app however many `<foreign>`s ask.
|
|
1992
|
+
- `onRequestFocus()` — the client asked for focus; it is given through the
|
|
1993
|
+
focus manager, so the tree observes it normally.
|
|
1994
|
+
- `onError(err)` — the embed failed, or (once, at mount) the backend has no
|
|
1995
|
+
embedding; unhandled, a console warning (for the latter, one per app).
|
|
2658
1996
|
- `focusable` — default `true`, like `<textinput>`.
|
|
2659
|
-
- `backgroundColor` in
|
|
2660
|
-
in it: the container window's background, painted by the server.
|
|
1997
|
+
- `backgroundColor` in `style` shows while there is no client.
|
|
2661
1998
|
|
|
2662
1999
|
**Unmount reparents the client back to the root and drops it from the save
|
|
2663
|
-
set
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
Keyboard focus is where this element has a rule of its own: **while a
|
|
2675
|
-
`<foreign>` holds focus, the application's handlers see every key first and
|
|
2676
|
-
anything they do not consume with `preventDefault()` is forwarded to the
|
|
2677
|
-
client.** That, the XEmbed focus messages, the tab chain crossing the
|
|
2678
|
-
boundary and the one case the rule cannot cover are all in
|
|
2679
|
-
[embedding.md](embedding.md).
|
|
2000
|
+
set; it never destroys it.** Layout treats it as a leaf; its X window
|
|
2001
|
+
follows that rect, and the client gets the synthetic `ConfigureNotify`
|
|
2002
|
+
ICCCM requires, in root coordinates. It stacks above everything drawn in
|
|
2003
|
+
the parent and takes no children, so an overlay belongs in a sibling
|
|
2004
|
+
`<popup>`. Pointer events over it are the client's.
|
|
2005
|
+
|
|
2006
|
+
**While it holds focus, the app's handlers see every key first, and
|
|
2007
|
+
anything they do not `preventDefault()` is forwarded to the client.** That,
|
|
2008
|
+
the XEmbed focus messages, the tab chain crossing the boundary and the one
|
|
2009
|
+
case the rule cannot cover are in [embedding.md](embedding.md).
|
|
2680
2010
|
|
|
2681
2011
|
---
|
|
2682
2012
|
|
|
2683
2013
|
## 3D scene: `<mesh>`, `<group>`, geometries, materials
|
|
2684
2014
|
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
same JSX. Core keeps the surface and `onDraw`; see [gl.md](gl.md).
|
|
2015
|
+
These live in [`@react-x11/components/three`](ecosystem.md), a scene graph
|
|
2016
|
+
with its own reconciler over `<glarea>`, rendering on **both** GL backends
|
|
2017
|
+
from the same JSX. Core keeps the surface and `onDraw` ([gl.md](gl.md)).
|
|
2689
2018
|
|
|
2690
2019
|
## Vector drawings
|
|
2691
2020
|
|
|
2692
2021
|
`<svg>` is a drawing, laid out like an `<image>`: it reports an intrinsic
|
|
2693
|
-
size and scales into the content box it is given.
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
re-rendering only the block that changed while content streams in. Their
|
|
2700
|
-
successors are components in
|
|
2701
|
-
[`@react-x11/components`](https://github.com/sidorares/react-x11-components),
|
|
2702
|
-
composed from public host elements rather than wrapping a widget:
|
|
2703
|
-
`<Markdown>` (GFM, selectable, streaming-friendly) and `<Formula>` (KaTeX,
|
|
2704
|
-
selectable), with `<RichText>` and `<CodeBlock>` underneath them.
|
|
2022
|
+
size and scales into the content box it is given. For documents use
|
|
2023
|
+
[`@react-x11/components`](https://github.com/sidorares/react-x11-components)'
|
|
2024
|
+
`<Markdown>` (GFM) and `<Formula>` (KaTeX), with `<RichText>` and
|
|
2025
|
+
`<CodeBlock>` beneath: built from host elements, they select across blocks
|
|
2026
|
+
and re-render only the changed block while content streams in. (The
|
|
2027
|
+
`<markdown>`, `<html>` and `<tex>` elements were removed in 2.0.)
|
|
2705
2028
|
|
|
2706
2029
|
### `<svg>`
|
|
2707
2030
|
|
|
2708
2031
|
ntk `SvgView` (static SVG via Path2D — paths, shapes, gradients,
|
|
2709
|
-
transforms, basic text). Sized like `<image
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
Content is **JSX children, like SVG in React DOM** — SVG elements are
|
|
2715
|
-
declarative children with camelCase props (`strokeWidth`, `fillRule`;
|
|
2716
|
-
native-camelCase attributes like `viewBox` stay as-is), re-rendered on
|
|
2717
|
-
any prop change:
|
|
2032
|
+
transforms, basic text). Sized like `<image>`: natural `viewBox` size,
|
|
2033
|
+
aspect-preserving when the style sets one of `width`/`height`, scaled into
|
|
2034
|
+
the content box when it sets both. Content is **JSX children, like SVG in
|
|
2035
|
+
React DOM** — camelCase props (`strokeWidth`, `fillRule`; `viewBox` as-is),
|
|
2036
|
+
re-rendered on any prop change:
|
|
2718
2037
|
|
|
2719
2038
|
```jsx
|
|
2720
2039
|
<svg viewBox="0 0 24 24" style={{ width: 40, height: 40 }}>
|
|
@@ -2723,24 +2042,21 @@ any prop change:
|
|
|
2723
2042
|
</svg>
|
|
2724
2043
|
```
|
|
2725
2044
|
|
|
2726
|
-
A `source` markup string is also accepted (children win when both are
|
|
2727
|
-
present). Supported elements/attributes are SvgView's (unsupported tags
|
|
2728
|
-
are skipped); per-child event handlers are not dispatched — put handlers
|
|
2729
|
-
on the `<svg>` element itself.
|
|
2730
|
-
|
|
2731
2045
|
| prop | |
|
|
2732
2046
|
| --------- | --------------------------------- |
|
|
2733
2047
|
| children | declarative SVG elements |
|
|
2734
2048
|
| `source` | SVG markup string |
|
|
2735
2049
|
| `viewBox` | coordinate system (children form) |
|
|
2736
2050
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2051
|
+
Children win over `source` when both are given. Unsupported tags are
|
|
2052
|
+
skipped. Per-child event handlers are not dispatched — put them on the
|
|
2053
|
+
`<svg>`.
|
|
2054
|
+
|
|
2055
|
+
**`currentColor`** in `fill`/`stroke` resolves as in CSS: the node's own
|
|
2056
|
+
`color`, else the inherited ink, else the palette's `text`
|
|
2057
|
+
([styling.md](styling.md#inheritance-the-ink-the-face-and-the-size)) — so
|
|
2058
|
+
one drawing serves every state, and an unstyled one is readable on a dark
|
|
2059
|
+
desktop:
|
|
2744
2060
|
|
|
2745
2061
|
```jsx
|
|
2746
2062
|
<svg
|
|
@@ -2754,9 +2070,10 @@ readable on a dark desktop rather than black on it:
|
|
|
2754
2070
|
/>
|
|
2755
2071
|
```
|
|
2756
2072
|
|
|
2757
|
-
A drawing
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
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).
|