@pilates/core 1.0.0 → 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.
Files changed (53) hide show
  1. package/dist/algorithm/index.d.ts +31 -0
  2. package/dist/algorithm/index.d.ts.map +1 -1
  3. package/dist/algorithm/index.js +85 -1
  4. package/dist/algorithm/index.js.map +1 -1
  5. package/dist/algorithm/round.d.ts +13 -0
  6. package/dist/algorithm/round.d.ts.map +1 -1
  7. package/dist/algorithm/round.js +17 -0
  8. package/dist/algorithm/round.js.map +1 -1
  9. package/dist/algorithm/spineless/flex-grammar.d.ts +394 -0
  10. package/dist/algorithm/spineless/flex-grammar.d.ts.map +1 -0
  11. package/dist/algorithm/spineless/flex-grammar.js +2373 -0
  12. package/dist/algorithm/spineless/flex-grammar.js.map +1 -0
  13. package/dist/algorithm/spineless/grammar.d.ts +150 -0
  14. package/dist/algorithm/spineless/grammar.d.ts.map +1 -0
  15. package/dist/algorithm/spineless/grammar.js +144 -0
  16. package/dist/algorithm/spineless/grammar.js.map +1 -0
  17. package/dist/algorithm/spineless/layout.d.ts +130 -0
  18. package/dist/algorithm/spineless/layout.d.ts.map +1 -0
  19. package/dist/algorithm/spineless/layout.js +755 -0
  20. package/dist/algorithm/spineless/layout.js.map +1 -0
  21. package/dist/algorithm/spineless/order-maintenance.bench.d.ts +25 -0
  22. package/dist/algorithm/spineless/order-maintenance.bench.d.ts.map +1 -0
  23. package/dist/algorithm/spineless/order-maintenance.bench.js +78 -0
  24. package/dist/algorithm/spineless/order-maintenance.bench.js.map +1 -0
  25. package/dist/algorithm/spineless/order-maintenance.d.ts +192 -0
  26. package/dist/algorithm/spineless/order-maintenance.d.ts.map +1 -0
  27. package/dist/algorithm/spineless/order-maintenance.js +294 -0
  28. package/dist/algorithm/spineless/order-maintenance.js.map +1 -0
  29. package/dist/algorithm/spineless/priority-queue.bench.d.ts +17 -0
  30. package/dist/algorithm/spineless/priority-queue.bench.d.ts.map +1 -0
  31. package/dist/algorithm/spineless/priority-queue.bench.js +57 -0
  32. package/dist/algorithm/spineless/priority-queue.bench.js.map +1 -0
  33. package/dist/algorithm/spineless/priority-queue.d.ts +73 -0
  34. package/dist/algorithm/spineless/priority-queue.d.ts.map +1 -0
  35. package/dist/algorithm/spineless/priority-queue.js +149 -0
  36. package/dist/algorithm/spineless/priority-queue.js.map +1 -0
  37. package/dist/algorithm/spineless/runtime.d.ts +239 -0
  38. package/dist/algorithm/spineless/runtime.d.ts.map +1 -0
  39. package/dist/algorithm/spineless/runtime.js +458 -0
  40. package/dist/algorithm/spineless/runtime.js.map +1 -0
  41. package/dist/algorithm/spineless/style-dirty.d.ts +65 -0
  42. package/dist/algorithm/spineless/style-dirty.d.ts.map +1 -0
  43. package/dist/algorithm/spineless/style-dirty.js +75 -0
  44. package/dist/algorithm/spineless/style-dirty.js.map +1 -0
  45. package/dist/index.d.ts +3 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +6 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/inspect.d.ts +27 -0
  50. package/dist/inspect.d.ts.map +1 -0
  51. package/dist/inspect.js +61 -0
  52. package/dist/inspect.js.map +1 -0
  53. package/package.json +1 -1
@@ -0,0 +1,755 @@
1
+ /**
2
+ * `SpinelessLayout` — drives the Spineless incremental layout engine
3
+ * as a `calculateLayout`-equivalent (phase 8).
4
+ *
5
+ * The grammar (`buildFlexGrammar`) + runtime (`SpinelessRuntime`)
6
+ * compute each node's `{width, height, left, top}` in floating-point
7
+ * space; this driver writes those into `node._layout`, then runs
8
+ * integer-cell rounding and a scroll-extent pass — mirroring the
9
+ * tail of the imperative `calculateLayoutImpl`.
10
+ *
11
+ * The driver persists the grammar + runtime between `layout()` calls
12
+ * and keeps every step incremental:
13
+ *
14
+ * - DETECTION (v22) — the `Node` dirty flags scope change detection
15
+ * to the mutated region.
16
+ * - VALUE relayout — `recompute()` reports the fields it changed;
17
+ * write-back, rounding and scroll-extents (v23) touch only the
18
+ * subtrees whose layout actually moved.
19
+ * - GRAFT relayout (v21) — a single child append patches the
20
+ * runtime via `buildAppendFragment` + `graft`.
21
+ * - full REBUILD — any other structural change.
22
+ *
23
+ * @internal
24
+ */
25
+ import { roundLayout, roundLayoutFrom } from '../round.js';
26
+ import { buildAppendFragment, buildFlexGrammar, buildRemoveFragment, buildReorderFragment, } from './flex-grammar.js';
27
+ import { SpinelessRuntime } from './runtime.js';
28
+ /** An input field's `compute` never calls `read` — guard against it. */
29
+ const NEVER_READ = () => {
30
+ throw new Error('[spineless-layout] an input field compute must not read');
31
+ };
32
+ /** Every leaf input Field (`deps: []`) the runtime currently tracks. */
33
+ function collectInputs(grammar, runtime) {
34
+ const inputs = [];
35
+ for (const [field, rule] of grammar) {
36
+ if (rule.deps.length === 0 && runtime.isTracked(field))
37
+ inputs.push(field);
38
+ }
39
+ return inputs;
40
+ }
41
+ /** The leaf input Fields a single node owns (its style inputs). */
42
+ function inputFieldsOf(entry, out) {
43
+ if (entry === undefined)
44
+ return;
45
+ for (const k of [
46
+ 'width',
47
+ 'height',
48
+ 'flexBasis',
49
+ 'flexGrow',
50
+ 'flexShrink',
51
+ 'gapRow',
52
+ 'gapColumn',
53
+ 'minWidth',
54
+ 'minHeight',
55
+ 'maxWidth',
56
+ 'maxHeight',
57
+ ]) {
58
+ const f = entry[k];
59
+ if (f !== undefined)
60
+ out.push(f);
61
+ }
62
+ for (const k of ['padding', 'margin']) {
63
+ const arr = entry[k];
64
+ if (arr === undefined)
65
+ continue;
66
+ for (const f of arr)
67
+ if (f !== undefined)
68
+ out.push(f);
69
+ }
70
+ }
71
+ /** The structural signature of one node — see `NodeSnap`. */
72
+ function nodeSig(node) {
73
+ const s = node.style;
74
+ return [
75
+ s.flexDirection,
76
+ s.flexWrap,
77
+ s.justifyContent,
78
+ s.alignItems,
79
+ s.alignContent,
80
+ s.alignSelf,
81
+ s.positionType,
82
+ s.display,
83
+ typeof s.width,
84
+ typeof s.height,
85
+ typeof s.flexBasis,
86
+ // Only the zero / positive BOUNDARY of a flex weight is
87
+ // structural (it flips `parentNeedsFlexDistribution`); a
88
+ // positive → positive tweak stays an incremental value change.
89
+ s.flexGrow > 0 ? 'g' : '_',
90
+ s.flexShrink > 0 ? 's' : '_',
91
+ // `aspectRatio` is captured by value at build time, so any
92
+ // change to it needs a rebuild.
93
+ s.aspectRatio === undefined ? 'n' : String(s.aspectRatio),
94
+ // Absolute children capture their `position` edges by value.
95
+ s.position
96
+ .map((p) => (p === undefined ? '_' : String(p)))
97
+ .join(','),
98
+ ].join('|');
99
+ }
100
+ function captureSnaps(root) {
101
+ const snaps = new Map();
102
+ function visit(n) {
103
+ const children = [];
104
+ for (let i = 0; i < n.getChildCount(); i++)
105
+ children.push(n.getChild(i));
106
+ snaps.set(n, { sig: nodeSig(n), measure: n.getMeasureFunc(), children });
107
+ for (const c of children)
108
+ visit(c);
109
+ }
110
+ visit(root);
111
+ return snaps;
112
+ }
113
+ /** True iff `node`'s current child list still matches `snap.children`. */
114
+ function childrenUnchanged(snap, node) {
115
+ if (node.getChildCount() !== snap.children.length)
116
+ return false;
117
+ for (let i = 0; i < snap.children.length; i++) {
118
+ if (node.getChild(i) !== snap.children[i])
119
+ return false;
120
+ }
121
+ return true;
122
+ }
123
+ /**
124
+ * Collect every dirty node — descending only into subtrees the dirty
125
+ * flags say contain a change, so the walk is O(dirty region).
126
+ */
127
+ function collectDirty(node, out) {
128
+ const dirty = node.isDirty();
129
+ if (dirty)
130
+ out.push(node);
131
+ if (dirty || node._hasDirtyDescendant) {
132
+ for (let i = 0; i < node.getChildCount(); i++)
133
+ collectDirty(node.getChild(i), out);
134
+ }
135
+ }
136
+ /** Clear the dirty flags over the same region `collectDirty` walks. */
137
+ function clearDirtyRegion(node) {
138
+ if (!node.isDirty() && !node._hasDirtyDescendant)
139
+ return;
140
+ node.clearDirty();
141
+ for (let i = 0; i < node.getChildCount(); i++)
142
+ clearDirtyRegion(node.getChild(i));
143
+ }
144
+ function clearDirtyDeep(node) {
145
+ node.clearDirty();
146
+ for (let i = 0; i < node.getChildCount(); i++)
147
+ clearDirtyDeep(node.getChild(i));
148
+ }
149
+ /** Build the `fields` / `owner` indexes from a `FlexGrammarOutput`. */
150
+ function indexFields(output) {
151
+ const fields = new Map();
152
+ const owner = new Map();
153
+ for (const f of output.allFields) {
154
+ fields.set(f.node, { width: f.width, height: f.height, left: f.left, top: f.top });
155
+ owner.set(f.width, f.node);
156
+ owner.set(f.height, f.node);
157
+ owner.set(f.left, f.node);
158
+ owner.set(f.top, f.node);
159
+ }
160
+ return { fields, owner };
161
+ }
162
+ /**
163
+ * A layout driver bound to one root `Node`. Call `layout()` to
164
+ * produce a layout byte-equivalent to imperative `calculateLayout`;
165
+ * repeat calls reuse the runtime, relaying incrementally.
166
+ *
167
+ * @internal
168
+ */
169
+ export class SpinelessLayout {
170
+ root;
171
+ built = null;
172
+ /** Build / relayout counters — for tests and diagnostics. */
173
+ stats = {
174
+ fullBuilds: 0,
175
+ incrementalRelayouts: 0,
176
+ graftRelayouts: 0,
177
+ detachRelayouts: 0,
178
+ reorderRelayouts: 0,
179
+ };
180
+ /** What the most recent `layout()` call did (phase 9). */
181
+ _lastTrace = null;
182
+ constructor(root) {
183
+ this.root = root;
184
+ }
185
+ /** The `LayoutTrace` of the most recent `layout()` call, or `null`
186
+ * if `layout()` has not run yet. */
187
+ get lastTrace() {
188
+ return this._lastTrace;
189
+ }
190
+ /**
191
+ * Lay the tree out. `availableWidth` / `availableHeight` size an
192
+ * `'auto'` root, matching `calculateLayout`'s availability args.
193
+ */
194
+ layout(availableWidth, availableHeight) {
195
+ // `available` PRESENCE (defined vs not) is structural — it
196
+ // selects the root size rule shape (`rootAxisIsBareZero`).
197
+ const samePresence = this.built !== null &&
198
+ (this.built.available.width !== undefined) === (availableWidth !== undefined) &&
199
+ (this.built.available.height !== undefined) === (availableHeight !== undefined);
200
+ if (!samePresence) {
201
+ this.fullBuild(availableWidth, availableHeight);
202
+ this.stats.fullBuilds++;
203
+ this.finishWhole();
204
+ return;
205
+ }
206
+ // Classify the dirty region: any structural change forces the
207
+ // graft / rebuild paths; otherwise it is a pure value relayout.
208
+ const dirty = [];
209
+ collectDirty(this.root, dirty);
210
+ const snaps = this.built.snaps;
211
+ let structural = false;
212
+ for (const n of dirty) {
213
+ const snap = snaps.get(n);
214
+ if (snap === undefined ||
215
+ snap.sig !== nodeSig(n) ||
216
+ snap.measure !== n.getMeasureFunc() ||
217
+ !childrenUnchanged(snap, n)) {
218
+ structural = true;
219
+ break;
220
+ }
221
+ }
222
+ if (!structural) {
223
+ const moved = this.relayoutValues(dirty, availableWidth, availableHeight);
224
+ this.stats.incrementalRelayouts++;
225
+ const rs = this.built.runtime.stats;
226
+ this._lastTrace = {
227
+ path: 'incremental',
228
+ dirtyNodes: dirty.length,
229
+ fieldsRecomputed: rs.recomputeVisited,
230
+ fieldsChanged: rs.recomputeChanged,
231
+ movedSubtrees: moved.length,
232
+ };
233
+ this.finishIncremental(moved);
234
+ clearDirtyRegion(this.root);
235
+ return;
236
+ }
237
+ if (this.tryGraftAppend(availableWidth, availableHeight)) {
238
+ this.stats.graftRelayouts++;
239
+ const rs = this.built.runtime.stats;
240
+ this._lastTrace = {
241
+ path: 'graft',
242
+ dirtyNodes: dirty.length,
243
+ fieldsRecomputed: rs.recomputeVisited,
244
+ fieldsChanged: rs.recomputeChanged,
245
+ movedSubtrees: 0,
246
+ };
247
+ }
248
+ else if (this.tryDetachRemove(availableWidth, availableHeight)) {
249
+ this.stats.detachRelayouts++;
250
+ const rs = this.built.runtime.stats;
251
+ this._lastTrace = {
252
+ path: 'detach',
253
+ dirtyNodes: dirty.length,
254
+ fieldsRecomputed: rs.recomputeVisited,
255
+ fieldsChanged: rs.recomputeChanged,
256
+ movedSubtrees: 0,
257
+ };
258
+ }
259
+ else if (this.tryReorder(availableWidth, availableHeight)) {
260
+ this.stats.reorderRelayouts++;
261
+ const rs = this.built.runtime.stats;
262
+ this._lastTrace = {
263
+ path: 'reorder',
264
+ dirtyNodes: dirty.length,
265
+ fieldsRecomputed: rs.recomputeVisited,
266
+ fieldsChanged: rs.recomputeChanged,
267
+ movedSubtrees: 0,
268
+ };
269
+ }
270
+ else {
271
+ this.fullBuild(availableWidth, availableHeight);
272
+ this.stats.fullBuilds++;
273
+ }
274
+ this.finishWhole();
275
+ }
276
+ /** Discard any persisted state and build the grammar afresh. */
277
+ fullBuild(availableWidth, availableHeight) {
278
+ const available = {};
279
+ if (availableWidth !== undefined)
280
+ available.width = availableWidth;
281
+ if (availableHeight !== undefined)
282
+ available.height = availableHeight;
283
+ const output = buildFlexGrammar(this.root, available);
284
+ const rootFields = [];
285
+ for (const f of output.allFields) {
286
+ rootFields.push(f.width, f.height, f.left, f.top);
287
+ }
288
+ const runtime = new SpinelessRuntime(output.grammar, rootFields);
289
+ runtime.init();
290
+ this.built = {
291
+ available,
292
+ output,
293
+ runtime,
294
+ inputs: collectInputs(output.grammar, runtime),
295
+ snaps: captureSnaps(this.root),
296
+ ...indexFields(output),
297
+ };
298
+ // A build computes every Field once during `init` (counted by
299
+ // `runtime.stats.initFields`) — it runs no `recompute()`, so the
300
+ // recompute-derived trace counts are all zero.
301
+ this._lastTrace = {
302
+ path: 'build',
303
+ dirtyNodes: 0,
304
+ fieldsRecomputed: 0,
305
+ fieldsChanged: 0,
306
+ movedSubtrees: 0,
307
+ };
308
+ }
309
+ /**
310
+ * Fast-path a structural change that is exactly a single child
311
+ * append: `buildAppendFragment` + `graft`, no whole-tree rebuild.
312
+ * Returns `false` (and changes nothing) when the change is not a
313
+ * clean append the fast-path covers — the caller then rebuilds.
314
+ */
315
+ tryGraftAppend(availableWidth, availableHeight) {
316
+ const built = this.built;
317
+ const snaps = built.snaps;
318
+ // Walk the current tree; no previously-snapshotted node may be
319
+ // gone (a removal is not an append), and at least one must be new.
320
+ const curNodes = new Set();
321
+ (function visit(n) {
322
+ curNodes.add(n);
323
+ for (let i = 0; i < n.getChildCount(); i++)
324
+ visit(n.getChild(i));
325
+ })(this.root);
326
+ for (const n of snaps.keys()) {
327
+ if (!curNodes.has(n))
328
+ return false;
329
+ }
330
+ const added = [];
331
+ for (const n of curNodes) {
332
+ if (!snaps.has(n))
333
+ added.push(n);
334
+ }
335
+ if (added.length === 0)
336
+ return false;
337
+ // No surviving node's signature / measure may have changed —
338
+ // `graft` patches only the append.
339
+ for (const [n, snap] of snaps) {
340
+ if (snap.sig !== nodeSig(n) || snap.measure !== n.getMeasureFunc())
341
+ return false;
342
+ }
343
+ // The added nodes must form exactly one subtree — its root is the
344
+ // unique added node whose parent is not itself added.
345
+ const addedSet = new Set(added);
346
+ let child = null;
347
+ for (const n of added) {
348
+ const p = n.getParent();
349
+ if (p === null || !addedSet.has(p)) {
350
+ if (child !== null)
351
+ return false; // two separate appends
352
+ child = n;
353
+ }
354
+ }
355
+ if (child === null)
356
+ return false;
357
+ const parent = child.getParent();
358
+ if (parent === null)
359
+ return false;
360
+ // A change inside a `display: 'none'` subtree: the hidden region
361
+ // has no grammar fields to graft onto. A node with no entry in
362
+ // `fields` is hidden (or under a hidden ancestor) — fall back to
363
+ // a rebuild, which correctly skips the whole hidden subtree.
364
+ if (!built.fields.has(parent))
365
+ return false;
366
+ const fragment = buildAppendFragment(built.output, this.root, parent, child, built.available);
367
+ if (fragment === null)
368
+ return false;
369
+ built.runtime.graft(fragment.additions, fragment.newRoots);
370
+ for (const [field, rule] of fragment.rebinds)
371
+ built.runtime.rebindRule(field, rule);
372
+ built.output = fragment.next;
373
+ built.inputs = collectInputs(fragment.next.grammar, built.runtime);
374
+ built.snaps = captureSnaps(this.root);
375
+ const idx = indexFields(fragment.next);
376
+ built.fields = idx.fields;
377
+ built.owner = idx.owner;
378
+ // Pick up any value mutations in the same gap, then recompute
379
+ // (covering the grafted / rebound fields too).
380
+ this.applyAvailable(availableWidth, availableHeight);
381
+ for (const field of built.inputs) {
382
+ if (built.output.grammar.get(field).compute(NEVER_READ) !== built.runtime.evaluate(field)) {
383
+ built.runtime.markDirty(field);
384
+ }
385
+ }
386
+ built.runtime.recompute();
387
+ return true;
388
+ }
389
+ /**
390
+ * Fast-path a structural change that is exactly a single subtree
391
+ * removal: `buildRemoveFragment` + `rebindRule` / `detach`, no
392
+ * whole-tree rebuild. Returns `false` (changing nothing) when the
393
+ * change is not a clean removal — the caller then rebuilds.
394
+ *
395
+ * `buildRemoveFragment` must see the removed `child` still attached
396
+ * (its regime check reads the parent's live child list and it walks
397
+ * the subtree for the fields to detach), but by the time `layout()`
398
+ * runs the caller has already detached it — so the removed subtree
399
+ * is briefly re-inserted at its old index for the fragment build,
400
+ * then detached again.
401
+ */
402
+ tryDetachRemove(availableWidth, availableHeight) {
403
+ const built = this.built;
404
+ const snaps = built.snaps;
405
+ // Walk the current tree. No previously-snapshotted node may be new
406
+ // (an addition is not a removal); at least one must be gone.
407
+ const curNodes = new Set();
408
+ (function visit(n) {
409
+ curNodes.add(n);
410
+ for (let i = 0; i < n.getChildCount(); i++)
411
+ visit(n.getChild(i));
412
+ })(this.root);
413
+ for (const n of curNodes) {
414
+ if (!snaps.has(n))
415
+ return false;
416
+ }
417
+ const removed = [];
418
+ for (const n of snaps.keys()) {
419
+ if (!curNodes.has(n))
420
+ removed.push(n);
421
+ }
422
+ if (removed.length === 0)
423
+ return false;
424
+ // No surviving node's signature / measure may have changed —
425
+ // `detach` patches only the removal.
426
+ for (const [n, snap] of snaps) {
427
+ if (!curNodes.has(n))
428
+ continue;
429
+ if (snap.sig !== nodeSig(n) || snap.measure !== n.getMeasureFunc())
430
+ return false;
431
+ }
432
+ // The snapped child -> parent map — the live tree no longer holds
433
+ // the removed nodes' edges.
434
+ const snapParent = new Map();
435
+ for (const [n, snap] of snaps) {
436
+ for (const c of snap.children)
437
+ snapParent.set(c, n);
438
+ }
439
+ // The removed nodes must form exactly one subtree — its root is
440
+ // the unique removed node whose snapped parent is not itself
441
+ // removed; that parent must survive.
442
+ const removedSet = new Set(removed);
443
+ let child = null;
444
+ for (const n of removed) {
445
+ const p = snapParent.get(n);
446
+ if (p === undefined || !removedSet.has(p)) {
447
+ if (child !== null)
448
+ return false; // two separate removals
449
+ child = n;
450
+ }
451
+ }
452
+ if (child === null)
453
+ return false;
454
+ const parent = snapParent.get(child);
455
+ if (parent === undefined || !curNodes.has(parent))
456
+ return false;
457
+ // A removal inside a `display: 'none'` subtree: the hidden region
458
+ // has no fields to `detach`. A node absent from `fields` is hidden
459
+ // (or under a hidden ancestor) — rebuild instead.
460
+ if (!built.fields.has(parent))
461
+ return false;
462
+ // `removed` must be exactly `child`'s snapped subtree.
463
+ let subtreeCount = 0;
464
+ (function count(n) {
465
+ subtreeCount++;
466
+ for (const c of snaps.get(n).children)
467
+ count(c);
468
+ })(child);
469
+ if (subtreeCount !== removed.length)
470
+ return false;
471
+ // `parent`'s current children must be its snapped children with
472
+ // `child` spliced out — so re-inserting `child` at `idx` restores
473
+ // the exact pre-removal tree for `buildRemoveFragment`.
474
+ const snapChildren = snaps.get(parent).children;
475
+ const idx = snapChildren.indexOf(child);
476
+ if (idx === -1 || parent.getChildCount() !== snapChildren.length - 1)
477
+ return false;
478
+ for (let i = 0, j = 0; i < snapChildren.length; i++) {
479
+ if (snapChildren[i] === child)
480
+ continue;
481
+ if (parent.getChild(j) !== snapChildren[i])
482
+ return false;
483
+ j++;
484
+ }
485
+ // Re-attach `child` for the fragment build, then detach it again.
486
+ parent.insertChild(child, idx);
487
+ const fragment = buildRemoveFragment(built.output, this.root, parent, child, built.available);
488
+ parent.removeChild(child);
489
+ if (fragment === null)
490
+ return false;
491
+ // Apply: rebind survivors FIRST (so they stop reading the removed
492
+ // fields), then `detach`, then adopt the next grammar.
493
+ for (const [field, rule] of fragment.rebinds)
494
+ built.runtime.rebindRule(field, rule);
495
+ built.runtime.detach(fragment.removed);
496
+ built.output = fragment.next;
497
+ built.inputs = collectInputs(fragment.next.grammar, built.runtime);
498
+ built.snaps = captureSnaps(this.root);
499
+ const ix = indexFields(fragment.next);
500
+ built.fields = ix.fields;
501
+ built.owner = ix.owner;
502
+ // Pick up any value mutations in the same batch, then recompute.
503
+ this.applyAvailable(availableWidth, availableHeight);
504
+ for (const field of built.inputs) {
505
+ if (built.output.grammar.get(field).compute(NEVER_READ) !== built.runtime.evaluate(field)) {
506
+ built.runtime.markDirty(field);
507
+ }
508
+ }
509
+ built.runtime.recompute();
510
+ return true;
511
+ }
512
+ /**
513
+ * Fast-path a structural change that is exactly one parent's
514
+ * children being reordered (a permutation — no node added or
515
+ * removed): `buildReorderFragment` + `rebindRule`, no whole-tree
516
+ * rebuild. Returns `false` (changing nothing) when the change is
517
+ * not a clean single-parent reorder — the caller then rebuilds.
518
+ */
519
+ tryReorder(availableWidth, availableHeight) {
520
+ const built = this.built;
521
+ const snaps = built.snaps;
522
+ // The node set must be unchanged — no addition, no removal.
523
+ const curNodes = new Set();
524
+ (function visit(n) {
525
+ curNodes.add(n);
526
+ for (let i = 0; i < n.getChildCount(); i++)
527
+ visit(n.getChild(i));
528
+ })(this.root);
529
+ if (curNodes.size !== snaps.size)
530
+ return false;
531
+ for (const n of curNodes) {
532
+ if (!snaps.has(n))
533
+ return false;
534
+ }
535
+ // Every node's signature / measure must be unchanged, and exactly
536
+ // one node's child ORDER may differ — the reordered parent.
537
+ let reordered = null;
538
+ for (const [n, snap] of snaps) {
539
+ if (snap.sig !== nodeSig(n) || snap.measure !== n.getMeasureFunc())
540
+ return false;
541
+ if (!childrenUnchanged(snap, n)) {
542
+ if (reordered !== null)
543
+ return false; // two changed parents — not one reorder
544
+ reordered = n;
545
+ }
546
+ }
547
+ if (reordered === null)
548
+ return false;
549
+ // `reordered`'s children must be a permutation of the snapped
550
+ // set (same count, same members) — otherwise it is an add/remove.
551
+ const before = snaps.get(reordered).children;
552
+ if (before.length !== reordered.getChildCount())
553
+ return false;
554
+ const beforeSet = new Set(before);
555
+ for (let i = 0; i < reordered.getChildCount(); i++) {
556
+ if (!beforeSet.has(reordered.getChild(i)))
557
+ return false;
558
+ }
559
+ // A reorder inside a `display: 'none'` subtree touches no laid-out
560
+ // node — the hidden region has no fields. Rebuild instead.
561
+ if (!built.fields.has(reordered))
562
+ return false;
563
+ const fragment = buildReorderFragment(built.output, this.root, reordered, built.available);
564
+ // Order: integrate the newly-read inputs, rebind the rewritten
565
+ // rules (their new deps are now all present), then detach the
566
+ // inputs no rebound rule reads any more.
567
+ built.runtime.graft(fragment.additions, fragment.newRoots);
568
+ for (const [field, rule] of fragment.rebinds)
569
+ built.runtime.rebindRule(field, rule);
570
+ if (fragment.removed.length > 0)
571
+ built.runtime.detach(fragment.removed);
572
+ built.output = fragment.next;
573
+ built.inputs = collectInputs(fragment.next.grammar, built.runtime);
574
+ built.snaps = captureSnaps(this.root);
575
+ const ix = indexFields(fragment.next);
576
+ built.fields = ix.fields;
577
+ built.owner = ix.owner;
578
+ // Pick up any value mutations in the same batch, then recompute.
579
+ this.applyAvailable(availableWidth, availableHeight);
580
+ for (const field of built.inputs) {
581
+ if (built.output.grammar.get(field).compute(NEVER_READ) !== built.runtime.evaluate(field)) {
582
+ built.runtime.markDirty(field);
583
+ }
584
+ }
585
+ built.runtime.recompute();
586
+ return true;
587
+ }
588
+ /**
589
+ * Value relayout: re-`markDirty` only the input Fields of the dirty
590
+ * nodes (plus the root `available:*` inputs) whose value drifted,
591
+ * then `recompute()`. Returns the maximal subtree roots whose
592
+ * layout moved — for `finishIncremental` to write back.
593
+ */
594
+ relayoutValues(dirty, availableWidth, availableHeight) {
595
+ const built = this.built;
596
+ this.applyAvailable(availableWidth, availableHeight);
597
+ const fields = [];
598
+ if (built.output.availableInputs.width !== undefined) {
599
+ fields.push(built.output.availableInputs.width);
600
+ }
601
+ if (built.output.availableInputs.height !== undefined) {
602
+ fields.push(built.output.availableInputs.height);
603
+ }
604
+ for (const n of dirty)
605
+ inputFieldsOf(built.output.styleInputs.get(n), fields);
606
+ const { runtime, output } = built;
607
+ for (const field of fields) {
608
+ // `styleInputs` can hold an input Field no rule reads (e.g. a
609
+ // flex-start container's main-END padding) — untracked, and a
610
+ // change to it cannot move any layout field. Skip it.
611
+ if (!runtime.isTracked(field))
612
+ continue;
613
+ const live = output.grammar.get(field).compute(NEVER_READ);
614
+ if (live !== runtime.evaluate(field))
615
+ runtime.markDirty(field);
616
+ }
617
+ // The changed layout Fields name the nodes whose box moved.
618
+ const changed = runtime.recompute();
619
+ const moved = new Set();
620
+ for (const f of changed) {
621
+ const n = built.owner.get(f);
622
+ if (n !== undefined)
623
+ moved.add(n);
624
+ }
625
+ // Keep only the maximal moved subtree roots — a moved node with
626
+ // no moved ancestor. Re-rounding such a root covers its whole
627
+ // (shifted) subtree.
628
+ const roots = [];
629
+ for (const n of moved) {
630
+ let maximal = true;
631
+ for (let p = n.getParent(); p !== null; p = p.getParent()) {
632
+ if (moved.has(p)) {
633
+ maximal = false;
634
+ break;
635
+ }
636
+ }
637
+ if (maximal)
638
+ roots.push(n);
639
+ }
640
+ return roots;
641
+ }
642
+ /** Push new `available` values into the holder the grammar closes over. */
643
+ applyAvailable(availableWidth, availableHeight) {
644
+ const a = this.built.available;
645
+ if (availableWidth !== undefined)
646
+ a.width = availableWidth;
647
+ if (availableHeight !== undefined)
648
+ a.height = availableHeight;
649
+ }
650
+ /** Write-back + round + scroll the whole tree (after a build / graft). */
651
+ finishWhole() {
652
+ const { runtime, output } = this.built;
653
+ for (const f of output.allFields) {
654
+ writeNode(f.node, runtime, { width: f.width, height: f.height, left: f.left, top: f.top });
655
+ }
656
+ roundLayout(this.root);
657
+ recordScrollSizes(this.root);
658
+ clearDirtyDeep(this.root);
659
+ }
660
+ /**
661
+ * Write-back + round + scroll, scoped to the subtrees that moved.
662
+ * A moved subtree's parent did not move, so its rounding is stable
663
+ * and the subtree can be re-rounded in isolation; only that
664
+ * parent's own scroll extent then needs a recompute.
665
+ */
666
+ finishIncremental(roots) {
667
+ const { runtime, fields } = this.built;
668
+ for (const root of roots) {
669
+ // Write the float layout for the whole moved subtree, so the
670
+ // re-round below has float values throughout.
671
+ const stack = [root];
672
+ while (stack.length > 0) {
673
+ const n = stack.pop();
674
+ // A `display: 'none'` node has no grammar fields — the
675
+ // emitter skips it (v29). Skip it (and its subtree) here too,
676
+ // mirroring `finishWhole`, which writes only `allFields`.
677
+ const f = fields.get(n);
678
+ if (f === undefined)
679
+ continue;
680
+ writeNode(n, runtime, f);
681
+ for (let i = 0; i < n.getChildCount(); i++)
682
+ stack.push(n.getChild(i));
683
+ }
684
+ const pos = ancestorPositions(root);
685
+ roundLayoutFrom(root, pos.floatX, pos.floatY, pos.roundedX, pos.roundedY);
686
+ recordScrollSizes(root);
687
+ }
688
+ // A moved root's parent did not move, so `recordScrollSizes`
689
+ // above never touched it — but one of its children's box did
690
+ // change, so its own scroll extent needs a recompute.
691
+ const scrollParents = new Set();
692
+ for (const root of roots) {
693
+ const p = root.getParent();
694
+ if (p !== null)
695
+ scrollParents.add(p);
696
+ }
697
+ for (const p of scrollParents)
698
+ recomputeScroll(p);
699
+ }
700
+ }
701
+ /** Write one node's evaluated float layout into `_layout`. */
702
+ function writeNode(node, runtime, f) {
703
+ const left = runtime.evaluate(f.left);
704
+ const top = runtime.evaluate(f.top);
705
+ node._layout.left = left;
706
+ node._layout.top = top;
707
+ node._layout.width = runtime.evaluate(f.width);
708
+ node._layout.height = runtime.evaluate(f.height);
709
+ node._floatLeft = left;
710
+ node._floatTop = top;
711
+ }
712
+ /**
713
+ * The float and rounded absolute position of `node`'s parent — the
714
+ * origin `roundLayoutFrom` needs to re-round the `node` subtree. The
715
+ * ancestors did not move, so their `_floatLeft/Top` (float) and
716
+ * `_layout.left/top` (rounded) are still current.
717
+ */
718
+ function ancestorPositions(node) {
719
+ let floatX = 0;
720
+ let floatY = 0;
721
+ let roundedX = 0;
722
+ let roundedY = 0;
723
+ for (let a = node.getParent(); a !== null; a = a.getParent()) {
724
+ floatX += a._floatLeft;
725
+ floatY += a._floatTop;
726
+ roundedX += a._layout.left;
727
+ roundedY += a._layout.top;
728
+ }
729
+ return { floatX, floatY, roundedX, roundedY };
730
+ }
731
+ /**
732
+ * Post-order walk recording each node's content bounding box on
733
+ * `_layout.scrollWidth` / `scrollHeight`. Mirrors the scroll-extent
734
+ * half of `calculateLayoutImpl`'s `computeScrollSizes` — without the
735
+ * imperative layout-cache writes, which belong to the imperative
736
+ * path only.
737
+ */
738
+ function recordScrollSizes(node) {
739
+ for (let i = 0; i < node.getChildCount(); i++)
740
+ recordScrollSizes(node.getChild(i));
741
+ recomputeScroll(node);
742
+ }
743
+ /** Recompute one node's scroll extent from its direct children's boxes. */
744
+ function recomputeScroll(node) {
745
+ let contentRight = 0;
746
+ let contentBottom = 0;
747
+ for (let i = 0; i < node.getChildCount(); i++) {
748
+ const cl = node.getChild(i)._layout;
749
+ contentRight = Math.max(contentRight, cl.left + cl.width);
750
+ contentBottom = Math.max(contentBottom, cl.top + cl.height);
751
+ }
752
+ node._layout.scrollWidth = Math.max(node._layout.width, contentRight);
753
+ node._layout.scrollHeight = Math.max(node._layout.height, contentBottom);
754
+ }
755
+ //# sourceMappingURL=layout.js.map