@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.
- 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,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
|