react-x11 2.52.0 → 2.54.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 +1096 -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/imagedecode-worker.js +32 -0
- package/src/imagedecode.js +140 -2
- 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/ntk.d.ts +12 -0
- 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
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
# Custom layout: layouts and positions that run inside the pass
|
|
2
|
-
|
|
3
|
-
_Design record, 2026-09-10. Written against `master` at 91e1d1c (react-x11
|
|
4
|
-
2.11, `src/nodes.js` @ 13.5k lines) and the branch that adds the two seams.
|
|
5
|
-
The yoga numbers are from a standalone spike (§6) run in-process on one
|
|
6
|
-
machine (Apple M1 Pro, node 26); file references are to that branch and will
|
|
7
|
-
drift._
|
|
8
|
-
|
|
9
|
-
_[styling.md](../styling.md#custom-layouts) is the reference for the style
|
|
10
|
-
properties and [extending.md](../extending.md#a-layout-algorithm-of-your-own)
|
|
11
|
-
for the two registration contracts; this is why they are the shape they are._
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 0. TL;DR
|
|
16
|
-
|
|
17
|
-
**Two seams, both run by the core inside the frame's layout pass:**
|
|
18
|
-
|
|
19
|
-
- **A layout** arranges a box's children: `registerLayout(name, { layout })`,
|
|
20
|
-
then `style={{ layout: name }}` on any box. The algorithm is asked how big
|
|
21
|
-
the box's content is for the room on offer, and — once the pass has given
|
|
22
|
-
the box its size — where each child goes. `grid` (which `display: 'grid'`
|
|
23
|
-
names too), `masonry` and `equal-row` are built in.
|
|
24
|
-
- **A position** moves one node after the pass, by an offset, resizing
|
|
25
|
-
nothing: `registerPosition(name, { place })`, then
|
|
26
|
-
`style={{ position: name }}`. It is CSS's positioning scheme opened up, and
|
|
27
|
-
`position: 'sticky'` is now the built-in one, written against the same
|
|
28
|
-
context a registered one is handed.
|
|
29
|
-
|
|
30
|
-
**Why in the core rather than in an effect** (§1): an arrangement that
|
|
31
|
-
depends on a layout output has to be computed inside the pass that produces
|
|
32
|
-
the output. `onLayout` hears about a pass after it painted, `useLayoutEffect`
|
|
33
|
-
runs before the pass it would need to read, and the passes a scroll, a
|
|
34
|
-
resize, a live resize or a layout-property transition run never reach React
|
|
35
|
-
at all.
|
|
36
|
-
|
|
37
|
-
**How** (§3): a box with a `layout` is a measured leaf in yoga's tree, and
|
|
38
|
-
each of its children a yoga tree of its own. The measure function is the
|
|
39
|
-
algorithm's sizing answer; after the pass, the algorithm's final call places
|
|
40
|
-
the children, each laid out at the size its rect names, and `absolutize`
|
|
41
|
-
reads the slot. The content floors are measured tree by tree inside those
|
|
42
|
-
children (§4). A layout that throws is reported and the box is laid out as
|
|
43
|
-
flexbox in the same frame.
|
|
44
|
-
|
|
45
|
-
## 1. Why the core
|
|
46
|
-
|
|
47
|
-
What one change costs when an arrangement is done in React, with
|
|
48
|
-
`onLayout` → `setState`:
|
|
49
|
-
|
|
50
|
-
1. **Commit.** The nodes change and a frame is scheduled. `useLayoutEffect`
|
|
51
|
-
runs here, before yoga has run for this commit: `abs` holds the previous
|
|
52
|
-
frame's rect — zeros on a mount. There is no forced synchronous layout
|
|
53
|
-
([react-features.md](../react-features.md#measuring-a-node)).
|
|
54
|
-
2. **The frame** (`WindowNode._flushFrame`): layout, the container queries,
|
|
55
|
-
`absolutize`, the placement pass, damage, paint. **This frame shows the
|
|
56
|
-
arrangement the effect has not corrected yet.**
|
|
57
|
-
3. **`onLayout`** is queued with `setImmediate` from inside that pass
|
|
58
|
-
(`Node._reportLayout`), so it fires after the paint; its `setState` is a
|
|
59
|
-
second render, a second commit, a second layout pass and a second paint.
|
|
60
|
-
|
|
61
|
-
A probe against the mock backend with production scheduling, a row whose
|
|
62
|
-
column count is one per 100px of its width:
|
|
63
|
-
|
|
64
|
-
| scenario | `useLayoutEffect` measuring `abs`, then `setState` | `onLayout` → `setState` |
|
|
65
|
-
| ---------------------- | ----------------------------------------------------- | ------------------------ |
|
|
66
|
-
| mount at 200px | reads 0 → paints 1 column, **stays wrong** | paints 1 column, then 2 |
|
|
67
|
-
| React widens it to 400 | reads 200 (stale) → paints 2 columns, **stays wrong** | paints 2 columns, then 4 |
|
|
68
|
-
| the window widens | **never runs** — nothing committed | paints 2 columns, then 4 |
|
|
69
|
-
|
|
70
|
-
`useLayoutEffect` is on time and blind; `onLayout` sees and is late. The
|
|
71
|
-
DOM gets both only because reading geometry forces a synchronous reflow,
|
|
72
|
-
and React Native's New Architecture got both by laying out synchronously in
|
|
73
|
-
the commit. Neither covers the third row, which is the one that matters
|
|
74
|
-
most, because most layout passes are not React's:
|
|
75
|
-
|
|
76
|
-
- **A scroll.** ntk's frame runs before React commits an `onScroll` update —
|
|
77
|
-
the reason sticky had to be native (#523), whose probe painted every wheel
|
|
78
|
-
notch twice. And a pure scroll takes the fast path (#405), which moves the
|
|
79
|
-
children without walking them: only a pass run after `_absolutizeChildren`
|
|
80
|
-
sees those frames.
|
|
81
|
-
- **A live resize on macOS.** AppKit's modal loop owns the thread; the resize
|
|
82
|
-
callback flushes the frame synchronously (`src/cocoa/app.js`), so an
|
|
83
|
-
in-frame arrangement follows the drag. Timers and microtasks wait for the
|
|
84
|
-
release, and `onLayout` is a `setImmediate`: an arrangement built on it
|
|
85
|
-
freezes for the whole drag.
|
|
86
|
-
- **A transition on a layout property**, which costs a layout pass per frame
|
|
87
|
-
([styling.md](../styling.md#transitions)) — an effect-driven arrangement
|
|
88
|
-
trails it by a frame on every frame.
|
|
89
|
-
- **Everything else that changes a layout inside a frame:** a container
|
|
90
|
-
query flipping, a font or image arriving through `invalidateMeasure`.
|
|
91
|
-
|
|
92
|
-
The components package was already paying for this. Its virtualization
|
|
93
|
-
window builds a guessed number of rows because "there is always one render
|
|
94
|
-
that has to guess"; Table measures its rows "a tick after the commit because
|
|
95
|
-
layout runs on the frame flush"; Reorder's drop flight reads its landing
|
|
96
|
-
place in an `afterLayout` tick. And `<Html>` implements block, inline, float
|
|
97
|
-
and table layout itself, inside one node, to arrange children at all.
|
|
98
|
-
|
|
99
|
-
The core already had five of these in-frame passes, each written by hand:
|
|
100
|
-
the container queries (restyle and relayout, bounded and pinned), sticky
|
|
101
|
-
(placement and blit-aware damage), anchored popups, `scrollIntoView`, and
|
|
102
|
-
the content floors. The seams are the same slot, opened, with the
|
|
103
|
-
mechanics — when it runs, damage, the blit, hit testing, order — left in
|
|
104
|
-
the core and only the policy handed out. That is AGENTS.md's "decide it,
|
|
105
|
-
then leave a way out" twice over: sticky and flexbox are the decided
|
|
106
|
-
defaults, and these are the ways out.
|
|
107
|
-
|
|
108
|
-
## 2. Positions
|
|
109
|
-
|
|
110
|
-
```js
|
|
111
|
-
registerPosition('parallax', {
|
|
112
|
-
options: { rate: { type: 'number', default: 0.5 } },
|
|
113
|
-
place: (node, { pane, options }) =>
|
|
114
|
-
pane ? { x: 0, y: Math.round(pane.scrollY * options.rate) } : null,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
<box style={{ position: { name: 'parallax', rate: 0.3 } }} />;
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
A node with a registered position is laid out in flow, as `relative` is,
|
|
121
|
-
and after the pass `place(node, context)` returns how far to move it from
|
|
122
|
-
where layout put it, or null for not at all. The context is everything
|
|
123
|
-
sticky needed, in window coordinates and device pixels like `abs`: the
|
|
124
|
-
laid-out rect, the nearest scroll pane's scrollport and offsets, the box the
|
|
125
|
-
node is contained by (its parent's content box, or the whole scrolled
|
|
126
|
-
content for a direct child of the pane), its margins, its direction, the
|
|
127
|
-
scale, the frame clock, and the options resolved. `sticky` is exactly a
|
|
128
|
-
function of that context (`placeSticky` in `src/layouts.js`), and all 28
|
|
129
|
-
sticky tests pass against it unchanged.
|
|
130
|
-
|
|
131
|
-
What the core keeps, because each is easy to get wrong and was, once, while
|
|
132
|
-
sticky was built:
|
|
133
|
-
|
|
134
|
-
- **Nothing remembers an offset.** The scroll fast path moves a placed node
|
|
135
|
-
along with its pane's content, offset and all, so each pass re-derives
|
|
136
|
-
where layout put the node (`Node._laidOutAt`) and moves it from wherever
|
|
137
|
-
it is.
|
|
138
|
-
- **Its moves are claimed by hand**, because neither the layout diff nor the
|
|
139
|
-
fast path sees them: the reach the node was shown at, moved by the pending
|
|
140
|
-
blit, and the reach it lands at, written into the blitting pane's ledger.
|
|
141
|
-
A node that rode the scroll lands where the blit put it and claims
|
|
142
|
-
nothing.
|
|
143
|
-
- **It paints and hit-tests over its siblings** of the same `zIndex`, as CSS
|
|
144
|
-
paints a positioned box over the ones in flow.
|
|
145
|
-
- **It may animate.** `{ x, y, again: true }` asks for another frame, which
|
|
146
|
-
runs the placement pass with nothing to lay out — the shape a drop flight
|
|
147
|
-
or a collapsing header wants, and the way transitions already keep the
|
|
148
|
-
clock running.
|
|
149
|
-
|
|
150
|
-
The insets are the scheme's to read, never offsets: sticky reads them as
|
|
151
|
-
thresholds, and a registered position may read them as anything. Yoga never
|
|
152
|
-
sees them (`applyLayoutStyle`).
|
|
153
|
-
|
|
154
|
-
**Decided: `position`, not a property of its own.** The first name was
|
|
155
|
-
`placement`, and it collided on the first full run: `placement` is the
|
|
156
|
-
anchor vocabulary — which side of its trigger a popup opens on — in
|
|
157
|
-
`anchor.js`, `Tooltip`, `Select`, `Menu`, `Dialog` and their docs, 58 uses,
|
|
158
|
-
and a `<popup placement>` threw the flat-style assertion the moment the
|
|
159
|
-
word became a style name. `offset` is the anchor's gap. And the concept
|
|
160
|
-
already had a home: CSS's `position` picks a positioning scheme, sticky is
|
|
161
|
-
one, and a scheme's insets are its own to interpret. What that costs is
|
|
162
|
-
composability with `absolute` — a node has one scheme, as in CSS — and a
|
|
163
|
-
wrapping box is the answer for the rare case that wants both.
|
|
164
|
-
|
|
165
|
-
## 3. Layouts
|
|
166
|
-
|
|
167
|
-
```js
|
|
168
|
-
registerLayout('equal-row', {
|
|
169
|
-
layout(children, c, options, { style }) {
|
|
170
|
-
// …how wide a cell is, then one rect per child
|
|
171
|
-
return { width, height, children: rects };
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
<box style={{ layout: 'equal-row', gap: 8, justifyContent: 'flex-end' }} />;
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
**The contract** is one function, asked two kinds of question. With
|
|
179
|
-
`constraints` in the vocabulary `measureContent` speaks — `{ width, height,
|
|
180
|
-
widthMode, heightMode }`, `'exactly'`, `'at-most'` or `'unconstrained'`,
|
|
181
|
-
`Infinity` for no bound — it answers how big the box's content is; that is
|
|
182
|
-
asked several times per pass, at sizes nothing is drawn at, so it must be a
|
|
183
|
-
pure function of its arguments. When both modes are `'exactly'`, it also
|
|
184
|
-
answers where each child goes: one rect per child, `{ x, y }` with an
|
|
185
|
-
optional `width` and `height`, from the content box's corner. SwiftUI's
|
|
186
|
-
`Layout` splits these into two methods; one function serves every layout
|
|
187
|
-
here, and masonry places while it sizes.
|
|
188
|
-
|
|
189
|
-
**What an algorithm sees of a child** (`LayoutChild`) is something to
|
|
190
|
-
measure and read options off, never a node:
|
|
191
|
-
`measure({ width, height, widthMode, heightMode })`, `intrinsicSizes()` →
|
|
192
|
-
`{ minContentWidth, maxContentWidth }`, `options` (its `layoutItem`, against
|
|
193
|
-
the layout's `childOptions`) and `index`. Every size is the child's **margin
|
|
194
|
-
box**, which is what makes an algorithm that stacks children right about
|
|
195
|
-
margins without knowing they exist — yoga already treats the size a root is
|
|
196
|
-
laid out at as the margin box. An axis given a number and no mode is
|
|
197
|
-
`'exactly'`; left out it is `'unconstrained'`; `'at-most'` is CSS's
|
|
198
|
-
fit-content, never below the child's floor.
|
|
199
|
-
|
|
200
|
-
**Options are typed.** `options` and `childOptions` declare what the style
|
|
201
|
-
may say — `length`, `number`, `integer`, `boolean`, `string`, `any`, or a
|
|
202
|
-
list of values — so a wrong one is reported naming the option and the type,
|
|
203
|
-
and a `length`, written in logical pixels like every length in a style,
|
|
204
|
-
arrives in device pixels. `info.style` is the host's own resolved style, so
|
|
205
|
-
a layout reads `gap`, `justifyContent` and `alignItems` rather than growing
|
|
206
|
-
options that mean the same things: `equal-row` has no options of its own.
|
|
207
|
-
|
|
208
|
-
### The mechanism
|
|
209
|
-
|
|
210
|
-
- **A host is a measured leaf.** `_syncLayoutHost`, run from the style
|
|
211
|
-
funnel, takes the node's children out of its yoga node and gives it a
|
|
212
|
-
measure function (`_measureHost`) that asks the algorithm. Yoga can do no
|
|
213
|
-
other: a node with a measure function may have no yoga children, and it
|
|
214
|
-
aborts rather than refuse.
|
|
215
|
-
- **Each child is a yoga tree of its own**, laid out by `LayoutChild` with
|
|
216
|
-
`calculateLayout` at the size the algorithm asks for. The host's direction
|
|
217
|
-
goes in as the third argument, the way a window's reaches its tree.
|
|
218
|
-
- **The final call runs at the end of every layout step**
|
|
219
|
-
(`WindowNode._placeLayoutHosts`), parents before the hosts inside their
|
|
220
|
-
children, and before anything reads a child's size — the container
|
|
221
|
-
queries settle on what it leaves. It lays each child out at its rect and
|
|
222
|
-
records the slot, which `absolutize` then reads (`offsetInParent` is the
|
|
223
|
-
one sum that, `onLayout`, `scrollIntoView` and a scroll pane's extent
|
|
224
|
-
make). It is skipped when nothing asked the algorithm anything since the
|
|
225
|
-
last placement and the box is the size it was: a scroll never asks.
|
|
226
|
-
- **Dirt.** A child's yoga tree stops its dirt at its own root, so the
|
|
227
|
-
window looks for it before every pass (`_sweepLayoutHosts`) and makes it
|
|
228
|
-
the host's. Yoga's dirtied callback is no substitute: it fires only on the
|
|
229
|
-
way from clean to dirty, and a child hidden since it mounted was never
|
|
230
|
-
laid out and would never say so. A second path covers the same ground
|
|
231
|
-
today: a changed child owes the floors pass a width, and that pass
|
|
232
|
-
measures off the pixel grid, which has yoga ask every host again. Take
|
|
233
|
-
either away and the tests pass; take both and they fail.
|
|
234
|
-
- **What a child is asked is remembered.** An algorithm asks each child the
|
|
235
|
-
same questions every run, and a host runs several times a frame — the
|
|
236
|
-
floors passes, the pass proper, the placement. Each answer is a layout of
|
|
237
|
-
the child's tree, and two different questions evict each other from
|
|
238
|
-
yoga's one-entry cache, so the answers are kept (`Node._hostSizesNow`)
|
|
239
|
-
until something inside the child changes. That shows as dirt on its root:
|
|
240
|
-
every layout of the root goes through `layoutHostChild` or the memo, both
|
|
241
|
-
drop the memo when they find it dirty, and nothing else clears the dirt.
|
|
242
|
-
Answers taken off the pixel grid are kept apart from the rest. A placement
|
|
243
|
-
leaves a tree already laid out at its rect where it is (`_hostLaidAt`),
|
|
244
|
-
since the floors pass voids yoga's own cache for every tree in the window.
|
|
245
|
-
Before this, a 300-card grid's frame took 3.4 times flexbox's
|
|
246
|
-
([grid-layout.md](grid-layout.md#3-performance)).
|
|
247
|
-
- **Right to left is the core's.** An algorithm is written once, from the
|
|
248
|
-
left; the placement mirrors each slot across the content box when the host
|
|
249
|
-
reads right to left, and each child's own tree mirrors inside it.
|
|
250
|
-
- **Absolutely positioned children** are not the algorithm's. They are laid
|
|
251
|
-
out by yoga's own rules against a holder standing in for the padding box.
|
|
252
|
-
- **A layout that throws**, or answers something that is not a size, is
|
|
253
|
-
reported like a handler's throw (`reportLayoutError`, `onUncaughtError`
|
|
254
|
-
included) and turned off: the box is laid out as flexbox in the same
|
|
255
|
-
frame, and stays so until its style names another layout. A name nobody
|
|
256
|
-
registered is reported once and is flexbox too.
|
|
257
|
-
- **Not a host:** a scroll pane — whose size is its viewport's, not its
|
|
258
|
-
content's; the layout goes on a box inside it — a window, and anything
|
|
259
|
-
that measures its own content.
|
|
260
|
-
|
|
261
|
-
## 4. The content floors inside the children
|
|
262
|
-
|
|
263
|
-
The floors (#249) are measured over the window's yoga tree: one pass with no
|
|
264
|
-
room on offer, read back by `contentSpan`, written onto every flex item as
|
|
265
|
-
its minimum. A host's children are not in that tree, so the window's walkers
|
|
266
|
-
stop at a host and treat it as the leaf yoga sees — its minimum is what its
|
|
267
|
-
algorithm answers at `at-most 0` — and the children are measured tree by
|
|
268
|
-
tree:
|
|
269
|
-
|
|
270
|
-
- **Widths**, before the window's own pass (`_measureHostChildWidths`):
|
|
271
|
-
each stale child's tree with no room, the shrink borrowed, its span read
|
|
272
|
-
back into the extents of everything stale inside it. The child's own
|
|
273
|
-
extent is its min-content width — what `intrinsicSizes()` hands its
|
|
274
|
-
algorithm, and so what `equal-row` squeezes to and `masonry`'s minimum is
|
|
275
|
-
made of. Deepest hosts first, so a host inside a card has its children's
|
|
276
|
-
extents when the card's pass asks it for its minimum.
|
|
277
|
-
- **Heights**, at the width the placement gave each child
|
|
278
|
-
(`_measureHostChildHeights`) — a minimum height is a height for a width,
|
|
279
|
-
and the width is the algorithm's. Only for a child that can come out
|
|
280
|
-
shorter than its content: one naming a height of its own, or one its rect
|
|
281
|
-
gives less height than its content takes at the rect's width. A masonry's
|
|
282
|
-
cards and a stretched grid row's items, sized to fit, squeeze nothing, so
|
|
283
|
-
the common case pays nothing. A live resize defers them with the rest.
|
|
284
|
-
|
|
285
|
-
## 5. Cost
|
|
286
|
-
|
|
287
|
-
**No layout, no cost.** Every added branch is behind `_layoutHosts.size` or
|
|
288
|
-
a `_host !== null` read; the sweep returns on an empty registry.
|
|
289
|
-
|
|
290
|
-
**With a layout.** Measured on the seam as built
|
|
291
|
-
(`scripts/bench/grid/bench.mjs`, in the in-process server with real text
|
|
292
|
-
shaping): 300 cards of 9 yoga nodes each, with wrapping titles, laid out by
|
|
293
|
-
`masonry` and, for comparison, as a flex-wrap row. Layout time per frame;
|
|
294
|
-
the changes and resizes are medians of nine:
|
|
295
|
-
|
|
296
|
-
| case | masonry | flex-wrap |
|
|
297
|
-
| ----------------------- | ------: | --------: |
|
|
298
|
-
| mount | 72.3 ms | 64.8 ms |
|
|
299
|
-
| one card's text changed | 1.6 ms | 13.0 ms |
|
|
300
|
-
| window resized 1000↔900 | 4.2 ms | 20.3 ms |
|
|
301
|
-
|
|
302
|
-
The two are different algorithms, so read the last two rows as "not
|
|
303
|
-
slower" before "faster". What they do show is structural: each child's tree is a
|
|
304
|
-
containment boundary, so a change inside one card re-lays out that card
|
|
305
|
-
(one text measure of 300), and no cycle can form inside a pass — a child
|
|
306
|
-
cannot reach the constraints its host gives it. Container queries needed a
|
|
307
|
-
runtime pin for that guarantee; here it holds by construction.
|
|
308
|
-
|
|
309
|
-
## 6. The spike
|
|
310
|
-
|
|
311
|
-
`calculateLayout` nested inside a measure call on another tree works — the
|
|
312
|
-
components package's `<Html>` flex has shipped on it for months — and the
|
|
313
|
-
questions it left were the ones above: how often yoga asks (once per pass in
|
|
314
|
-
a column and as a shrinking row item), whether a clean frame re-asks (no),
|
|
315
|
-
and whether a detached child's dirt can reach the host (only by a sweep or a
|
|
316
|
-
dirtied callback, and only the sweep covers a child never laid out).
|
|
317
|
-
|
|
318
|
-
## 7. Not done
|
|
319
|
-
|
|
320
|
-
- **A scroll pane or a window as the host.** Both are sized by something
|
|
321
|
-
other than their content; a box inside them is the host today.
|
|
322
|
-
- **Baselines** across a host (`alignItems: 'baseline'` in its parent), which
|
|
323
|
-
needs yoga's baseline function.
|
|
324
|
-
- **Fragmentation.** Houdini's layout API has break tokens for pages and
|
|
325
|
-
columns; nothing here paginates.
|
|
326
|
-
- **Min-content heights** for an algorithm to read; only widths are handed
|
|
327
|
-
over.
|
|
328
|
-
- **Subgrid.** [Grid](grid-layout.md) is built on this seam, and this is
|
|
329
|
-
the part of it the seam cannot carry: a subgrid's items take part in the
|
|
330
|
-
parent grid's track sizing, and an algorithm here sees a child as
|
|
331
|
-
something to measure, never its children.
|
|
332
|
-
- **Hot reload** keeps a mounted host on the definition it started with
|
|
333
|
-
until its style names a layout again — the contract registered elements
|
|
334
|
-
have.
|