@pilates/core 1.0.1 → 1.1.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/dist/algorithm/index.d.ts +31 -0
- package/dist/algorithm/index.d.ts.map +1 -1
- package/dist/algorithm/index.js +85 -1
- package/dist/algorithm/index.js.map +1 -1
- package/dist/algorithm/round.d.ts +13 -0
- package/dist/algorithm/round.d.ts.map +1 -1
- package/dist/algorithm/round.js +17 -0
- package/dist/algorithm/round.js.map +1 -1
- package/dist/algorithm/spineless/flex-grammar.d.ts +394 -0
- package/dist/algorithm/spineless/flex-grammar.d.ts.map +1 -0
- package/dist/algorithm/spineless/flex-grammar.js +2373 -0
- package/dist/algorithm/spineless/flex-grammar.js.map +1 -0
- package/dist/algorithm/spineless/grammar.d.ts +150 -0
- package/dist/algorithm/spineless/grammar.d.ts.map +1 -0
- package/dist/algorithm/spineless/grammar.js +144 -0
- package/dist/algorithm/spineless/grammar.js.map +1 -0
- package/dist/algorithm/spineless/layout.d.ts +130 -0
- package/dist/algorithm/spineless/layout.d.ts.map +1 -0
- package/dist/algorithm/spineless/layout.js +755 -0
- package/dist/algorithm/spineless/layout.js.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.bench.d.ts +25 -0
- package/dist/algorithm/spineless/order-maintenance.bench.d.ts.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.bench.js +78 -0
- package/dist/algorithm/spineless/order-maintenance.bench.js.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.d.ts +192 -0
- package/dist/algorithm/spineless/order-maintenance.d.ts.map +1 -0
- package/dist/algorithm/spineless/order-maintenance.js +294 -0
- package/dist/algorithm/spineless/order-maintenance.js.map +1 -0
- package/dist/algorithm/spineless/priority-queue.bench.d.ts +17 -0
- package/dist/algorithm/spineless/priority-queue.bench.d.ts.map +1 -0
- package/dist/algorithm/spineless/priority-queue.bench.js +57 -0
- package/dist/algorithm/spineless/priority-queue.bench.js.map +1 -0
- package/dist/algorithm/spineless/priority-queue.d.ts +73 -0
- package/dist/algorithm/spineless/priority-queue.d.ts.map +1 -0
- package/dist/algorithm/spineless/priority-queue.js +149 -0
- package/dist/algorithm/spineless/priority-queue.js.map +1 -0
- package/dist/algorithm/spineless/runtime.d.ts +239 -0
- package/dist/algorithm/spineless/runtime.d.ts.map +1 -0
- package/dist/algorithm/spineless/runtime.js +458 -0
- package/dist/algorithm/spineless/runtime.js.map +1 -0
- package/dist/algorithm/spineless/style-dirty.d.ts +65 -0
- package/dist/algorithm/spineless/style-dirty.d.ts.map +1 -0
- package/dist/algorithm/spineless/style-dirty.js +75 -0
- package/dist/algorithm/spineless/style-dirty.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +27 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +61 -0
- package/dist/inspect.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flexbox layout expressed as an attribute grammar.
|
|
3
|
+
*
|
|
4
|
+
* Current slice (v16b) covers:
|
|
5
|
+
*
|
|
6
|
+
* - flex-direction: `row`, `column`, `row-reverse`, `column-reverse`
|
|
7
|
+
* - flex-grow / flex-shrink / flex-basis (v3-v4)
|
|
8
|
+
* - padding / margin / gap (v5)
|
|
9
|
+
* - justify-content + align-items / align-self (v6)
|
|
10
|
+
* - flex-wrap (v7) — single-line wrap and multi-line packing,
|
|
11
|
+
* each line independently distributed / justified / aligned
|
|
12
|
+
* - positionType: `'absolute'` (v8) — out-of-flow children
|
|
13
|
+
* positioned against the parent's OUTER box via `style.position`
|
|
14
|
+
* and `style.margin`. Width / height resolve from explicit
|
|
15
|
+
* style, from opposing edges (`left`+`right` or `top`+`bottom`),
|
|
16
|
+
* or fall back to 0. Absolute children are filtered out of every
|
|
17
|
+
* in-flow computation (flex distribution, justify leftover,
|
|
18
|
+
* wrap line packing).
|
|
19
|
+
* - align-content (v9) — for multi-line wrap containers, the cross-
|
|
20
|
+
* axis leftover is distributed among / around the lines per
|
|
21
|
+
* `flex-start` / `flex-end` / `center` / `space-between` /
|
|
22
|
+
* `space-around` / `stretch`
|
|
23
|
+
* - flex-wrap: `wrap-reverse` (v10) — the line stack is mirrored
|
|
24
|
+
* on the cross axis
|
|
25
|
+
* - flex-direction: `row-reverse` / `column-reverse` (v11) — the
|
|
26
|
+
* main axis runs from the container's main END; each in-flow
|
|
27
|
+
* child's main position is reflected across the inner-main box,
|
|
28
|
+
* mirroring the imperative `flipMainAxis`
|
|
29
|
+
* - min / max size clamping (v12) — every node's main size, cross
|
|
30
|
+
* size, and an absolute child's width / height are clamped to
|
|
31
|
+
* the node's own `[minWidth/Height, maxWidth/Height]`. v12a
|
|
32
|
+
* covered the single-shot sites (non-distributed main size,
|
|
33
|
+
* cross size, absolute children); v12b folds min/max into the
|
|
34
|
+
* flex-distribution freeze loop and the wrap line packer — an
|
|
35
|
+
* item whose proportional grow / shrink target breaches a clamp
|
|
36
|
+
* is frozen at its bound and its share redistributed, iterating
|
|
37
|
+
* to a fixpoint, exactly as the imperative `distributeGrow` /
|
|
38
|
+
* `distributeShrink`.
|
|
39
|
+
* - `'auto'` main size (v13) — a non-measured `'auto'` main-axis
|
|
40
|
+
* size resolves to 0 (mirroring `resolveHypotheticalMainSize`);
|
|
41
|
+
* the root's `'auto'` axis resolves from the caller-supplied
|
|
42
|
+
* `available` size, modelled as a root input Field so a terminal
|
|
43
|
+
* resize is incremental.
|
|
44
|
+
* - `'auto'` cross size + `align-items: stretch` (v14) — an
|
|
45
|
+
* `'auto'` cross-axis size is 0 under a non-stretch align, but
|
|
46
|
+
* `stretch` (the default) resizes it to fill the line's inner
|
|
47
|
+
* cross, mirroring the imperative `crossAlignItemsInLine`
|
|
48
|
+
* stretch branch.
|
|
49
|
+
* - `aspectRatio` (v15) — an `'auto'` axis whose perpendicular
|
|
50
|
+
* axis is an explicit number derives `width = height × ratio` /
|
|
51
|
+
* `height = width ÷ ratio`, mirroring `effectivePreferredSize`.
|
|
52
|
+
* A derived axis is definite (not content-sized) — so it is not
|
|
53
|
+
* stretched.
|
|
54
|
+
* - measure-func leaves (v16) — a childless node with a measure
|
|
55
|
+
* function resolves its `'auto'` axes by calling the measurer.
|
|
56
|
+
* v16a covers the MAIN axis (main free, cross constrained,
|
|
57
|
+
* mirroring `resolveHypotheticalMainSize`); v16b the CROSS axis
|
|
58
|
+
* (cross constrained `AtMost` the parent inner cross, main free
|
|
59
|
+
* with a hint, mirroring `naturalCrossSize`). A measured cross
|
|
60
|
+
* feeds the wrap line cross-size aggregation and the non-stretch
|
|
61
|
+
* cross size; `align-items: stretch` still overrides it.
|
|
62
|
+
*
|
|
63
|
+
* With v16 the grammar covers the full imperative `'auto'` /
|
|
64
|
+
* measure / `aspectRatio` resolution: the Spineless engine is a
|
|
65
|
+
* drop-in for `calculateLayout` on real content-sized trees.
|
|
66
|
+
*
|
|
67
|
+
* Fields emitted per node:
|
|
68
|
+
*
|
|
69
|
+
* - `width` — main-axis size when parent is `row`, cross-axis size
|
|
70
|
+
* when parent is `column`. Cross axis reads
|
|
71
|
+
* `style.width` verbatim. Main axis is the result of
|
|
72
|
+
* flex distribution when the parent has any child with
|
|
73
|
+
* grow > 0, shrink > 0, or numeric `flexBasis`;
|
|
74
|
+
* otherwise it equals the basis.
|
|
75
|
+
* - `height` — symmetric to `width`.
|
|
76
|
+
* - `left` — position relative to parent. For row-parent children
|
|
77
|
+
* this is the main-axis cursor: `padLeft + myMarginLeft
|
|
78
|
+
* + sum_priors(marginLeft + width + marginRight) +
|
|
79
|
+
* i*gapColumn`. For column-parent children it's the
|
|
80
|
+
* cross-axis position: `padLeft + myMarginLeft`. Root
|
|
81
|
+
* is at 0.
|
|
82
|
+
* - `top` — symmetric to `left`.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
import { MeasureMode } from '../../measure-func.js';
|
|
87
|
+
import { isReverse, mainAxis } from '../axis.js';
|
|
88
|
+
import { field } from './grammar.js';
|
|
89
|
+
/**
|
|
90
|
+
* True iff `node` participates in its parent's flex flow — neither
|
|
91
|
+
* out-of-flow (`positionType: 'absolute'`) nor hidden
|
|
92
|
+
* (`display: 'none'`). A `display: 'none'` node is emitted no rules
|
|
93
|
+
* at all and skipped everywhere an in-flow sibling is consulted,
|
|
94
|
+
* mirroring the imperative algorithm — which `continue`s it in
|
|
95
|
+
* `layoutChildren` and never writes its `_layout`.
|
|
96
|
+
*/
|
|
97
|
+
function isInFlow(node) {
|
|
98
|
+
return node.style.positionType !== 'absolute' && node.style.display !== 'none';
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build the topological per-node emitter — the `visit` recursion and
|
|
102
|
+
* the input-field helpers it closes over — bound to one
|
|
103
|
+
* `EmitContext`. `buildFlexGrammar` and the subtree fragment builders
|
|
104
|
+
* share this, so a fragment emits rules byte-identical to a full
|
|
105
|
+
* build.
|
|
106
|
+
*
|
|
107
|
+
* @internal
|
|
108
|
+
*/
|
|
109
|
+
function makeEmitter(ctx) {
|
|
110
|
+
const { grammar, allFields, styleInputs, boundary, mainDistributionByParent } = ctx;
|
|
111
|
+
// Register (once) the input Field for the root's caller-supplied
|
|
112
|
+
// `available` size on one axis. Its `compute` reads `ctx.available`
|
|
113
|
+
// live, so a caller that mutates that object and `markDirty`s the
|
|
114
|
+
// field drives an incremental relayout after a terminal resize.
|
|
115
|
+
function availableInput(root, axis) {
|
|
116
|
+
const f = field(root, `available:${axis}`);
|
|
117
|
+
if (boundary?.has(f))
|
|
118
|
+
return f;
|
|
119
|
+
if (!grammar.has(f)) {
|
|
120
|
+
grammar.set(f, {
|
|
121
|
+
deps: [],
|
|
122
|
+
compute: () => ctx.available[axis] ?? 0,
|
|
123
|
+
});
|
|
124
|
+
ctx.availableInputs[axis] = f;
|
|
125
|
+
}
|
|
126
|
+
return f;
|
|
127
|
+
}
|
|
128
|
+
// Register (once) the `measure:main` input Field for a measure-leaf
|
|
129
|
+
// node's `'auto'` MAIN-axis size (v16a). Mirrors the imperative
|
|
130
|
+
// `resolveHypotheticalMainSize` measure branch: the measurer is
|
|
131
|
+
// called with the main axis FREE (`Undefined`) and the cross axis
|
|
132
|
+
// constrained `AtMost` the cross constraint — the cross style size
|
|
133
|
+
// when numeric, else the parent's inner cross. `mainProp` is the
|
|
134
|
+
// node's main-axis prop; `parent` supplies the inner-cross fields.
|
|
135
|
+
function measureMainInput(n, mainProp, parent) {
|
|
136
|
+
const f = field(n, 'measure:main');
|
|
137
|
+
if (boundary?.has(f))
|
|
138
|
+
return f;
|
|
139
|
+
if (!grammar.has(f)) {
|
|
140
|
+
const fn = n.getMeasureFunc();
|
|
141
|
+
const crossProp = mainProp === 'width' ? 'height' : 'width';
|
|
142
|
+
const deps = [];
|
|
143
|
+
let readCrossConstraint;
|
|
144
|
+
if (typeof n.style[crossProp] === 'number') {
|
|
145
|
+
const csInput = styleSizeInput(n, crossProp);
|
|
146
|
+
deps.push(csInput);
|
|
147
|
+
readCrossConstraint = (read) => read(csInput);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// Cross is `'auto'` — constrain to the parent's inner cross.
|
|
151
|
+
const pdir = mainAxis(parent.style.flexDirection);
|
|
152
|
+
const parentCrossF = field(parent, crossProp);
|
|
153
|
+
const padStartF = paddingInput(parent, crossStartEdge(pdir));
|
|
154
|
+
const padEndF = paddingInput(parent, crossEndEdge(pdir));
|
|
155
|
+
deps.push(parentCrossF, padStartF, padEndF);
|
|
156
|
+
readCrossConstraint = (read) => Math.max(0, read(parentCrossF) - read(padStartF) - read(padEndF));
|
|
157
|
+
}
|
|
158
|
+
grammar.set(f, {
|
|
159
|
+
deps,
|
|
160
|
+
compute: (read) => {
|
|
161
|
+
const cc = readCrossConstraint(read);
|
|
162
|
+
// Main axis free; cross axis constrained AtMost.
|
|
163
|
+
const r = mainProp === 'width'
|
|
164
|
+
? fn(0, MeasureMode.Undefined, cc, MeasureMode.AtMost)
|
|
165
|
+
: fn(cc, MeasureMode.AtMost, 0, MeasureMode.Undefined);
|
|
166
|
+
return mainProp === 'width' ? r.width : r.height;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return f;
|
|
171
|
+
}
|
|
172
|
+
// Register (once) the `measure:cross` input Field for a
|
|
173
|
+
// measure-leaf node's `'auto'` CROSS-axis size (v16b). Mirrors the
|
|
174
|
+
// imperative `naturalCrossSize`: the measurer is called with the
|
|
175
|
+
// cross axis constrained `AtMost` the parent's inner cross and the
|
|
176
|
+
// main axis FREE (`Undefined`) with a hint — the main style size
|
|
177
|
+
// when numeric, else the parent's inner cross. `crossProp` is the
|
|
178
|
+
// node's cross-axis prop; `parent` supplies the inner-cross fields.
|
|
179
|
+
function measureCrossInput(n, crossProp, parent) {
|
|
180
|
+
const f = field(n, 'measure:cross');
|
|
181
|
+
if (boundary?.has(f))
|
|
182
|
+
return f;
|
|
183
|
+
if (!grammar.has(f)) {
|
|
184
|
+
const fn = n.getMeasureFunc();
|
|
185
|
+
const mainProp = crossProp === 'width' ? 'height' : 'width';
|
|
186
|
+
const pdir = mainAxis(parent.style.flexDirection);
|
|
187
|
+
const parentCrossF = field(parent, crossProp);
|
|
188
|
+
const padStartF = paddingInput(parent, crossStartEdge(pdir));
|
|
189
|
+
const padEndF = paddingInput(parent, crossEndEdge(pdir));
|
|
190
|
+
const deps = [
|
|
191
|
+
parentCrossF,
|
|
192
|
+
padStartF,
|
|
193
|
+
padEndF,
|
|
194
|
+
];
|
|
195
|
+
// The main-axis hint is the main STYLE size when numeric (a
|
|
196
|
+
// raw `preferredSize`, no aspectRatio), else the parent inner
|
|
197
|
+
// cross — matching `naturalCrossSize`'s `mainHint`.
|
|
198
|
+
let mainHintInput = null;
|
|
199
|
+
if (typeof n.style[mainProp] === 'number') {
|
|
200
|
+
mainHintInput = styleSizeInput(n, mainProp);
|
|
201
|
+
deps.push(mainHintInput);
|
|
202
|
+
}
|
|
203
|
+
grammar.set(f, {
|
|
204
|
+
deps,
|
|
205
|
+
compute: (read) => {
|
|
206
|
+
const innerCross = Math.max(0, read(parentCrossF) - read(padStartF) - read(padEndF));
|
|
207
|
+
const mainHint = mainHintInput !== null ? read(mainHintInput) : innerCross;
|
|
208
|
+
// Cross axis constrained AtMost the inner cross; main free.
|
|
209
|
+
const r = crossProp === 'width'
|
|
210
|
+
? fn(innerCross, MeasureMode.AtMost, mainHint, MeasureMode.Undefined)
|
|
211
|
+
: fn(mainHint, MeasureMode.Undefined, innerCross, MeasureMode.AtMost);
|
|
212
|
+
return crossProp === 'width' ? r.width : r.height;
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return f;
|
|
217
|
+
}
|
|
218
|
+
// Resolve the input Field a node's main / cross size rule reads for
|
|
219
|
+
// its preferred size on `axis`. Regimes (the auto/numeric split is
|
|
220
|
+
// structural — a mutation across it needs a fresh build):
|
|
221
|
+
// - numeric `style[axis]` → the live `styleSizeInput`;
|
|
222
|
+
// - `'auto'` + `aspectRatio` + the perpendicular axis numeric →
|
|
223
|
+
// an `aspect:*` Field deriving the size from the other axis
|
|
224
|
+
// (v15), mirroring `effectivePreferredSize`;
|
|
225
|
+
// - `'auto'` MAIN axis of a measure-leaf → a `measure:main` Field
|
|
226
|
+
// (v16a), mirroring `resolveHypotheticalMainSize`;
|
|
227
|
+
// - `'auto'` CROSS axis of a measure-leaf → a `measure:cross`
|
|
228
|
+
// Field (v16b), mirroring `naturalCrossSize`;
|
|
229
|
+
// - `'auto'` on the root → the `available:*` input;
|
|
230
|
+
// - `'auto'` elsewhere → a constant `0` (v13 — matches the
|
|
231
|
+
// imperative fallback for a non-measured `'auto'` node).
|
|
232
|
+
function preferredSizeInput(n, axis, role, parentOfN) {
|
|
233
|
+
if (typeof n.style[axis] === 'number')
|
|
234
|
+
return styleSizeInput(n, axis);
|
|
235
|
+
if (aspectDerivable(n, axis)) {
|
|
236
|
+
const other = axis === 'width' ? 'height' : 'width';
|
|
237
|
+
const ratio = n.style.aspectRatio;
|
|
238
|
+
const otherInput = styleSizeInput(n, other);
|
|
239
|
+
const f = field(n, `aspect:${axis}`);
|
|
240
|
+
if (boundary?.has(f))
|
|
241
|
+
return f;
|
|
242
|
+
if (!grammar.has(f)) {
|
|
243
|
+
grammar.set(f, {
|
|
244
|
+
deps: [otherInput],
|
|
245
|
+
// width = height × ratio; height = width ÷ ratio.
|
|
246
|
+
compute: (read) => axis === 'width' ? read(otherInput) * ratio : read(otherInput) / ratio,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return f;
|
|
250
|
+
}
|
|
251
|
+
if (parentOfN !== null && isMeasureLeaf(n)) {
|
|
252
|
+
return role === 'main'
|
|
253
|
+
? measureMainInput(n, axis, parentOfN)
|
|
254
|
+
: measureCrossInput(n, axis, parentOfN);
|
|
255
|
+
}
|
|
256
|
+
if (parentOfN === null)
|
|
257
|
+
return availableInput(n, axis);
|
|
258
|
+
const f = field(n, `preferred:${axis}`);
|
|
259
|
+
if (boundary?.has(f))
|
|
260
|
+
return f;
|
|
261
|
+
if (!grammar.has(f)) {
|
|
262
|
+
grammar.set(f, {
|
|
263
|
+
deps: [],
|
|
264
|
+
compute: () => 0,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return f;
|
|
268
|
+
}
|
|
269
|
+
// The imperative `resolveRootAxisSize` clamps the root's size to
|
|
270
|
+
// [min, max] for an explicit / `aspectRatio` / `available`-derived
|
|
271
|
+
// axis — but its `'auto'` + no-`available` fallback returns a bare
|
|
272
|
+
// `0`, *unclamped*. This predicate flags exactly that case so the
|
|
273
|
+
// root size rule can skip the clamp and mirror the quirk (a root
|
|
274
|
+
// `minWidth` must not inflate an unavailable axis).
|
|
275
|
+
function rootAxisIsBareZero(n, axis) {
|
|
276
|
+
return (typeof n.style[axis] !== 'number' &&
|
|
277
|
+
!aspectDerivable(n, axis) &&
|
|
278
|
+
ctx.available[axis] === undefined);
|
|
279
|
+
}
|
|
280
|
+
function styleInputEntry(n) {
|
|
281
|
+
let entry = styleInputs.get(n);
|
|
282
|
+
if (entry === undefined) {
|
|
283
|
+
entry = {};
|
|
284
|
+
styleInputs.set(n, entry);
|
|
285
|
+
}
|
|
286
|
+
return entry;
|
|
287
|
+
}
|
|
288
|
+
// Register (once) the leaf input Field for a node's style SIZE
|
|
289
|
+
// prop and return it. The field has no deps; its `compute` reads
|
|
290
|
+
// `node.style` live. Layout fields that read a size declare the
|
|
291
|
+
// returned field as a dependency, so a `markDirty` on it
|
|
292
|
+
// propagates precisely through `recompute()`.
|
|
293
|
+
function styleSizeInput(n, prop) {
|
|
294
|
+
const f = field(n, `style:${prop}`);
|
|
295
|
+
if (boundary?.has(f))
|
|
296
|
+
return f;
|
|
297
|
+
if (!grammar.has(f)) {
|
|
298
|
+
grammar.set(f, {
|
|
299
|
+
deps: [],
|
|
300
|
+
compute: () => n.style[prop],
|
|
301
|
+
});
|
|
302
|
+
styleInputEntry(n)[prop] = f;
|
|
303
|
+
}
|
|
304
|
+
return f;
|
|
305
|
+
}
|
|
306
|
+
// Register (once) the leaf input Field for a node's min / max
|
|
307
|
+
// size clamp. `min*` reads `style.min{Width,Height}` (a number,
|
|
308
|
+
// default 0); `max*` reads `style.max{Width,Height}` and folds the
|
|
309
|
+
// `undefined` "no upper bound" sentinel to `Infinity`, so every
|
|
310
|
+
// consumer can clamp with one unconditional `clampMinMax`.
|
|
311
|
+
function minMaxInput(n, prop) {
|
|
312
|
+
const f = field(n, `style:${prop}`);
|
|
313
|
+
if (boundary?.has(f))
|
|
314
|
+
return f;
|
|
315
|
+
if (!grammar.has(f)) {
|
|
316
|
+
const isMax = prop === 'maxWidth' || prop === 'maxHeight';
|
|
317
|
+
grammar.set(f, {
|
|
318
|
+
deps: [],
|
|
319
|
+
compute: () => isMax ? (n.style[prop] ?? Number.POSITIVE_INFINITY) : n.style[prop],
|
|
320
|
+
});
|
|
321
|
+
styleInputEntry(n)[prop] = f;
|
|
322
|
+
}
|
|
323
|
+
return f;
|
|
324
|
+
}
|
|
325
|
+
// Register (once) the leaf input Field for a flex weight
|
|
326
|
+
// (`flexGrow` / `flexShrink`). Mutating a weight between two
|
|
327
|
+
// POSITIVE values (or two zeros) is an in-regime change driven via
|
|
328
|
+
// this field; crossing the zero boundary flips whether the parent
|
|
329
|
+
// flex-distributes and so needs a fresh `buildFlexGrammar()`.
|
|
330
|
+
function flexWeightInput(n, prop) {
|
|
331
|
+
const f = field(n, `style:${prop}`);
|
|
332
|
+
if (boundary?.has(f))
|
|
333
|
+
return f;
|
|
334
|
+
if (!grammar.has(f)) {
|
|
335
|
+
grammar.set(f, {
|
|
336
|
+
deps: [],
|
|
337
|
+
compute: () => n.style[prop],
|
|
338
|
+
});
|
|
339
|
+
styleInputEntry(n)[prop] = f;
|
|
340
|
+
}
|
|
341
|
+
return f;
|
|
342
|
+
}
|
|
343
|
+
// Register (once) the leaf input Field for a container's `gap`
|
|
344
|
+
// along one output axis (`gapRow` separates column-stacked items,
|
|
345
|
+
// `gapColumn` separates row-stacked items).
|
|
346
|
+
function gapInput(n, axis) {
|
|
347
|
+
const prop = axis === 'row' ? 'gapRow' : 'gapColumn';
|
|
348
|
+
const f = field(n, `style:${prop}`);
|
|
349
|
+
if (boundary?.has(f))
|
|
350
|
+
return f;
|
|
351
|
+
if (!grammar.has(f)) {
|
|
352
|
+
grammar.set(f, {
|
|
353
|
+
deps: [],
|
|
354
|
+
compute: () => n.style[prop],
|
|
355
|
+
});
|
|
356
|
+
styleInputEntry(n)[prop] = f;
|
|
357
|
+
}
|
|
358
|
+
return f;
|
|
359
|
+
}
|
|
360
|
+
// Register (once) the leaf input Field for one `padding` edge of a
|
|
361
|
+
// container (`edge` is a [top,right,bottom,left] index). Defaults
|
|
362
|
+
// to 0 when that edge is unset.
|
|
363
|
+
function paddingInput(n, edge) {
|
|
364
|
+
const f = field(n, `style:padding:${edge}`);
|
|
365
|
+
if (boundary?.has(f))
|
|
366
|
+
return f;
|
|
367
|
+
if (!grammar.has(f)) {
|
|
368
|
+
grammar.set(f, {
|
|
369
|
+
deps: [],
|
|
370
|
+
compute: () => n.style.padding[edge] ?? 0,
|
|
371
|
+
});
|
|
372
|
+
const entry = styleInputEntry(n);
|
|
373
|
+
if (entry.padding === undefined)
|
|
374
|
+
entry.padding = [];
|
|
375
|
+
entry.padding[edge] = f;
|
|
376
|
+
}
|
|
377
|
+
return f;
|
|
378
|
+
}
|
|
379
|
+
// Register (once) the leaf input Field for one `margin` edge of a
|
|
380
|
+
// node (`edge` is a [top,right,bottom,left] index). Defaults to 0
|
|
381
|
+
// when that edge is unset.
|
|
382
|
+
function marginInput(n, edge) {
|
|
383
|
+
const f = field(n, `style:margin:${edge}`);
|
|
384
|
+
if (boundary?.has(f))
|
|
385
|
+
return f;
|
|
386
|
+
if (!grammar.has(f)) {
|
|
387
|
+
grammar.set(f, {
|
|
388
|
+
deps: [],
|
|
389
|
+
compute: () => n.style.margin[edge] ?? 0,
|
|
390
|
+
});
|
|
391
|
+
const entry = styleInputEntry(n);
|
|
392
|
+
if (entry.margin === undefined)
|
|
393
|
+
entry.margin = [];
|
|
394
|
+
entry.margin[edge] = f;
|
|
395
|
+
}
|
|
396
|
+
return f;
|
|
397
|
+
}
|
|
398
|
+
function visit(node, parent, indexInParent, priorSiblings) {
|
|
399
|
+
const width = field(node, 'width');
|
|
400
|
+
const height = field(node, 'height');
|
|
401
|
+
const left = field(node, 'left');
|
|
402
|
+
const top = field(node, 'top');
|
|
403
|
+
// All four flex-direction values are supported (v11): the base
|
|
404
|
+
// axis (`row` / `column`) drives field assignment; reverse
|
|
405
|
+
// (`row-reverse` / `column-reverse`) flips child main positions.
|
|
406
|
+
// Absolute children short-circuit the in-flow flex pipeline:
|
|
407
|
+
// they're positioned independently against the parent's OUTER
|
|
408
|
+
// box (no padding subtraction) using their own `style.position`
|
|
409
|
+
// and `style.margin`. Their width / height can be `'auto'`, with
|
|
410
|
+
// size derived from opposing edges or falling back to 0 — so the
|
|
411
|
+
// in-flow "explicit numeric size" precondition is relaxed here.
|
|
412
|
+
if (parent !== null && node.style.positionType === 'absolute') {
|
|
413
|
+
emitAbsoluteRules(grammar, styleSizeInput, marginInput, minMaxInput, parent, node, width, height, left, top);
|
|
414
|
+
allFields.push({ node, width, height, left, top });
|
|
415
|
+
const childCount = node.getChildCount();
|
|
416
|
+
const childSiblings = [];
|
|
417
|
+
for (let i = 0; i < childCount; i++) {
|
|
418
|
+
const child = node.getChild(i);
|
|
419
|
+
// A `display: 'none'` child is laid out by nothing — emit it
|
|
420
|
+
// no rules and do not recurse, mirroring the imperative
|
|
421
|
+
// `layoutChildren` `continue`.
|
|
422
|
+
if (child.style.display === 'none')
|
|
423
|
+
continue;
|
|
424
|
+
if (child.style.positionType === 'absolute') {
|
|
425
|
+
visit(child, node, -1, []);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
visit(child, node, childSiblings.length, [...childSiblings]);
|
|
429
|
+
childSiblings.push(child);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// `'auto'` width / height are supported (v13): a non-measured
|
|
435
|
+
// `'auto'` axis resolves to 0, the root's to its caller-supplied
|
|
436
|
+
// `available`. See `preferredSizeInput`.
|
|
437
|
+
// The parent's base axis decides which of {width, height} is the
|
|
438
|
+
// main-axis size for THIS child (and which of {left, top} is the
|
|
439
|
+
// main-axis position). Root is parent-less and treats both axes as
|
|
440
|
+
// cross — sizes from style, positions at 0. `parentDirection` is
|
|
441
|
+
// the base axis (`mainAxis` collapses `*-reverse` onto `row` /
|
|
442
|
+
// `column`); `parentReverse` records whether the parent runs its
|
|
443
|
+
// main axis backwards, applied as a post-hoc position flip.
|
|
444
|
+
const parentDirection = parent === null ? null : mainAxis(parent.style.flexDirection);
|
|
445
|
+
const parentReverse = parent !== null && isReverse(parent.style.flexDirection);
|
|
446
|
+
const mainSizeField = parentDirection === 'column' ? height : width;
|
|
447
|
+
const crossSizeField = parentDirection === 'column' ? width : height;
|
|
448
|
+
const mainPosField = parentDirection === 'column' ? top : left;
|
|
449
|
+
const crossPosField = parentDirection === 'column' ? left : top;
|
|
450
|
+
const mainSizeName = parentDirection === 'column' ? 'height' : 'width';
|
|
451
|
+
// Spacing inputs for this child. Both the parent's padding and
|
|
452
|
+
// this child's own margin are modelled as leaf input Fields (see
|
|
453
|
+
// `paddingInput` / `marginInput`) — one per [top,right,bottom,
|
|
454
|
+
// left] edge — so each consumer declares the edge it reads as a
|
|
455
|
+
// dependency and a `setPadding` / `setMargin` propagates
|
|
456
|
+
// precisely through `recompute()`. The fields below are the
|
|
457
|
+
// edges THIS child's layout reads: parent padding on the main /
|
|
458
|
+
// cross axes, and this child's own main-start / cross-start /
|
|
459
|
+
// cross-end margins. They are `null` for the root (which has no
|
|
460
|
+
// parent and takes the constant path in every position rule).
|
|
461
|
+
// (Structural mutations — flex-direction, flex-wrap on/off,
|
|
462
|
+
// justify / align category — still need a fresh
|
|
463
|
+
// `buildFlexGrammar()`.)
|
|
464
|
+
const padMainStartF = parent === null ? null : paddingInput(parent, mainStartEdge(parentDirection));
|
|
465
|
+
const padMainEndF = parent === null ? null : paddingInput(parent, mainEndEdge(parentDirection));
|
|
466
|
+
const padCrossStartF = parent === null ? null : paddingInput(parent, crossStartEdge(parentDirection));
|
|
467
|
+
const padCrossEndF = parent === null ? null : paddingInput(parent, crossEndEdge(parentDirection));
|
|
468
|
+
const myMarginMainStartF = parent === null ? null : marginInput(node, mainStartEdge(parentDirection));
|
|
469
|
+
const myMarginCrossStartF = parent === null ? null : marginInput(node, crossStartEdge(parentDirection));
|
|
470
|
+
const myMarginCrossEndF = parent === null ? null : marginInput(node, crossEndEdge(parentDirection));
|
|
471
|
+
// Alignment for this child: justify-content lives on the parent,
|
|
472
|
+
// applies along the main axis once per line. align-items lives
|
|
473
|
+
// on the parent; align-self overrides per child (with 'auto'
|
|
474
|
+
// falling back to align-items).
|
|
475
|
+
const justify = parent === null ? 'flex-start' : parent.style.justifyContent;
|
|
476
|
+
const align = parent === null
|
|
477
|
+
? 'auto'
|
|
478
|
+
: node.style.alignSelf === 'auto'
|
|
479
|
+
? parent.style.alignItems
|
|
480
|
+
: node.style.alignSelf;
|
|
481
|
+
// Cross-axis size. For a numeric cross style, an `aspectRatio`-
|
|
482
|
+
// derived cross, the `'auto'` root (sized from `available`), or a
|
|
483
|
+
// content-`'auto'` cross under a non-stretch align, the size is
|
|
484
|
+
// the resolved input clamped to the node's own [min, max]
|
|
485
|
+
// (v12/v15). `align-items: stretch` (the default) instead
|
|
486
|
+
// resizes a CONTENT-`'auto'` cross size to fill the line's inner
|
|
487
|
+
// cross (v14) — mirroring the imperative `crossAlignItemsInLine`
|
|
488
|
+
// stretch branch. An aspectRatio-derived cross is definite, so it
|
|
489
|
+
// is not stretched. For a non-wrap parent the line cross IS the
|
|
490
|
+
// parent's inner cross; a wrapping parent overrides
|
|
491
|
+
// `crossSizeField` below with the per-line value.
|
|
492
|
+
const crossKey = parentDirection === 'column' ? 'width' : 'height';
|
|
493
|
+
const crossIsContentAuto = typeof node.style[crossKey] !== 'number' && !aspectDerivable(node, crossKey);
|
|
494
|
+
const crossSizeInput = preferredSizeInput(node, crossKey, 'cross', parent);
|
|
495
|
+
const minCrossInput = minMaxInput(node, crossKey === 'width' ? 'minWidth' : 'minHeight');
|
|
496
|
+
const maxCrossInput = minMaxInput(node, crossKey === 'width' ? 'maxWidth' : 'maxHeight');
|
|
497
|
+
if (crossIsContentAuto && parent !== null && align === 'stretch') {
|
|
498
|
+
const parentCrossF = field(parent, crossKey);
|
|
499
|
+
grammar.set(crossSizeField, {
|
|
500
|
+
deps: [
|
|
501
|
+
parentCrossF,
|
|
502
|
+
padCrossStartF,
|
|
503
|
+
padCrossEndF,
|
|
504
|
+
myMarginCrossStartF,
|
|
505
|
+
myMarginCrossEndF,
|
|
506
|
+
minCrossInput,
|
|
507
|
+
maxCrossInput,
|
|
508
|
+
],
|
|
509
|
+
compute: (read) => {
|
|
510
|
+
const innerCross = Math.max(0, read(parentCrossF) - read(padCrossStartF) - read(padCrossEndF));
|
|
511
|
+
const lineInner = innerCross - read(myMarginCrossStartF) - read(myMarginCrossEndF);
|
|
512
|
+
return clampMinMax(Math.max(0, lineInner), read(minCrossInput), read(maxCrossInput));
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
else if (parent === null && rootAxisIsBareZero(node, crossKey)) {
|
|
517
|
+
// `'auto'` root, no `available` → bare 0, unclamped (see
|
|
518
|
+
// `rootAxisIsBareZero`).
|
|
519
|
+
grammar.set(crossSizeField, {
|
|
520
|
+
deps: [crossSizeInput],
|
|
521
|
+
compute: (read) => read(crossSizeInput),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
grammar.set(crossSizeField, {
|
|
526
|
+
deps: [
|
|
527
|
+
crossSizeInput,
|
|
528
|
+
minCrossInput,
|
|
529
|
+
maxCrossInput,
|
|
530
|
+
],
|
|
531
|
+
compute: (read) => clampMinMax(read(crossSizeInput), read(minCrossInput), read(maxCrossInput)),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
// When the parent has flex-wrap='wrap', all three position fields
|
|
535
|
+
// (mainSize, mainPos, crossPos) flow through a single per-line
|
|
536
|
+
// helper that packs the line set on demand. The helper depends on
|
|
537
|
+
// the parent's main-axis size (line capacity), the parent's
|
|
538
|
+
// cross-axis size (for the single-line-wrap case), and only on
|
|
539
|
+
// the constant sibling style data — bases, margins, cross sizes
|
|
540
|
+
// are all captured inline. The dep graph stays compact (parent
|
|
541
|
+
// size fields only) at the cost of redoing the packing once per
|
|
542
|
+
// child read.
|
|
543
|
+
if (parent !== null && parent.style.flexWrap !== 'nowrap') {
|
|
544
|
+
// Capture the in-flow siblings and this child's index among
|
|
545
|
+
// them (both structural — a fresh build is needed if children
|
|
546
|
+
// are inserted / removed). Every per-sibling value the line
|
|
547
|
+
// packer reads — basis / main / cross size, grow / shrink
|
|
548
|
+
// weights, margins — is a declared input-field dep, so a
|
|
549
|
+
// size / flex / spacing mutation on any sibling propagates.
|
|
550
|
+
const crossKeyName = parentDirection === 'column' ? 'width' : 'height';
|
|
551
|
+
const mainStart = mainStartEdge(parentDirection);
|
|
552
|
+
const mainEnd = mainEndEdge(parentDirection);
|
|
553
|
+
const crossStart = crossStartEdge(parentDirection);
|
|
554
|
+
const crossEnd = crossEndEdge(parentDirection);
|
|
555
|
+
const wrapSibs = [];
|
|
556
|
+
let myIndex = -1;
|
|
557
|
+
for (let i = 0; i < parent.getChildCount(); i++) {
|
|
558
|
+
const sib = parent.getChild(i);
|
|
559
|
+
if (!isInFlow(sib))
|
|
560
|
+
continue;
|
|
561
|
+
if (sib === node)
|
|
562
|
+
myIndex = wrapSibs.length;
|
|
563
|
+
wrapSibs.push({
|
|
564
|
+
node: sib,
|
|
565
|
+
flexBasisInput: styleSizeInput(sib, 'flexBasis'),
|
|
566
|
+
mainInput: preferredSizeInput(sib, mainSizeName, 'main', parent),
|
|
567
|
+
crossInput: preferredSizeInput(sib, crossKeyName, 'cross', parent),
|
|
568
|
+
growInput: flexWeightInput(sib, 'flexGrow'),
|
|
569
|
+
shrinkInput: flexWeightInput(sib, 'flexShrink'),
|
|
570
|
+
marginMainStartInput: marginInput(sib, mainStart),
|
|
571
|
+
marginMainEndInput: marginInput(sib, mainEnd),
|
|
572
|
+
marginCrossStartInput: marginInput(sib, crossStart),
|
|
573
|
+
marginCrossEndInput: marginInput(sib, crossEnd),
|
|
574
|
+
minInput: minMaxInput(sib, mainSizeName === 'width' ? 'minWidth' : 'minHeight'),
|
|
575
|
+
maxInput: minMaxInput(sib, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight'),
|
|
576
|
+
minCrossInput: minMaxInput(sib, crossKeyName === 'width' ? 'minWidth' : 'minHeight'),
|
|
577
|
+
maxCrossInput: minMaxInput(sib, crossKeyName === 'width' ? 'maxWidth' : 'maxHeight'),
|
|
578
|
+
crossIsContentAuto: typeof sib.style[crossKeyName] !== 'number' && !aspectDerivable(sib, crossKeyName),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
const parentMainField = field(parent, mainSizeName);
|
|
582
|
+
const parentCrossField = field(parent, crossKeyName);
|
|
583
|
+
// Main-axis gap separates items along the stacking axis; the
|
|
584
|
+
// cross-axis gap separates wrapped lines. Both are declared
|
|
585
|
+
// deps so a `setGap` propagates here.
|
|
586
|
+
const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
|
|
587
|
+
const crossGapInput = gapInput(parent, parentDirection === 'column' ? 'column' : 'row');
|
|
588
|
+
const wrapDeps = [
|
|
589
|
+
parentMainField,
|
|
590
|
+
parentCrossField,
|
|
591
|
+
mainGapInput,
|
|
592
|
+
crossGapInput,
|
|
593
|
+
padMainStartF,
|
|
594
|
+
padMainEndF,
|
|
595
|
+
padCrossStartF,
|
|
596
|
+
padCrossEndF,
|
|
597
|
+
];
|
|
598
|
+
for (const s of wrapSibs) {
|
|
599
|
+
wrapDeps.push(s.flexBasisInput, s.mainInput, s.crossInput, s.growInput, s.shrinkInput, s.marginMainStartInput, s.marginMainEndInput, s.marginCrossStartInput, s.marginCrossEndInput, s.minInput, s.maxInput, s.minCrossInput, s.maxCrossInput);
|
|
600
|
+
}
|
|
601
|
+
const evalWrapped = (read) => {
|
|
602
|
+
const containerMain = read(parentMainField);
|
|
603
|
+
const containerCross = read(parentCrossField);
|
|
604
|
+
const padMainStart = read(padMainStartF);
|
|
605
|
+
const padCrossStart = read(padCrossStartF);
|
|
606
|
+
const innerMain = Math.max(0, containerMain - padMainStart - read(padMainEndF));
|
|
607
|
+
const innerCross = Math.max(0, containerCross - padCrossStart - read(padCrossEndF));
|
|
608
|
+
return evaluateWrappedChild(liveWrapSiblings(wrapSibs, parent, read), myIndex, innerMain, innerCross, read(mainGapInput), read(crossGapInput), justify, parent.style.alignContent, parent.style.flexWrap === 'wrap-reverse', padMainStart, padCrossStart);
|
|
609
|
+
};
|
|
610
|
+
grammar.set(mainSizeField, {
|
|
611
|
+
deps: wrapDeps,
|
|
612
|
+
compute: (read) => evalWrapped(read).mainSize,
|
|
613
|
+
});
|
|
614
|
+
grammar.set(mainPosField, {
|
|
615
|
+
deps: wrapDeps,
|
|
616
|
+
compute: (read) => evalWrapped(read).mainPos,
|
|
617
|
+
});
|
|
618
|
+
if (parentReverse) {
|
|
619
|
+
applyReverseMainPos(grammar, parent, mainPosField, mainSizeField, mainSizeName, padMainStartF, padMainEndF);
|
|
620
|
+
}
|
|
621
|
+
grammar.set(crossPosField, {
|
|
622
|
+
deps: wrapDeps,
|
|
623
|
+
compute: (read) => evalWrapped(read).crossPos,
|
|
624
|
+
});
|
|
625
|
+
// Override the shared cross-size rule: a wrapped child's cross
|
|
626
|
+
// size depends on its own LINE's cross size (v14 stretch
|
|
627
|
+
// resize), which only the line packer knows.
|
|
628
|
+
grammar.set(crossSizeField, {
|
|
629
|
+
deps: wrapDeps,
|
|
630
|
+
compute: (read) => evalWrapped(read).crossSize,
|
|
631
|
+
});
|
|
632
|
+
allFields.push({ node, width, height, left, top });
|
|
633
|
+
// Recurse into children. Absolute children are out-of-flow:
|
|
634
|
+
// they must NOT advance the in-flow index or the priorSiblings
|
|
635
|
+
// list (the same filtering the non-wrap path does below) —
|
|
636
|
+
// otherwise an absolute child's margin / size leaks into a
|
|
637
|
+
// later in-flow sibling's main position.
|
|
638
|
+
const childCount = node.getChildCount();
|
|
639
|
+
const inFlowSiblings = [];
|
|
640
|
+
for (let i = 0; i < childCount; i++) {
|
|
641
|
+
const child = node.getChild(i);
|
|
642
|
+
if (child.style.display === 'none')
|
|
643
|
+
continue;
|
|
644
|
+
if (child.style.positionType === 'absolute') {
|
|
645
|
+
visit(child, node, -1, []);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
visit(child, node, inFlowSiblings.length, [...inFlowSiblings]);
|
|
649
|
+
inFlowSiblings.push(child);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
// Main-axis size: depends on whether the parent flex-distributes
|
|
655
|
+
// its children. A parent flex-distributes when ANY of its children
|
|
656
|
+
// has grow > 0, shrink > 0, or a numeric flexBasis — i.e. anywhere
|
|
657
|
+
// a child's main size could legitimately differ from its raw
|
|
658
|
+
// style.{width|height}. Outside this case the main size is just
|
|
659
|
+
// the resolved basis.
|
|
660
|
+
if (parent === null || !parentNeedsFlexDistribution(parent)) {
|
|
661
|
+
// No flex distribution: this node's main size is its resolved
|
|
662
|
+
// basis, clamped to the node's own [min, max] (v12) — the
|
|
663
|
+
// imperative `buildItem` clamps the hypothetical main size even
|
|
664
|
+
// when no distribution follows. Declare deps on the node's own
|
|
665
|
+
// flexBasis + main-size + min/max inputs so a size or clamp
|
|
666
|
+
// mutation reaches this field precisely.
|
|
667
|
+
//
|
|
668
|
+
// The ROOT is special: `flexBasis` describes how a node behaves
|
|
669
|
+
// as a flex CHILD, and the root is not one. `resolveRootAxisSize`
|
|
670
|
+
// never consults it — so the root's main size is its preferred
|
|
671
|
+
// size directly, no `flexBasis` short-circuit.
|
|
672
|
+
const mainInput = preferredSizeInput(node, mainSizeName, 'main', parent);
|
|
673
|
+
const minMainInput = minMaxInput(node, mainSizeName === 'width' ? 'minWidth' : 'minHeight');
|
|
674
|
+
const maxMainInput = minMaxInput(node, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight');
|
|
675
|
+
if (parent === null) {
|
|
676
|
+
if (rootAxisIsBareZero(node, mainSizeName)) {
|
|
677
|
+
// `'auto'` root, no `available` → bare 0, unclamped.
|
|
678
|
+
grammar.set(mainSizeField, {
|
|
679
|
+
deps: [mainInput],
|
|
680
|
+
compute: (read) => read(mainInput),
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
grammar.set(mainSizeField, {
|
|
685
|
+
deps: [
|
|
686
|
+
mainInput,
|
|
687
|
+
minMainInput,
|
|
688
|
+
maxMainInput,
|
|
689
|
+
],
|
|
690
|
+
compute: (read) => clampMinMax(read(mainInput), read(minMainInput), read(maxMainInput)),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
const flexBasisInput = styleSizeInput(node, 'flexBasis');
|
|
696
|
+
grammar.set(mainSizeField, {
|
|
697
|
+
deps: [
|
|
698
|
+
flexBasisInput,
|
|
699
|
+
mainInput,
|
|
700
|
+
minMainInput,
|
|
701
|
+
maxMainInput,
|
|
702
|
+
],
|
|
703
|
+
compute: (read) => clampMinMax(resolveBasisFromRead(read, flexBasisInput, mainInput), read(minMainInput), read(maxMainInput)),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
// Flex distribution. Capture the in-flow siblings + this
|
|
709
|
+
// child's index (structural); declare every per-sibling value
|
|
710
|
+
// the distribution reads — flexBasis / main size, grow /
|
|
711
|
+
// shrink weights, main-axis margins — as input-field deps, so
|
|
712
|
+
// a size / flex / spacing mutation on any sibling propagates
|
|
713
|
+
// here. The size is derived from the parent's main-axis size
|
|
714
|
+
// minus padding (the inner main).
|
|
715
|
+
const flexMainStart = mainStartEdge(parentDirection);
|
|
716
|
+
const flexMainEnd = mainEndEdge(parentDirection);
|
|
717
|
+
const flexSibs = [];
|
|
718
|
+
let myIndex = -1;
|
|
719
|
+
for (let i = 0; i < parent.getChildCount(); i++) {
|
|
720
|
+
const sib = parent.getChild(i);
|
|
721
|
+
if (!isInFlow(sib))
|
|
722
|
+
continue;
|
|
723
|
+
if (sib === node)
|
|
724
|
+
myIndex = flexSibs.length;
|
|
725
|
+
flexSibs.push({
|
|
726
|
+
node: sib,
|
|
727
|
+
flexBasisInput: styleSizeInput(sib, 'flexBasis'),
|
|
728
|
+
mainInput: preferredSizeInput(sib, mainSizeName, 'main', parent),
|
|
729
|
+
growInput: flexWeightInput(sib, 'flexGrow'),
|
|
730
|
+
shrinkInput: flexWeightInput(sib, 'flexShrink'),
|
|
731
|
+
marginMainStartInput: marginInput(sib, flexMainStart),
|
|
732
|
+
marginMainEndInput: marginInput(sib, flexMainEnd),
|
|
733
|
+
minInput: minMaxInput(sib, mainSizeName === 'width' ? 'minWidth' : 'minHeight'),
|
|
734
|
+
maxInput: minMaxInput(sib, mainSizeName === 'width' ? 'maxWidth' : 'maxHeight'),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
const parentMainField = field(parent, mainSizeName);
|
|
738
|
+
const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
|
|
739
|
+
// Phase 12 regime check: single-line + flex-distributing qualifies
|
|
740
|
+
// for the O(N) intermediate `mainDistribution` Field. This gates
|
|
741
|
+
// both the per-cell mainSize collapse (here) and the mainPos
|
|
742
|
+
// collapse (Task 3); mainPos additionally narrows on
|
|
743
|
+
// justify-content === 'flex-start'.
|
|
744
|
+
const isPhase12DistributionRegime = parent.style.flexWrap === undefined || parent.style.flexWrap === 'nowrap';
|
|
745
|
+
let parentMainDist;
|
|
746
|
+
if (isPhase12DistributionRegime) {
|
|
747
|
+
// Emit once per parent — memoize so subsequent children reuse
|
|
748
|
+
// the same Field rather than emitting duplicate rules.
|
|
749
|
+
parentMainDist = mainDistributionByParent.get(parent);
|
|
750
|
+
if (parentMainDist === undefined) {
|
|
751
|
+
parentMainDist = emitMainDistribution(grammar, parent, flexSibs, parentMainField, mainGapInput, padMainStartF, padMainEndF, marginInput, parentDirection);
|
|
752
|
+
mainDistributionByParent.set(parent, parentMainDist);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (parentMainDist !== undefined) {
|
|
756
|
+
// Phase 12: cell mainSize is a trivial index read into the
|
|
757
|
+
// parent's pre-computed distribution array — O(1) dep edge
|
|
758
|
+
// instead of O(N siblings).
|
|
759
|
+
const myIndexCapture = myIndex;
|
|
760
|
+
grammar.set(mainSizeField, {
|
|
761
|
+
deps: [parentMainDist],
|
|
762
|
+
compute: (read) => read(parentMainDist).sizes[myIndexCapture],
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
// Non-qualifying regime (wrap): keep today's per-cell rule with
|
|
767
|
+
// full sibling deps — distributeMainAxis called inline.
|
|
768
|
+
const deps = [
|
|
769
|
+
parentMainField,
|
|
770
|
+
mainGapInput,
|
|
771
|
+
padMainStartF,
|
|
772
|
+
padMainEndF,
|
|
773
|
+
];
|
|
774
|
+
for (const s of flexSibs) {
|
|
775
|
+
deps.push(s.flexBasisInput, s.mainInput, s.growInput, s.shrinkInput, s.marginMainStartInput, s.marginMainEndInput, s.minInput, s.maxInput);
|
|
776
|
+
}
|
|
777
|
+
grammar.set(mainSizeField, {
|
|
778
|
+
deps,
|
|
779
|
+
compute: (read) => {
|
|
780
|
+
const innerMain = Math.max(0, read(parentMainField) - read(padMainStartF) - read(padMainEndF));
|
|
781
|
+
const siblings = liveFlexSiblings(flexSibs, read);
|
|
782
|
+
return distributeMainAxis(siblings, innerMain, read(mainGapInput))[myIndex];
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
// Main-axis position. Two regimes:
|
|
788
|
+
// - Default (justify === 'flex-start'): a child's main position
|
|
789
|
+
// depends only on its prior siblings' main sizes — a constant
|
|
790
|
+
// offset (padding + own margins + sum of prior margins + gaps)
|
|
791
|
+
// plus the read of prior sizes. This is the v1-v5 dep
|
|
792
|
+
// pattern; no value redistribution along the main axis.
|
|
793
|
+
// - Any other justify value: leftover space is computed from
|
|
794
|
+
// ALL siblings' final main sizes, then distributed as a
|
|
795
|
+
// leading offset and/or extra gap. mainPos now depends on
|
|
796
|
+
// every sibling's main size and on the parent's main size.
|
|
797
|
+
if (parent === null || indexInParent === 0) {
|
|
798
|
+
if (parent === null) {
|
|
799
|
+
// Root is parent-less — anchor at 0.
|
|
800
|
+
grammar.set(mainPosField, {
|
|
801
|
+
deps: [],
|
|
802
|
+
compute: () => 0,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
else if (justify === 'flex-start') {
|
|
806
|
+
// Phase 12: read directly from the parent's mainDistribution if
|
|
807
|
+
// it was emitted (parent flex-distributes + single-line).
|
|
808
|
+
// Fallback to today's padding+margin rule when parent didn't qualify.
|
|
809
|
+
const parentMainDist = mainDistributionByParent.get(parent);
|
|
810
|
+
if (parentMainDist !== undefined) {
|
|
811
|
+
const myIndexCapture = priorSiblings.length; // in-flow index
|
|
812
|
+
grammar.set(mainPosField, {
|
|
813
|
+
deps: [parentMainDist],
|
|
814
|
+
compute: (read) => read(parentMainDist).positions[myIndexCapture],
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
grammar.set(mainPosField, {
|
|
819
|
+
deps: [padMainStartF, myMarginMainStartF],
|
|
820
|
+
compute: (read) => read(padMainStartF) + read(myMarginMainStartF),
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
// First child but parent uses non-default justify. Leading
|
|
826
|
+
// offset still depends on leftover, which depends on every
|
|
827
|
+
// sibling's main size.
|
|
828
|
+
emitJustifiedMainPos(grammar, parent, mainPosField, mainSizeName, justify, indexInParent, parentDirection, gapInput(parent, parentDirection === 'column' ? 'row' : 'column'), padMainStartF, padMainEndF, marginInput);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
else if (justify === 'flex-start') {
|
|
832
|
+
// Phase 12: read directly from the parent's mainDistribution if it
|
|
833
|
+
// was emitted (parent flex-distributes + single-line). Fallback to
|
|
834
|
+
// today's prior-siblings rule when the parent didn't qualify.
|
|
835
|
+
const parentMainDist = mainDistributionByParent.get(parent);
|
|
836
|
+
if (parentMainDist !== undefined) {
|
|
837
|
+
const myIndexCapture = priorSiblings.length; // in-flow index
|
|
838
|
+
grammar.set(mainPosField, {
|
|
839
|
+
deps: [parentMainDist],
|
|
840
|
+
compute: (read) => read(parentMainDist).positions[myIndexCapture],
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
// Non-qualifying regime (wrap): keep today's prior-siblings-sum
|
|
845
|
+
// rule — size / spacing mutation on any prior sibling propagates here.
|
|
846
|
+
const priorMainSizes = priorSiblings.map((s) => field(s, mainSizeName));
|
|
847
|
+
const priorMargins = priorSiblings.map((s) => ({
|
|
848
|
+
start: marginInput(s, mainStartEdge(parentDirection)),
|
|
849
|
+
end: marginInput(s, mainEndEdge(parentDirection)),
|
|
850
|
+
}));
|
|
851
|
+
const mainGapInput = gapInput(parent, parentDirection === 'column' ? 'row' : 'column');
|
|
852
|
+
grammar.set(mainPosField, {
|
|
853
|
+
deps: [
|
|
854
|
+
mainGapInput,
|
|
855
|
+
padMainStartF,
|
|
856
|
+
myMarginMainStartF,
|
|
857
|
+
...priorMainSizes,
|
|
858
|
+
...priorMargins.flatMap((m) => [m.start, m.end]),
|
|
859
|
+
],
|
|
860
|
+
compute: (read) => {
|
|
861
|
+
let sum = read(padMainStartF) + read(myMarginMainStartF) + indexInParent * read(mainGapInput);
|
|
862
|
+
for (const m of priorMargins)
|
|
863
|
+
sum += read(m.start) + read(m.end);
|
|
864
|
+
for (const m of priorMainSizes)
|
|
865
|
+
sum += read(m);
|
|
866
|
+
return sum;
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
emitJustifiedMainPos(grammar, parent, mainPosField, mainSizeName, justify, indexInParent, parentDirection, gapInput(parent, parentDirection === 'column' ? 'row' : 'column'), padMainStartF, padMainEndF, marginInput);
|
|
873
|
+
}
|
|
874
|
+
// Reverse flex-direction (`row-reverse` / `column-reverse`): the
|
|
875
|
+
// main axis runs from the container's main END. The position
|
|
876
|
+
// rules above computed the forward-axis cursor; reflect it across
|
|
877
|
+
// the inner-main box, exactly as the imperative `flipMainAxis`.
|
|
878
|
+
if (parent !== null && parentReverse) {
|
|
879
|
+
applyReverseMainPos(grammar, parent, mainPosField, mainSizeField, mainSizeName, padMainStartF, padMainEndF);
|
|
880
|
+
}
|
|
881
|
+
// Cross-axis position. Default (flex-start, stretch with explicit
|
|
882
|
+
// cross size, and any other value the imperative doesn't special-
|
|
883
|
+
// case) is a constant offset. flex-end and center derive an offset
|
|
884
|
+
// from the parent's cross-axis size, gaining a dep edge on it.
|
|
885
|
+
if (parent === null || align === 'flex-end') {
|
|
886
|
+
if (parent !== null && align === 'flex-end') {
|
|
887
|
+
const parentCrossField = field(parent, parentDirection === 'column' ? 'width' : 'height');
|
|
888
|
+
grammar.set(crossPosField, {
|
|
889
|
+
deps: [
|
|
890
|
+
parentCrossField,
|
|
891
|
+
crossSizeField,
|
|
892
|
+
padCrossStartF,
|
|
893
|
+
padCrossEndF,
|
|
894
|
+
myMarginCrossEndF,
|
|
895
|
+
],
|
|
896
|
+
// Anchor against the line's inner cross, which the
|
|
897
|
+
// imperative `crossAlignItemsInLine` clamps to >= 0 — a
|
|
898
|
+
// container whose cross padding exceeds its cross size has
|
|
899
|
+
// a zero-width line, not a negative one.
|
|
900
|
+
compute: (read) => {
|
|
901
|
+
const padStart = read(padCrossStartF);
|
|
902
|
+
const innerCross = Math.max(0, read(parentCrossField) - padStart - read(padCrossEndF));
|
|
903
|
+
return (padStart +
|
|
904
|
+
innerCross -
|
|
905
|
+
read(crossSizeField) -
|
|
906
|
+
read(myMarginCrossEndF));
|
|
907
|
+
},
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
// Root: no parent, no alignment to apply — anchor at 0.
|
|
912
|
+
grammar.set(crossPosField, {
|
|
913
|
+
deps: [],
|
|
914
|
+
compute: () => 0,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
else if (align === 'center') {
|
|
919
|
+
const parentCrossField = field(parent, parentDirection === 'column' ? 'width' : 'height');
|
|
920
|
+
grammar.set(crossPosField, {
|
|
921
|
+
deps: [
|
|
922
|
+
parentCrossField,
|
|
923
|
+
crossSizeField,
|
|
924
|
+
padCrossStartF,
|
|
925
|
+
padCrossEndF,
|
|
926
|
+
myMarginCrossStartF,
|
|
927
|
+
myMarginCrossEndF,
|
|
928
|
+
],
|
|
929
|
+
compute: (read) => {
|
|
930
|
+
const padStart = read(padCrossStartF);
|
|
931
|
+
const innerCross = Math.max(0, read(parentCrossField) - padStart - read(padCrossEndF));
|
|
932
|
+
const marginStart = read(myMarginCrossStartF);
|
|
933
|
+
const innerLine = innerCross - marginStart - read(myMarginCrossEndF);
|
|
934
|
+
const myCross = read(crossSizeField);
|
|
935
|
+
return padStart + marginStart + Math.max(0, (innerLine - myCross) / 2);
|
|
936
|
+
},
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
// flex-start, stretch (with explicit cross size — no resize),
|
|
941
|
+
// and any other value (the imperative falls through to
|
|
942
|
+
// flex-start) all share this offset: the parent's cross-start
|
|
943
|
+
// padding plus this child's cross-start margin, both declared
|
|
944
|
+
// input deps.
|
|
945
|
+
grammar.set(crossPosField, {
|
|
946
|
+
deps: [padCrossStartF, myMarginCrossStartF],
|
|
947
|
+
compute: (read) => read(padCrossStartF) + read(myMarginCrossStartF),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
allFields.push({ node, width, height, left, top });
|
|
951
|
+
// Recurse into children. Absolute children are out-of-flow: they
|
|
952
|
+
// get visited (so their own subtree emits rules) but they don't
|
|
953
|
+
// contribute to the in-flow sibling index or the priorSiblings
|
|
954
|
+
// list that fuels positioning of subsequent in-flow siblings.
|
|
955
|
+
const childCount = node.getChildCount();
|
|
956
|
+
const inFlowSiblings = [];
|
|
957
|
+
for (let i = 0; i < childCount; i++) {
|
|
958
|
+
const child = node.getChild(i);
|
|
959
|
+
if (child.style.display === 'none')
|
|
960
|
+
continue;
|
|
961
|
+
if (child.style.positionType === 'absolute') {
|
|
962
|
+
visit(child, node, -1, []);
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
visit(child, node, inFlowSiblings.length, [...inFlowSiblings]);
|
|
966
|
+
inFlowSiblings.push(child);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return visit;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Walk the tree rooted at `root` and emit a `Grammar` that computes
|
|
974
|
+
* each node's `{width, height, left, top}`. See the module header
|
|
975
|
+
* for the field rules. A whole-tree build — `boundary` is `null`.
|
|
976
|
+
*
|
|
977
|
+
* `'auto'` width / height are supported (v13): a non-measured
|
|
978
|
+
* `'auto'` axis resolves to 0; the root's `'auto'` axis resolves
|
|
979
|
+
* from `available` (matching `calculateLayout`'s availability args).
|
|
980
|
+
*
|
|
981
|
+
* @internal
|
|
982
|
+
*/
|
|
983
|
+
export function buildFlexGrammar(root, available = {}) {
|
|
984
|
+
const grammar = new Map();
|
|
985
|
+
const allFields = [];
|
|
986
|
+
const styleInputs = new Map();
|
|
987
|
+
const availableInputs = {};
|
|
988
|
+
const mainDistributionByParent = new Map();
|
|
989
|
+
makeEmitter({
|
|
990
|
+
grammar,
|
|
991
|
+
allFields,
|
|
992
|
+
styleInputs,
|
|
993
|
+
boundary: null,
|
|
994
|
+
available,
|
|
995
|
+
availableInputs,
|
|
996
|
+
mainDistributionByParent,
|
|
997
|
+
})(root, null, 0, []);
|
|
998
|
+
return {
|
|
999
|
+
grammar,
|
|
1000
|
+
rootFields: {
|
|
1001
|
+
width: field(root, 'width'),
|
|
1002
|
+
height: field(root, 'height'),
|
|
1003
|
+
left: field(root, 'left'),
|
|
1004
|
+
top: field(root, 'top'),
|
|
1005
|
+
},
|
|
1006
|
+
allFields,
|
|
1007
|
+
styleInputs,
|
|
1008
|
+
availableInputs,
|
|
1009
|
+
mainDistributionByParent,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
/** Merge two `StyleInputs` — `b`'s present fields win over `a`'s. */
|
|
1013
|
+
function mergeStyleInputs(a, b) {
|
|
1014
|
+
const m = { ...a };
|
|
1015
|
+
if (b.width !== undefined)
|
|
1016
|
+
m.width = b.width;
|
|
1017
|
+
if (b.height !== undefined)
|
|
1018
|
+
m.height = b.height;
|
|
1019
|
+
if (b.flexBasis !== undefined)
|
|
1020
|
+
m.flexBasis = b.flexBasis;
|
|
1021
|
+
if (b.gapRow !== undefined)
|
|
1022
|
+
m.gapRow = b.gapRow;
|
|
1023
|
+
if (b.gapColumn !== undefined)
|
|
1024
|
+
m.gapColumn = b.gapColumn;
|
|
1025
|
+
if (b.minWidth !== undefined)
|
|
1026
|
+
m.minWidth = b.minWidth;
|
|
1027
|
+
if (b.minHeight !== undefined)
|
|
1028
|
+
m.minHeight = b.minHeight;
|
|
1029
|
+
if (b.maxWidth !== undefined)
|
|
1030
|
+
m.maxWidth = b.maxWidth;
|
|
1031
|
+
if (b.maxHeight !== undefined)
|
|
1032
|
+
m.maxHeight = b.maxHeight;
|
|
1033
|
+
for (const k of ['padding', 'margin']) {
|
|
1034
|
+
const bArr = b[k];
|
|
1035
|
+
if (bArr === undefined)
|
|
1036
|
+
continue;
|
|
1037
|
+
const arr = m[k] !== undefined ? [...m[k]] : [];
|
|
1038
|
+
for (let i = 0; i < bArr.length; i++) {
|
|
1039
|
+
if (bArr[i] !== undefined)
|
|
1040
|
+
arr[i] = bArr[i];
|
|
1041
|
+
}
|
|
1042
|
+
m[k] = arr;
|
|
1043
|
+
}
|
|
1044
|
+
return m;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Merge the per-node `styleInputs` of a fragment (`extra`) into a
|
|
1048
|
+
* copy of `base`. A node present in both — the previous last child,
|
|
1049
|
+
* which gains a main-end margin input when it acquires a follower —
|
|
1050
|
+
* has its `StyleInputs` deep-merged rather than overwritten.
|
|
1051
|
+
*/
|
|
1052
|
+
function mergeStyleInputsMap(base, extra) {
|
|
1053
|
+
const merged = new Map(base);
|
|
1054
|
+
for (const [node, entry] of extra) {
|
|
1055
|
+
const existing = merged.get(node);
|
|
1056
|
+
merged.set(node, existing === undefined ? entry : mergeStyleInputs(existing, entry));
|
|
1057
|
+
}
|
|
1058
|
+
return merged;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Fast-path a child INSERT for the Spineless runtime. If adding
|
|
1062
|
+
* `child` under `parent` can be absorbed without a fresh runtime,
|
|
1063
|
+
* return the patch inputs; return `null` when a full rebuild is
|
|
1064
|
+
* required — `child` is not a child of `parent`, or `parent` uses a
|
|
1065
|
+
* reverse `flex-direction` (supported by the grammar since v11, but
|
|
1066
|
+
* not by this structural fast-path — reflecting every sibling's
|
|
1067
|
+
* position is a whole-subtree rewrite).
|
|
1068
|
+
*
|
|
1069
|
+
* The new subtree's fields are always topological-tail additions
|
|
1070
|
+
* handled by `graft` (`additions` / `newRoots`). When `parent` is in
|
|
1071
|
+
* the "simple" regime (no flex distribution, default `flex-start`
|
|
1072
|
+
* justify, no wrap) AND `child` is its LAST child — or `child` is
|
|
1073
|
+
* absolute — that is the whole patch and `rebinds` is empty, and the
|
|
1074
|
+
* fragment is built in **O(subtree)**: `makeEmitter` emits just the
|
|
1075
|
+
* appended subtree against the runtime's grammar as a boundary, no
|
|
1076
|
+
* whole-tree rebuild. Otherwise the insert also rewrites existing
|
|
1077
|
+
* in-flow siblings' rules — a flex-distributing / justified /
|
|
1078
|
+
* wrapping parent reads every sibling, and a MID-LIST insert (v32)
|
|
1079
|
+
* shifts every later in-flow sibling's main position — so the
|
|
1080
|
+
* grammar is rebuilt O(tree) and `rebinds` carries those siblings'
|
|
1081
|
+
* rewritten rules.
|
|
1082
|
+
*
|
|
1083
|
+
* `next.grammar` is always `prev.grammar` — the runtime's own Map,
|
|
1084
|
+
* which `graft` / `rebindRule` patch in place; `next.allFields` /
|
|
1085
|
+
* `next.styleInputs` are refreshed lookup tables the caller adopts.
|
|
1086
|
+
*
|
|
1087
|
+
* @internal
|
|
1088
|
+
*/
|
|
1089
|
+
export function buildAppendFragment(prev, root, parent, child, available = {}) {
|
|
1090
|
+
// `child` must be a child of `parent`. Its index decides the path,
|
|
1091
|
+
// not whether the fragment is built: a last in-flow child can take
|
|
1092
|
+
// the O(subtree) graft; a mid-list in-flow insert shifts the later
|
|
1093
|
+
// siblings, so it takes the rebuild + rebind path below (v32).
|
|
1094
|
+
const count = parent.getChildCount();
|
|
1095
|
+
let childIndex = -1;
|
|
1096
|
+
for (let i = 0; i < count; i++) {
|
|
1097
|
+
if (parent.getChild(i) === child) {
|
|
1098
|
+
childIndex = i;
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (childIndex === -1)
|
|
1103
|
+
return null;
|
|
1104
|
+
const isLast = childIndex === count - 1;
|
|
1105
|
+
// A `display: 'none'` appended child has no fields to graft — let
|
|
1106
|
+
// the rebuild path absorb it (a hidden node perturbs nothing, so
|
|
1107
|
+
// the rebuild is the simple correct route, not a fast-path miss).
|
|
1108
|
+
if (child.style.display === 'none')
|
|
1109
|
+
return null;
|
|
1110
|
+
// A reverse-direction parent is supported by the grammar (v11) but
|
|
1111
|
+
// not by this fast-path: the flip reflects every sibling, so fall
|
|
1112
|
+
// back to a full rebuild.
|
|
1113
|
+
const dir = parent.style.flexDirection;
|
|
1114
|
+
if (dir !== 'row' && dir !== 'column')
|
|
1115
|
+
return null;
|
|
1116
|
+
// An absolute child never perturbs in-flow siblings, so it is
|
|
1117
|
+
// always a pure tail graft. An in-flow child stays one only when it
|
|
1118
|
+
// is the LAST child of a simple-regime parent — a mid-list in-flow
|
|
1119
|
+
// insert shifts the later siblings and so takes the rebuild path.
|
|
1120
|
+
const simple = child.style.positionType === 'absolute' ||
|
|
1121
|
+
(isLast &&
|
|
1122
|
+
parent.style.flexWrap === 'nowrap' &&
|
|
1123
|
+
parent.style.justifyContent === 'flex-start' &&
|
|
1124
|
+
!parentNeedsFlexDistribution(parent));
|
|
1125
|
+
if (simple) {
|
|
1126
|
+
// O(subtree): emit just the appended subtree against the
|
|
1127
|
+
// runtime's grammar as a boundary, so the fragment's `grammar`
|
|
1128
|
+
// holds only genuinely-new fields. The expensive whole-tree walk
|
|
1129
|
+
// is skipped entirely.
|
|
1130
|
+
const ctx = {
|
|
1131
|
+
grammar: new Map(),
|
|
1132
|
+
allFields: [],
|
|
1133
|
+
styleInputs: new Map(),
|
|
1134
|
+
boundary: prev.grammar,
|
|
1135
|
+
available: {},
|
|
1136
|
+
availableInputs: {},
|
|
1137
|
+
mainDistributionByParent: new Map(),
|
|
1138
|
+
};
|
|
1139
|
+
const priors = [];
|
|
1140
|
+
for (let i = 0; i < childIndex; i++) {
|
|
1141
|
+
const sib = parent.getChild(i);
|
|
1142
|
+
if (isInFlow(sib))
|
|
1143
|
+
priors.push(sib);
|
|
1144
|
+
}
|
|
1145
|
+
makeEmitter(ctx)(child, parent, priors.length, priors);
|
|
1146
|
+
const newRoots = [];
|
|
1147
|
+
for (const e of ctx.allFields) {
|
|
1148
|
+
newRoots.push(e.width, e.height, e.left, e.top);
|
|
1149
|
+
}
|
|
1150
|
+
const next = {
|
|
1151
|
+
grammar: prev.grammar,
|
|
1152
|
+
rootFields: prev.rootFields,
|
|
1153
|
+
allFields: [...prev.allFields, ...ctx.allFields],
|
|
1154
|
+
styleInputs: mergeStyleInputsMap(prev.styleInputs, ctx.styleInputs),
|
|
1155
|
+
availableInputs: prev.availableInputs,
|
|
1156
|
+
mainDistributionByParent: prev.mainDistributionByParent,
|
|
1157
|
+
};
|
|
1158
|
+
return { additions: ctx.grammar, newRoots, rebinds: [], next };
|
|
1159
|
+
}
|
|
1160
|
+
// Non-simple: appending rewrites every surviving sibling's rules.
|
|
1161
|
+
// Rebuild the grammar O(tree) and diff it against `prev` for the
|
|
1162
|
+
// new fields; Field identity is stable across builds, so a key
|
|
1163
|
+
// absent from `prev.grammar` belongs to a newly-added node. The
|
|
1164
|
+
// rebuild needs the caller's `available` — the root's `'auto'`
|
|
1165
|
+
// size rule shape depends on it (`rootAxisIsBareZero`).
|
|
1166
|
+
const fresh = buildFlexGrammar(root, available);
|
|
1167
|
+
const additions = new Map();
|
|
1168
|
+
for (const [f, rule] of fresh.grammar) {
|
|
1169
|
+
if (!prev.grammar.has(f))
|
|
1170
|
+
additions.set(f, rule);
|
|
1171
|
+
}
|
|
1172
|
+
// `graft` integrates the DFS-closure of `newRoots`. A new node's
|
|
1173
|
+
// own layout fields don't reach every new field: a mid-list insert
|
|
1174
|
+
// leaves the inserted node's main-END margin read only by its
|
|
1175
|
+
// follower's (rebound, existing) rule — never by the node itself.
|
|
1176
|
+
// Starting the DFS from EVERY new field covers those orphans; the
|
|
1177
|
+
// DFS dedups, so the extra roots cost nothing.
|
|
1178
|
+
const newRoots = [...additions.keys()];
|
|
1179
|
+
const rebinds = [];
|
|
1180
|
+
for (let i = 0; i < parent.getChildCount(); i++) {
|
|
1181
|
+
const sib = parent.getChild(i);
|
|
1182
|
+
if (sib === child || !isInFlow(sib))
|
|
1183
|
+
continue;
|
|
1184
|
+
for (const name of ['width', 'height', 'left', 'top']) {
|
|
1185
|
+
const f = field(sib, name);
|
|
1186
|
+
const rule = fresh.grammar.get(f);
|
|
1187
|
+
if (rule !== undefined)
|
|
1188
|
+
rebinds.push([f, rule]);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
// Phase 12: if the parent's mainDistribution field already existed in
|
|
1192
|
+
// prev (the parent was already flex-distributing pre-append) its
|
|
1193
|
+
// closure now covers different siblings — rebind it so recompute sees
|
|
1194
|
+
// the updated sibling list.
|
|
1195
|
+
const parentMainDistF = fresh.mainDistributionByParent.get(parent);
|
|
1196
|
+
if (parentMainDistF !== undefined && prev.grammar.has(parentMainDistF)) {
|
|
1197
|
+
const freshRule = fresh.grammar.get(parentMainDistF);
|
|
1198
|
+
if (freshRule !== undefined)
|
|
1199
|
+
rebinds.unshift([parentMainDistF, freshRule]);
|
|
1200
|
+
}
|
|
1201
|
+
const next = {
|
|
1202
|
+
grammar: prev.grammar,
|
|
1203
|
+
rootFields: prev.rootFields,
|
|
1204
|
+
allFields: fresh.allFields,
|
|
1205
|
+
styleInputs: fresh.styleInputs,
|
|
1206
|
+
availableInputs: fresh.availableInputs,
|
|
1207
|
+
mainDistributionByParent: fresh.mainDistributionByParent,
|
|
1208
|
+
};
|
|
1209
|
+
return { additions, newRoots, rebinds, next };
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Candidate grammar fields belonging to `node` — its four layout
|
|
1213
|
+
* fields, its `measure:*` fields (a measure leaf's `'auto'`-axis
|
|
1214
|
+
* measure inputs), and its style input fields recorded in
|
|
1215
|
+
* `styleInputs`. The caller filters to those actually in the grammar.
|
|
1216
|
+
*
|
|
1217
|
+
* Every NON-leaf per-node field MUST be listed — the four layout
|
|
1218
|
+
* fields, `measure:main` / `measure:cross` (a measure leaf's
|
|
1219
|
+
* `'auto'`-axis inputs) and `aspect:width` / `aspect:height` (an
|
|
1220
|
+
* `aspectRatio` node's derived sizes). Unlike a leaf style input,
|
|
1221
|
+
* these carry dependencies, so `SpinelessRuntime.detach`'s orphan
|
|
1222
|
+
* cleanup (which only drops dependency-free leaves) cannot reclaim
|
|
1223
|
+
* them — they have to be in the removed set explicitly, or a removed
|
|
1224
|
+
* style input they read dangles. Leaf style inputs not listed here
|
|
1225
|
+
* are reclaimed by that orphan cleanup.
|
|
1226
|
+
*/
|
|
1227
|
+
function nodeFields(node, styleInputs) {
|
|
1228
|
+
const out = [
|
|
1229
|
+
field(node, 'width'),
|
|
1230
|
+
field(node, 'height'),
|
|
1231
|
+
field(node, 'left'),
|
|
1232
|
+
field(node, 'top'),
|
|
1233
|
+
field(node, 'measure:main'),
|
|
1234
|
+
field(node, 'measure:cross'),
|
|
1235
|
+
field(node, 'aspect:width'),
|
|
1236
|
+
field(node, 'aspect:height'),
|
|
1237
|
+
// Phase 12: the per-parent mainDistribution intermediate field is a
|
|
1238
|
+
// non-leaf that has deps (siblings' size/flex inputs) and may be read
|
|
1239
|
+
// by each child's width/height rule. It MUST be in the removed set so
|
|
1240
|
+
// detach() does not see dangling reverse-dep edges from child layout
|
|
1241
|
+
// fields back to it.
|
|
1242
|
+
field(node, 'mainDistribution'),
|
|
1243
|
+
];
|
|
1244
|
+
const si = styleInputs.get(node);
|
|
1245
|
+
if (si !== undefined) {
|
|
1246
|
+
for (const k of ['width', 'height', 'flexBasis', 'gapRow', 'gapColumn']) {
|
|
1247
|
+
const f = si[k];
|
|
1248
|
+
if (f !== undefined)
|
|
1249
|
+
out.push(f);
|
|
1250
|
+
}
|
|
1251
|
+
for (const arr of [si.padding, si.margin]) {
|
|
1252
|
+
if (arr === undefined)
|
|
1253
|
+
continue;
|
|
1254
|
+
for (const f of arr) {
|
|
1255
|
+
if (f !== undefined)
|
|
1256
|
+
out.push(f);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return out;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* Fast-path a child REMOVAL for the Spineless runtime — the mirror
|
|
1264
|
+
* of `buildAppendFragment`. Call this **before** detaching `child`
|
|
1265
|
+
* from `parent`: the simple-regime check needs `child` still in
|
|
1266
|
+
* place. Returns the patch inputs, or `null` when a full rebuild is
|
|
1267
|
+
* required (`child` is not `parent`'s child, or `parent` uses a
|
|
1268
|
+
* reverse `flex-direction` — supported by the grammar since v11 but
|
|
1269
|
+
* not by this fast-path).
|
|
1270
|
+
*
|
|
1271
|
+
* In the "simple" regime (no flex distribution, default `flex-start`
|
|
1272
|
+
* justify, no wrap — or `child` is absolute) the patch is built in
|
|
1273
|
+
* **O(subtree)**: `removed` is collected directly from `prev` by
|
|
1274
|
+
* walking the removed subtree, with no whole-tree rebuild, and
|
|
1275
|
+
* `rebinds` is empty. `SpinelessRuntime.detach` then drops those
|
|
1276
|
+
* fields and auto-cleans any input field they orphaned (e.g. the new
|
|
1277
|
+
* last child's now-unread main-end margin). Otherwise the removal
|
|
1278
|
+
* shrinks every surviving in-flow sibling's dependency set, so the
|
|
1279
|
+
* grammar is rebuilt O(tree) and `rebinds` carries those siblings'
|
|
1280
|
+
* rewritten rules. The caller applies `rebindRule` for each rebind
|
|
1281
|
+
* FIRST (so survivors stop depending on the removed fields), then
|
|
1282
|
+
* `detach`, then `recompute()`.
|
|
1283
|
+
*
|
|
1284
|
+
* Does not mutate the tree. `next.grammar` is `prev.grammar` — the
|
|
1285
|
+
* runtime's own Map, patched in place by `detach` / `rebindRule`.
|
|
1286
|
+
*
|
|
1287
|
+
* @internal
|
|
1288
|
+
*/
|
|
1289
|
+
export function buildRemoveFragment(prev, root, parent, child, available = {}) {
|
|
1290
|
+
// `child` must be a child of `parent`.
|
|
1291
|
+
let index = -1;
|
|
1292
|
+
for (let i = 0; i < parent.getChildCount(); i++) {
|
|
1293
|
+
if (parent.getChild(i) === child) {
|
|
1294
|
+
index = i;
|
|
1295
|
+
break;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (index === -1)
|
|
1299
|
+
return null;
|
|
1300
|
+
// A `display: 'none'` child had no fields to begin with — there is
|
|
1301
|
+
// nothing to detach; let the rebuild path absorb the removal.
|
|
1302
|
+
if (child.style.display === 'none')
|
|
1303
|
+
return null;
|
|
1304
|
+
// A reverse-direction parent is supported by the grammar (v11) but
|
|
1305
|
+
// not by this fast-path: the flip reflects every sibling, so fall
|
|
1306
|
+
// back to a full rebuild.
|
|
1307
|
+
const dir = parent.style.flexDirection;
|
|
1308
|
+
if (dir !== 'row' && dir !== 'column')
|
|
1309
|
+
return null;
|
|
1310
|
+
// Removing a last child from a simple-regime parent perturbs no
|
|
1311
|
+
// surviving sibling. `child` is still attached, so
|
|
1312
|
+
// `parentNeedsFlexDistribution` sees it: a parent flex-distributing
|
|
1313
|
+
// *because of* `child` is correctly non-simple.
|
|
1314
|
+
const isLast = index === parent.getChildCount() - 1;
|
|
1315
|
+
const simple = child.style.positionType === 'absolute' ||
|
|
1316
|
+
(isLast &&
|
|
1317
|
+
parent.style.flexWrap === 'nowrap' &&
|
|
1318
|
+
parent.style.justifyContent === 'flex-start' &&
|
|
1319
|
+
!parentNeedsFlexDistribution(parent));
|
|
1320
|
+
// Collect the removed subtree's nodes (`child` + descendants).
|
|
1321
|
+
const removedNodes = new Set();
|
|
1322
|
+
const stack = [child];
|
|
1323
|
+
while (stack.length > 0) {
|
|
1324
|
+
const n = stack.pop();
|
|
1325
|
+
removedNodes.add(n);
|
|
1326
|
+
for (let i = 0; i < n.getChildCount(); i++)
|
|
1327
|
+
stack.push(n.getChild(i));
|
|
1328
|
+
}
|
|
1329
|
+
if (simple) {
|
|
1330
|
+
// O(subtree): the removed set is exactly the subtree's own
|
|
1331
|
+
// fields, gathered straight from `prev` — no grammar rebuild.
|
|
1332
|
+
// `detach` auto-cleans the input fields they orphan.
|
|
1333
|
+
const removed = [];
|
|
1334
|
+
for (const n of removedNodes) {
|
|
1335
|
+
for (const f of nodeFields(n, prev.styleInputs)) {
|
|
1336
|
+
if (prev.grammar.has(f))
|
|
1337
|
+
removed.push(f);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
const next = {
|
|
1341
|
+
grammar: prev.grammar,
|
|
1342
|
+
rootFields: prev.rootFields,
|
|
1343
|
+
allFields: prev.allFields.filter((e) => !removedNodes.has(e.node)),
|
|
1344
|
+
styleInputs: new Map([...prev.styleInputs].filter(([n]) => !removedNodes.has(n))),
|
|
1345
|
+
availableInputs: prev.availableInputs,
|
|
1346
|
+
mainDistributionByParent: new Map([...prev.mainDistributionByParent].filter(([n]) => !removedNodes.has(n))),
|
|
1347
|
+
};
|
|
1348
|
+
return { removed, rebinds: [], next };
|
|
1349
|
+
}
|
|
1350
|
+
// Non-simple: the removal rewrites every surviving sibling's rules.
|
|
1351
|
+
// Rebuild the grammar O(tree) — detach `child` around the build,
|
|
1352
|
+
// then restore the tree — and diff `prev \ fresh` for `removed`.
|
|
1353
|
+
// The rebuild needs the caller's `available` (root size rule shape).
|
|
1354
|
+
parent.removeChild(child);
|
|
1355
|
+
const fresh = buildFlexGrammar(root, available);
|
|
1356
|
+
parent.insertChild(child, index);
|
|
1357
|
+
const removed = [];
|
|
1358
|
+
for (const f of prev.grammar.keys()) {
|
|
1359
|
+
if (!fresh.grammar.has(f))
|
|
1360
|
+
removed.push(f);
|
|
1361
|
+
}
|
|
1362
|
+
const rebinds = [];
|
|
1363
|
+
for (let i = 0; i < parent.getChildCount(); i++) {
|
|
1364
|
+
const sib = parent.getChild(i);
|
|
1365
|
+
if (sib === child || !isInFlow(sib))
|
|
1366
|
+
continue;
|
|
1367
|
+
for (const name of ['width', 'height', 'left', 'top']) {
|
|
1368
|
+
const f = field(sib, name);
|
|
1369
|
+
const rule = fresh.grammar.get(f);
|
|
1370
|
+
if (rule !== undefined)
|
|
1371
|
+
rebinds.push([f, rule]);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
// Phase 12: if the parent's mainDistribution field survived the
|
|
1375
|
+
// removal (it's still in fresh.grammar), its closure now covers
|
|
1376
|
+
// fewer siblings — rebind it so recompute sees the updated list.
|
|
1377
|
+
const parentMainDistFRem = fresh.mainDistributionByParent.get(parent);
|
|
1378
|
+
if (parentMainDistFRem !== undefined && prev.grammar.has(parentMainDistFRem)) {
|
|
1379
|
+
const freshRule = fresh.grammar.get(parentMainDistFRem);
|
|
1380
|
+
if (freshRule !== undefined)
|
|
1381
|
+
rebinds.unshift([parentMainDistFRem, freshRule]);
|
|
1382
|
+
}
|
|
1383
|
+
const next = {
|
|
1384
|
+
grammar: prev.grammar,
|
|
1385
|
+
rootFields: prev.rootFields,
|
|
1386
|
+
allFields: fresh.allFields,
|
|
1387
|
+
styleInputs: fresh.styleInputs,
|
|
1388
|
+
availableInputs: fresh.availableInputs,
|
|
1389
|
+
mainDistributionByParent: fresh.mainDistributionByParent,
|
|
1390
|
+
};
|
|
1391
|
+
return { removed, rebinds, next };
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Fast-path a child REORDER for the Spineless runtime — `parent`'s
|
|
1395
|
+
* children are a permutation of their former order (no node added or
|
|
1396
|
+
* removed). The grammar is rebuilt O(tree) and the patch applied to
|
|
1397
|
+
* the existing runtime without a fresh `init`.
|
|
1398
|
+
*
|
|
1399
|
+
* Reordering `parent`'s children can only change the rules of
|
|
1400
|
+
* `parent` itself (its `'auto'` / wrap content size now packs the
|
|
1401
|
+
* children in a new order) and of its in-flow children (their main
|
|
1402
|
+
* positions, and — under wrap — their line-dependent sizes), so the
|
|
1403
|
+
* rebind set is `parent` + its in-flow children's
|
|
1404
|
+
* `width / height / left / top`. Descendants and ancestors recompute
|
|
1405
|
+
* from the changed VALUES; their rules are untouched.
|
|
1406
|
+
*
|
|
1407
|
+
* A reorder also shifts the "has a follower" boundary, so a node's
|
|
1408
|
+
* main-end margin input can become newly read (`additions`) or
|
|
1409
|
+
* newly unread (`removed`) — the same diff `buildAppendFragment`'s
|
|
1410
|
+
* non-simple branch takes. The caller applies `graft` then
|
|
1411
|
+
* `rebindRule` then `detach`.
|
|
1412
|
+
*
|
|
1413
|
+
* `next.grammar` is `prev.grammar` — the runtime's own Map, patched
|
|
1414
|
+
* in place.
|
|
1415
|
+
*
|
|
1416
|
+
* @internal
|
|
1417
|
+
*/
|
|
1418
|
+
export function buildReorderFragment(prev, root, parent, available = {}) {
|
|
1419
|
+
const fresh = buildFlexGrammar(root, available);
|
|
1420
|
+
const additions = new Map();
|
|
1421
|
+
for (const [f, rule] of fresh.grammar) {
|
|
1422
|
+
if (!prev.grammar.has(f))
|
|
1423
|
+
additions.set(f, rule);
|
|
1424
|
+
}
|
|
1425
|
+
const removed = [];
|
|
1426
|
+
for (const f of prev.grammar.keys()) {
|
|
1427
|
+
if (!fresh.grammar.has(f))
|
|
1428
|
+
removed.push(f);
|
|
1429
|
+
}
|
|
1430
|
+
const rebinds = [];
|
|
1431
|
+
const touched = [parent];
|
|
1432
|
+
for (let i = 0; i < parent.getChildCount(); i++) {
|
|
1433
|
+
const c = parent.getChild(i);
|
|
1434
|
+
if (isInFlow(c))
|
|
1435
|
+
touched.push(c);
|
|
1436
|
+
}
|
|
1437
|
+
for (const n of touched) {
|
|
1438
|
+
for (const name of ['width', 'height', 'left', 'top']) {
|
|
1439
|
+
const f = field(n, name);
|
|
1440
|
+
const rule = fresh.grammar.get(f);
|
|
1441
|
+
if (rule !== undefined)
|
|
1442
|
+
rebinds.push([f, rule]);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
// Phase 12: the parent's mainDistribution field captures flexSibs in
|
|
1446
|
+
// child-position order. A reorder changes that order, so rebind it
|
|
1447
|
+
// (prepend so it is rebound before any child width that reads it).
|
|
1448
|
+
const parentMainDistFReorder = fresh.mainDistributionByParent.get(parent);
|
|
1449
|
+
if (parentMainDistFReorder !== undefined &&
|
|
1450
|
+
prev.grammar.has(parentMainDistFReorder)) {
|
|
1451
|
+
const freshRule = fresh.grammar.get(parentMainDistFReorder);
|
|
1452
|
+
if (freshRule !== undefined)
|
|
1453
|
+
rebinds.unshift([parentMainDistFReorder, freshRule]);
|
|
1454
|
+
}
|
|
1455
|
+
const next = {
|
|
1456
|
+
grammar: prev.grammar,
|
|
1457
|
+
rootFields: prev.rootFields,
|
|
1458
|
+
allFields: fresh.allFields,
|
|
1459
|
+
styleInputs: fresh.styleInputs,
|
|
1460
|
+
availableInputs: fresh.availableInputs,
|
|
1461
|
+
mainDistributionByParent: fresh.mainDistributionByParent,
|
|
1462
|
+
};
|
|
1463
|
+
return { additions, newRoots: [...additions.keys()], removed, rebinds, next };
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Resolve a node's main-axis basis from its style input fields:
|
|
1467
|
+
* numeric `flexBasis` wins over the main-axis size. Mirrors the
|
|
1468
|
+
* imperative `resolveHypotheticalMainSize`. Both inputs are declared
|
|
1469
|
+
* deps of the calling rule, so this reads only cached values.
|
|
1470
|
+
*
|
|
1471
|
+
* @internal
|
|
1472
|
+
*/
|
|
1473
|
+
function resolveBasisFromRead(read, flexBasisInput, mainInput) {
|
|
1474
|
+
const basis = read(flexBasisInput);
|
|
1475
|
+
return typeof basis === 'number' ? basis : read(mainInput);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Clamp `value` to `[min, max]`. `max` carries `Infinity` for "no
|
|
1479
|
+
* upper bound" (see `minMaxInput`). Mirrors the imperative
|
|
1480
|
+
* `clampSize`: the floor is applied before the cap, so when
|
|
1481
|
+
* `min > max` the cap wins — `clampMinMax(5, 10, 3) === 3`.
|
|
1482
|
+
*
|
|
1483
|
+
* @internal
|
|
1484
|
+
*/
|
|
1485
|
+
function clampMinMax(value, min, max) {
|
|
1486
|
+
return Math.min(Math.max(value, min), max);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* True iff `axis` resolves from `aspectRatio` (v15): the axis is
|
|
1490
|
+
* `'auto'`, an `aspectRatio` is set, and the perpendicular axis is
|
|
1491
|
+
* an explicit number — exactly the case where the imperative
|
|
1492
|
+
* `effectivePreferredSize` derives a concrete size for an otherwise
|
|
1493
|
+
* `'auto'` axis. When this holds the axis is NOT content-sized: it
|
|
1494
|
+
* has a definite preferred size, so (e.g.) `align-items: stretch`
|
|
1495
|
+
* does not resize it.
|
|
1496
|
+
*
|
|
1497
|
+
* @internal
|
|
1498
|
+
*/
|
|
1499
|
+
function aspectDerivable(node, axis) {
|
|
1500
|
+
if (typeof node.style[axis] === 'number')
|
|
1501
|
+
return false;
|
|
1502
|
+
if (node.style.aspectRatio === undefined)
|
|
1503
|
+
return false;
|
|
1504
|
+
const other = axis === 'width' ? 'height' : 'width';
|
|
1505
|
+
return typeof node.style[other] === 'number';
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* True iff `node` is a measure-function leaf: a childless node with
|
|
1509
|
+
* a measure function. The imperative algorithm consults the measurer
|
|
1510
|
+
* for such a node's `'auto'` axes (`resolveHypotheticalMainSize` /
|
|
1511
|
+
* `naturalCrossSize`); a non-leaf or measure-less node does not.
|
|
1512
|
+
*
|
|
1513
|
+
* @internal
|
|
1514
|
+
*/
|
|
1515
|
+
function isMeasureLeaf(node) {
|
|
1516
|
+
return node.getChildCount() === 0 && node.getMeasureFunc() !== null;
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Build the flex-distribution inputs for a fixed in-flow sibling set.
|
|
1520
|
+
* Every value — basis, grow / shrink weights, main-axis margins — is
|
|
1521
|
+
* read from a declared input field via `read`, so the calling rule's
|
|
1522
|
+
* dep list fully covers them.
|
|
1523
|
+
*
|
|
1524
|
+
* @internal
|
|
1525
|
+
*/
|
|
1526
|
+
function liveFlexSiblings(sibs, read) {
|
|
1527
|
+
return sibs.map((s) => ({
|
|
1528
|
+
basis: resolveBasisFromRead(read, s.flexBasisInput, s.mainInput),
|
|
1529
|
+
grow: read(s.growInput),
|
|
1530
|
+
shrink: read(s.shrinkInput),
|
|
1531
|
+
marginStart: read(s.marginMainStartInput),
|
|
1532
|
+
marginEnd: read(s.marginMainEndInput),
|
|
1533
|
+
min: read(s.minInput),
|
|
1534
|
+
max: read(s.maxInput),
|
|
1535
|
+
}));
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Build the `WrapSibling` set for a wrapping container's in-flow
|
|
1539
|
+
* children. Like `liveFlexSiblings` but also carries cross-axis size
|
|
1540
|
+
* (from the declared `crossInput` field) + margins and the resolved
|
|
1541
|
+
* align value, which the wrap line packer needs.
|
|
1542
|
+
*
|
|
1543
|
+
* @internal
|
|
1544
|
+
*/
|
|
1545
|
+
function liveWrapSiblings(sibs, parent, read) {
|
|
1546
|
+
return sibs.map((s) => {
|
|
1547
|
+
const alignSelf = s.node.style.alignSelf;
|
|
1548
|
+
const crossNatural = read(s.crossInput);
|
|
1549
|
+
return {
|
|
1550
|
+
basis: resolveBasisFromRead(read, s.flexBasisInput, s.mainInput),
|
|
1551
|
+
grow: read(s.growInput),
|
|
1552
|
+
shrink: read(s.shrinkInput),
|
|
1553
|
+
min: read(s.minInput),
|
|
1554
|
+
max: read(s.maxInput),
|
|
1555
|
+
mainMarginStart: read(s.marginMainStartInput),
|
|
1556
|
+
mainMarginEnd: read(s.marginMainEndInput),
|
|
1557
|
+
// Two cross sizes (v12b): the imperative computes a line's
|
|
1558
|
+
// cross size from each item's UNCLAMPED natural cross
|
|
1559
|
+
// (`computeLineCrossSizes` → `naturalCross`), but positions an
|
|
1560
|
+
// item within its line using the CLAMPED cross
|
|
1561
|
+
// (`crossAlignItemsInLine` → `clampSize(naturalCross)`). A
|
|
1562
|
+
// min/max clamp can therefore make an item overflow its line.
|
|
1563
|
+
crossSizeNatural: crossNatural,
|
|
1564
|
+
crossSize: clampMinMax(crossNatural, read(s.minCrossInput), read(s.maxCrossInput)),
|
|
1565
|
+
crossMarginStart: read(s.marginCrossStartInput),
|
|
1566
|
+
crossMarginEnd: read(s.marginCrossEndInput),
|
|
1567
|
+
crossIsContentAuto: s.crossIsContentAuto,
|
|
1568
|
+
crossMin: read(s.minCrossInput),
|
|
1569
|
+
crossMax: read(s.maxCrossInput),
|
|
1570
|
+
align: alignSelf === 'auto' ? parent.style.alignItems : alignSelf,
|
|
1571
|
+
};
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Emit the parent-level main-axis distribution Field for a
|
|
1576
|
+
* flex-distributing parent. Returns the Field so the per-child
|
|
1577
|
+
* mainSize / mainPos rules can declare it as their dependency.
|
|
1578
|
+
*
|
|
1579
|
+
* `deps` is the SAME deps list today's per-cell mainSizeField
|
|
1580
|
+
* rule declares (parent main, gap, padding, plus each in-flow
|
|
1581
|
+
* sibling's eight flex-related inputs).
|
|
1582
|
+
*/
|
|
1583
|
+
function emitMainDistribution(grammar, parent, flexSibs, parentMainField, mainGapInput, padMainStartF, padMainEndF, marginInput, parentDirection) {
|
|
1584
|
+
const mainDistField = field(parent, 'mainDistribution');
|
|
1585
|
+
const deps = [
|
|
1586
|
+
parentMainField,
|
|
1587
|
+
mainGapInput,
|
|
1588
|
+
padMainStartF,
|
|
1589
|
+
padMainEndF,
|
|
1590
|
+
];
|
|
1591
|
+
for (const s of flexSibs) {
|
|
1592
|
+
deps.push(s.flexBasisInput, s.mainInput, s.growInput, s.shrinkInput, s.marginMainStartInput, s.marginMainEndInput, s.minInput, s.maxInput);
|
|
1593
|
+
}
|
|
1594
|
+
const mainStartEdgeName = mainStartEdge(parentDirection);
|
|
1595
|
+
const mainEndEdgeName = mainEndEdge(parentDirection);
|
|
1596
|
+
grammar.set(mainDistField, {
|
|
1597
|
+
deps,
|
|
1598
|
+
compute: (read) => {
|
|
1599
|
+
const innerMain = Math.max(0, read(parentMainField) - read(padMainStartF) - read(padMainEndF));
|
|
1600
|
+
const siblings = liveFlexSiblings(flexSibs, read);
|
|
1601
|
+
const sizes = distributeMainAxis(siblings, innerMain, read(mainGapInput));
|
|
1602
|
+
// Fold sizes + margins + gaps into a prefix-sum positions array.
|
|
1603
|
+
// positions[i] is the i-th in-flow child's main offset from the
|
|
1604
|
+
// parent's border-box origin (so it INCLUDES the parent's main-
|
|
1605
|
+
// start padding), matching what the existing flex-start mainPos
|
|
1606
|
+
// rule returns. Task 2 assigns this directly to the child's
|
|
1607
|
+
// mainPos Field.
|
|
1608
|
+
const positions = new Array(sizes.length);
|
|
1609
|
+
const gap = read(mainGapInput);
|
|
1610
|
+
const startPad = read(padMainStartF);
|
|
1611
|
+
let cursor = startPad;
|
|
1612
|
+
for (let i = 0; i < sizes.length; i++) {
|
|
1613
|
+
const sib = flexSibs[i];
|
|
1614
|
+
const marginStart = read(marginInput(sib.node, mainStartEdgeName));
|
|
1615
|
+
const marginEnd = read(marginInput(sib.node, mainEndEdgeName));
|
|
1616
|
+
if (i > 0)
|
|
1617
|
+
cursor += gap;
|
|
1618
|
+
cursor += marginStart;
|
|
1619
|
+
positions[i] = cursor;
|
|
1620
|
+
cursor += sizes[i] + marginEnd;
|
|
1621
|
+
}
|
|
1622
|
+
return { sizes, positions };
|
|
1623
|
+
},
|
|
1624
|
+
});
|
|
1625
|
+
return mainDistField;
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Emit the main-position rule for a child when the parent's
|
|
1629
|
+
* `justify-content` is not the default `flex-start`. The leftover
|
|
1630
|
+
* along the main axis is `max(0, innerMain - usedMain)`, where
|
|
1631
|
+
* `usedMain` is the sum of post-distribution main sizes plus margins
|
|
1632
|
+
* plus inter-item gaps. The leftover is distributed as a leading
|
|
1633
|
+
* cursor offset and/or an extra gap between items (the CSS rule).
|
|
1634
|
+
*
|
|
1635
|
+
* Dep graph: every sibling's main size and the parent's main size.
|
|
1636
|
+
* This is broader than the default flex-start case (priors only) but
|
|
1637
|
+
* matches what CSS requires — change any sibling's size and every
|
|
1638
|
+
* item's position can move under space-* or center.
|
|
1639
|
+
*
|
|
1640
|
+
* Dep graph: every in-flow sibling's main size, main-axis margins,
|
|
1641
|
+
* the parent's main size, the main-axis `gap`, and the parent's
|
|
1642
|
+
* main-axis `padding` edges — all declared input / layout Fields, so
|
|
1643
|
+
* any of them changing re-runs this rule.
|
|
1644
|
+
*
|
|
1645
|
+
* @internal
|
|
1646
|
+
*/
|
|
1647
|
+
function emitJustifiedMainPos(grammar, parent, mainPosField, mainSizeName, justify, indexInParent, direction, gapField, padStartField, padEndField, marginInput) {
|
|
1648
|
+
// In-flow siblings only — absolute and `display: 'none'` children
|
|
1649
|
+
// don't contribute to justify-content's leftover calculation.
|
|
1650
|
+
const inFlow = [];
|
|
1651
|
+
for (let i = 0; i < parent.getChildCount(); i++) {
|
|
1652
|
+
const sib = parent.getChild(i);
|
|
1653
|
+
if (!isInFlow(sib))
|
|
1654
|
+
continue;
|
|
1655
|
+
inFlow.push(sib);
|
|
1656
|
+
}
|
|
1657
|
+
const allSizes = inFlow.map((s) => field(s, mainSizeName));
|
|
1658
|
+
const startEdge = mainStartEdge(direction);
|
|
1659
|
+
const endEdge = mainEndEdge(direction);
|
|
1660
|
+
const marginStarts = inFlow.map((s) => marginInput(s, startEdge));
|
|
1661
|
+
const marginEnds = inFlow.map((s) => marginInput(s, endEdge));
|
|
1662
|
+
const n = allSizes.length;
|
|
1663
|
+
const parentMainField = field(parent, mainSizeName);
|
|
1664
|
+
grammar.set(mainPosField, {
|
|
1665
|
+
deps: [
|
|
1666
|
+
parentMainField,
|
|
1667
|
+
gapField,
|
|
1668
|
+
padStartField,
|
|
1669
|
+
padEndField,
|
|
1670
|
+
...allSizes,
|
|
1671
|
+
...marginStarts,
|
|
1672
|
+
...marginEnds,
|
|
1673
|
+
],
|
|
1674
|
+
compute: (read) => {
|
|
1675
|
+
const padStart = read(padStartField);
|
|
1676
|
+
const gap = read(gapField);
|
|
1677
|
+
const innerMain = Math.max(0, read(parentMainField) - padStart - read(padEndField));
|
|
1678
|
+
let usedMain = 0;
|
|
1679
|
+
for (let i = 0; i < n; i++) {
|
|
1680
|
+
usedMain += read(allSizes[i]) + read(marginStarts[i]) + read(marginEnds[i]);
|
|
1681
|
+
}
|
|
1682
|
+
if (n > 1)
|
|
1683
|
+
usedMain += (n - 1) * gap;
|
|
1684
|
+
const leftover = Math.max(0, innerMain - usedMain);
|
|
1685
|
+
let leadingOffset = 0;
|
|
1686
|
+
let extraGap = 0;
|
|
1687
|
+
switch (justify) {
|
|
1688
|
+
case 'flex-end':
|
|
1689
|
+
leadingOffset = leftover;
|
|
1690
|
+
break;
|
|
1691
|
+
case 'center':
|
|
1692
|
+
leadingOffset = leftover / 2;
|
|
1693
|
+
break;
|
|
1694
|
+
case 'space-between':
|
|
1695
|
+
if (n > 1)
|
|
1696
|
+
extraGap = leftover / (n - 1);
|
|
1697
|
+
break;
|
|
1698
|
+
case 'space-around': {
|
|
1699
|
+
const slot = leftover / n;
|
|
1700
|
+
leadingOffset = slot / 2;
|
|
1701
|
+
extraGap = slot;
|
|
1702
|
+
break;
|
|
1703
|
+
}
|
|
1704
|
+
case 'space-evenly': {
|
|
1705
|
+
const slot = leftover / (n + 1);
|
|
1706
|
+
leadingOffset = slot;
|
|
1707
|
+
extraGap = slot;
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
// Cursor walks the line up to this child, mirroring the
|
|
1712
|
+
// imperative positionItemsInLine.
|
|
1713
|
+
let cursor = padStart + leadingOffset;
|
|
1714
|
+
for (let i = 0; i < indexInParent; i++) {
|
|
1715
|
+
cursor += read(marginStarts[i]) + read(allSizes[i]) + read(marginEnds[i]);
|
|
1716
|
+
cursor += gap + extraGap;
|
|
1717
|
+
}
|
|
1718
|
+
cursor += read(marginStarts[indexInParent]);
|
|
1719
|
+
return cursor;
|
|
1720
|
+
},
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Re-wrap a child's already-emitted main-position rule so a
|
|
1725
|
+
* reverse-direction parent (`row-reverse` / `column-reverse`) lays
|
|
1726
|
+
* the child out from the main-axis END.
|
|
1727
|
+
*
|
|
1728
|
+
* Mirrors the imperative `flipMainAxis`: with `innerPos` the child's
|
|
1729
|
+
* forward offset inside the parent's inner-main box, the reflected
|
|
1730
|
+
* position is `padStart + innerMain - innerPos - childMain`. The
|
|
1731
|
+
* forward rule is preserved and invoked for `innerPos`; this wrapper
|
|
1732
|
+
* only reflects its result, so every regime (flex-start, justified,
|
|
1733
|
+
* wrap) reverses uniformly.
|
|
1734
|
+
*
|
|
1735
|
+
* The deps become the union of the forward rule's deps and the three
|
|
1736
|
+
* fields the reflection adds — the parent's main size, both main-axis
|
|
1737
|
+
* padding edges, and the child's own main size.
|
|
1738
|
+
*
|
|
1739
|
+
* @internal
|
|
1740
|
+
*/
|
|
1741
|
+
function applyReverseMainPos(grammar, parent, mainPosField, mainSizeField, mainSizeName, padMainStartF, padMainEndF) {
|
|
1742
|
+
const forward = grammar.get(mainPosField);
|
|
1743
|
+
const parentMainField = field(parent, mainSizeName);
|
|
1744
|
+
const deps = [...forward.deps];
|
|
1745
|
+
for (const d of [
|
|
1746
|
+
parentMainField,
|
|
1747
|
+
padMainStartF,
|
|
1748
|
+
padMainEndF,
|
|
1749
|
+
mainSizeField,
|
|
1750
|
+
]) {
|
|
1751
|
+
if (!deps.includes(d))
|
|
1752
|
+
deps.push(d);
|
|
1753
|
+
}
|
|
1754
|
+
grammar.set(mainPosField, {
|
|
1755
|
+
deps,
|
|
1756
|
+
compute: (read) => {
|
|
1757
|
+
const forwardPos = forward.compute(read);
|
|
1758
|
+
const padStart = read(padMainStartF);
|
|
1759
|
+
const innerMain = Math.max(0, read(parentMainField) - padStart - read(padMainEndF));
|
|
1760
|
+
const childMain = read(mainSizeField);
|
|
1761
|
+
const innerPos = forwardPos - padStart;
|
|
1762
|
+
return padStart + innerMain - innerPos - childMain;
|
|
1763
|
+
},
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* True iff a parent's children carry any flex property that lets a
|
|
1768
|
+
* child's main size differ from its raw `style.{width|height}`: a
|
|
1769
|
+
* positive grow weight, a positive shrink weight, or a numeric
|
|
1770
|
+
* `flexBasis`.
|
|
1771
|
+
*/
|
|
1772
|
+
function parentNeedsFlexDistribution(parent) {
|
|
1773
|
+
const count = parent.getChildCount();
|
|
1774
|
+
for (let i = 0; i < count; i++) {
|
|
1775
|
+
const c = parent.getChild(i);
|
|
1776
|
+
if (!isInFlow(c))
|
|
1777
|
+
continue;
|
|
1778
|
+
const s = c.style;
|
|
1779
|
+
if (s.flexGrow > 0)
|
|
1780
|
+
return true;
|
|
1781
|
+
if (s.flexShrink > 0)
|
|
1782
|
+
return true;
|
|
1783
|
+
if (typeof s.flexBasis === 'number')
|
|
1784
|
+
return true;
|
|
1785
|
+
}
|
|
1786
|
+
return false;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Distribute `budget` across siblings using CSS flex semantics with
|
|
1790
|
+
* min/max clamping (v12b). Returns each sibling's final main-axis
|
|
1791
|
+
* size in input order.
|
|
1792
|
+
*
|
|
1793
|
+
* `budget` is the parent's inner main size (containerMain minus
|
|
1794
|
+
* leading + trailing padding). Margins and gaps are accounted for via
|
|
1795
|
+
* the hypothetical sum: only the basis part of each sibling expands
|
|
1796
|
+
* (grow) or contracts (shrink); margins and gaps are fixed-width
|
|
1797
|
+
* spacers that consume budget but never resize.
|
|
1798
|
+
*
|
|
1799
|
+
* Each sibling's hypothetical size is its basis clamped to its own
|
|
1800
|
+
* `[min, max]` — the imperative `buildItem` clamps before packing /
|
|
1801
|
+
* distribution. The grow / shrink passes then run the CSS freeze
|
|
1802
|
+
* loop (`freezeLoopGrow` / `freezeLoopShrink`): an item whose
|
|
1803
|
+
* proportional target would breach a clamp is pinned ("frozen") at
|
|
1804
|
+
* its bound and its share is redistributed among the rest, iterating
|
|
1805
|
+
* to a fixpoint. Mirrors `distributeGrow` / `distributeShrink` in
|
|
1806
|
+
* `main-axis.ts`.
|
|
1807
|
+
*
|
|
1808
|
+
* @internal
|
|
1809
|
+
*/
|
|
1810
|
+
function distributeMainAxis(siblings, budget, gap) {
|
|
1811
|
+
const n = siblings.length;
|
|
1812
|
+
// Hypothetical = basis clamped to the sibling's own [min, max].
|
|
1813
|
+
const hyp = siblings.map((s) => clampMinMax(s.basis, s.min, s.max));
|
|
1814
|
+
let hypotheticalMain = 0;
|
|
1815
|
+
for (let i = 0; i < n; i++) {
|
|
1816
|
+
hypotheticalMain += hyp[i] + siblings[i].marginStart + siblings[i].marginEnd;
|
|
1817
|
+
}
|
|
1818
|
+
if (n > 1)
|
|
1819
|
+
hypotheticalMain += (n - 1) * gap;
|
|
1820
|
+
const slack = budget - hypotheticalMain;
|
|
1821
|
+
const final = hyp.slice();
|
|
1822
|
+
if (slack > 0) {
|
|
1823
|
+
freezeLoopGrow(siblings, hyp, final, slack);
|
|
1824
|
+
}
|
|
1825
|
+
else if (slack < 0) {
|
|
1826
|
+
freezeLoopShrink(siblings, hyp, final, -slack);
|
|
1827
|
+
}
|
|
1828
|
+
return final;
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* The flex-grow freeze loop. `hyp` holds each sibling's clamped
|
|
1832
|
+
* hypothetical; `final` is seeded with `hyp` and mutated in place to
|
|
1833
|
+
* the post-distribution sizes. Items with `grow <= 0` never grow;
|
|
1834
|
+
* an item whose proportional target breaches its `[min, max]` is
|
|
1835
|
+
* frozen at the clamped bound and drops out of subsequent rounds.
|
|
1836
|
+
* Mirrors the imperative `distributeGrow`.
|
|
1837
|
+
*/
|
|
1838
|
+
function freezeLoopGrow(siblings, hyp, final, slack) {
|
|
1839
|
+
const n = siblings.length;
|
|
1840
|
+
const frozen = new Array(n).fill(false);
|
|
1841
|
+
for (let i = 0; i < n; i++) {
|
|
1842
|
+
if (siblings[i].grow <= 0)
|
|
1843
|
+
frozen[i] = true;
|
|
1844
|
+
}
|
|
1845
|
+
for (let iter = 0; iter < n + 1; iter++) {
|
|
1846
|
+
let totalGrow = 0;
|
|
1847
|
+
let frozenContribution = 0;
|
|
1848
|
+
for (let i = 0; i < n; i++) {
|
|
1849
|
+
if (frozen[i])
|
|
1850
|
+
frozenContribution += final[i] - hyp[i];
|
|
1851
|
+
else
|
|
1852
|
+
totalGrow += siblings[i].grow;
|
|
1853
|
+
}
|
|
1854
|
+
if (totalGrow <= 0)
|
|
1855
|
+
return;
|
|
1856
|
+
const remaining = slack - frozenContribution;
|
|
1857
|
+
if (remaining <= 0)
|
|
1858
|
+
return;
|
|
1859
|
+
let frozeAny = false;
|
|
1860
|
+
for (let i = 0; i < n; i++) {
|
|
1861
|
+
if (frozen[i])
|
|
1862
|
+
continue;
|
|
1863
|
+
const s = siblings[i];
|
|
1864
|
+
const target = hyp[i] + (remaining * s.grow) / totalGrow;
|
|
1865
|
+
const clamped = clampMinMax(target, s.min, s.max);
|
|
1866
|
+
final[i] = clamped;
|
|
1867
|
+
if (clamped !== target) {
|
|
1868
|
+
frozen[i] = true;
|
|
1869
|
+
frozeAny = true;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (!frozeAny)
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* The flex-shrink freeze loop — symmetric to `freezeLoopGrow`. The
|
|
1878
|
+
* shrink share is scaled by `shrink * hypothetical` (CSS weights
|
|
1879
|
+
* shrink by base size). Mirrors the imperative `distributeShrink`.
|
|
1880
|
+
*/
|
|
1881
|
+
function freezeLoopShrink(siblings, hyp, final, overflow) {
|
|
1882
|
+
const n = siblings.length;
|
|
1883
|
+
const frozen = new Array(n).fill(false);
|
|
1884
|
+
for (let i = 0; i < n; i++) {
|
|
1885
|
+
if (siblings[i].shrink <= 0)
|
|
1886
|
+
frozen[i] = true;
|
|
1887
|
+
}
|
|
1888
|
+
for (let iter = 0; iter < n + 1; iter++) {
|
|
1889
|
+
let totalScaled = 0;
|
|
1890
|
+
let frozenContribution = 0;
|
|
1891
|
+
for (let i = 0; i < n; i++) {
|
|
1892
|
+
if (frozen[i])
|
|
1893
|
+
frozenContribution += hyp[i] - final[i];
|
|
1894
|
+
else
|
|
1895
|
+
totalScaled += siblings[i].shrink * hyp[i];
|
|
1896
|
+
}
|
|
1897
|
+
if (totalScaled <= 0)
|
|
1898
|
+
return;
|
|
1899
|
+
const remaining = overflow - frozenContribution;
|
|
1900
|
+
if (remaining <= 0)
|
|
1901
|
+
return;
|
|
1902
|
+
let frozeAny = false;
|
|
1903
|
+
for (let i = 0; i < n; i++) {
|
|
1904
|
+
if (frozen[i])
|
|
1905
|
+
continue;
|
|
1906
|
+
const s = siblings[i];
|
|
1907
|
+
const scaled = s.shrink * hyp[i];
|
|
1908
|
+
if (scaled <= 0) {
|
|
1909
|
+
frozen[i] = true;
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
const reduction = (remaining * scaled) / totalScaled;
|
|
1913
|
+
const target = hyp[i] - reduction;
|
|
1914
|
+
const clamped = clampMinMax(target, s.min, s.max);
|
|
1915
|
+
final[i] = clamped;
|
|
1916
|
+
if (clamped !== target) {
|
|
1917
|
+
frozen[i] = true;
|
|
1918
|
+
frozeAny = true;
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (!frozeAny)
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
// ─── axis-aware spacing readers ─────────────────────────────────────────
|
|
1926
|
+
// Edge order in style boxes is [top, right, bottom, left]. Gap layout
|
|
1927
|
+
// is keyed on the OUTPUT axis (the one items stack along), not the
|
|
1928
|
+
// flex-direction name — `gapColumn` separates row-stacked items
|
|
1929
|
+
// (column between columns), `gapRow` separates column-stacked items.
|
|
1930
|
+
const TOP = 0;
|
|
1931
|
+
const RIGHT = 1;
|
|
1932
|
+
const BOTTOM = 2;
|
|
1933
|
+
const LEFT = 3;
|
|
1934
|
+
function mainStartEdge(direction) {
|
|
1935
|
+
return direction === 'column' ? TOP : LEFT;
|
|
1936
|
+
}
|
|
1937
|
+
function mainEndEdge(direction) {
|
|
1938
|
+
return direction === 'column' ? BOTTOM : RIGHT;
|
|
1939
|
+
}
|
|
1940
|
+
function crossStartEdge(direction) {
|
|
1941
|
+
return direction === 'column' ? LEFT : TOP;
|
|
1942
|
+
}
|
|
1943
|
+
function crossEndEdge(direction) {
|
|
1944
|
+
// Cross axis is perpendicular to the main axis; its end edge sits
|
|
1945
|
+
// opposite the cross start edge.
|
|
1946
|
+
return direction === 'column' ? RIGHT : BOTTOM;
|
|
1947
|
+
}
|
|
1948
|
+
// ─── absolute positioning ───────────────────────────────────────────────
|
|
1949
|
+
/**
|
|
1950
|
+
* Emit the four field rules for an out-of-flow (`positionType ===
|
|
1951
|
+
* 'absolute'`) child. Mirrors the imperative `layoutAbsoluteChild`
|
|
1952
|
+
* in `main-axis.ts`:
|
|
1953
|
+
*
|
|
1954
|
+
* - width: explicit `style.width` if numeric, else (if both LEFT
|
|
1955
|
+
* and RIGHT edges are set) derived from `parent.width - left -
|
|
1956
|
+
* right - margins`, else 0.
|
|
1957
|
+
* - height: symmetric, using TOP / BOTTOM edges.
|
|
1958
|
+
* - left: if LEFT edge set, `left + margin.left`; else if RIGHT
|
|
1959
|
+
* edge set, `parent.width - width - right - margin.right`; else
|
|
1960
|
+
* `margin.left`.
|
|
1961
|
+
* - top: symmetric, using TOP / BOTTOM and `parent.height`.
|
|
1962
|
+
*
|
|
1963
|
+
* The parent's OUTER size is used (no padding subtraction) —
|
|
1964
|
+
* matches Yoga / RN semantics that Pilates follows. Width and height
|
|
1965
|
+
* are clamped to the child's own [min, max] in every branch (v12),
|
|
1966
|
+
* exactly as the imperative `layoutAbsoluteChild` — including the
|
|
1967
|
+
* `0` fallback, so e.g. a `minWidth` with no explicit width still
|
|
1968
|
+
* binds.
|
|
1969
|
+
*
|
|
1970
|
+
* @internal
|
|
1971
|
+
*/
|
|
1972
|
+
function emitAbsoluteRules(grammar, styleSizeInput, marginInput, minMaxInput, parent, child, width, height, left, top) {
|
|
1973
|
+
const pos = child.style.position;
|
|
1974
|
+
const posTop = pos[TOP];
|
|
1975
|
+
const posRight = pos[RIGHT];
|
|
1976
|
+
const posBottom = pos[BOTTOM];
|
|
1977
|
+
const posLeft = pos[LEFT];
|
|
1978
|
+
// Margins are declared input-field deps so a `setMargin` on the
|
|
1979
|
+
// absolute child propagates precisely. The `position` edges stay
|
|
1980
|
+
// captured: their presence selects the branch (structural).
|
|
1981
|
+
const mTop = marginInput(child, TOP);
|
|
1982
|
+
const mRight = marginInput(child, RIGHT);
|
|
1983
|
+
const mBottom = marginInput(child, BOTTOM);
|
|
1984
|
+
const mLeft = marginInput(child, LEFT);
|
|
1985
|
+
const styleW = child.style.width;
|
|
1986
|
+
const styleH = child.style.height;
|
|
1987
|
+
const parentWField = field(parent, 'width');
|
|
1988
|
+
const parentHField = field(parent, 'height');
|
|
1989
|
+
// Min / max clamp inputs — declared deps so a `setMinWidth` … on
|
|
1990
|
+
// the absolute child propagates precisely.
|
|
1991
|
+
const minW = minMaxInput(child, 'minWidth');
|
|
1992
|
+
const maxW = minMaxInput(child, 'maxWidth');
|
|
1993
|
+
const minH = minMaxInput(child, 'minHeight');
|
|
1994
|
+
const maxH = minMaxInput(child, 'maxHeight');
|
|
1995
|
+
// Width. The explicit-width branch reads the child's `style:width`
|
|
1996
|
+
// input field, so a `setWidth` on the absolute child propagates
|
|
1997
|
+
// precisely through markDirty + recompute. Mutating the child from
|
|
1998
|
+
// explicit to 'auto' (or vice-versa) requires a fresh grammar build
|
|
1999
|
+
// since that crosses branch boundaries — out of scope here. Every
|
|
2000
|
+
// branch clamps to [minW, maxW], matching `layoutAbsoluteChild`.
|
|
2001
|
+
if (typeof styleW === 'number') {
|
|
2002
|
+
const wInput = styleSizeInput(child, 'width');
|
|
2003
|
+
grammar.set(width, {
|
|
2004
|
+
deps: [wInput, minW, maxW],
|
|
2005
|
+
compute: (read) => clampMinMax(read(wInput), read(minW), read(maxW)),
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
else if (posLeft !== undefined && posRight !== undefined) {
|
|
2009
|
+
grammar.set(width, {
|
|
2010
|
+
deps: [
|
|
2011
|
+
parentWField,
|
|
2012
|
+
mLeft,
|
|
2013
|
+
mRight,
|
|
2014
|
+
minW,
|
|
2015
|
+
maxW,
|
|
2016
|
+
],
|
|
2017
|
+
compute: (read) => clampMinMax(Math.max(0, read(parentWField) - posLeft - posRight - read(mLeft) - read(mRight)), read(minW), read(maxW)),
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
else if (isMeasureLeaf(child)) {
|
|
2021
|
+
// `'auto'` width, no opposing edges: the measurer sizes it.
|
|
2022
|
+
// Mirrors `layoutAbsoluteChild` — call the measurer with the
|
|
2023
|
+
// parent's outer box `AtMost` on both axes and take `.width`.
|
|
2024
|
+
const measure = child.getMeasureFunc();
|
|
2025
|
+
grammar.set(width, {
|
|
2026
|
+
deps: [
|
|
2027
|
+
parentWField,
|
|
2028
|
+
parentHField,
|
|
2029
|
+
minW,
|
|
2030
|
+
maxW,
|
|
2031
|
+
],
|
|
2032
|
+
compute: (read) => clampMinMax(measure(read(parentWField), MeasureMode.AtMost, read(parentHField), MeasureMode.AtMost)
|
|
2033
|
+
.width, read(minW), read(maxW)),
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
else {
|
|
2037
|
+
grammar.set(width, {
|
|
2038
|
+
deps: [minW, maxW],
|
|
2039
|
+
compute: (read) => clampMinMax(0, read(minW), read(maxW)),
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
// Height — symmetric to width.
|
|
2043
|
+
if (typeof styleH === 'number') {
|
|
2044
|
+
const hInput = styleSizeInput(child, 'height');
|
|
2045
|
+
grammar.set(height, {
|
|
2046
|
+
deps: [hInput, minH, maxH],
|
|
2047
|
+
compute: (read) => clampMinMax(read(hInput), read(minH), read(maxH)),
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
else if (posTop !== undefined && posBottom !== undefined) {
|
|
2051
|
+
grammar.set(height, {
|
|
2052
|
+
deps: [
|
|
2053
|
+
parentHField,
|
|
2054
|
+
mTop,
|
|
2055
|
+
mBottom,
|
|
2056
|
+
minH,
|
|
2057
|
+
maxH,
|
|
2058
|
+
],
|
|
2059
|
+
compute: (read) => clampMinMax(Math.max(0, read(parentHField) - posTop - posBottom - read(mTop) - read(mBottom)), read(minH), read(maxH)),
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
else if (isMeasureLeaf(child)) {
|
|
2063
|
+
// `'auto'` height, no opposing edges: measure with the resolved
|
|
2064
|
+
// width `Exactly` and the parent's outer height `AtMost` — the
|
|
2065
|
+
// `layoutAbsoluteChild` height branch. The dep on `width` orders
|
|
2066
|
+
// this rule after the width rule above.
|
|
2067
|
+
const measure = child.getMeasureFunc();
|
|
2068
|
+
grammar.set(height, {
|
|
2069
|
+
deps: [
|
|
2070
|
+
width,
|
|
2071
|
+
parentHField,
|
|
2072
|
+
minH,
|
|
2073
|
+
maxH,
|
|
2074
|
+
],
|
|
2075
|
+
compute: (read) => clampMinMax(measure(read(width), MeasureMode.Exactly, read(parentHField), MeasureMode.AtMost).height, read(minH), read(maxH)),
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
else {
|
|
2079
|
+
grammar.set(height, {
|
|
2080
|
+
deps: [minH, maxH],
|
|
2081
|
+
compute: (read) => clampMinMax(0, read(minH), read(maxH)),
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
// Left
|
|
2085
|
+
if (posLeft !== undefined) {
|
|
2086
|
+
grammar.set(left, {
|
|
2087
|
+
deps: [mLeft],
|
|
2088
|
+
compute: (read) => posLeft + read(mLeft),
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
else if (posRight !== undefined) {
|
|
2092
|
+
grammar.set(left, {
|
|
2093
|
+
deps: [parentWField, width, mRight],
|
|
2094
|
+
compute: (read) => read(parentWField) - read(width) - posRight - read(mRight),
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
else {
|
|
2098
|
+
grammar.set(left, {
|
|
2099
|
+
deps: [mLeft],
|
|
2100
|
+
compute: (read) => read(mLeft),
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
// Top
|
|
2104
|
+
if (posTop !== undefined) {
|
|
2105
|
+
grammar.set(top, {
|
|
2106
|
+
deps: [mTop],
|
|
2107
|
+
compute: (read) => posTop + read(mTop),
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
else if (posBottom !== undefined) {
|
|
2111
|
+
grammar.set(top, {
|
|
2112
|
+
deps: [parentHField, height, mBottom],
|
|
2113
|
+
compute: (read) => read(parentHField) - read(height) - posBottom - read(mBottom),
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
else {
|
|
2117
|
+
grammar.set(top, {
|
|
2118
|
+
deps: [mTop],
|
|
2119
|
+
compute: (read) => read(mTop),
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Pack `siblings` greedily into lines along the main axis, run flex
|
|
2125
|
+
* distribution per line, compute each line's cross size and start,
|
|
2126
|
+
* then resolve the indicated child's `{mainSize, mainPos, crossPos}`.
|
|
2127
|
+
*
|
|
2128
|
+
* Mirrors the imperative `packIntoLines` → `distributeFlexInLine` →
|
|
2129
|
+
* `computeLineCrossSizes` → `positionLinesOnCross` (the `alignContent`
|
|
2130
|
+
* line distribution) → `positionItemsInLine` → `crossAlignItemsInLine`
|
|
2131
|
+
* chain. The single-line case (one packed line) collapses crossSize
|
|
2132
|
+
* to `innerCross` and crossPos of the line to 0, matching the
|
|
2133
|
+
* imperative's `singleLineMode` branch.
|
|
2134
|
+
*
|
|
2135
|
+
* Called once per child per layout pass; the per-child callbacks pick
|
|
2136
|
+
* out their own value from the returned struct. Total work is O(N²)
|
|
2137
|
+
* for an N-child wrapped container — acceptable for v7; later
|
|
2138
|
+
* Spineless tiers can extract shared per-line fields.
|
|
2139
|
+
*
|
|
2140
|
+
* @internal
|
|
2141
|
+
*/
|
|
2142
|
+
function evaluateWrappedChild(siblings, childIndex, innerMain, innerCross, mainGap, crossGap, justify, alignContent, reverse, padMainStart, padCrossStart) {
|
|
2143
|
+
const n = siblings.length;
|
|
2144
|
+
// Pack greedily, recording per-line start index and count.
|
|
2145
|
+
const lineFirst = [];
|
|
2146
|
+
const lineCount = [];
|
|
2147
|
+
{
|
|
2148
|
+
let start = 0;
|
|
2149
|
+
let acc = 0;
|
|
2150
|
+
for (let i = 0; i < n; i++) {
|
|
2151
|
+
const s = siblings[i];
|
|
2152
|
+
// Pack on the clamped hypothetical (v12b) — the imperative
|
|
2153
|
+
// `packIntoLines` keys on `item.hypothetical`, not raw basis.
|
|
2154
|
+
const itemMain = clampMinMax(s.basis, s.min, s.max) + s.mainMarginStart + s.mainMarginEnd;
|
|
2155
|
+
const inLine = i > start;
|
|
2156
|
+
const wouldUse = acc + (inLine ? mainGap : 0) + itemMain;
|
|
2157
|
+
if (inLine && wouldUse > innerMain) {
|
|
2158
|
+
lineFirst.push(start);
|
|
2159
|
+
lineCount.push(i - start);
|
|
2160
|
+
start = i;
|
|
2161
|
+
acc = itemMain;
|
|
2162
|
+
}
|
|
2163
|
+
else {
|
|
2164
|
+
if (inLine)
|
|
2165
|
+
acc += mainGap;
|
|
2166
|
+
acc += itemMain;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (start < n) {
|
|
2170
|
+
lineFirst.push(start);
|
|
2171
|
+
lineCount.push(n - start);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
const numLines = lineFirst.length;
|
|
2175
|
+
const isMultiline = numLines > 1;
|
|
2176
|
+
// Per-line distribution. WrapSibling renames the main-axis margins
|
|
2177
|
+
// to `mainMargin*` (they share fields with the cross-axis margins);
|
|
2178
|
+
// distributeMainAxis takes a smaller shape so we map at the boundary.
|
|
2179
|
+
const finalMainSizes = new Array(n);
|
|
2180
|
+
for (let li = 0; li < numLines; li++) {
|
|
2181
|
+
const first = lineFirst[li];
|
|
2182
|
+
const count = lineCount[li];
|
|
2183
|
+
const lineSiblings = siblings.slice(first, first + count).map((s) => ({
|
|
2184
|
+
basis: s.basis,
|
|
2185
|
+
grow: s.grow,
|
|
2186
|
+
shrink: s.shrink,
|
|
2187
|
+
marginStart: s.mainMarginStart,
|
|
2188
|
+
marginEnd: s.mainMarginEnd,
|
|
2189
|
+
min: s.min,
|
|
2190
|
+
max: s.max,
|
|
2191
|
+
}));
|
|
2192
|
+
const distributed = distributeMainAxis(lineSiblings, innerMain, mainGap);
|
|
2193
|
+
for (let k = 0; k < count; k++) {
|
|
2194
|
+
finalMainSizes[first + k] = distributed[k];
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
// Per-line cross size: container's inner cross for single-line,
|
|
2198
|
+
// max of (item.crossSize + cross margins) otherwise.
|
|
2199
|
+
const lineCrossSizes = new Array(numLines);
|
|
2200
|
+
if (!isMultiline) {
|
|
2201
|
+
lineCrossSizes[0] = innerCross;
|
|
2202
|
+
}
|
|
2203
|
+
else {
|
|
2204
|
+
for (let li = 0; li < numLines; li++) {
|
|
2205
|
+
const first = lineFirst[li];
|
|
2206
|
+
const count = lineCount[li];
|
|
2207
|
+
let max = 0;
|
|
2208
|
+
for (let k = 0; k < count; k++) {
|
|
2209
|
+
const s = siblings[first + k];
|
|
2210
|
+
// Unclamped natural cross — a clamped item may overflow.
|
|
2211
|
+
const candidate = s.crossSizeNatural + s.crossMarginStart + s.crossMarginEnd;
|
|
2212
|
+
if (candidate > max)
|
|
2213
|
+
max = candidate;
|
|
2214
|
+
}
|
|
2215
|
+
lineCrossSizes[li] = max;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
// Per-line cross start (align-content). Single-line: the one line
|
|
2219
|
+
// sits at 0. Multi-line: distribute the cross-axis leftover among
|
|
2220
|
+
// or around the lines per `alignContent`, mirroring the imperative
|
|
2221
|
+
// `positionLinesOnCross`. `stretch` / `auto` grows each line's
|
|
2222
|
+
// cross size to absorb the leftover instead.
|
|
2223
|
+
const lineCrossStarts = new Array(numLines);
|
|
2224
|
+
if (!isMultiline) {
|
|
2225
|
+
lineCrossStarts[0] = 0;
|
|
2226
|
+
}
|
|
2227
|
+
else {
|
|
2228
|
+
let used = 0;
|
|
2229
|
+
for (let li = 0; li < numLines; li++)
|
|
2230
|
+
used += lineCrossSizes[li];
|
|
2231
|
+
used += (numLines - 1) * crossGap;
|
|
2232
|
+
const leftover = innerCross - used;
|
|
2233
|
+
let cursor = 0;
|
|
2234
|
+
let extraGap = 0;
|
|
2235
|
+
let lineSizeBoost = 0;
|
|
2236
|
+
switch (alignContent) {
|
|
2237
|
+
case 'flex-end':
|
|
2238
|
+
cursor = leftover;
|
|
2239
|
+
break;
|
|
2240
|
+
case 'center':
|
|
2241
|
+
cursor = leftover / 2;
|
|
2242
|
+
break;
|
|
2243
|
+
case 'space-between':
|
|
2244
|
+
if (numLines > 1 && leftover > 0)
|
|
2245
|
+
extraGap = leftover / (numLines - 1);
|
|
2246
|
+
break;
|
|
2247
|
+
case 'space-around':
|
|
2248
|
+
if (leftover > 0) {
|
|
2249
|
+
const slot = leftover / numLines;
|
|
2250
|
+
cursor = slot / 2;
|
|
2251
|
+
extraGap = slot;
|
|
2252
|
+
}
|
|
2253
|
+
break;
|
|
2254
|
+
case 'stretch':
|
|
2255
|
+
case 'auto':
|
|
2256
|
+
if (leftover > 0)
|
|
2257
|
+
lineSizeBoost = leftover / numLines;
|
|
2258
|
+
break;
|
|
2259
|
+
default:
|
|
2260
|
+
// flex-start: lines stacked from the cross start, no extra.
|
|
2261
|
+
break;
|
|
2262
|
+
}
|
|
2263
|
+
for (let li = 0; li < numLines; li++) {
|
|
2264
|
+
if (lineSizeBoost > 0)
|
|
2265
|
+
lineCrossSizes[li] = lineCrossSizes[li] + lineSizeBoost;
|
|
2266
|
+
lineCrossStarts[li] = cursor;
|
|
2267
|
+
cursor += lineCrossSizes[li] + crossGap + extraGap;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
// `flex-wrap: wrap-reverse` mirrors the line stack on the cross
|
|
2271
|
+
// axis — each line is measured from the cross END. Mirrors the
|
|
2272
|
+
// imperative `reverseLineStack`. (A no-op for a single line, whose
|
|
2273
|
+
// cross size already fills `innerCross`.)
|
|
2274
|
+
if (reverse) {
|
|
2275
|
+
for (let li = 0; li < numLines; li++) {
|
|
2276
|
+
lineCrossStarts[li] = innerCross - lineCrossStarts[li] - lineCrossSizes[li];
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
// Locate the target child.
|
|
2280
|
+
let myLine = 0;
|
|
2281
|
+
let myPositionInLine = 0;
|
|
2282
|
+
for (let li = 0; li < numLines; li++) {
|
|
2283
|
+
const first = lineFirst[li];
|
|
2284
|
+
const count = lineCount[li];
|
|
2285
|
+
if (childIndex >= first && childIndex < first + count) {
|
|
2286
|
+
myLine = li;
|
|
2287
|
+
myPositionInLine = childIndex - first;
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const myLineFirst = lineFirst[myLine];
|
|
2292
|
+
const myLineCount = lineCount[myLine];
|
|
2293
|
+
const myLineCrossSize = lineCrossSizes[myLine];
|
|
2294
|
+
const myLineCrossStart = lineCrossStarts[myLine];
|
|
2295
|
+
const me = siblings[childIndex];
|
|
2296
|
+
// justify-content per line: compute leftover from this line's
|
|
2297
|
+
// used main, then leading offset / extra gap.
|
|
2298
|
+
let usedMain = 0;
|
|
2299
|
+
for (let k = 0; k < myLineCount; k++) {
|
|
2300
|
+
const idx = myLineFirst + k;
|
|
2301
|
+
const s = siblings[idx];
|
|
2302
|
+
usedMain += finalMainSizes[idx] + s.mainMarginStart + s.mainMarginEnd;
|
|
2303
|
+
}
|
|
2304
|
+
if (myLineCount > 1)
|
|
2305
|
+
usedMain += (myLineCount - 1) * mainGap;
|
|
2306
|
+
const leftover = Math.max(0, innerMain - usedMain);
|
|
2307
|
+
let leadingOffset = 0;
|
|
2308
|
+
let extraGap = 0;
|
|
2309
|
+
switch (justify) {
|
|
2310
|
+
case 'flex-end':
|
|
2311
|
+
leadingOffset = leftover;
|
|
2312
|
+
break;
|
|
2313
|
+
case 'center':
|
|
2314
|
+
leadingOffset = leftover / 2;
|
|
2315
|
+
break;
|
|
2316
|
+
case 'space-between':
|
|
2317
|
+
if (myLineCount > 1)
|
|
2318
|
+
extraGap = leftover / (myLineCount - 1);
|
|
2319
|
+
break;
|
|
2320
|
+
case 'space-around': {
|
|
2321
|
+
const slot = leftover / myLineCount;
|
|
2322
|
+
leadingOffset = slot / 2;
|
|
2323
|
+
extraGap = slot;
|
|
2324
|
+
break;
|
|
2325
|
+
}
|
|
2326
|
+
case 'space-evenly': {
|
|
2327
|
+
const slot = leftover / (myLineCount + 1);
|
|
2328
|
+
leadingOffset = slot;
|
|
2329
|
+
extraGap = slot;
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
default:
|
|
2333
|
+
// flex-start
|
|
2334
|
+
break;
|
|
2335
|
+
}
|
|
2336
|
+
// Main-axis cursor walks this line up to the target child.
|
|
2337
|
+
let cursor = padMainStart + leadingOffset;
|
|
2338
|
+
for (let k = 0; k < myPositionInLine; k++) {
|
|
2339
|
+
const idx = myLineFirst + k;
|
|
2340
|
+
const s = siblings[idx];
|
|
2341
|
+
cursor += s.mainMarginStart + finalMainSizes[idx] + s.mainMarginEnd;
|
|
2342
|
+
cursor += mainGap + extraGap;
|
|
2343
|
+
}
|
|
2344
|
+
cursor += me.mainMarginStart;
|
|
2345
|
+
const mainPos = cursor;
|
|
2346
|
+
// Cross-axis position via align-items / align-self within line.
|
|
2347
|
+
let withinLineCross = me.crossMarginStart;
|
|
2348
|
+
if (me.align === 'flex-end') {
|
|
2349
|
+
withinLineCross = myLineCrossSize - me.crossSize - me.crossMarginEnd;
|
|
2350
|
+
}
|
|
2351
|
+
else if (me.align === 'center') {
|
|
2352
|
+
const innerLine = myLineCrossSize - me.crossMarginStart - me.crossMarginEnd;
|
|
2353
|
+
withinLineCross = me.crossMarginStart + Math.max(0, (innerLine - me.crossSize) / 2);
|
|
2354
|
+
}
|
|
2355
|
+
const crossPos = padCrossStart + myLineCrossStart + withinLineCross;
|
|
2356
|
+
// Cross size: `align-items: stretch` (the default) resizes an
|
|
2357
|
+
// `'auto'` cross to fill the line's inner cross (v14); otherwise
|
|
2358
|
+
// the already-clamped `me.crossSize` stands (clamped explicit, or
|
|
2359
|
+
// 0 for a non-stretched `'auto'`). Mirrors the imperative
|
|
2360
|
+
// `crossAlignItemsInLine` stretch branch.
|
|
2361
|
+
let crossSize = me.crossSize;
|
|
2362
|
+
if (me.crossIsContentAuto && me.align === 'stretch') {
|
|
2363
|
+
const lineInner = myLineCrossSize - me.crossMarginStart - me.crossMarginEnd;
|
|
2364
|
+
crossSize = clampMinMax(Math.max(0, lineInner), me.crossMin, me.crossMax);
|
|
2365
|
+
}
|
|
2366
|
+
return {
|
|
2367
|
+
mainSize: finalMainSizes[childIndex],
|
|
2368
|
+
mainPos,
|
|
2369
|
+
crossPos,
|
|
2370
|
+
crossSize,
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
//# sourceMappingURL=flex-grammar.js.map
|