react-x11 2.52.0 → 2.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +10 -10
  2. package/docs/README.md +160 -314
  3. package/docs/accessibility.md +176 -229
  4. package/docs/appearance.md +77 -176
  5. package/docs/click-to-component.md +18 -51
  6. package/docs/clipboard.md +40 -59
  7. package/docs/components.md +518 -1036
  8. package/docs/dbus.md +61 -133
  9. package/docs/debugging.md +152 -245
  10. package/docs/desktop-calendar.md +51 -88
  11. package/docs/desktop.md +222 -347
  12. package/docs/devtools.md +50 -167
  13. package/docs/drag-and-drop.md +126 -204
  14. package/docs/ecosystem/animation.md +91 -125
  15. package/docs/ecosystem/data-fetching.md +38 -53
  16. package/docs/ecosystem/dev-tooling.md +159 -202
  17. package/docs/ecosystem/forms.md +61 -97
  18. package/docs/ecosystem/headless.md +64 -89
  19. package/docs/ecosystem/i18n.md +18 -21
  20. package/docs/ecosystem/icons.md +93 -126
  21. package/docs/ecosystem/layout.md +42 -52
  22. package/docs/ecosystem/routing.md +18 -29
  23. package/docs/ecosystem/state.md +71 -88
  24. package/docs/ecosystem/testing.md +131 -157
  25. package/docs/ecosystem/theming.md +119 -165
  26. package/docs/ecosystem.md +145 -179
  27. package/docs/elements.md +1091 -1774
  28. package/docs/embedding.md +83 -132
  29. package/docs/events.md +412 -695
  30. package/docs/extending.md +784 -1326
  31. package/docs/eyedropper.md +63 -122
  32. package/docs/filedialog.md +71 -107
  33. package/docs/frame.md +195 -339
  34. package/docs/gl.md +65 -62
  35. package/docs/globalmenu.md +63 -146
  36. package/docs/macos.md +531 -3234
  37. package/docs/notifications.md +33 -47
  38. package/docs/packaging.md +166 -309
  39. package/docs/permissions.md +9 -14
  40. package/docs/react-features.md +81 -106
  41. package/docs/remote.md +32 -76
  42. package/docs/scale.md +72 -186
  43. package/docs/styling.md +329 -547
  44. package/docs/system.md +200 -280
  45. package/docs/testing.md +63 -83
  46. package/docs/typescript.md +6 -48
  47. package/docs/uri-schemes.md +67 -107
  48. package/docs/wayland-backend.md +200 -459
  49. package/docs/windows-integrations.md +75 -131
  50. package/docs/windows.md +343 -1367
  51. package/package.json +3 -2
  52. package/src/Reconciler.js +2 -2
  53. package/src/a11y.js +1 -1
  54. package/src/backend/context2d.js +3 -3
  55. package/src/bootstrap.js +1 -1
  56. package/src/cocoa/app.js +6 -6
  57. package/src/cocoa/bezels.js +1 -1
  58. package/src/cocoa/fonts.js +1 -1
  59. package/src/cocoa/globalmenu.js +1 -1
  60. package/src/cocoa/keymap.js +1 -1
  61. package/src/cocoa/panewindow.js +1 -1
  62. package/src/cocoa/player.js +1 -1
  63. package/src/cocoa/presenter.js +2 -2
  64. package/src/cocoa/promotion.js +1 -1
  65. package/src/cocoa/relaunch.js +1 -1
  66. package/src/cocoa/threaded.js +1 -1
  67. package/src/cocoa/video.js +1 -1
  68. package/src/cocoa/window.js +4 -4
  69. package/src/components/Menu.js +7 -2
  70. package/src/components/Switch.js +1 -1
  71. package/src/components/native.js +1 -1
  72. package/src/dnd.js +2 -2
  73. package/src/glnodes.js +1 -1
  74. package/src/grid.js +1 -1
  75. package/src/layouts.js +1 -1
  76. package/src/nodes/animation.js +1 -1
  77. package/src/nodes/invalidate.js +1 -1
  78. package/src/nodes/layouthost.js +1 -1
  79. package/src/nodes/video.js +19 -8
  80. package/src/nodes/window/droptarget.js +1 -1
  81. package/src/pacing.js +1 -1
  82. package/src/refresh/index.js +2 -1
  83. package/src/types/video.d.ts +5 -2
  84. package/src/videobackend.js +28 -0
  85. package/src/videoframes.js +29 -16
  86. package/src/wayland/app.js +27 -0
  87. package/src/wayland/video.js +376 -0
  88. package/src/win32/a11y.js +1 -1
  89. package/src/win32/app.js +2 -2
  90. package/src/win32/fonts.js +1 -1
  91. package/src/win32/glarea.js +4 -4
  92. package/src/win32/overlay.js +1 -1
  93. package/src/win32/panewindow.js +3 -3
  94. package/src/win32/shell.js +1 -1
  95. package/src/win32/window.js +2 -2
  96. package/src/windowid.js +1 -1
  97. package/src/xvideo.js +189 -0
  98. package/docs/architecture/accessibility.md +0 -470
  99. package/docs/architecture/animation.md +0 -707
  100. package/docs/architecture/container-queries.md +0 -525
  101. package/docs/architecture/custom-layout.md +0 -334
  102. package/docs/architecture/drag-and-drop.md +0 -941
  103. package/docs/architecture/element-layer-contents.md +0 -622
  104. package/docs/architecture/frame-pacing.md +0 -255
  105. package/docs/architecture/grid-layout.md +0 -358
  106. package/docs/architecture/protocol-efficiency.md +0 -357
  107. package/docs/architecture/video.md +0 -1149
  108. package/docs/architecture/windowed-regions.md +0 -854
  109. package/docs/glx.md +0 -137
  110. package/docs/wayland.md +0 -545
  111. package/docs/windows-embedding.md +0 -271
  112. package/docs/windows-gl.md +0 -221
@@ -1,525 +0,0 @@
1
- # Container queries: what the window queries already answer, and what a per-node one would cost
2
-
3
- _Research document, 2026-09-06. Written against the `master` checkout at c9ce902
4
- (react-x11 2.6.1; `src/nodes.js` @ 12,354 lines, `src/styles.js` @ 1,694). The
5
- one measurement is a yoga-only spike (§7), run in-process on one machine (Apple
6
- M1 Pro, node 26); nothing here was run against an X server, because nothing
7
- here needs one. file:line references are to that checkout and will drift._
8
-
9
- _Implemented after this was written, along the lines of §5 and §8:
10
- [styling.md](../styling.md#container-queries) is the reference and
11
- `examples/container-queries.jsx` the demonstration._
12
-
13
- ---
14
-
15
- ## 0. TL;DR
16
-
17
- **Feasible, and most of the machinery already exists.** A window size query
18
- (`'@width >= 600'`, [styling.md](../styling.md#window-size-queries)) and a
19
- container query differ in exactly one structural way: a window's size is an
20
- _input_ to layout and a container's size is an _output_ of it. So a container
21
- query cannot be resolved where the window queries are resolved — before the
22
- pass — but has to be resolved _after_ a pass, and the tree laid out once more
23
- if any answer changed. That loop is not new either: it is what `realize()`
24
- already runs for an auto-sized window that carries `@width` blocks
25
- (`nodes.js:9480-9503`, the "measure, re-resolve, measure once more" rule).
26
- Everything else — the grammar, the per-window registry, per-node
27
- re-resolution, pushing a style diff into yoga, the layout diff that turns a
28
- reflow into damage — is reusable as it stands.
29
-
30
- Findings, compressed:
31
-
32
- 1. **The cost is bounded and small.** After a pass, reading the size of 300
33
- registered containers costs 0.04 ms; a second yoga pass costs in proportion
34
- to the subtrees whose style actually changed — 0.46 ms when one card's
35
- block flips, 3.0 ms when all 300 do (≈ 1.7× a window-resize pass), on a
36
- 3,600-node tree. Frames where no answer crosses a threshold — every scroll
37
- notch, every hover, most resize frames — pay the reads and nothing else
38
- (§6).
39
-
40
- 2. **The cycle is the real design question, and it is a detection problem
41
- here rather than a prevention one.** CSS makes a container query safe by
42
- _containment_: `container-type: inline-size` computes the container's
43
- inline size as if it had no content, so nothing a descendant does can move
44
- it. Yoga has no containment. It does have three constructions where a
45
- node's size is already independent of its subtree (an explicit size, a
46
- flex item with its content floor off, a scroller) and one clean signal
47
- when a design is unsatisfiable: the second look disagrees with the first.
48
- Settle once, pin the answer, and warn naming the node (§3).
49
-
50
- 3. **On targeting** (§4): the recommendation is CSS's rule — the **nearest
51
- ancestor that declares itself a container**, with an optional **name** to
52
- skip past nearer ones — as one grammar, `'@container [name] width >= 400'`,
53
- with the declaration a _style property_ (`container: true | 'sidebar'`).
54
- **Direct parent** is rejected as a grammar (one wrapper `<box>` changes the
55
- answer silently, which is the exact failure that made CSS abandon element
56
- queries), though `container: true` on the parent spells the same thing
57
- explicitly. **Self** queries are possible only paint-only and are deferred.
58
- Making **scrollers implicit containers** is rejected in favour of
59
- documenting them as the ideal thing to declare one on. And a general
60
- `onLayout` on any `<box>` — today only a scroller has `onViewport` — is the
61
- React-side seam that should ship first regardless.
62
-
63
- ## 1. What exists: the window size queries
64
-
65
- The pieces, because the design below is mostly "the same, later":
66
-
67
- - **Grammar and resolution** (`styles.js`). `SIZE_QUERY` (`:479`) parses
68
- `@width|@height op number`; `parseQuery` (`:498`) memoizes per key;
69
- `resolveQueries(style, { size, supports })` (`:561`) merges every matching
70
- block in declaration order over the base and returns the base itself when
71
- nothing matches, so the identity fast path survives. `validateStyle`
72
- (`:602`) makes a malformed key an error at `createStyles` time, and a query
73
- block — unlike a state block — may carry layout properties, because it is
74
- only ever re-evaluated inside a layout pass a resize already required.
75
- - **Per-node registration** (`nodes.js`). `_syncStyle` (`:1975`) registers a
76
- node with its window's `_sizeQueryNodes` when `styleHasSizeQueries`
77
- (`:2000-2007`) and folds the matching blocks into `_baseStyle` _before_ the
78
- state blocks (`:2036-2042`), so a `:hover` inside the wide layout still
79
- wins over the wide layout. `_registerSizeQueries` (`:2391`) is the recursive
80
- hook `insertBefore` runs so a subtree attached after a resize matches
81
- against the size the window is now.
82
- - **Per-node re-resolution.** `_sizeQueriesChanged` (`:2708`) is the whole of
83
- it: `_syncStyle` again, `localTextStyleChanged` for the node-local text
84
- props, and `applyLayoutStyle(yoga, style, before)` (`styles.js:1543`) to
85
- push only the layout properties that moved into yoga — which dirties the
86
- yoga node, so the next pass re-lays out that subtree and nothing else.
87
- - **The window's trigger.** `_resolveSizeQueries(deviceW, deviceH)`
88
- (`:10987`) divides by the scale, compares with the last `querySize`,
89
- re-resolves every registered node and marks the content floors dirty
90
- (`_floorsDirty`). It runs from `flush()` before `_layoutRoot` (`:11385`),
91
- and twice inside `realize()`'s auto-size `measure()` loop (`:9488`,
92
- `:9503`) — the second time _after_ a measurement, re-measuring once if the
93
- answer moved. That second call is a container query in everything but name:
94
- it resolves blocks against a size the layout produced.
95
- - **Damage.** `_absolutizeChildren` (`:11416`) runs once after whatever
96
- layout passes the frame needed, and `_assignAbs` (`:3719`) claims the old
97
- and new rect of every node the pass moved through `layoutDiffSink`. Any
98
- number of yoga passes before that walk cost nothing in damage terms.
99
-
100
- One property of the current path is worth naming because the container path
101
- must not inherit it: every registered node re-runs `_syncStyle` whenever the
102
- window's logical size changes at all (`:11002`), not only when one of _its_
103
- blocks changed answer. At window-resize frequency that is fine. A container's
104
- size can move on far more frames than a window's, so the container path has
105
- to gate the re-sync on an answer actually flipping (§5).
106
-
107
- ## 2. What is different about a container
108
-
109
- Four things, and only the first is structural:
110
-
111
- 1. **Input versus output.** The window's size is known before the pass; a
112
- container's size is what the pass produces. Resolution moves to _after_
113
- `_layoutRoot`, and a changed answer costs one more pass.
114
- 2. **A target.** Which ancestor. The window query has one implicit target per
115
- tree; a container query needs a rule for finding its container (§4) and a
116
- cache for the answer, dropped when the tree above the node changes — the
117
- shape `direction` already has (`:2482`, resolved nearest-first through
118
- `parent`, cached, invalidated by `_redirectSubtree`).
119
- 3. **Many registries.** One `_sizeQueryNodes` per window becomes a set of
120
- dependents per window, each with a pointer to the container(s) it asks
121
- about, and a remembered last answer per block.
122
- 4. **The cycle** (§3). A window cannot be moved by the styles inside it; a
123
- container can.
124
-
125
- ## 3. The cycle
126
-
127
- CSS's answer is `container-type: inline-size`: size containment on that axis,
128
- so the container is sized as if empty and no descendant's style can feed back
129
- into the size the descendants are querying. Yoga has no containment. What it
130
- has instead are the three constructions under which a node's size on an axis
131
- is _already_ independent of its subtree:
132
-
133
- - an explicit `width` / `height`;
134
- - a flex item whose size the parent distributes, with its **content floor
135
- off** — `minWidth: 0` plus a fixed `flexBasis` (or `flex: 1`, which expands
136
- to basis `0`). The automatic minimum is the one route by which content
137
- feeds back into a flex item's size, and `minWidth: 0` is its documented
138
- switch ([elements.md](../elements.md#a-floor-the-content-decides));
139
- - a **scroller**: `overflow: 'scroll'` implies `minWidth: 0`,
140
- `minHeight: 0` and — when it grows — `flexBasis: 0`
141
- (`styles.js:1343`, `resolveComputedStyle`). Content overflows a scroller
142
- rather than growing it, which is containment in everything but name. It is
143
- the natural container, and the docs should say so.
144
-
145
- Where none of those hold, the design can be unsatisfiable. The spike's last
146
- section (§7) shows the shape: a card at 230 px whose block, below 250, sets a
147
- `flexBasis` that makes it 310, whose block above 250 sets one that makes it
148
- 230 — and so on for as many passes as one is willing to run. No number of
149
- passes settles it, and no size satisfies it.
150
-
151
- Three ways to live with that:
152
-
153
- | option | verdict |
154
- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
155
- | **(i) Settle once, pin, warn.** Re-lay out once; if a second look disagrees, keep the answer and say so in development. | **Recommended.** Mirrors `realize()`'s rule. CSS makes the bad case impossible; this makes it _visible_, with the container, the dependent and the axis named. The common responsive pattern (less content when narrow) is monotone and settles on the first re-layout, so a warning means a genuine design contradiction. |
156
- | **(ii) Paint-only container blocks**, like state blocks. | No cycle can form, but the main use case — a column becoming a row, a sidebar folding away — is layout. Rejected as the default; it is the right rule for _self_ queries (§4 D). |
157
- | **(iii) A development-time check that the queried axis is pinned** by one of the three constructions above. | Rejected: it is a heuristic (a parent with an explicit width pins a child's row too, invisibly to a per-node check), it would refuse designs that settle, and the runtime detection in (i) is exact. |
158
-
159
- The pinning rule in (i) needs one refinement, or it strobes. Take a frame
160
- that starts with blocks resolved as B₀ and lays out to size S₁. Resolving
161
- against S₁ gives B₁. If B₁ = B₀, done — the common case, and no extra pass.
162
- Otherwise apply B₁ and lay out again, to S₂; resolve to B₂. If B₂ = B₁,
163
- settled at one extra pass. If not, the design oscillates — and _leaving it
164
- there_ means the next frame's first pass starts from B₁/S₂, finds B₂, applies
165
- it, gets S₁, finds B₁ again, and every frame that runs layout flips the block.
166
- The fix is to remember the pair (S₂, B₁) on the dependent and hold B₁ while
167
- the first pass keeps producing S₂; the query re-evaluates when the container's
168
- size moves for some _other_ reason. A few lines, and the warning fires once.
169
-
170
- Nested containers need the cap stated correctly too. If an outer container's
171
- dependent flips, the second pass can legitimately move an _inner_ container
172
- past one of its own thresholds — a cascade, not an oscillation. So the loop
173
- runs up to a small fixed number of passes (three is enough for any nesting
174
- seen in the examples), and "oscillation" is defined as a dependent returning
175
- to an answer it already held _this frame_, which is exact where "changed on
176
- the second look" would pin a cascade wrongly.
177
-
178
- ## 4. Targeting the container
179
-
180
- The question this document was asked. Each row is a way for a style to say
181
- _which_ box it is asking about.
182
-
183
- | how it reads | setup | survives a wrapper `<box>`? | lookup | cycle exposure | CSS transfer |
184
- | -------------------------------------------------------------- | -------------------------------------- | ------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------- | ----------------------------------------------------- |
185
- | **A. nearest declared ancestor** — `'@container width >= 400'` | `container: true` on the ancestor | yes | walk `parent` to the first declaring node; cached like `direction` | as §3 | `@container (min-width: 400px)` |
186
- | **B. named** — `'@container sidebar width >= 400'` | `container: 'sidebar'` on the ancestor | yes | same walk, filtered by name; skips past nearer containers | as §3 | `container-name` / `@container sidebar (…)` |
187
- | **C. direct parent** — `'@parent width >= 400'` | none | **no** | one property read | as §3 | none — this is the element-query proposal CSS dropped |
188
- | **D. self** — `'@self width >= 400'` | none | n/a | none | the self-cycle — safe only if the block is **paint-only** | none |
189
- | **E. nearest scroller, implicitly** | none | yes, unless the wrapper scrolls | walk `parent` to the first `overflow: 'scroll'` | none — a scroller's size is content-independent | none |
190
- | **F. React-side** — `onLayout` + context, `useContainerSize()` | a component around the subtree | yes | none in the renderer | none (it is a state update) | `ResizeObserver` + a hook |
191
- | **G. by kind or role** — `'@container [role=list] …'` | none | yes | walk with a predicate | as §3 | relational selectors |
192
-
193
- **Recommendation: A and B as one grammar; F beside them as the seam; D
194
- deferred; C, E and G rejected.** The reasons, one per row:
195
-
196
- - **A** is CSS's default and the one that survives composition. A component
197
- that adapts to where it is put must not have its answer changed by the
198
- consumer wrapping it in a `<box>` for a border, and an explicit declaration
199
- on the container is the only thing that guarantees that.
200
- - **B** exists for the case A gets wrong: a component that is itself a
201
- container for its own rows (a card sizing its columns) would otherwise
202
- capture every unnamed query inside it, including the ones its children
203
- meant for the page. A name lets a query reach past. The one decision B
204
- forces is what an **unmatched name** does. CSS's rule is that the query
205
- simply does not apply, and that is the right one here too — it is what lets
206
- the same component render inside and outside the `sidebar` — even though it
207
- means a typo in a name is not an error. Worth a line in the debugging
208
- guide; not worth making the component unusable outside its context.
209
- An **unnamed** query with no declared container above it, on the other
210
- hand, should be a development-time **error**: CSS silently falls back to
211
- the viewport there, but this codebase already has a spelling for the window
212
- (`'@width'`), so the fallback would only ever hide a forgotten declaration.
213
- - **C** reads as the least setup, and it is the closest to "adapt to the slot
214
- I was put in" — which is the point of the feature. But the parent of a
215
- reusable component is whatever its consumer rendered, and the moment that
216
- consumer adds a wrapper for padding or a border the query is asking a
217
- different box a question with a different answer, with nothing in the
218
- program having changed its mind. CSS went through exactly this — element
219
- queries were proposed for a decade and never shipped, and container
220
- queries shipped with a required declaration. A with `container: true` on
221
- the parent is C spelled so that it cannot drift.
222
- - **D** is the one shape CSS cannot offer at all, because of the self-cycle,
223
- and this codebase _could_: the paint-only restriction state blocks already
224
- live under (`STATE_PROPS`, `styles.js:429`) is exactly the restriction that
225
- makes a self query safe. The use cases are thin — a card muting its title
226
- when narrow, a border that drops at a small size — so it is a possible
227
- later addition, not part of this design.
228
- - **E** is tempting because the scroller is the one place a cycle cannot
229
- form (§3). But _implicit_ containers mean the presence of a scroller
230
- between a node and its intended container changes which box answers, with
231
- no declaration anywhere to point at. Explicit wins; the docs should
232
- recommend declaring `container` on the scroller.
233
- - **F** is not a competitor but the seam ("decide it, then leave a way out",
234
- AGENTS.md). Decisions that are not styles — how many columns to
235
- virtualize, which component to render at all — need a size in React, and
236
- today only a scroller offers one (`onViewport`, `nodes.js:6216`, fired from
237
- layout via `setImmediate`). Generalizing it to `onLayout` on any `<box>` is
238
- small: `_assignAbs` already detects the size change; it lacks only the
239
- deferred notification. What F cannot do is what makes the renderer-native
240
- query worth having: its answer lands a **frame late**. A size change
241
- reaches React after the commit that caused it, React re-renders, a second
242
- commit re-lays out — one frame drawn at the wrong layout on every threshold
243
- crossing during a drag. A renderer query resolves inside the same flush and
244
- is never seen wrong.
245
- - **G** is a relational selector, which styling.md's "Decided" section rules
246
- out for the whole style system: inheritance and node state, no matching
247
- against other nodes.
248
-
249
- **Where the declaration lives.** As a **style property**, not an element
250
- prop. AGENTS.md draws the line — props carry element semantics, `style`
251
- carries the CSS-like vocabulary — and `container` is CSS vocabulary
252
- (`container-type`/`container-name`). It also has to be settable from _inside_
253
- a query block (a box that becomes a container only in the wide layout), which
254
- only a style property can be. It joins `STYLE_PROPS` beside `cursor` and
255
- `hitSlop` as a property that is neither yoga nor paint, and DevTools' style
256
- editor lists it for free through `STYLE_PROP_NAMES`.
257
-
258
- **Units.** A threshold is in logical pixels like every other length in the
259
- block, and it should be divided out by the **dependent's** `scale`, not the
260
- container's: the author wrote `'@container width >= 400'` next to their own
261
- `width: 400`, and the two numbers must mean the same thing. The two scales
262
- differ only when the dependent adds a `scale` of its own under the container,
263
- which is rare and then reads as CSS's rule that query lengths resolve in the
264
- querying element's context.
265
-
266
- ## 5. Mechanism, at the hook points
267
-
268
- `styles.js`:
269
-
270
- - `CONTAINER_QUERY = /^@container(?:\s+([A-Za-z_][\w-]*))?\s+(width|height)\s*(>=|<=|>|<)\s*(\d+(?:\.\d+)?)$/`,
271
- parsed into `{ kind: 'container', name, axis, op, value }` by `parseQuery`;
272
- `sizeMatches` is reused unchanged. `validateStyle`'s message grows a third
273
- example. `styleHasContainerQueries` joins the two existing `hasQueryOfKind`
274
- predicates, and `resolveQueries` takes a third option, `containers`: a map
275
- from name (`''` for unnamed) to the container's logical size, built per
276
- dependent by the node. `resolveQueries` is public through `react-x11/style`;
277
- widening its options object is the shape it already has.
278
- - `container` joins `STYLE_PROPS`, validated as `true | string`.
279
-
280
- `nodes.js`, on `Node`:
281
-
282
- - `_syncStyle` records `_containerQueried` and the set of names the style asks
283
- about, and registers the node with `root._containerQueryNodes`, exactly as
284
- `_queried` does today (`:2000-2007`). `_registerSizeQueries` (`:2391`)
285
- re-registers it on insert.
286
- - `_containerFor(name)`: walk `parent` to the first node whose `style.container`
287
- matches (`true` for unnamed, the string for named), cached on the node and
288
- dropped when the node is inserted and when any ancestor's `container` value
289
- changes (a subtree walk from the changed node, the way `_redirectSubtree`
290
- (`:2506`) drops `direction`). Containers are declared rarely and changed
291
- more rarely, so the walk is not a hot path.
292
- - Per dependent, the last boolean answer of each block, so that a re-sync runs
293
- only when one flips (§1's caveat). `_sizeQueriesChanged` (`:2708`) is then
294
- the re-sync as it stands.
295
-
296
- `nodes.js`, on `WindowNode`, in `flush()`:
297
-
298
- - `_resolveContainerQueries()` runs **after** the layout passes (`_layoutRoot`
299
- or `_applyContentFloors`, `:11385-11392`) and **before**
300
- `_absolutizeChildren(0, 0)` (`:11416`). For each dependent it reads its
301
- containers' `yoga.getComputedWidth()`/`Height()` — yoga's computed values,
302
- not `abs`, which is still the previous frame's — divides by the dependent's
303
- scale, evaluates each block, and calls `_sizeQueriesChanged()` on the
304
- dependents whose answer set changed. Their yoga nodes are dirty now; the
305
- same style route a prop change takes marks their content floors stale
306
- (`_floorsStale` / `_floorsDirty`, `:2350`), and one more layout step runs.
307
- Then the §3 loop: re-read, pin what returned to an earlier answer, stop at
308
- the cap. `_absolutizeChildren` runs once, against the final layout, so the
309
- damage claims are right by construction.
310
- - The same call goes inside `realize()`'s `measure()` loop (`:9480-9503`),
311
- under the same cap, so an auto-sized window's natural size accounts for the
312
- container blocks its content will resolve to.
313
- - Windows are boundaries, as for every other walk (`!child.isWindow`): a
314
- `<popup>` inside a container is a root of its own and asks its own window.
315
- - `yoga.hasNewLayout()` is **not** the way to find containers that moved: it
316
- is the scroller's witness, "consumed here and nowhere else" (`:6089`), and
317
- reading it for 3,600 nodes costs seven times what reading the registered
318
- containers does (§6). The container pass keeps its own last-size
319
- bookkeeping and never calls `markLayoutSeen`.
320
-
321
- One pre-existing quirk the new blocks would inherit, and the same PR should
322
- fix: `flattenStyle` (`styles.js:663`) merges a **state** block that appears in
323
- two entries of a style array but **replaces** a query block
324
- (`isState(key) && into[key]`), so `[{ '@width >= 600': { a } }, { '@width >= 600': { b } }]`
325
- keeps only `b`. Query keys should merge the same way.
326
-
327
- ## 6. Cost
328
-
329
- The spike (§7) builds 3,613 yoga nodes — 12 sections of 25 cards of 11 leaves,
330
- the shape of a card gallery — lays it out, then changes one layout property on
331
- some of the cards and times the second pass. Medians of 7, in-process,
332
- Apple M1 Pro:
333
-
334
- | case | second pass | nodes yoga re-laid out |
335
- | ---------------------------------------------- | ----------: | ---------------------: |
336
- | nothing changed | 0.005 ms | 1 |
337
- | 1 card flipped a block | 0.46 ms | 93 |
338
- | 3 cards (1%) | 0.65 ms | 297 |
339
- | 30 cards (10%) | 2.1 ms | 2,007 |
340
- | all 300 cards | 3.0 ms | 3,613 |
341
- | the window resized by 100 px (for scale) | 1.8 ms | 3,613 |
342
- | **reading** width of 300 registered containers | 0.044 ms | |
343
- | reading width + height of all 3,613 nodes | 0.82 ms | |
344
- | `hasNewLayout()` over all 3,613 nodes | 0.33 ms | |
345
-
346
- What this says: yoga's second pass is incremental — it re-lays out the dirtied
347
- subtrees and reuses the cached layout of everything else — so the extra pass
348
- costs in proportion to how much of the design actually changed, and even the
349
- worst case (every card flips at once, which is a threshold crossing during a
350
- resize, not a steady state) is under twice the resize pass it follows. The
351
- counts are larger than the flipped subtrees alone because a card sits in a
352
- wrapping row: a card that grows taller re-stretches every card on its line,
353
- which is yoga doing exactly what the layout asks and what a real design pays
354
- too.
355
- Reading the containers is negligible **if** it reads the registered
356
- containers directly and not a whole-tree flag walk.
357
-
358
- What it does not measure, and where the real cost is:
359
-
360
- - The JS half of a flip: `_syncStyle` on each dependent whose answer moved —
361
- flatten, tokens, scale, `_retarget` — and the incremental content floors
362
- for those nodes. This is the same per-node cost the window path pays today
363
- for _every_ registered node on every resize; here it is paid per _flipped_
364
- node, which is what the answer gating buys.
365
- - The content floors are the expensive half of any relayout — three extra
366
- passes were 21 of a 44 ms frame on 3,600 nodes before they were made
367
- incremental (`_applyContentFloors`, `nodes.js:9257`). A container flip
368
- re-measures only the flipped nodes' extents, but a flip that changes a
369
- row's main-axis items does re-run the width pass for that row.
370
-
371
- Frames where no block crosses a threshold — every scroll notch, every hover,
372
- every resize frame between two breakpoints — pay the reads: tens of
373
- microseconds at hundreds of containers.
374
-
375
- ## 7. The spike
376
-
377
- Reproduced so the numbers can be re-run: save it at the repo root and run it
378
- with `node`; it needs only `src/yoga.js`.
379
-
380
- ```js
381
- import { loadLayout, Yoga } from './src/yoga.js';
382
- await loadLayout();
383
- const all = [],
384
- cards = [];
385
- const node = (parent, i, setup) => {
386
- const n = Yoga.Node.create();
387
- setup?.(n);
388
- parent?.insertChild(n, i);
389
- all.push(n);
390
- return n;
391
- };
392
- const root = node(null, 0, (n) => {
393
- n.setWidth(1200);
394
- n.setHeight(800);
395
- });
396
- for (let s = 0; s < 12; s++) {
397
- const sec = node(root, s, (n) => {
398
- n.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
399
- n.setFlexWrap(Yoga.WRAP_WRAP);
400
- n.setGap(Yoga.GUTTER_ALL, 8);
401
- n.setPadding(Yoga.EDGE_ALL, 8);
402
- });
403
- for (let c = 0; c < 25; c++) {
404
- const card = node(sec, c, (n) => {
405
- n.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
406
- n.setFlexWrap(Yoga.WRAP_WRAP);
407
- n.setGap(Yoga.GUTTER_ALL, 4);
408
- n.setPadding(Yoga.EDGE_ALL, 6);
409
- n.setFlexGrow(1);
410
- n.setFlexBasis(200);
411
- n.setMinWidth(0);
412
- });
413
- cards.push(card);
414
- for (let l = 0; l < 11; l++)
415
- node(card, l, (n) => {
416
- n.setWidth(40);
417
- n.setHeight(20);
418
- });
419
- }
420
- }
421
- let width = 1200;
422
- const layout = () => root.calculateLayout(width, 800, Yoga.DIRECTION_LTR);
423
- const time = (fn) => {
424
- const t = performance.now();
425
- fn();
426
- return performance.now() - t;
427
- };
428
- const median = (xs) => xs.sort((a, b) => a - b)[xs.length >> 1];
429
- const trial = (name, prep) => {
430
- const xs = [];
431
- let touched;
432
- for (let i = 0; i < 7; i++) {
433
- prep(i);
434
- all.forEach((n) => n.markLayoutSeen());
435
- xs.push(time(layout));
436
- touched = all.filter((n) => n.hasNewLayout()).length;
437
- }
438
- console.log(name, median(xs).toFixed(3), 'ms', touched, 'nodes re-laid out');
439
- };
440
- const flip = (card, i) =>
441
- card.setFlexDirection(
442
- i & 1 ? Yoga.FLEX_DIRECTION_ROW : Yoga.FLEX_DIRECTION_COLUMN,
443
- );
444
- layout();
445
- trial('nothing changed', () => {});
446
- trial('1 card', (i) => flip(cards[7], i));
447
- trial('3 cards', (i) => {
448
- for (let k = 0; k < 300; k += 100) flip(cards[k], i);
449
- });
450
- trial('30 cards', (i) => {
451
- for (let k = 0; k < 300; k += 10) flip(cards[k], i);
452
- });
453
- trial('all cards', (i) => cards.forEach((c) => flip(c, i)));
454
- trial('window resized', (i) => {
455
- width = i & 1 ? 1100 : 1200;
456
- root.setWidth(width);
457
- });
458
- const reads = (name, fn) =>
459
- console.log(
460
- name,
461
- median(Array.from({ length: 7 }, () => time(fn))).toFixed(3),
462
- 'ms',
463
- );
464
- reads('read 300 containers', () => cards.forEach((c) => c.getComputedWidth()));
465
- reads('read all nodes w+h', () =>
466
- all.forEach((n) => n.getComputedWidth() + n.getComputedHeight()),
467
- );
468
- reads(
469
- 'hasNewLayout over all',
470
- () => all.filter((n) => n.hasNewLayout()).length,
471
- );
472
-
473
- // pass 1: queried 230 -> now 310 / pass 2: queried 310 -> now 230 / …
474
- // the cycle (§3): a block that moves the axis it queries never settles
475
- const c = cards[0];
476
- c.setFlexBasis(200);
477
- layout();
478
- for (let i = 0; i < 4; i++) {
479
- const w = c.getComputedWidth();
480
- c.setFlexBasis(w < 250 ? 300 : 200);
481
- layout();
482
- console.log(`pass ${i + 1}: queried ${w} -> now ${c.getComputedWidth()}`);
483
- }
484
- ```
485
-
486
- ## 8. Decisions the implementing PR has to make, and the suggested answer
487
-
488
- | question | suggested answer |
489
- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
490
- | spelling | `'@container width >= 400'`, `'@container sidebar width >= 400'`; declaration `container: true` or `container: 'sidebar'` in style. Same operators and unit as `'@width'`. |
491
- | unnamed query, no container above | development-time error naming the node, pointing at `container` and at `'@width'` for the window. |
492
- | named query, no such container above | the block does not apply. Component reuse across contexts wins over catching a typo. |
493
- | a block that never settles | pinned at the answer it first held this frame, one development warning naming container, dependent and axis; re-evaluated when the container moves for another reason (§3). |
494
- | how many extra passes | a fixed small cap (3); "oscillation" is a dependent returning to an answer it already held this frame, so a nested cascade is not mistaken for one. |
495
- | scale | thresholds divided by the dependent's `scale`. |
496
- | window boundaries | a window/popup never depends on a container in another window. |
497
- | `display: 'none'` container | nothing inside it is laid out or shown; nothing to decide. |
498
- | transitions | a flipped block's paint properties transition through `_retarget` exactly as a window query's do today. |
499
- | nested state block inside a container block | already allowed inside a window block (`validateStyle` recurses); same behaviour. |
500
- | types | a `ContainerQuery` template-literal type beside `SizeQuery` (`style.d.ts:362`); `container` typed as `true` or a string in `StyleProperties`; one line in `test/types/api.tsx`. |
501
- | docs | a section after "Window size queries" in styling.md; the "Next" list there drops its container-queries line; a scroller is named as the natural container. |
502
- | the seam | `onLayout` on any `<box>` (generalizing `_reportViewport`'s trigger to `_assignAbs`), shipped first and on its own — useful with or without the query. |
503
-
504
- Tests, in the shape of `test/size-queries.test.js`: a container crossing a
505
- threshold under a window resize changes a dependent's style **and** its `abs`;
506
- a `_layoutPasses` count that grows by one only on the frame an answer flips;
507
- a wrapper `<box>` inserted between container and dependent changing nothing;
508
- a named query reaching past a nearer unnamed container; an unnamed query with
509
- no container throwing; a flattened array merging two blocks under the same
510
- query key; and the oscillating design warning once and holding still across
511
- frames.
512
-
513
- ## 9. Verdict
514
-
515
- Container queries are feasible on this renderer at moderate size — on the
516
- order of 60 lines in `styles.js`, 150 in `nodes.js`, the declaration and type
517
- test, a doc section and a test file — and with no new primitive: the
518
- after-layout resolve-and-relayout loop is the one `realize()` already runs,
519
- made general and gated on answers actually flipping. The one thing CSS has
520
- that yoga does not, containment, is replaced by exact runtime detection of
521
- the only failure containment prevents, at a cost of one development warning
522
- where CSS would have made the design unwritable. The targeting grammar should
523
- be CSS's — nearest declared ancestor, optional name — for the reason CSS
524
- arrived at it: it is the only rule under which a component's answer does not
525
- depend on how its consumer chose to wrap it.