@pilates/core 1.1.0 → 2.0.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/cache.d.ts +7 -19
- package/dist/algorithm/cache.d.ts.map +1 -1
- package/dist/algorithm/cache.js +31 -27
- package/dist/algorithm/cache.js.map +1 -1
- package/dist/algorithm/index.js +22 -14
- package/dist/algorithm/index.js.map +1 -1
- package/dist/algorithm/round.d.ts.map +1 -1
- package/dist/algorithm/round.js +19 -19
- package/dist/algorithm/round.js.map +1 -1
- package/dist/algorithm/spineless/classify.d.ts +47 -0
- package/dist/algorithm/spineless/classify.d.ts.map +1 -0
- package/dist/algorithm/spineless/classify.js +109 -0
- package/dist/algorithm/spineless/classify.js.map +1 -0
- package/dist/algorithm/spineless/field-id-pool.d.ts +28 -0
- package/dist/algorithm/spineless/field-id-pool.d.ts.map +1 -0
- package/dist/algorithm/spineless/field-id-pool.js +35 -0
- package/dist/algorithm/spineless/field-id-pool.js.map +1 -0
- package/dist/algorithm/spineless/flex-grammar.d.ts.map +1 -1
- package/dist/algorithm/spineless/flex-grammar.js +194 -58
- package/dist/algorithm/spineless/flex-grammar.js.map +1 -1
- package/dist/algorithm/spineless/grammar.d.ts +6 -0
- package/dist/algorithm/spineless/grammar.d.ts.map +1 -1
- package/dist/algorithm/spineless/grammar.js +2 -1
- package/dist/algorithm/spineless/grammar.js.map +1 -1
- package/dist/algorithm/spineless/layout.d.ts +52 -15
- package/dist/algorithm/spineless/layout.d.ts.map +1 -1
- package/dist/algorithm/spineless/layout.js +450 -312
- package/dist/algorithm/spineless/layout.js.map +1 -1
- package/dist/algorithm/spineless/order-maintenance.d.ts +9 -0
- package/dist/algorithm/spineless/order-maintenance.d.ts.map +1 -1
- package/dist/algorithm/spineless/order-maintenance.js +6 -0
- package/dist/algorithm/spineless/order-maintenance.js.map +1 -1
- package/dist/algorithm/spineless/runtime.d.ts +61 -8
- package/dist/algorithm/spineless/runtime.d.ts.map +1 -1
- package/dist/algorithm/spineless/runtime.js +201 -50
- package/dist/algorithm/spineless/runtime.js.map +1 -1
- package/dist/dirty-flags.d.ts +30 -0
- package/dist/dirty-flags.d.ts.map +1 -0
- package/dist/dirty-flags.js +35 -0
- package/dist/dirty-flags.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/layout-pool.d.ts +49 -0
- package/dist/layout-pool.d.ts.map +1 -0
- package/dist/layout-pool.js +75 -0
- package/dist/layout-pool.js.map +1 -0
- package/dist/node.d.ts +20 -3
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +63 -42
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { roundLayout, roundLayoutFrom } from '../round.js';
|
|
26
26
|
import { buildAppendFragment, buildFlexGrammar, buildRemoveFragment, buildReorderFragment, } from './flex-grammar.js';
|
|
27
|
+
import { field } from './grammar.js';
|
|
27
28
|
import { SpinelessRuntime } from './runtime.js';
|
|
28
29
|
/** An input field's `compute` never calls `read` — guard against it. */
|
|
29
30
|
const NEVER_READ = () => {
|
|
@@ -31,10 +32,10 @@ const NEVER_READ = () => {
|
|
|
31
32
|
};
|
|
32
33
|
/** Every leaf input Field (`deps: []`) the runtime currently tracks. */
|
|
33
34
|
function collectInputs(grammar, runtime) {
|
|
34
|
-
const inputs =
|
|
35
|
-
for (const [
|
|
36
|
-
if (rule.deps.length === 0 && runtime.isTracked(
|
|
37
|
-
inputs.
|
|
35
|
+
const inputs = new Set();
|
|
36
|
+
for (const [f, rule] of grammar) {
|
|
37
|
+
if (rule.deps.length === 0 && runtime.isTracked(f))
|
|
38
|
+
inputs.add(f);
|
|
38
39
|
}
|
|
39
40
|
return inputs;
|
|
40
41
|
}
|
|
@@ -95,21 +96,43 @@ function nodeSig(node) {
|
|
|
95
96
|
s.position
|
|
96
97
|
.map((p) => (p === undefined ? '_' : String(p)))
|
|
97
98
|
.join(','),
|
|
99
|
+
// Fold-predicate bits for Phase 17: each bit flips when the
|
|
100
|
+
// property crosses its fold boundary (default → non-default).
|
|
101
|
+
// Mutating a folded property must change nodeSig so the
|
|
102
|
+
// classifier triggers a grammar rebuild.
|
|
103
|
+
s.minWidth === 0 ? '_' : 'mw',
|
|
104
|
+
s.minHeight === 0 ? '_' : 'mh',
|
|
105
|
+
s.maxWidth === undefined ? '_' : 'xw',
|
|
106
|
+
s.maxHeight === undefined ? '_' : 'xh',
|
|
107
|
+
s.margin[0] === 0 ? '_' : 'm0',
|
|
108
|
+
s.margin[1] === 0 ? '_' : 'm1',
|
|
109
|
+
s.margin[2] === 0 ? '_' : 'm2',
|
|
110
|
+
s.margin[3] === 0 ? '_' : 'm3',
|
|
98
111
|
].join('|');
|
|
99
112
|
}
|
|
100
113
|
function captureSnaps(root) {
|
|
101
114
|
const snaps = new Map();
|
|
102
|
-
|
|
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);
|
|
115
|
+
captureSnapsInto(root, snaps);
|
|
111
116
|
return snaps;
|
|
112
117
|
}
|
|
118
|
+
/** Write `node`'s subtree snaps into `target` (used by the
|
|
119
|
+
* fast-path graft to extend the snap map by the appended region
|
|
120
|
+
* without rewalking the whole tree). */
|
|
121
|
+
function captureSnapsInto(node, target) {
|
|
122
|
+
const children = [];
|
|
123
|
+
for (let i = 0; i < node.getChildCount(); i++)
|
|
124
|
+
children.push(node.getChild(i));
|
|
125
|
+
target.set(node, { sig: nodeSig(node), measure: node.getMeasureFunc(), children });
|
|
126
|
+
for (const c of children)
|
|
127
|
+
captureSnapsInto(c, target);
|
|
128
|
+
}
|
|
129
|
+
/** A fresh `NodeSnap` for `node` against its current children. */
|
|
130
|
+
function freshSnap(node) {
|
|
131
|
+
const children = [];
|
|
132
|
+
for (let i = 0; i < node.getChildCount(); i++)
|
|
133
|
+
children.push(node.getChild(i));
|
|
134
|
+
return { sig: nodeSig(node), measure: node.getMeasureFunc(), children };
|
|
135
|
+
}
|
|
113
136
|
/** True iff `node`'s current child list still matches `snap.children`. */
|
|
114
137
|
function childrenUnchanged(snap, node) {
|
|
115
138
|
if (node.getChildCount() !== snap.children.length)
|
|
@@ -203,23 +226,43 @@ export class SpinelessLayout {
|
|
|
203
226
|
this.finishWhole();
|
|
204
227
|
return;
|
|
205
228
|
}
|
|
206
|
-
// Classify the dirty region
|
|
207
|
-
//
|
|
229
|
+
// Classify the dirty region. The classifier collects PIVOTS —
|
|
230
|
+
// dirty nodes whose CHILDREN LIST changed since the last layout —
|
|
231
|
+
// and short-circuits to a full rebuild when any snapped node's
|
|
232
|
+
// sig / measure changed (those need a fresh grammar). A dirty
|
|
233
|
+
// node WITHOUT a snap is a freshly-introduced node (part of an
|
|
234
|
+
// appended subtree); the graft validator handles it, so the
|
|
235
|
+
// classifier just skips it.
|
|
236
|
+
//
|
|
237
|
+
// Dispatch:
|
|
238
|
+
// - sig / measure change → fullBuild
|
|
239
|
+
// - pivots.length === 0 → value relayout
|
|
240
|
+
// - pivots.length === 1 → graft / detach / reorder
|
|
241
|
+
// - pivots.length > 1 → fullBuild (multi-parent
|
|
242
|
+
// structural change; no
|
|
243
|
+
// fast-path handles it)
|
|
208
244
|
const dirty = [];
|
|
209
245
|
collectDirty(this.root, dirty);
|
|
210
246
|
const snaps = this.built.snaps;
|
|
211
|
-
|
|
247
|
+
const pivots = [];
|
|
248
|
+
let needsRebuild = false;
|
|
212
249
|
for (const n of dirty) {
|
|
213
250
|
const snap = snaps.get(n);
|
|
214
|
-
if (snap === undefined
|
|
215
|
-
snap.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
251
|
+
if (snap === undefined) {
|
|
252
|
+
// Freshly-introduced node — has no pre-layout snap. The graft
|
|
253
|
+
// validator below verifies the new region forms one subtree
|
|
254
|
+
// rooted under a single pivot; don't treat as a rebuild trigger.
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (snap.sig !== nodeSig(n) || snap.measure !== n.getMeasureFunc()) {
|
|
258
|
+
needsRebuild = true;
|
|
219
259
|
break;
|
|
220
260
|
}
|
|
261
|
+
if (!childrenUnchanged(snap, n)) {
|
|
262
|
+
pivots.push(n);
|
|
263
|
+
}
|
|
221
264
|
}
|
|
222
|
-
if (!
|
|
265
|
+
if (!needsRebuild && pivots.length === 0) {
|
|
223
266
|
const moved = this.relayoutValues(dirty, availableWidth, availableHeight);
|
|
224
267
|
this.stats.incrementalRelayouts++;
|
|
225
268
|
const rs = this.built.runtime.stats;
|
|
@@ -230,48 +273,73 @@ export class SpinelessLayout {
|
|
|
230
273
|
fieldsChanged: rs.recomputeChanged,
|
|
231
274
|
movedSubtrees: moved.length,
|
|
232
275
|
};
|
|
233
|
-
this.
|
|
276
|
+
this.finishMoved(moved, []);
|
|
234
277
|
clearDirtyRegion(this.root);
|
|
235
278
|
return;
|
|
236
279
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
if (!needsRebuild && pivots.length === 1) {
|
|
281
|
+
const pivot = pivots[0];
|
|
282
|
+
const graft = this.tryGraftAppend(pivot, dirty, availableWidth, availableHeight);
|
|
283
|
+
if (graft !== null) {
|
|
284
|
+
this.stats.graftRelayouts++;
|
|
285
|
+
const rs = this.built.runtime.stats;
|
|
286
|
+
const survivorRoots = this.movedSubtreeRoots(graft.changed);
|
|
287
|
+
// CRITICAL: include graft.child explicitly. Its fields were
|
|
288
|
+
// computed via integrate() at graft time, not via recompute(),
|
|
289
|
+
// so they don't appear in `changed`. Without this, a simple-
|
|
290
|
+
// regime append (changed is empty) writes back NOTHING and
|
|
291
|
+
// the appended subtree ships with _layout = 0. The structural-
|
|
292
|
+
// differential fuzzer catches this.
|
|
293
|
+
const roots = [graft.child, ...survivorRoots.filter((r) => !isInSubtree(r, graft.child))];
|
|
294
|
+
this._lastTrace = {
|
|
295
|
+
path: 'graft',
|
|
296
|
+
dirtyNodes: dirty.length,
|
|
297
|
+
fieldsRecomputed: rs.recomputeVisited,
|
|
298
|
+
fieldsChanged: rs.recomputeChanged,
|
|
299
|
+
movedSubtrees: roots.length,
|
|
300
|
+
};
|
|
301
|
+
this.finishMoved(roots, []);
|
|
302
|
+
clearDirtyRegion(this.root);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const detach = this.tryDetachRemove(pivot, dirty, availableWidth, availableHeight);
|
|
306
|
+
if (detach !== null) {
|
|
307
|
+
this.stats.detachRelayouts++;
|
|
308
|
+
const rs = this.built.runtime.stats;
|
|
309
|
+
const survivorRoots = this.movedSubtreeRoots(detach.changed);
|
|
310
|
+
this._lastTrace = {
|
|
311
|
+
path: 'detach',
|
|
312
|
+
dirtyNodes: dirty.length,
|
|
313
|
+
fieldsRecomputed: rs.recomputeVisited,
|
|
314
|
+
fieldsChanged: rs.recomputeChanged,
|
|
315
|
+
movedSubtrees: survivorRoots.length,
|
|
316
|
+
};
|
|
317
|
+
// Survivors may be empty for simple-regime removes; explicitly
|
|
318
|
+
// include detach.parent so its scroll extent is recomputed.
|
|
319
|
+
this.finishMoved(survivorRoots, [detach.parent]);
|
|
320
|
+
clearDirtyRegion(this.root);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const reorder = this.tryReorder(pivot, dirty, availableWidth, availableHeight);
|
|
324
|
+
if (reorder !== null) {
|
|
325
|
+
this.stats.reorderRelayouts++;
|
|
326
|
+
const rs = this.built.runtime.stats;
|
|
327
|
+
const movedRoots = this.movedSubtreeRoots(reorder.changed);
|
|
328
|
+
this._lastTrace = {
|
|
329
|
+
path: 'reorder',
|
|
330
|
+
dirtyNodes: dirty.length,
|
|
331
|
+
fieldsRecomputed: rs.recomputeVisited,
|
|
332
|
+
fieldsChanged: rs.recomputeChanged,
|
|
333
|
+
movedSubtrees: movedRoots.length,
|
|
334
|
+
};
|
|
335
|
+
this.finishMoved(movedRoots, [reorder.reordered]);
|
|
336
|
+
clearDirtyRegion(this.root);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
273
339
|
}
|
|
274
|
-
this.
|
|
340
|
+
this.fullBuild(availableWidth, availableHeight);
|
|
341
|
+
this.stats.fullBuilds++;
|
|
342
|
+
this.finishWhole(); // Only reached by fullBuild fallback now
|
|
275
343
|
}
|
|
276
344
|
/** Discard any persisted state and build the grammar afresh. */
|
|
277
345
|
fullBuild(availableWidth, availableHeight) {
|
|
@@ -308,89 +376,137 @@ export class SpinelessLayout {
|
|
|
308
376
|
}
|
|
309
377
|
/**
|
|
310
378
|
* Fast-path a structural change that is exactly a single child
|
|
311
|
-
* append
|
|
312
|
-
* Returns `
|
|
313
|
-
* clean append the fast-path covers — the caller then
|
|
379
|
+
* append at `pivot`: `buildAppendFragment` + `graft`, no whole-tree
|
|
380
|
+
* rebuild. Returns `null` (and changes nothing) when the change is
|
|
381
|
+
* not a clean append the fast-path covers — the caller then tries
|
|
382
|
+
* the next fast-path / rebuilds.
|
|
383
|
+
*
|
|
384
|
+
* The classifier supplies `pivot` — a dirty node whose snapshot
|
|
385
|
+
* exists (so sig / measure are unchanged) and whose children list
|
|
386
|
+
* differs from its snap's. The validator inspects ONLY pivot's
|
|
387
|
+
* children, not the whole tree.
|
|
314
388
|
*/
|
|
315
|
-
tryGraftAppend(availableWidth, availableHeight) {
|
|
389
|
+
tryGraftAppend(pivot, dirty, availableWidth, availableHeight) {
|
|
316
390
|
const built = this.built;
|
|
317
391
|
const snaps = built.snaps;
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
for (
|
|
332
|
-
if (
|
|
333
|
-
|
|
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;
|
|
392
|
+
const snap = snaps.get(pivot);
|
|
393
|
+
// Pivot's live children must be its snapped children with exactly
|
|
394
|
+
// ONE inserted child (anywhere — last for simple regime, mid-list
|
|
395
|
+
// for non-simple). Walk pivot's children positionally —
|
|
396
|
+
// O(pivot's children), not O(tree).
|
|
397
|
+
const live = pivot.getChildCount();
|
|
398
|
+
const snapped = snap.children.length;
|
|
399
|
+
if (live !== snapped + 1)
|
|
400
|
+
return null;
|
|
401
|
+
// Find the insertion index: the first position where pivot's live
|
|
402
|
+
// child diverges from snap. After the new child, the tail must
|
|
403
|
+
// match the rest of the snap.
|
|
404
|
+
let insertAt = snapped; // default: appended at the end
|
|
405
|
+
for (let i = 0; i < snapped; i++) {
|
|
406
|
+
if (pivot.getChild(i) !== snap.children[i]) {
|
|
407
|
+
insertAt = i;
|
|
408
|
+
break;
|
|
353
409
|
}
|
|
354
410
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
411
|
+
// Verify the tail past the insertion matches.
|
|
412
|
+
for (let i = insertAt; i < snapped; i++) {
|
|
413
|
+
if (pivot.getChild(i + 1) !== snap.children[i])
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const child = pivot.getChild(insertAt);
|
|
360
417
|
// A change inside a `display: 'none'` subtree: the hidden region
|
|
361
418
|
// has no grammar fields to graft onto. A node with no entry in
|
|
362
419
|
// `fields` is hidden (or under a hidden ancestor) — fall back to
|
|
363
420
|
// a rebuild, which correctly skips the whole hidden subtree.
|
|
364
|
-
if (!built.fields.has(
|
|
365
|
-
return
|
|
366
|
-
|
|
421
|
+
if (!built.fields.has(pivot))
|
|
422
|
+
return null;
|
|
423
|
+
// Every other dirty-with-no-snap node must be a descendant of
|
|
424
|
+
// `child` (i.e. the appended subtree). The classifier already
|
|
425
|
+
// collected them; if any escaped this subtree, it would be an
|
|
426
|
+
// independent append elsewhere — handled by the multi-pivot
|
|
427
|
+
// fullBuild fallback, but a single-pivot append must be tight.
|
|
428
|
+
//
|
|
429
|
+
// Verifying this requires walking just the appended subtree:
|
|
430
|
+
// O(added subtree).
|
|
431
|
+
const subtreeMembers = new Set();
|
|
432
|
+
{
|
|
433
|
+
const stack = [child];
|
|
434
|
+
while (stack.length > 0) {
|
|
435
|
+
const n = stack.pop();
|
|
436
|
+
subtreeMembers.add(n);
|
|
437
|
+
for (let i = 0; i < n.getChildCount(); i++)
|
|
438
|
+
stack.push(n.getChild(i));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const fragment = buildAppendFragment(built.output, this.root, pivot, child, built.available);
|
|
367
442
|
if (fragment === null)
|
|
368
|
-
return
|
|
443
|
+
return null;
|
|
369
444
|
built.runtime.graft(fragment.additions, fragment.newRoots);
|
|
370
|
-
for (const [
|
|
371
|
-
built.runtime.rebindRule(
|
|
445
|
+
for (const [rf, rule] of fragment.rebinds)
|
|
446
|
+
built.runtime.rebindRule(rf, rule);
|
|
372
447
|
built.output = fragment.next;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
built.
|
|
378
|
-
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
448
|
+
// Incremental bookkeeping (Part C):
|
|
449
|
+
//
|
|
450
|
+
// `built.snaps` — update pivot's snap (children list changed) and
|
|
451
|
+
// add a fresh snap for every node in the appended subtree.
|
|
452
|
+
built.snaps.set(pivot, freshSnap(pivot));
|
|
453
|
+
captureSnapsInto(child, built.snaps);
|
|
454
|
+
// `built.fields` / `built.owner` — every new layout field belongs
|
|
455
|
+
// to a node in the appended subtree. Walk the subtree and gather
|
|
456
|
+
// each node's four layout fields from `fragment.additions`. A
|
|
457
|
+
// `display: 'none'` descendant has no fields (it isn't in
|
|
458
|
+
// additions for its layout-field keys); skip it.
|
|
459
|
+
for (const n of subtreeMembers) {
|
|
460
|
+
if (n.style.display === 'none')
|
|
461
|
+
continue;
|
|
462
|
+
const w = field(n, 'width');
|
|
463
|
+
const h = field(n, 'height');
|
|
464
|
+
const l = field(n, 'left');
|
|
465
|
+
const t = field(n, 'top');
|
|
466
|
+
if (!fragment.next.grammar.has(w))
|
|
467
|
+
continue;
|
|
468
|
+
built.fields.set(n, { width: w, height: h, left: l, top: t });
|
|
469
|
+
built.owner.set(w, n);
|
|
470
|
+
built.owner.set(h, n);
|
|
471
|
+
built.owner.set(l, n);
|
|
472
|
+
built.owner.set(t, n);
|
|
473
|
+
}
|
|
474
|
+
// `built.inputs` — add new leaf input fields from additions. For
|
|
475
|
+
// simple regime nothing existing changes tracking, so this is
|
|
476
|
+
// additive. For non-simple regime, existing inputs may have just
|
|
477
|
+
// become tracked (e.g. the previous last child's main-END
|
|
478
|
+
// margin); the runtime's tracking is post-graft, so filter the
|
|
479
|
+
// OLD list to only tracked + add new tracked leaves.
|
|
480
|
+
if (fragment.rebinds.length === 0) {
|
|
481
|
+
// Simple regime: no existing input changed tracking — just
|
|
482
|
+
// append the new tracked leaves.
|
|
483
|
+
for (const [f, rule] of fragment.additions) {
|
|
484
|
+
if (rule.deps.length === 0 && built.runtime.isTracked(f)) {
|
|
485
|
+
built.inputs.add(f);
|
|
486
|
+
}
|
|
384
487
|
}
|
|
385
488
|
}
|
|
386
|
-
|
|
387
|
-
|
|
489
|
+
else {
|
|
490
|
+
// Non-simple regime: existing inputs may have gained or lost
|
|
491
|
+
// tracking. The cheapest correct update is a full recompute
|
|
492
|
+
// (same cost as the fragment builder's O(tree) rebuild).
|
|
493
|
+
built.inputs = collectInputs(fragment.next.grammar, built.runtime);
|
|
494
|
+
}
|
|
495
|
+
// Pick up any value mutations in the same gap, then recompute
|
|
496
|
+
// (covering the grafted / rebound fields too). Iterate ONLY the
|
|
497
|
+
// dirty nodes' inputs — every value mutation marks its node
|
|
498
|
+
// dirty, so we don't need to scan the whole input set.
|
|
499
|
+
this.applyAvailable(availableWidth, availableHeight);
|
|
500
|
+
this.markDriftedInputs(dirty);
|
|
501
|
+
const changed = built.runtime.recompute();
|
|
502
|
+
return { child, changed };
|
|
388
503
|
}
|
|
389
504
|
/**
|
|
390
505
|
* Fast-path a structural change that is exactly a single subtree
|
|
391
|
-
* removal
|
|
392
|
-
* whole-tree rebuild. Returns `
|
|
393
|
-
* change is not a clean removal — the caller then
|
|
506
|
+
* removal at `pivot`: `buildRemoveFragment` + `rebindRule` /
|
|
507
|
+
* `detach`, no whole-tree rebuild. Returns `null` (changing nothing)
|
|
508
|
+
* when the change is not a clean removal — the caller then tries
|
|
509
|
+
* the next fast-path / rebuilds.
|
|
394
510
|
*
|
|
395
511
|
* `buildRemoveFragment` must see the removed `child` still attached
|
|
396
512
|
* (its regime check reads the parent's live child list and it walks
|
|
@@ -398,202 +514,207 @@ export class SpinelessLayout {
|
|
|
398
514
|
* runs the caller has already detached it — so the removed subtree
|
|
399
515
|
* is briefly re-inserted at its old index for the fragment build,
|
|
400
516
|
* then detached again.
|
|
517
|
+
*
|
|
518
|
+
* The classifier supplies `pivot` — the parent whose children list
|
|
519
|
+
* differs from its snap's. The validator inspects ONLY pivot's
|
|
520
|
+
* children, not the whole tree.
|
|
401
521
|
*/
|
|
402
|
-
tryDetachRemove(availableWidth, availableHeight) {
|
|
522
|
+
tryDetachRemove(pivot, dirty, availableWidth, availableHeight) {
|
|
403
523
|
const built = this.built;
|
|
404
524
|
const snaps = built.snaps;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
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;
|
|
525
|
+
const snap = snaps.get(pivot);
|
|
526
|
+
const snapChildren = snap.children;
|
|
527
|
+
// Pivot's live children must be its snapped children with exactly
|
|
528
|
+
// one contiguous removal. Walk pivot's children positionally —
|
|
529
|
+
// O(pivot's children), not O(tree).
|
|
530
|
+
const liveCount = pivot.getChildCount();
|
|
531
|
+
const snappedCount = snapChildren.length;
|
|
532
|
+
if (liveCount >= snappedCount)
|
|
533
|
+
return null;
|
|
534
|
+
// Find the index where they diverge; this is the start of the
|
|
535
|
+
// removed run.
|
|
536
|
+
let removedStart = 0;
|
|
537
|
+
while (removedStart < liveCount &&
|
|
538
|
+
pivot.getChild(removedStart) === snapChildren[removedStart]) {
|
|
539
|
+
removedStart++;
|
|
540
|
+
}
|
|
541
|
+
const removedCount = snappedCount - liveCount;
|
|
542
|
+
// Verify the snapped tail past the removed run still matches the
|
|
543
|
+
// live tail.
|
|
544
|
+
for (let i = removedStart; i < liveCount; i++) {
|
|
545
|
+
if (pivot.getChild(i) !== snapChildren[i + removedCount])
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
// Single-child removal: simple-regime requirement. (A wider
|
|
549
|
+
// contiguous removal would still be a valid fragment for
|
|
550
|
+
// non-simple regime, but the fragment builder is single-`child`
|
|
551
|
+
// only, so split-merge runs fall through to fullBuild.)
|
|
552
|
+
if (removedCount !== 1)
|
|
553
|
+
return null;
|
|
554
|
+
const child = snapChildren[removedStart];
|
|
457
555
|
// A removal inside a `display: 'none'` subtree: the hidden region
|
|
458
556
|
// has no fields to `detach`. A node absent from `fields` is hidden
|
|
459
557
|
// (or under a hidden ancestor) — rebuild instead.
|
|
460
|
-
if (!built.fields.has(
|
|
461
|
-
return
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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++;
|
|
558
|
+
if (!built.fields.has(pivot))
|
|
559
|
+
return null;
|
|
560
|
+
// The removed subtree's nodes must still be reachable from
|
|
561
|
+
// `child` through SNAP edges (the live tree no longer has them).
|
|
562
|
+
// Collect them via DFS over snaps.
|
|
563
|
+
const removedNodes = new Set();
|
|
564
|
+
{
|
|
565
|
+
const stack = [child];
|
|
566
|
+
while (stack.length > 0) {
|
|
567
|
+
const n = stack.pop();
|
|
568
|
+
removedNodes.add(n);
|
|
569
|
+
const cs = snaps.get(n);
|
|
570
|
+
if (cs !== undefined)
|
|
571
|
+
for (const c of cs.children)
|
|
572
|
+
stack.push(c);
|
|
573
|
+
}
|
|
484
574
|
}
|
|
485
575
|
// Re-attach `child` for the fragment build, then detach it again.
|
|
486
|
-
|
|
487
|
-
const fragment = buildRemoveFragment(built.output, this.root,
|
|
488
|
-
|
|
576
|
+
pivot.insertChild(child, removedStart);
|
|
577
|
+
const fragment = buildRemoveFragment(built.output, this.root, pivot, child, built.available);
|
|
578
|
+
pivot.removeChild(child);
|
|
489
579
|
if (fragment === null)
|
|
490
|
-
return
|
|
580
|
+
return null;
|
|
491
581
|
// Apply: rebind survivors FIRST (so they stop reading the removed
|
|
492
582
|
// fields), then `detach`, then adopt the next grammar.
|
|
493
|
-
for (const [
|
|
494
|
-
built.runtime.rebindRule(
|
|
495
|
-
built.runtime.detach(fragment.removed);
|
|
583
|
+
for (const [f, rule] of fragment.rebinds)
|
|
584
|
+
built.runtime.rebindRule(f, rule);
|
|
585
|
+
const dropped = built.runtime.detach(fragment.removed);
|
|
496
586
|
built.output = fragment.next;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
built.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
587
|
+
// Incremental bookkeeping (Part C):
|
|
588
|
+
//
|
|
589
|
+
// `built.snaps` — update pivot's snap (children list changed) and
|
|
590
|
+
// drop the removed subtree's snaps.
|
|
591
|
+
built.snaps.set(pivot, freshSnap(pivot));
|
|
592
|
+
for (const n of removedNodes)
|
|
593
|
+
built.snaps.delete(n);
|
|
594
|
+
// `built.fields` / `built.owner` — drop the removed subtree's
|
|
595
|
+
// entries. Each removed node may have had `display: 'none'` (no
|
|
596
|
+
// entry) or normal layout fields — `built.fields.delete` on a
|
|
597
|
+
// missing key is harmless; `built.owner.delete` likewise.
|
|
598
|
+
for (const n of removedNodes) {
|
|
599
|
+
const lf = built.fields.get(n);
|
|
600
|
+
if (lf !== undefined) {
|
|
601
|
+
built.owner.delete(lf.width);
|
|
602
|
+
built.owner.delete(lf.height);
|
|
603
|
+
built.owner.delete(lf.left);
|
|
604
|
+
built.owner.delete(lf.top);
|
|
605
|
+
built.fields.delete(n);
|
|
507
606
|
}
|
|
508
607
|
}
|
|
509
|
-
built.
|
|
510
|
-
|
|
608
|
+
// `built.inputs` — remove every field detach dropped (the removed
|
|
609
|
+
// subtree's inputs + any survivor orphan-cleaned by detach).
|
|
610
|
+
// O(|dropped|) — detach's returned set is complete by construction:
|
|
611
|
+
// its `drop` closure is the single choke point for ALL removals,
|
|
612
|
+
// including orphan-cleaned surviving deps (e.g. the previous last
|
|
613
|
+
// child's now-unread main-end margin). Set.delete is a no-op for
|
|
614
|
+
// absent keys, so iterating all of `dropped` (which includes
|
|
615
|
+
// non-input layout fields) correctly removes only the dropped
|
|
616
|
+
// leaf inputs from `built.inputs`.
|
|
617
|
+
for (const f of dropped)
|
|
618
|
+
built.inputs.delete(f);
|
|
619
|
+
// Pick up any value mutations in the same batch, then recompute.
|
|
620
|
+
// Iterate ONLY the dirty nodes' inputs — every value mutation
|
|
621
|
+
// marks its node dirty.
|
|
622
|
+
this.applyAvailable(availableWidth, availableHeight);
|
|
623
|
+
this.markDriftedInputs(dirty);
|
|
624
|
+
const changed = built.runtime.recompute();
|
|
625
|
+
return { parent: pivot, changed };
|
|
511
626
|
}
|
|
512
627
|
/**
|
|
513
|
-
* Fast-path a structural change that is exactly
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
517
|
-
*
|
|
628
|
+
* Fast-path a structural change that is exactly `pivot`'s children
|
|
629
|
+
* being reordered (a permutation — no node added or removed):
|
|
630
|
+
* `buildReorderFragment` + `rebindRule`, no whole-tree rebuild.
|
|
631
|
+
* Returns `null` (changing nothing) when the change is not a clean
|
|
632
|
+
* single-parent reorder — the caller then rebuilds.
|
|
633
|
+
*
|
|
634
|
+
* The classifier supplies `pivot` — the single node whose children
|
|
635
|
+
* list differs from its snap's. The validator inspects ONLY pivot's
|
|
636
|
+
* children, not the whole tree.
|
|
518
637
|
*/
|
|
519
|
-
tryReorder(availableWidth, availableHeight) {
|
|
638
|
+
tryReorder(pivot, dirty, availableWidth, availableHeight) {
|
|
520
639
|
const built = this.built;
|
|
521
|
-
const
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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;
|
|
640
|
+
const snap = built.snaps.get(pivot);
|
|
641
|
+
// Pivot's children must be a permutation of the snapped set
|
|
642
|
+
// (same count, same members). Otherwise it is an add/remove —
|
|
643
|
+
// tryGraftAppend / tryDetachRemove would have caught it.
|
|
644
|
+
const before = snap.children;
|
|
645
|
+
if (before.length !== pivot.getChildCount())
|
|
646
|
+
return null;
|
|
554
647
|
const beforeSet = new Set(before);
|
|
555
|
-
for (let i = 0; i <
|
|
556
|
-
if (!beforeSet.has(
|
|
557
|
-
return
|
|
648
|
+
for (let i = 0; i < pivot.getChildCount(); i++) {
|
|
649
|
+
if (!beforeSet.has(pivot.getChild(i)))
|
|
650
|
+
return null;
|
|
558
651
|
}
|
|
559
652
|
// A reorder inside a `display: 'none'` subtree touches no laid-out
|
|
560
653
|
// node — the hidden region has no fields. Rebuild instead.
|
|
561
|
-
if (!built.fields.has(
|
|
562
|
-
return
|
|
563
|
-
const fragment = buildReorderFragment(built.output, this.root,
|
|
654
|
+
if (!built.fields.has(pivot))
|
|
655
|
+
return null;
|
|
656
|
+
const fragment = buildReorderFragment(built.output, this.root, pivot, built.available);
|
|
564
657
|
// Order: integrate the newly-read inputs, rebind the rewritten
|
|
565
658
|
// rules (their new deps are now all present), then detach the
|
|
566
659
|
// inputs no rebound rule reads any more.
|
|
567
660
|
built.runtime.graft(fragment.additions, fragment.newRoots);
|
|
568
|
-
for (const [
|
|
569
|
-
built.runtime.rebindRule(
|
|
661
|
+
for (const [f, rule] of fragment.rebinds)
|
|
662
|
+
built.runtime.rebindRule(f, rule);
|
|
570
663
|
if (fragment.removed.length > 0)
|
|
571
664
|
built.runtime.detach(fragment.removed);
|
|
572
665
|
built.output = fragment.next;
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
built.
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
666
|
+
// Incremental bookkeeping (Part C):
|
|
667
|
+
//
|
|
668
|
+
// `built.snaps` — pivot's children list changed; everything else
|
|
669
|
+
// stayed put. Update just pivot's snap.
|
|
670
|
+
built.snaps.set(pivot, freshSnap(pivot));
|
|
671
|
+
// `built.fields` / `built.owner` — the same nodes own the same
|
|
672
|
+
// four layout fields they did before; no entry adds or drops.
|
|
673
|
+
// (Field identity is stable across grammar rebuilds.)
|
|
674
|
+
//
|
|
675
|
+
// `built.inputs` — a reorder can newly-track or newly-untrack
|
|
676
|
+
// sibling main-end margins (`additions` / `removed`). Update
|
|
677
|
+
// incrementally.
|
|
678
|
+
if (fragment.removed.length > 0) {
|
|
679
|
+
for (const f of fragment.removed)
|
|
680
|
+
built.inputs.delete(f);
|
|
681
|
+
}
|
|
682
|
+
for (const [f, rule] of fragment.additions) {
|
|
683
|
+
if (rule.deps.length === 0 && built.runtime.isTracked(f)) {
|
|
684
|
+
built.inputs.add(f);
|
|
583
685
|
}
|
|
584
686
|
}
|
|
585
|
-
|
|
586
|
-
|
|
687
|
+
// Pick up any value mutations in the same batch, then recompute.
|
|
688
|
+
// Iterate ONLY the dirty nodes' inputs — every value mutation
|
|
689
|
+
// marks its node dirty.
|
|
690
|
+
this.applyAvailable(availableWidth, availableHeight);
|
|
691
|
+
this.markDriftedInputs(dirty);
|
|
692
|
+
const changed = built.runtime.recompute();
|
|
693
|
+
return { reordered: pivot, changed };
|
|
587
694
|
}
|
|
588
695
|
/**
|
|
589
696
|
* Value relayout: re-`markDirty` only the input Fields of the dirty
|
|
590
697
|
* nodes (plus the root `available:*` inputs) whose value drifted,
|
|
591
698
|
* then `recompute()`. Returns the maximal subtree roots whose
|
|
592
|
-
* layout moved — for `
|
|
699
|
+
* layout moved — for `finishMoved` to write back.
|
|
593
700
|
*/
|
|
594
701
|
relayoutValues(dirty, availableWidth, availableHeight) {
|
|
595
|
-
const built = this.built;
|
|
596
702
|
this.applyAvailable(availableWidth, availableHeight);
|
|
703
|
+
this.markDriftedInputs(dirty);
|
|
704
|
+
return this.movedSubtreeRoots(this.built.runtime.recompute());
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Re-`markDirty` only the input Fields of the dirty nodes (plus the
|
|
708
|
+
* root `available:*` inputs) whose live value drifted from the
|
|
709
|
+
* runtime's stored value. Shared by every relayout path — the value
|
|
710
|
+
* relayout and all three structural fast-paths.
|
|
711
|
+
*
|
|
712
|
+
* Iterates O(dirty inputs), not O(built.inputs) — every value
|
|
713
|
+
* mutation marks its owning node dirty, so non-dirty nodes can't
|
|
714
|
+
* have drifted inputs.
|
|
715
|
+
*/
|
|
716
|
+
markDriftedInputs(dirty) {
|
|
717
|
+
const built = this.built;
|
|
597
718
|
const fields = [];
|
|
598
719
|
if (built.output.availableInputs.width !== undefined) {
|
|
599
720
|
fields.push(built.output.availableInputs.width);
|
|
@@ -604,27 +725,48 @@ export class SpinelessLayout {
|
|
|
604
725
|
for (const n of dirty)
|
|
605
726
|
inputFieldsOf(built.output.styleInputs.get(n), fields);
|
|
606
727
|
const { runtime, output } = built;
|
|
607
|
-
for (const
|
|
728
|
+
for (const f of fields) {
|
|
608
729
|
// `styleInputs` can hold an input Field no rule reads (e.g. a
|
|
609
730
|
// flex-start container's main-END padding) — untracked, and a
|
|
610
731
|
// change to it cannot move any layout field. Skip it.
|
|
611
|
-
if (!runtime.isTracked(
|
|
732
|
+
if (!runtime.isTracked(f))
|
|
612
733
|
continue;
|
|
613
|
-
const live = output.grammar.get(
|
|
614
|
-
if (live !== runtime.evaluate(
|
|
615
|
-
runtime.markDirty(
|
|
734
|
+
const live = output.grammar.get(f).compute(NEVER_READ);
|
|
735
|
+
if (live !== runtime.evaluate(f))
|
|
736
|
+
runtime.markDirty(f);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/** Push new `available` values into the holder the grammar closes over. */
|
|
740
|
+
applyAvailable(availableWidth, availableHeight) {
|
|
741
|
+
const a = this.built.available;
|
|
742
|
+
if (availableWidth !== undefined)
|
|
743
|
+
a.width = availableWidth;
|
|
744
|
+
if (availableHeight !== undefined)
|
|
745
|
+
a.height = availableHeight;
|
|
746
|
+
}
|
|
747
|
+
/** Write-back + round + scroll the whole tree (after a build / graft). */
|
|
748
|
+
finishWhole() {
|
|
749
|
+
const { runtime, output } = this.built;
|
|
750
|
+
for (const f of output.allFields) {
|
|
751
|
+
writeNode(f.node, runtime, { width: f.width, height: f.height, left: f.left, top: f.top });
|
|
616
752
|
}
|
|
617
|
-
|
|
618
|
-
|
|
753
|
+
roundLayout(this.root);
|
|
754
|
+
recordScrollSizes(this.root);
|
|
755
|
+
clearDirtyDeep(this.root);
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Reduce a set of changed Fields to the maximal moved-subtree roots.
|
|
759
|
+
* Used by the structural fast-paths to determine which subtrees need
|
|
760
|
+
* write-back after an incremental recompute.
|
|
761
|
+
*/
|
|
762
|
+
movedSubtreeRoots(changed) {
|
|
763
|
+
const built = this.built;
|
|
619
764
|
const moved = new Set();
|
|
620
765
|
for (const f of changed) {
|
|
621
766
|
const n = built.owner.get(f);
|
|
622
767
|
if (n !== undefined)
|
|
623
768
|
moved.add(n);
|
|
624
769
|
}
|
|
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
770
|
const roots = [];
|
|
629
771
|
for (const n of moved) {
|
|
630
772
|
let maximal = true;
|
|
@@ -639,31 +781,17 @@ export class SpinelessLayout {
|
|
|
639
781
|
}
|
|
640
782
|
return roots;
|
|
641
783
|
}
|
|
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
784
|
/**
|
|
661
785
|
* Write-back + round + scroll, scoped to the subtrees that moved.
|
|
662
786
|
* A moved subtree's parent did not move, so its rounding is stable
|
|
663
787
|
* and the subtree can be re-rounded in isolation; only that
|
|
664
788
|
* parent's own scroll extent then needs a recompute.
|
|
789
|
+
*
|
|
790
|
+
* `extraScrollParents` are additional nodes whose scroll extents
|
|
791
|
+
* must be recomputed — used by structural fast-paths to include
|
|
792
|
+
* the surviving parent of a removed or reordered subtree.
|
|
665
793
|
*/
|
|
666
|
-
|
|
794
|
+
finishMoved(roots, extraScrollParents) {
|
|
667
795
|
const { runtime, fields } = this.built;
|
|
668
796
|
for (const root of roots) {
|
|
669
797
|
// Write the float layout for the whole moved subtree, so the
|
|
@@ -694,6 +822,8 @@ export class SpinelessLayout {
|
|
|
694
822
|
if (p !== null)
|
|
695
823
|
scrollParents.add(p);
|
|
696
824
|
}
|
|
825
|
+
for (const p of extraScrollParents)
|
|
826
|
+
scrollParents.add(p);
|
|
697
827
|
for (const p of scrollParents)
|
|
698
828
|
recomputeScroll(p);
|
|
699
829
|
}
|
|
@@ -752,4 +882,12 @@ function recomputeScroll(node) {
|
|
|
752
882
|
node._layout.scrollWidth = Math.max(node._layout.width, contentRight);
|
|
753
883
|
node._layout.scrollHeight = Math.max(node._layout.height, contentBottom);
|
|
754
884
|
}
|
|
885
|
+
/** True iff `candidate` is `root` or a descendant of `root`. */
|
|
886
|
+
function isInSubtree(candidate, root) {
|
|
887
|
+
for (let n = candidate; n !== null; n = n.getParent()) {
|
|
888
|
+
if (n === root)
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
755
893
|
//# sourceMappingURL=layout.js.map
|