react-x11 2.22.2 → 2.22.4
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/package.json +1 -1
- package/src/nodes/animation.js +1 -0
- package/src/nodes/damage.js +7 -0
- package/src/nodes/invalidate.js +9 -2
- package/src/nodes/layout.js +18 -11
- package/src/nodes/queries.js +1 -0
- package/src/nodes/scrollable.js +63 -16
- package/src/nodes/window/exactcopy.js +65 -0
- package/src/nodes/window/floors.js +76 -2
- package/src/nodes/window/flush.js +5 -0
- package/src/nodes/window/size.js +185 -35
- package/src/nodes/window/spine.js +294 -0
- package/src/nodes/window/window.js +7 -0
- package/src/styles.js +28 -0
package/package.json
CHANGED
package/src/nodes/animation.js
CHANGED
|
@@ -491,6 +491,7 @@ export class NodeAnimation {
|
|
|
491
491
|
this.root.needsLayout = true;
|
|
492
492
|
this.root._floorsDirty = true;
|
|
493
493
|
this.root._floorsSources.add(this);
|
|
494
|
+
this.root._floorsOtherSources.add(this);
|
|
494
495
|
}
|
|
495
496
|
}
|
|
496
497
|
// A tick writes `this.style` without going through `_retarget`, so it
|
package/src/nodes/damage.js
CHANGED
|
@@ -59,6 +59,13 @@ export const layoutDiff = {
|
|
|
59
59
|
// would have claimed its children — claims its own box instead, since the
|
|
60
60
|
// move claimed nothing for it.
|
|
61
61
|
ride: false,
|
|
62
|
+
|
|
63
|
+
// Whether this walk may leave alone a subtree yoga did not reach under a
|
|
64
|
+
// parent that did not move (`Node._followParent`) — everything in it is
|
|
65
|
+
// where the last walk put it. False on a frame something scrolled on, or
|
|
66
|
+
// asked to: a scroll moves boxes without a pass, and the walk is what
|
|
67
|
+
// carries the offset down to them (`WindowNode._walkWhole`).
|
|
68
|
+
skipUnreached: false,
|
|
62
69
|
};
|
|
63
70
|
|
|
64
71
|
// What an invalidate() may name as its reason — a small closed set, so the
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -309,6 +309,11 @@ export class WindowInvalidate {
|
|
|
309
309
|
const windowToo = reach !== PANES_ONLY;
|
|
310
310
|
if (layoutChanged) {
|
|
311
311
|
this.needsLayout = true;
|
|
312
|
+
// What the next walk may skip (`layoutDiff.skipUnreached`): nothing,
|
|
313
|
+
// after a scroll or a change that names no node.
|
|
314
|
+
if (reason === 'scroll' || !source || source === this) {
|
|
315
|
+
this._walkWhole = true;
|
|
316
|
+
}
|
|
312
317
|
// The content floors are measured from the tree, so anything that
|
|
313
318
|
// changed it has to give them up — and **scrolling does not**, which is
|
|
314
319
|
// the whole reason this is not just `needsLayout`: a scroll moves an
|
|
@@ -325,8 +330,10 @@ export class WindowInvalidate {
|
|
|
325
330
|
// …and which node changed, so that a change inside a box that sizes
|
|
326
331
|
// itself measures that box and nothing around it (`_floorsScope`).
|
|
327
332
|
// A claim that names no node, or names the window, is the tree's.
|
|
328
|
-
if (source && source !== this)
|
|
329
|
-
|
|
333
|
+
if (source && source !== this) {
|
|
334
|
+
this._floorsSources.add(source);
|
|
335
|
+
if (reason !== 'child-list') this._floorsOtherSources.add(source);
|
|
336
|
+
} else this._floorsUnscoped = true;
|
|
330
337
|
}
|
|
331
338
|
if (reason !== 'scroll' && reason !== 'resize' && reason !== 'position') {
|
|
332
339
|
this._floorsContentDirty = true;
|
package/src/nodes/layout.js
CHANGED
|
@@ -228,22 +228,29 @@ export class NodeLayout {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
|
-
* A child
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
231
|
+
* A child yoga did not lay out again: its box and everything under it
|
|
232
|
+
* sit exactly where the last pass put them relative to their parents, so
|
|
233
|
+
* the whole subtree moves by however far its parent did (`dx`, `dy`) —
|
|
234
|
+
* `_shiftAbs`, the scroll fast path's translation (issue #405), instead
|
|
235
|
+
* of the walk — and where its parent did not move, it has not moved at
|
|
236
|
+
* all, and there is nothing to do, unless something scrolled since the
|
|
237
|
+
* last walk (`layoutDiff.skipUnreached`): a scroll moves boxes with no
|
|
238
|
+
* pass, and it is the walk that carries the new offset down to them.
|
|
239
|
+
* Answers whether it was handled; false means walk it.
|
|
237
240
|
*
|
|
238
241
|
* The witness is yoga's has-new-layout flag, the one that fast path
|
|
239
242
|
* reads: a pass sets it on every node it lays out and on every child of
|
|
240
243
|
* one, cached or not, so a child it is clear on was not reached, nor was
|
|
241
244
|
* anything under it. Cleared here when it is found set, just before the
|
|
242
245
|
* walk that reads what the pass left — no pass runs in between — so the
|
|
243
|
-
* next
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
246
|
+
* next look at the same subtree finds it clear. A scroll pane is always
|
|
247
|
+
* walked, since its flag is its own (and a scroll moves its children
|
|
248
|
+
* with no pass at all).
|
|
249
|
+
*
|
|
250
|
+
* Walking a subtree nothing moved was the whole tree for a change in one
|
|
251
|
+
* place: a block that arrived at the end of a document of 13,000 boxes
|
|
252
|
+
* was laid out alone, and the walk still read four numbers out of yoga
|
|
253
|
+
* for every box in the document — 30 ms of a 60 ms frame.
|
|
247
254
|
*
|
|
248
255
|
* Walking such a subtree read four numbers out of yoga per node and wrote
|
|
249
256
|
* them back unchanged: the bodies of a graph's cards, fifty widgets deep,
|
|
@@ -255,13 +262,13 @@ export class NodeLayout {
|
|
|
255
262
|
* walk's claims came to.
|
|
256
263
|
*/
|
|
257
264
|
_followParent(dx, dy) {
|
|
258
|
-
if (dx === 0 && dy === 0) return false;
|
|
259
265
|
const yoga = this.yoga;
|
|
260
266
|
if (!yoga || !this._placed || this.isScroller?.()) return false;
|
|
261
267
|
if (yoga.hasNewLayout()) {
|
|
262
268
|
yoga.markLayoutSeen();
|
|
263
269
|
return false;
|
|
264
270
|
}
|
|
271
|
+
if (dx === 0 && dy === 0) return layoutDiff.skipUnreached;
|
|
265
272
|
const sink = layoutDiff.sink;
|
|
266
273
|
const shift = layoutDiff.shift;
|
|
267
274
|
if (
|
package/src/nodes/queries.js
CHANGED
package/src/nodes/scrollable.js
CHANGED
|
@@ -30,6 +30,56 @@ import { BLIT_POISONED } from './scrollblit.js';
|
|
|
30
30
|
|
|
31
31
|
const clampScroll = (v, max) => Math.min(Math.max(0, v), max);
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* How far everything under `node` reaches from its origin — the leftmost
|
|
35
|
+
* edge, the rightmost, the lowest — through every box that does not clip
|
|
36
|
+
* what it holds: the walk `measureScrollContent` sums a pane's extent from.
|
|
37
|
+
*
|
|
38
|
+
* Kept on each box (`_contentReach`) and reused while yoga has laid nothing
|
|
39
|
+
* under it out again. Its has-new-layout flag is set on every box a pass
|
|
40
|
+
* lays out and on each child of one, and a change anywhere below dirties
|
|
41
|
+
* every box above it, so a clear flag is a subtree whose boxes sit where
|
|
42
|
+
* they did relative to it — the same witness `_followParent` reads, read
|
|
43
|
+
* here before the walk that clears it. Hiding a box, or clipping one, goes
|
|
44
|
+
* through yoga too (`display`, `overflow`). A layout host places its
|
|
45
|
+
* children where yoga cannot see, so neither it nor they keep one.
|
|
46
|
+
*
|
|
47
|
+
* Walked whole, a pane's content cost every box in it on every pass that
|
|
48
|
+
* laid out any of it: a block arriving at the end of a document of 13,000
|
|
49
|
+
* boxes read three numbers out of yoga for each of them.
|
|
50
|
+
*/
|
|
51
|
+
function contentReach(node, top) {
|
|
52
|
+
const cached = node._contentReach;
|
|
53
|
+
if (
|
|
54
|
+
!top &&
|
|
55
|
+
cached !== undefined &&
|
|
56
|
+
node._host === null &&
|
|
57
|
+
node.parent?._host === null &&
|
|
58
|
+
!node.yoga.hasNewLayout()
|
|
59
|
+
) {
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
|
62
|
+
let left = Infinity;
|
|
63
|
+
let right = -Infinity;
|
|
64
|
+
let bottom = -Infinity;
|
|
65
|
+
for (const child of node.children) {
|
|
66
|
+
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
67
|
+
const offset = offsetInParent(child);
|
|
68
|
+
left = Math.min(left, offset.x);
|
|
69
|
+
right = Math.max(right, offset.x + child.yoga.getComputedWidth());
|
|
70
|
+
bottom = Math.max(bottom, offset.y + child.yoga.getComputedHeight());
|
|
71
|
+
if (!child.clipsChildren()) {
|
|
72
|
+
const inner = contentReach(child, false);
|
|
73
|
+
left = Math.min(left, offset.x + inner.left);
|
|
74
|
+
right = Math.max(right, offset.x + inner.right);
|
|
75
|
+
bottom = Math.max(bottom, offset.y + inner.bottom);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const reach = { left, right, bottom };
|
|
79
|
+
if (!top) node._contentReach = reach;
|
|
80
|
+
return reach;
|
|
81
|
+
}
|
|
82
|
+
|
|
33
83
|
/**
|
|
34
84
|
* One axis of a scroll request held for a pass
|
|
35
85
|
* (`Scrollable._holdScrollTo`): `{ base, steps }`, the request the extent in
|
|
@@ -435,21 +485,9 @@ export const Scrollable = (Base) =>
|
|
|
435
485
|
measureScrollContent() {
|
|
436
486
|
const rtl = this.direction === 'rtl';
|
|
437
487
|
const width = this.yoga.getComputedWidth();
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
for (const child of node.children) {
|
|
442
|
-
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
443
|
-
const offset = offsetInParent(child);
|
|
444
|
-
const x = dx + offset.x;
|
|
445
|
-
const y = dy + offset.y;
|
|
446
|
-
const w = child.yoga.getComputedWidth();
|
|
447
|
-
start = Math.max(start, rtl ? width - x : x + w);
|
|
448
|
-
bottom = Math.max(bottom, y + child.yoga.getComputedHeight());
|
|
449
|
-
if (!child.clipsChildren()) walk(child, x, y);
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
walk(this, 0, 0);
|
|
488
|
+
const reach = contentReach(this, true);
|
|
489
|
+
const start = Math.max(0, rtl ? width - reach.left : reach.right);
|
|
490
|
+
const bottom = Math.max(0, reach.bottom);
|
|
453
491
|
// the end padding is part of the content box a browser scrolls to, and
|
|
454
492
|
// it is the one part of it yoga has already resolved for us — on the
|
|
455
493
|
// left in RTL, since that is the end there
|
|
@@ -942,7 +980,16 @@ export const Scrollable = (Base) =>
|
|
|
942
980
|
const ny = ev.nativeEvent?.y ?? ev.y * this.scale;
|
|
943
981
|
const at = along(bar, nx, ny) - this._barGrab.offset - bar.trackStart;
|
|
944
982
|
const from = bar.reversed ? bar.travel - at : at;
|
|
945
|
-
|
|
983
|
+
// Whole device pixels, as a wheel's default action moves (events.js).
|
|
984
|
+
// The pointer's share of the travel times the range is a fraction at
|
|
985
|
+
// almost every position, and a fractional offset costs the scroll blit,
|
|
986
|
+
// which can only shift by whole pixels, and draws everything in the
|
|
987
|
+
// pane off the pixel grid, where the X11 backend fills an edge or a
|
|
988
|
+
// rounded box through a coverage mask it uploads shape by shape. A
|
|
989
|
+
// thumb dragged through a 100,000-row table ran at 25 fps on XQuartz,
|
|
990
|
+
// with two thirds of the server's time in those masks, and at 52 once
|
|
991
|
+
// the offset was whole.
|
|
992
|
+
const to = Math.round((from / bar.travel) * bar.range);
|
|
946
993
|
this._scrollToDevice(bar.axis === 'x' ? { x: to } : { y: to });
|
|
947
994
|
}
|
|
948
995
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Measuring a box alone without unsettling the tree around it.
|
|
2
|
+
//
|
|
3
|
+
// The renderer lays everything out in one yoga config, rounded to the pixel
|
|
4
|
+
// grid, and a measurement switches the rounding off (`measuringExactly`,
|
|
5
|
+
// styles.js), which yoga answers by treating every layout it has cached as
|
|
6
|
+
// stale: the pass after any measurement lays the whole tree out again. On a
|
|
7
|
+
// document of 13,000 boxes that pass is 58 ms, where one yoga answers from
|
|
8
|
+
// its cache is 0.05 ms. A measurement over the whole tree pays it anyway,
|
|
9
|
+
// having just laid everything out three times; a box measured alone — a
|
|
10
|
+
// card (`_measureBoundary`), a block on a column spine
|
|
11
|
+
// (`_measureSpineRoot`) — is a few dozen boxes, and made the pass after it
|
|
12
|
+
// cost the whole tree.
|
|
13
|
+
//
|
|
14
|
+
// So a box measured alone is measured on copies: its boxes and everything
|
|
15
|
+
// under them, copied into a config that is never rounded
|
|
16
|
+
// (`exactLayoutConfig`), laid out there, read, and freed. The config the tree
|
|
17
|
+
// is laid out in never changes, and yoga's record of the real boxes — their
|
|
18
|
+
// caches, what is dirty — is not touched.
|
|
19
|
+
|
|
20
|
+
import { Yoga } from '../../yoga.js';
|
|
21
|
+
import { exactLayoutConfig, measuringInExactConfig } from '../../styles.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run `measure` with `root` and everything under it standing on copies of
|
|
25
|
+
* their boxes: every node's `yoga` is its copy for the duration, so the
|
|
26
|
+
* floors code measures, reads and writes extents exactly as it would on the
|
|
27
|
+
* real boxes.
|
|
28
|
+
*
|
|
29
|
+
* What a measurement writes on a node stays — its extents. What it writes on
|
|
30
|
+
* a box — a floor taken off to measure what is under it, a shrink it
|
|
31
|
+
* borrows, a width it pins — goes with the copy, so the floors' record of
|
|
32
|
+
* what each real box holds (`_floorMinW`/`_floorMinH`) is put back, and the
|
|
33
|
+
* floors written after the measurement are judged against the real box.
|
|
34
|
+
*/
|
|
35
|
+
export function onExactCopy(root, measure) {
|
|
36
|
+
const swapped = [];
|
|
37
|
+
const copy = (node) => {
|
|
38
|
+
const real = node.yoga;
|
|
39
|
+
const box = Yoga.Node.create(exactLayoutConfig());
|
|
40
|
+
box.copyStyle(real);
|
|
41
|
+
if (node._measureFn) box.setMeasureFunc(node._measureFn);
|
|
42
|
+
swapped.push({ node, real, minW: node._floorMinW, minH: node._floorMinH });
|
|
43
|
+
let at = 0;
|
|
44
|
+
// the same children yoga has (`Node._joinsYoga`)
|
|
45
|
+
if (node._host === null) {
|
|
46
|
+
for (const child of node.children) {
|
|
47
|
+
if (!child.yoga || child.isWindow) continue;
|
|
48
|
+
box.insertChild(copy(child), at++);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
node.yoga = box;
|
|
52
|
+
return box;
|
|
53
|
+
};
|
|
54
|
+
const top = copy(root);
|
|
55
|
+
try {
|
|
56
|
+
return measuringInExactConfig(measure);
|
|
57
|
+
} finally {
|
|
58
|
+
for (const { node, real, minW, minH } of swapped) {
|
|
59
|
+
node.yoga = real;
|
|
60
|
+
node._floorMinW = minW;
|
|
61
|
+
node._floorMinH = minH;
|
|
62
|
+
}
|
|
63
|
+
top.freeRecursive();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -94,7 +94,7 @@ export function inFlow(node) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Which axis this node lays its children out along. */
|
|
97
|
-
const mainAxisOf = (node) => {
|
|
97
|
+
export const mainAxisOf = (node) => {
|
|
98
98
|
const direction = node.style.flexDirection ?? 'column';
|
|
99
99
|
return direction === 'row' || direction === 'row-reverse'
|
|
100
100
|
? 'width'
|
|
@@ -107,7 +107,7 @@ const mainAxisOf = (node) => {
|
|
|
107
107
|
* content. The same test `writeFloors` applies, asked ahead of time — it is
|
|
108
108
|
* what decides whether a stale extent is one anybody will read.
|
|
109
109
|
*/
|
|
110
|
-
function receivesFloor(child, axis) {
|
|
110
|
+
export function receivesFloor(child, axis) {
|
|
111
111
|
const parent = child.parent;
|
|
112
112
|
if (
|
|
113
113
|
!parent ||
|
|
@@ -279,6 +279,80 @@ export function dirtyOutside(node, boundaries, paths) {
|
|
|
279
279
|
return false;
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/**
|
|
283
|
+
* `contentSpan`'s height for a column, from the extents its children
|
|
284
|
+
* already carry rather than from a measuring pass — the sum a column spine
|
|
285
|
+
* (`spine.js`) is recomputed with, bottom up, where a pass would lay the
|
|
286
|
+
* whole tree out to read it.
|
|
287
|
+
*
|
|
288
|
+
* It is the same number. In a measuring pass `contentSpan` places each
|
|
289
|
+
* child at the sum of the extents before it (its `shift`), so what it reads
|
|
290
|
+
* back is every in-flow child's extent, the margins *between* them — the
|
|
291
|
+
* first child's leading margin and the last one's trailing margin fall
|
|
292
|
+
* outside the span, which runs from the first box to the last — the gap
|
|
293
|
+
* between each pair, and the column's own padding and border. That holds
|
|
294
|
+
* for a column whose height is its content's, which is the only kind a
|
|
295
|
+
* spine sums (`isSummableColumn`): with no height of its own the pass has
|
|
296
|
+
* no free space to justify into, and nothing in it to squash.
|
|
297
|
+
*
|
|
298
|
+
* Undefined where it cannot be summed: a child with no extent on hand, a
|
|
299
|
+
* negative margin (the boxes would overlap, and the last would not have to
|
|
300
|
+
* be the lowest), or nothing in flow at all — a leaf's answer, which is
|
|
301
|
+
* `contentSpan`'s to give.
|
|
302
|
+
*/
|
|
303
|
+
export function columnHeightSpan(node) {
|
|
304
|
+
const yoga = node.yoga;
|
|
305
|
+
let span = 0;
|
|
306
|
+
let count = 0;
|
|
307
|
+
let trailing = 0;
|
|
308
|
+
for (const child of node.children) {
|
|
309
|
+
if (!inFlow(child)) continue;
|
|
310
|
+
const extent = child._floorH;
|
|
311
|
+
if (extent === undefined) return undefined;
|
|
312
|
+
const top = marginDown(child.yoga, Yoga.EDGE_TOP);
|
|
313
|
+
const bottom = marginDown(child.yoga, Yoga.EDGE_BOTTOM);
|
|
314
|
+
if (!(top >= 0 && bottom >= 0)) return undefined;
|
|
315
|
+
span += count === 0 ? extent : trailing + top + extent;
|
|
316
|
+
trailing = bottom;
|
|
317
|
+
count += 1;
|
|
318
|
+
}
|
|
319
|
+
if (count === 0) return undefined;
|
|
320
|
+
const gap = columnGap(node);
|
|
321
|
+
if (gap === undefined) return undefined;
|
|
322
|
+
return (
|
|
323
|
+
span +
|
|
324
|
+
gap * (count - 1) +
|
|
325
|
+
yoga.getComputedPadding(Yoga.EDGE_TOP) +
|
|
326
|
+
yoga.getComputedPadding(Yoga.EDGE_BOTTOM) +
|
|
327
|
+
yoga.getComputedBorder(Yoga.EDGE_TOP) +
|
|
328
|
+
yoga.getComputedBorder(Yoga.EDGE_BOTTOM)
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* A box's margin on one edge down the page, from its style as yoga resolves
|
|
334
|
+
* it — the edge, else the vertical pair, else all four — rather than from a
|
|
335
|
+
* pass: a box that has just arrived has never been laid out in the tree, and
|
|
336
|
+
* its computed margins read 0 until it is. Undefined where it is not a
|
|
337
|
+
* length (a percentage, `auto`).
|
|
338
|
+
*/
|
|
339
|
+
function marginDown(yoga, edge) {
|
|
340
|
+
for (const at of [edge, Yoga.EDGE_VERTICAL, Yoga.EDGE_ALL]) {
|
|
341
|
+
const value = yoga.getMargin(at);
|
|
342
|
+
if (value.unit === Yoga.UNIT_POINT) return value.value;
|
|
343
|
+
if (value.unit !== Yoga.UNIT_UNDEFINED) return undefined;
|
|
344
|
+
}
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** The gap a column leaves between its rows, as yoga resolves it — the row
|
|
349
|
+
* gutter, else the both-ways one — or undefined where it is not a length. */
|
|
350
|
+
function columnGap(node) {
|
|
351
|
+
const style = node.style;
|
|
352
|
+
const gap = style.rowGap ?? style.gap ?? 0;
|
|
353
|
+
return typeof gap === 'number' ? gap : undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
282
356
|
/**
|
|
283
357
|
* After a pass that settled the widths: which of the height floors, each a
|
|
284
358
|
* height *for a width*, were measured for a width their node no longer has.
|
|
@@ -212,6 +212,9 @@ export class WindowFlush {
|
|
|
212
212
|
}
|
|
213
213
|
// the moves this walk hands over to be copied are this frame's alone
|
|
214
214
|
this._rigidMoves = null;
|
|
215
|
+
// a subtree no pass reached and nothing scrolled in is where the last
|
|
216
|
+
// walk left it (`_followParent`)
|
|
217
|
+
layoutDiff.skipUnreached = !this._walkWhole;
|
|
215
218
|
try {
|
|
216
219
|
// `_absolutizeChildren`, not the loop it wraps: a `<window
|
|
217
220
|
// style={{overflow: 'scroll'}}>` is a scroll container like any box,
|
|
@@ -219,7 +222,9 @@ export class WindowFlush {
|
|
|
219
222
|
this._absolutizeChildren(0, 0);
|
|
220
223
|
} finally {
|
|
221
224
|
layoutDiff.sink = null;
|
|
225
|
+
layoutDiff.skipUnreached = false;
|
|
222
226
|
}
|
|
227
|
+
this._walkWhole = false;
|
|
223
228
|
// …and placed nodes against the arrangement that walk produced: where
|
|
224
229
|
// one goes depends on where its pane and its parent landed
|
|
225
230
|
if (this._placedNodes.size !== 0) this._placeNodes();
|
package/src/nodes/window/size.js
CHANGED
|
@@ -8,9 +8,12 @@ import { Yoga } from '../../yoga.js';
|
|
|
8
8
|
import {
|
|
9
9
|
captureLeafHeights,
|
|
10
10
|
collectFloorStale,
|
|
11
|
+
columnHeightSpan,
|
|
12
|
+
declaresOwnMinimum,
|
|
11
13
|
dirtyOutside,
|
|
12
14
|
floorBoundaryOf,
|
|
13
15
|
probeHeightFloors,
|
|
16
|
+
receivesFloor,
|
|
14
17
|
setMeasuringShrink,
|
|
15
18
|
restoreShrink,
|
|
16
19
|
freezeWidths,
|
|
@@ -18,6 +21,8 @@ import {
|
|
|
18
21
|
contentSpan,
|
|
19
22
|
writeFloors,
|
|
20
23
|
} from './floors.js';
|
|
24
|
+
import { spineScope } from './spine.js';
|
|
25
|
+
import { onExactCopy } from './exactcopy.js';
|
|
21
26
|
import {
|
|
22
27
|
MAX_WINDOW_EXTENT,
|
|
23
28
|
clampExtent,
|
|
@@ -36,18 +41,14 @@ import {
|
|
|
36
41
|
// REACT_X11_NO_SCROLL_BLIT gives.
|
|
37
42
|
const NO_SCOPED_FLOORS = process.env.REACT_X11_NO_SCOPED_FLOORS === '1';
|
|
38
43
|
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
function relayOut(node) {
|
|
48
|
-
const shrink = node.style.flexShrink ?? 1;
|
|
49
|
-
node.yoga.setFlexShrink(shrink + 1);
|
|
50
|
-
node.yoga.setFlexShrink(shrink);
|
|
44
|
+
/** Take a node's extents off, as `collectFloorStale` does a dirty child's:
|
|
45
|
+
* both are measured again, and the minimum yoga holds may no longer be
|
|
46
|
+
* the one the floors wrote. */
|
|
47
|
+
function forgetExtents(node) {
|
|
48
|
+
node._floorW = undefined;
|
|
49
|
+
node._floorH = undefined;
|
|
50
|
+
node._floorMinW = null;
|
|
51
|
+
node._floorMinH = null;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** Content-sized windows, installed onto `WindowNode.prototype` by window.js. */
|
|
@@ -132,12 +133,43 @@ export class WindowSize {
|
|
|
132
133
|
const scope = this._floorsScope();
|
|
133
134
|
this._floorsScopeNow = scope;
|
|
134
135
|
if (scope !== null) {
|
|
136
|
+
const spine = this._floorsSpineNow;
|
|
135
137
|
for (const boundary of scope) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
if (spine?.roots.has(boundary)) {
|
|
139
|
+
// A spine's root is measured with what is inside it, unlike a box
|
|
140
|
+
// that sizes itself, and its column writes the floor it gets.
|
|
141
|
+
forgetExtents(boundary);
|
|
142
|
+
if (receivesFloor(boundary, 'width')) found.width = true;
|
|
143
|
+
if (receivesFloor(boundary, 'height')) found.height = true;
|
|
144
|
+
this._floorsStale.add(boundary.parent);
|
|
145
|
+
}
|
|
146
|
+
// its children's floors are written from here; a sized box's own
|
|
147
|
+
// extent is its style's, and stands
|
|
138
148
|
this._floorsStale.add(boundary);
|
|
139
149
|
collectFloorStale(boundary, this._floorsStale, found, false);
|
|
140
150
|
}
|
|
151
|
+
if (spine !== null) {
|
|
152
|
+
// the columns the roots sit in are summed again (`_sumSpine`), and
|
|
153
|
+
// every one of them writes its children's floors, as the box each
|
|
154
|
+
// spine stops at does
|
|
155
|
+
for (const column of spine.spine) {
|
|
156
|
+
column._floorW = undefined;
|
|
157
|
+
column._floorH = undefined;
|
|
158
|
+
this._floorsStale.add(column);
|
|
159
|
+
if (receivesFloor(column, 'height')) found.height = true;
|
|
160
|
+
}
|
|
161
|
+
for (const stop of spine.stops) this._floorsStale.add(stop);
|
|
162
|
+
// How wide a root is in the width pass is worked out on the way
|
|
163
|
+
// down from the window (spine.js), and past a row only a pass over
|
|
164
|
+
// the whole tree knows. A width owed there is the whole tree's.
|
|
165
|
+
if (found.width) {
|
|
166
|
+
for (const widths of spine.roots.values()) {
|
|
167
|
+
if (widths.widthPass !== null) continue;
|
|
168
|
+
this._floorsUnscoped = true;
|
|
169
|
+
return this._collectFloorStale();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
141
173
|
return found;
|
|
142
174
|
}
|
|
143
175
|
// the root's own children are written from here too, and its direction
|
|
@@ -189,16 +221,37 @@ export class WindowSize {
|
|
|
189
221
|
) {
|
|
190
222
|
return null;
|
|
191
223
|
}
|
|
224
|
+
this._floorsSpineNow = null;
|
|
192
225
|
const boundaries = new Set();
|
|
226
|
+
// the changes no sized box holds, which a column spine may (spine.js)
|
|
227
|
+
const loose = [];
|
|
193
228
|
for (const source of sources) {
|
|
194
229
|
// gone, or moved to another window: its old parent's child list is
|
|
195
230
|
// the change that counts, and that parent said so itself
|
|
196
231
|
if (source.destroyed || source.root !== this) continue;
|
|
197
232
|
const boundary = floorBoundaryOf(source, this);
|
|
198
|
-
if (boundary === null)
|
|
199
|
-
boundaries.add(boundary);
|
|
233
|
+
if (boundary === null) loose.push(source);
|
|
234
|
+
else boundaries.add(boundary);
|
|
235
|
+
}
|
|
236
|
+
let spine = null;
|
|
237
|
+
if (loose.length !== 0) {
|
|
238
|
+
spine = spineScope(
|
|
239
|
+
this,
|
|
240
|
+
loose,
|
|
241
|
+
(node) => !this._floorsOtherSources.has(node),
|
|
242
|
+
);
|
|
243
|
+
if (spine === null) return null;
|
|
244
|
+
// a sized box inside a root is measured with the root
|
|
245
|
+
for (const boundary of [...boundaries]) {
|
|
246
|
+
for (let n = boundary.parent; n && n !== this; n = n.parent) {
|
|
247
|
+
if (spine.roots.has(n)) {
|
|
248
|
+
boundaries.delete(boundary);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
200
253
|
}
|
|
201
|
-
if (boundaries.size === 0) return null;
|
|
254
|
+
if (boundaries.size === 0 && spine === null) return null;
|
|
202
255
|
// the outermost of nested ones: measuring it measures what it holds
|
|
203
256
|
const paths = new Set();
|
|
204
257
|
for (const boundary of [...boundaries]) {
|
|
@@ -214,10 +267,94 @@ export class WindowSize {
|
|
|
214
267
|
paths.add(n);
|
|
215
268
|
}
|
|
216
269
|
}
|
|
270
|
+
if (spine !== null) {
|
|
271
|
+
for (const root of spine.roots.keys()) {
|
|
272
|
+
boundaries.add(root);
|
|
273
|
+
for (let n = root.parent; n && n !== this; n = n.parent) paths.add(n);
|
|
274
|
+
}
|
|
275
|
+
// the columns summed, and the stops with the way down to them: a
|
|
276
|
+
// child that left one is a change to it, and nothing below it
|
|
277
|
+
for (const column of spine.spine) paths.add(column);
|
|
278
|
+
for (const stop of spine.stops) {
|
|
279
|
+
for (let n = stop; n && n !== this; n = n.parent) paths.add(n);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
217
282
|
if (dirtyOutside(this, boundaries, paths)) return null;
|
|
283
|
+
this._floorsSpineNow = spine;
|
|
218
284
|
return boundaries;
|
|
219
285
|
}
|
|
220
286
|
|
|
287
|
+
/**
|
|
288
|
+
* One spine root's floors, measured with it laid out alone — at the width
|
|
289
|
+
* it has in each of the passes over the whole tree (`spine.js`), and with
|
|
290
|
+
* no height on offer in either height pass, because down a column whose
|
|
291
|
+
* height is its content's the pass over the whole tree has none to offer
|
|
292
|
+
* it either. Its own extents are written here: unlike a box that sizes
|
|
293
|
+
* itself, a root's are its content's, and the column it sits in is summed
|
|
294
|
+
* from them (`_sumSpine`).
|
|
295
|
+
*/
|
|
296
|
+
_measureSpineRoot(root, widths, axis, probe = false) {
|
|
297
|
+
onExactCopy(root, () =>
|
|
298
|
+
this._measureSpineRootOnCopy(root, widths, axis, probe),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_measureSpineRootOnCopy(root, widths, axis, probe) {
|
|
303
|
+
const yoga = root.yoga;
|
|
304
|
+
const dir =
|
|
305
|
+
root.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
|
|
306
|
+
if (axis === 'width') {
|
|
307
|
+
const shrunk = [];
|
|
308
|
+
setMeasuringShrink(root, axis, shrunk);
|
|
309
|
+
this._scopedFloorPasses += 1;
|
|
310
|
+
yoga.calculateLayout(widths.widthPass, undefined, dir);
|
|
311
|
+
const span = contentSpan(root, axis, null, this);
|
|
312
|
+
root._floorW = declaresOwnMinimum(root, axis)
|
|
313
|
+
? yoga.getComputedWidth()
|
|
314
|
+
: span;
|
|
315
|
+
restoreShrink(shrunk);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this._scopedFloorPasses += 1;
|
|
319
|
+
yoga.calculateLayout(widths.width, undefined, dir);
|
|
320
|
+
if (probe) {
|
|
321
|
+
const hit = { marked: false, owed: false };
|
|
322
|
+
probeHeightFloors(root, this, hit, root);
|
|
323
|
+
if (hit.marked) {
|
|
324
|
+
this._writeFloors('height');
|
|
325
|
+
this._scopedFloorPasses += 1;
|
|
326
|
+
yoga.calculateLayout(widths.width, undefined, dir);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const intrinsic = new Map();
|
|
330
|
+
captureLeafHeights(root, intrinsic);
|
|
331
|
+
const frozen = [];
|
|
332
|
+
freezeWidths(root, frozen);
|
|
333
|
+
const shrunk = [];
|
|
334
|
+
setMeasuringShrink(root, axis, shrunk);
|
|
335
|
+
this._scopedFloorPasses += 1;
|
|
336
|
+
yoga.calculateLayout(widths.width, undefined, dir);
|
|
337
|
+
const span = contentSpan(root, axis, intrinsic, this);
|
|
338
|
+
root._floorH = declaresOwnMinimum(root, axis)
|
|
339
|
+
? yoga.getComputedHeight()
|
|
340
|
+
: span;
|
|
341
|
+
root._floorAtW = yoga.getComputedWidth();
|
|
342
|
+
this._floorsMeasured += 1;
|
|
343
|
+
restoreWidths(frozen);
|
|
344
|
+
restoreShrink(shrunk);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The columns of this frame's spines, deepest first, summed from their
|
|
348
|
+
* children's extents (`columnHeightSpan`) — their widths are the last
|
|
349
|
+
* pass's, which is what a height is for. */
|
|
350
|
+
_sumSpine(columns) {
|
|
351
|
+
for (const column of columns) {
|
|
352
|
+
column._floorH = columnHeightSpan(column);
|
|
353
|
+
column._floorAtW = column.yoga.getComputedWidth();
|
|
354
|
+
this._floorsMeasured += 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
221
358
|
/**
|
|
222
359
|
* One boundary's floors, measured with it laid out alone — the passes
|
|
223
360
|
* `_measureContentSpans` runs over the whole tree, run over the box: its
|
|
@@ -226,6 +363,15 @@ export class WindowSize {
|
|
|
226
363
|
* leaves whose width moved, stopped at the box, whose own extent stands.
|
|
227
364
|
*/
|
|
228
365
|
_measureBoundary(boundary, axis, probe = false) {
|
|
366
|
+
// on copies of its boxes (exactcopy.js): the tree's own are left as the
|
|
367
|
+
// last pass cached them, which is what keeps the pass after this one
|
|
368
|
+
// from laying the whole tree out again
|
|
369
|
+
onExactCopy(boundary, () =>
|
|
370
|
+
this._measureBoundaryOnCopy(boundary, axis, probe),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_measureBoundaryOnCopy(boundary, axis, probe) {
|
|
229
375
|
const yoga = boundary.yoga;
|
|
230
376
|
const dir =
|
|
231
377
|
boundary.direction === 'rtl' ? Yoga.DIRECTION_RTL : Yoga.DIRECTION_LTR;
|
|
@@ -236,7 +382,6 @@ export class WindowSize {
|
|
|
236
382
|
yoga.calculateLayout(undefined, undefined, dir);
|
|
237
383
|
contentSpan(boundary, axis, null, this);
|
|
238
384
|
restoreShrink(shrunk);
|
|
239
|
-
relayOut(boundary);
|
|
240
385
|
return;
|
|
241
386
|
}
|
|
242
387
|
this._scopedFloorPasses += 1;
|
|
@@ -261,7 +406,6 @@ export class WindowSize {
|
|
|
261
406
|
contentSpan(boundary, axis, intrinsic, this);
|
|
262
407
|
restoreWidths(frozen);
|
|
263
408
|
restoreShrink(shrunk);
|
|
264
|
-
relayOut(boundary);
|
|
265
409
|
}
|
|
266
410
|
|
|
267
411
|
/** The floors on the children of every node found stale, from the extents
|
|
@@ -386,8 +530,10 @@ export class WindowSize {
|
|
|
386
530
|
this._floorsSwept = true;
|
|
387
531
|
this._floorsWidth = width;
|
|
388
532
|
this._floorsSources.clear();
|
|
533
|
+
this._floorsOtherSources.clear();
|
|
389
534
|
this._floorsUnscoped = false;
|
|
390
535
|
this._floorsScopeNow = null;
|
|
536
|
+
this._floorsSpineNow = null;
|
|
391
537
|
}
|
|
392
538
|
|
|
393
539
|
/**
|
|
@@ -398,16 +544,21 @@ export class WindowSize {
|
|
|
398
544
|
* everywhere but the boxes and the way down to them.
|
|
399
545
|
*/
|
|
400
546
|
_applyScopedFloors(boundaries, found, width, height) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
547
|
+
const spine = this._floorsSpineNow;
|
|
548
|
+
const measure = (boundary, axis, probe) => {
|
|
549
|
+
const widths = spine?.roots.get(boundary);
|
|
550
|
+
if (widths) this._measureSpineRoot(boundary, widths, axis, probe);
|
|
551
|
+
else this._measureBoundary(boundary, axis, probe);
|
|
552
|
+
};
|
|
553
|
+
// Measured on copies in the exact config (exactcopy.js), so the
|
|
554
|
+
// renderer's config never leaves the grid, and the pass below answers
|
|
555
|
+
// from its cache everywhere these boxes and the way down to them are not.
|
|
556
|
+
if (found.width) {
|
|
408
557
|
this._writeFloors('width');
|
|
409
|
-
|
|
410
|
-
}
|
|
558
|
+
for (const boundary of boundaries) measure(boundary, 'width');
|
|
559
|
+
}
|
|
560
|
+
this._writeFloors('width');
|
|
561
|
+
this._writeFloors('height');
|
|
411
562
|
let heights = found.height;
|
|
412
563
|
let probed = false;
|
|
413
564
|
if (!heights) {
|
|
@@ -420,13 +571,12 @@ export class WindowSize {
|
|
|
420
571
|
probed = true;
|
|
421
572
|
}
|
|
422
573
|
if (heights) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
});
|
|
574
|
+
this._writeFloors('height');
|
|
575
|
+
for (const boundary of boundaries) {
|
|
576
|
+
measure(boundary, 'height', !probed);
|
|
577
|
+
}
|
|
578
|
+
if (spine !== null) this._sumSpine(spine.spine);
|
|
579
|
+
this._writeFloors('height');
|
|
430
580
|
this._layoutRoot(width, height);
|
|
431
581
|
}
|
|
432
582
|
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Column spines: the scope a change outside every sized box can still be
|
|
2
|
+
// measured in (`WindowNode._floorsScope`), when what stands between it and a
|
|
3
|
+
// box that sizes itself is a run of columns whose height is their content's.
|
|
4
|
+
//
|
|
5
|
+
// A document, a log, a chat transcript, a feed: one scroll pane, one column
|
|
6
|
+
// in it, thousands of blocks in the column. A block that arrives or changes
|
|
7
|
+
// there is confined to nothing `isFloorBoundary` recognises, so it used to
|
|
8
|
+
// be measured with the whole tree — three layout passes over every block,
|
|
9
|
+
// each evicting the one yoga cached for the pass before — to learn the
|
|
10
|
+
// extents of one block and the column it sits in. A spine measures the
|
|
11
|
+
// block alone and sums the column: `columnHeightSpan` over the extents its
|
|
12
|
+
// other children already carry.
|
|
13
|
+
//
|
|
14
|
+
// Three facts make that the same answer the whole tree gives:
|
|
15
|
+
//
|
|
16
|
+
// - Down a column whose height is its content's, the measuring passes have
|
|
17
|
+
// no free space to squash into, so a block in it is laid out at its
|
|
18
|
+
// content height in every pass. Laid out alone with no height on offer,
|
|
19
|
+
// it comes out the same.
|
|
20
|
+
// - Across, a block stretched over its column is as wide as the column's
|
|
21
|
+
// content box, in every pass: now, at the width the heights are measured
|
|
22
|
+
// for, and in the width pass, whose width the way down from the root
|
|
23
|
+
// decides (`widthPassWidth`).
|
|
24
|
+
// - Only heights travel up a spine. A column writes floors down its main
|
|
25
|
+
// axis, and the box a spine stops at names its own size on both axes
|
|
26
|
+
// (`isSpineStop`), so no width extent above the block is ever read. They
|
|
27
|
+
// are left unmeasured, which is what the floors do with every extent
|
|
28
|
+
// nobody reads (`collectFloorStale`).
|
|
29
|
+
//
|
|
30
|
+
// Anything else — a row on the way, a margin across the block, a width
|
|
31
|
+
// that is not a length — is not a spine, and the tree is measured whole.
|
|
32
|
+
|
|
33
|
+
import { declaresOwnMinimum, inFlow, mainAxisOf } from './floors.js';
|
|
34
|
+
import { Yoga } from '../../yoga.js';
|
|
35
|
+
|
|
36
|
+
const LENGTHS_ACROSS = [
|
|
37
|
+
'margin',
|
|
38
|
+
'marginHorizontal',
|
|
39
|
+
'marginLeft',
|
|
40
|
+
'marginRight',
|
|
41
|
+
'marginStart',
|
|
42
|
+
'marginEnd',
|
|
43
|
+
];
|
|
44
|
+
const MARGINS = [
|
|
45
|
+
...LENGTHS_ACROSS,
|
|
46
|
+
'marginVertical',
|
|
47
|
+
'marginTop',
|
|
48
|
+
'marginBottom',
|
|
49
|
+
];
|
|
50
|
+
const PADDINGS = [
|
|
51
|
+
'padding',
|
|
52
|
+
'paddingTop',
|
|
53
|
+
'paddingRight',
|
|
54
|
+
'paddingBottom',
|
|
55
|
+
'paddingLeft',
|
|
56
|
+
'paddingHorizontal',
|
|
57
|
+
'paddingVertical',
|
|
58
|
+
'paddingStart',
|
|
59
|
+
'paddingEnd',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A column a spine can sum (`columnHeightSpan`): laid out by flex down its
|
|
64
|
+
* main axis without wrapping, and with a height that is its content's —
|
|
65
|
+
* nothing named on the height axis (`declaresOwnMinimum`), so neither a
|
|
66
|
+
* size, a floor, a ceiling nor a clip stands between its children and its
|
|
67
|
+
* extent.
|
|
68
|
+
*/
|
|
69
|
+
export function isSummableColumn(node) {
|
|
70
|
+
const style = node.style;
|
|
71
|
+
return (
|
|
72
|
+
node.yoga != null &&
|
|
73
|
+
!node.isWindow &&
|
|
74
|
+
node._host === null &&
|
|
75
|
+
style.display !== 'none' &&
|
|
76
|
+
mainAxisOf(node) === 'height' &&
|
|
77
|
+
style.flexDirection !== 'column-reverse' &&
|
|
78
|
+
(style.flexWrap == null || style.flexWrap === 'nowrap') &&
|
|
79
|
+
!declaresOwnMinimum(node, 'height')
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Where a change stops mattering on its way up: the window, or a column
|
|
85
|
+
* whose extent on both axes is its style's — a scroll pane is the common
|
|
86
|
+
* one — so that what changed inside it moves nothing above it. A column,
|
|
87
|
+
* because it writes its children's floors down its main axis, and a spine
|
|
88
|
+
* only works out heights.
|
|
89
|
+
*/
|
|
90
|
+
export function isSpineStop(node) {
|
|
91
|
+
if (node.isWindow) return true;
|
|
92
|
+
return (
|
|
93
|
+
node.yoga != null &&
|
|
94
|
+
node._host === null &&
|
|
95
|
+
mainAxisOf(node) === 'height' &&
|
|
96
|
+
declaresOwnMinimum(node, 'height') &&
|
|
97
|
+
declaresOwnMinimum(node, 'width')
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The spine scope of this frame's changes, or null when one of them has
|
|
103
|
+
* none. `sources` are the nodes that changed and are inside no sized box;
|
|
104
|
+
* `listOnly(node)` says whether all a node did was gain or lose children.
|
|
105
|
+
*
|
|
106
|
+
* A change is measured from the highest box on its way up that no column
|
|
107
|
+
* above it can sum — a text inside a row inside a list is measured with the
|
|
108
|
+
* row — and everything above that is summed. A column that only gained or
|
|
109
|
+
* lost children is not measured itself: the children that are new or
|
|
110
|
+
* changed are, and it is summed with the rest.
|
|
111
|
+
*
|
|
112
|
+
* Answers `{ roots, spine, stops }`: the boxes measured alone, with the
|
|
113
|
+
* widths to measure each at (`rootWidths`); the columns summed, deepest
|
|
114
|
+
* first; and the boxes each spine ends at, whose children's floors are
|
|
115
|
+
* written.
|
|
116
|
+
*/
|
|
117
|
+
export function spineScope(root, sources, listOnly) {
|
|
118
|
+
const roots = new Map();
|
|
119
|
+
const spine = new Set();
|
|
120
|
+
const stops = new Set();
|
|
121
|
+
for (const source of sources) {
|
|
122
|
+
const list = listOnly(source);
|
|
123
|
+
// the way up to where the change stops mattering
|
|
124
|
+
const path = [];
|
|
125
|
+
let node = source;
|
|
126
|
+
while (!isSpineStop(node)) {
|
|
127
|
+
path.push(node);
|
|
128
|
+
node = node.parent;
|
|
129
|
+
if (!node) return null;
|
|
130
|
+
}
|
|
131
|
+
if (node !== root && node.root !== root) return null;
|
|
132
|
+
// a stop changed in itself: its padding, its direction — everything it
|
|
133
|
+
// holds is laid out anew, which is the whole tree's measurement
|
|
134
|
+
if (path.length === 0 && !list) return null;
|
|
135
|
+
stops.add(node);
|
|
136
|
+
// the highest box on the way that a column above it cannot sum
|
|
137
|
+
let top = -1;
|
|
138
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
139
|
+
const summed = i > 0 || list;
|
|
140
|
+
if (!summed || !isSummableColumn(path[i])) {
|
|
141
|
+
top = i;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const column of path.slice(top + 1)) spine.add(column);
|
|
146
|
+
const changed = top === -1 ? staleChildren(source) : [path[top]];
|
|
147
|
+
for (const child of changed) {
|
|
148
|
+
if (!child.yoga || child.isWindow || child.style.display === 'none') {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const widths = rootWidths(child, root);
|
|
152
|
+
if (widths === null) return null;
|
|
153
|
+
roots.set(child, widths);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// A root inside another root is measured with it, and so is a column it
|
|
157
|
+
// would have been summed through.
|
|
158
|
+
const within = (node) => {
|
|
159
|
+
for (let n = node.parent; n; n = n.parent) if (roots.has(n)) return true;
|
|
160
|
+
return false;
|
|
161
|
+
};
|
|
162
|
+
for (const node of [...roots.keys()]) if (within(node)) roots.delete(node);
|
|
163
|
+
for (const node of [...spine]) {
|
|
164
|
+
if (roots.has(node) || within(node)) spine.delete(node);
|
|
165
|
+
}
|
|
166
|
+
// Every other child of a summed column answers from the extent it carries.
|
|
167
|
+
for (const column of spine) {
|
|
168
|
+
for (const child of column.children) {
|
|
169
|
+
if (!inFlow(child) || roots.has(child) || spine.has(child)) continue;
|
|
170
|
+
if (child._floorH === undefined) return null;
|
|
171
|
+
if (MARGINS.some((key) => typeof child.style[key] === 'string')) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const depth = (node) => {
|
|
177
|
+
let d = 0;
|
|
178
|
+
for (let n = node; n; n = n.parent) d += 1;
|
|
179
|
+
return d;
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
roots,
|
|
183
|
+
spine: [...spine].sort((a, b) => depth(b) - depth(a)),
|
|
184
|
+
stops,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The children of a node that gained or lost some: the ones yoga has a
|
|
189
|
+
* change on record for, and the ones never measured. */
|
|
190
|
+
function staleChildren(node) {
|
|
191
|
+
const out = [];
|
|
192
|
+
for (const child of node.children) {
|
|
193
|
+
if (!child.yoga || child.isWindow) continue;
|
|
194
|
+
if (child.yoga.isDirty() || child._floorH === undefined) out.push(child);
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The widths a box is measured alone at, or null when it cannot be: in flow
|
|
201
|
+
* and stretched across its column with no margin, width or ceiling of its
|
|
202
|
+
* own across it, so that it is exactly as wide as the column's content box.
|
|
203
|
+
* `width` is that box now — the width its heights are for — and
|
|
204
|
+
* `widthPass` the one the width pass over the whole tree would give it
|
|
205
|
+
* (`widthPassWidth`), null where only a pass knows.
|
|
206
|
+
*/
|
|
207
|
+
function rootWidths(node, root) {
|
|
208
|
+
const style = node.style;
|
|
209
|
+
const parent = node.parent;
|
|
210
|
+
if (!parent || !inFlow(node) || node._host !== null) return null;
|
|
211
|
+
if (style.width !== undefined || style.maxWidth !== undefined) return null;
|
|
212
|
+
if (typeof style.minWidth === 'string') return null;
|
|
213
|
+
if (!stretched(node)) return null;
|
|
214
|
+
if (
|
|
215
|
+
LENGTHS_ACROSS.some((key) => style[key] !== undefined && style[key] !== 0)
|
|
216
|
+
) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
|
|
220
|
+
if (PADDINGS.some((key) => typeof style[key] === 'string')) return null;
|
|
221
|
+
return {
|
|
222
|
+
width: contentWidth(parent),
|
|
223
|
+
widthPass: widthPassContent(parent, root),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Stretched across its parent's column — `alignSelf`, or the column's
|
|
228
|
+
* `alignItems` when the box leaves it to them. */
|
|
229
|
+
function stretched(node) {
|
|
230
|
+
const parent = node.parent;
|
|
231
|
+
if (!parent || mainAxisOf(parent) !== 'height') return false;
|
|
232
|
+
const self = node.style.alignSelf ?? 'auto';
|
|
233
|
+
if (self === 'stretch') return true;
|
|
234
|
+
return (
|
|
235
|
+
self === 'auto' && (parent.style.alignItems ?? 'stretch') === 'stretch'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Padding and border across, as the last pass resolved them. */
|
|
240
|
+
function insetAcross(node) {
|
|
241
|
+
const yoga = node.yoga;
|
|
242
|
+
return (
|
|
243
|
+
yoga.getComputedPadding(Yoga.EDGE_LEFT) +
|
|
244
|
+
yoga.getComputedPadding(Yoga.EDGE_RIGHT) +
|
|
245
|
+
yoga.getComputedBorder(Yoga.EDGE_LEFT) +
|
|
246
|
+
yoga.getComputedBorder(Yoga.EDGE_RIGHT)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** A box's content width in the last pass. */
|
|
251
|
+
function contentWidth(node) {
|
|
252
|
+
return Math.max(0, node.yoga.getComputedWidth() - insetAcross(node));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* How wide `node` is in the width pass over the whole tree
|
|
257
|
+
* (`_measureContentSpans('width')`), worked out on the way down rather than
|
|
258
|
+
* laid out: the root at nothing, and each box below it either the width its
|
|
259
|
+
* style names or stretched over its column's content box — clamped as yoga
|
|
260
|
+
* clamps it, and never narrower than its own padding and border. Null where
|
|
261
|
+
* the way down is anything else: a row, whose children are as wide as the
|
|
262
|
+
* pass decides.
|
|
263
|
+
*/
|
|
264
|
+
function widthPassWidth(node, root) {
|
|
265
|
+
if (node === root) return insetAcross(node);
|
|
266
|
+
const style = node.style;
|
|
267
|
+
let width;
|
|
268
|
+
if (typeof style.width === 'number') width = style.width;
|
|
269
|
+
else if (style.width !== undefined) return null;
|
|
270
|
+
else {
|
|
271
|
+
if (!inFlow(node) || !stretched(node)) return null;
|
|
272
|
+
if (MARGINS.some((key) => typeof style[key] === 'string')) return null;
|
|
273
|
+
const inner = widthPassContent(node.parent, root);
|
|
274
|
+
if (inner === null) return null;
|
|
275
|
+
const yoga = node.yoga;
|
|
276
|
+
width =
|
|
277
|
+
inner -
|
|
278
|
+
yoga.getComputedMargin(Yoga.EDGE_LEFT) -
|
|
279
|
+
yoga.getComputedMargin(Yoga.EDGE_RIGHT);
|
|
280
|
+
}
|
|
281
|
+
if (typeof style.maxWidth === 'number')
|
|
282
|
+
width = Math.min(width, style.maxWidth);
|
|
283
|
+
else if (style.maxWidth !== undefined) return null;
|
|
284
|
+
if (typeof style.minWidth === 'number')
|
|
285
|
+
width = Math.max(width, style.minWidth);
|
|
286
|
+
else if (style.minWidth !== undefined) return null;
|
|
287
|
+
return Math.max(width, insetAcross(node));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** …and the content box inside it. */
|
|
291
|
+
function widthPassContent(node, root) {
|
|
292
|
+
const width = widthPassWidth(node, root);
|
|
293
|
+
return width === null ? null : Math.max(0, width - insetAcross(node));
|
|
294
|
+
}
|
|
@@ -277,8 +277,15 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
277
277
|
// the nodes whose changes the next measurement owes, and whether one
|
|
278
278
|
// came with no node to name (`_floorsScope`)
|
|
279
279
|
this._floorsSources = new Set();
|
|
280
|
+
// …the ones that changed for any reason but their child list, which
|
|
281
|
+
// are measured themselves rather than through the children they gained
|
|
282
|
+
// (spine.js), and the spines this frame's measurement runs on
|
|
283
|
+
this._floorsOtherSources = new Set();
|
|
280
284
|
this._floorsUnscoped = false;
|
|
281
285
|
this._floorsScopeNow = null;
|
|
286
|
+
this._floorsSpineNow = null;
|
|
287
|
+
// whether the next layout walk must visit every box (`invalidate`)
|
|
288
|
+
this._walkWhole = true;
|
|
282
289
|
this._scopedFloorPasses = 0;
|
|
283
290
|
// whether the first floors pass has run (`collectFloorStale`'s sweep)
|
|
284
291
|
this._floorsSwept = false;
|
package/src/styles.js
CHANGED
|
@@ -1950,6 +1950,34 @@ let exactDepth = 0;
|
|
|
1950
1950
|
* here is not the size the same layout comes to on it. */
|
|
1951
1951
|
export const isMeasuringExactly = () => exactDepth > 0;
|
|
1952
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* A second config, never rounded, for measuring a box on copies of its
|
|
1955
|
+
* boxes (nodes/window/exactcopy.js). Switching the renderer's config off the
|
|
1956
|
+
* grid and back is a change yoga answers by treating every layout it has
|
|
1957
|
+
* cached as stale, so the pass after any `measuringExactly` lays the whole
|
|
1958
|
+
* tree out again; a measurement confined to one box is made in this config
|
|
1959
|
+
* instead, and leaves the renderer's, and its caches, alone.
|
|
1960
|
+
*/
|
|
1961
|
+
let exactConfig = null;
|
|
1962
|
+
export const exactLayoutConfig = () => {
|
|
1963
|
+
if (exactConfig === null) {
|
|
1964
|
+
exactConfig = Yoga.Config.create();
|
|
1965
|
+
exactConfig.setPointScaleFactor(0);
|
|
1966
|
+
}
|
|
1967
|
+
return exactConfig;
|
|
1968
|
+
};
|
|
1969
|
+
|
|
1970
|
+
/** `measure` for layouts made in the exact config: off the grid as far as
|
|
1971
|
+
* `isMeasuringExactly` is concerned, with nothing to switch. */
|
|
1972
|
+
export function measuringInExactConfig(measure) {
|
|
1973
|
+
exactDepth++;
|
|
1974
|
+
try {
|
|
1975
|
+
return measure();
|
|
1976
|
+
} finally {
|
|
1977
|
+
exactDepth--;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1953
1981
|
/**
|
|
1954
1982
|
* The yoga defaults that are not CSS's, written once per node.
|
|
1955
1983
|
*
|