react-x11 2.22.5 → 2.22.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-x11",
3
- "version": "2.22.5",
3
+ "version": "2.22.6",
4
4
  "description": "react renderer with X11 as a target",
5
5
  "main": "./src/index.js",
6
6
  "files": [
@@ -83,6 +83,9 @@ export function offsetInParent(node) {
83
83
  };
84
84
  }
85
85
 
86
+ /** What a leaf a width pass reads nothing from answers with. */
87
+ const UNREAD_SIZE = Object.freeze({ width: 0, height: 0 });
88
+
86
89
  /** Layout, installed onto `Node.prototype` by node.js. */
87
90
  export class NodeLayout {
88
91
  /**
@@ -141,6 +144,10 @@ export class NodeLayout {
141
144
  if (heightMode === Yoga.MEASURE_MODE_UNDEFINED) {
142
145
  this._floorMeasureMode = MEASURE_MODES[widthMode];
143
146
  }
147
+ // A leaf a pass over the whole tree reads nothing from
148
+ // (`markUnreadLeaves`): its width there is one yoga replaces and its
149
+ // height one the pass does not read, so it is not shaped to find them.
150
+ if (this._widthUnread) return UNREAD_SIZE;
144
151
  const size = this.measureContent({
145
152
  width: measureOffer(width, widthMode),
146
153
  height: measureOffer(height, heightMode),
package/src/nodes/node.js CHANGED
@@ -111,6 +111,9 @@ export class Node {
111
111
  // the width mode yoga last measured this leaf in with no height on
112
112
  // offer — the question `_heightForWidth` repeats
113
113
  this._floorMeasureMode = null;
114
+ // set only for the length of a width pass that reads nothing from this
115
+ // leaf (`markUnreadLeaves`)
116
+ this._widthUnread = false;
114
117
  this.root = null; // owning WindowNode once attached
115
118
  this.hidden = false;
116
119
  // Composited on a layer of its own above the window's bitmap, by a
@@ -445,6 +445,142 @@ export function setMeasuringShrink(node, axis, out) {
445
445
  }
446
446
  }
447
447
 
448
+ /**
449
+ * Mark the leaves whose min-content width the pass over the whole tree has
450
+ * no use for (`_widthUnread`), listing them in `out`: their measure
451
+ * function answers without shaping them (`NodeLayout._useMeasureContent`)
452
+ * until `_measureContentSpans` takes the marks off.
453
+ *
454
+ * The pass lays every box out with no room on offer, and for a document that
455
+ * is every paragraph set a word to a line — 1 s of CoreText for 600 KB of
456
+ * Markdown, and more than half of the first frame — to read extents that
457
+ * almost nobody reads. An extent is read where a floor is written from it,
458
+ * which is a flex item on a `row`'s main axis (`receivesFloor`); by the
459
+ * extent of a node that is read, which adds up the ones of its children in
460
+ * flow (`contentSpan`); and at the root, when the window's own size hint is
461
+ * what is being measured. Down a column from a root nobody reads, none of
462
+ * that happens: a paragraph in a document is laid out, and nothing reads
463
+ * what it says.
464
+ *
465
+ * It is still laid out, though, and a leaf's answer could reach something
466
+ * that is read through its size. So it is marked only where it cannot:
467
+ *
468
+ * - every box from the root down to it is as wide as something other than
469
+ * its content — the root offered no room, a width it names, a column
470
+ * stretching it — so the width the leaf answers is one yoga replaces, and
471
+ * nothing above it is sized from it;
472
+ * - no box is handed a share of space the leaf's height changed, which is
473
+ * how a height could become somebody's width — an aspect ratio, a column
474
+ * that wraps, an image sized to the height it is given. Every column from
475
+ * the root down to it is either as tall as its content, and then holds no
476
+ * box out of flow whose height it sets, or has a height of its own, and
477
+ * then holds nothing that grows or gives way in the pass;
478
+ * - none of those columns wraps. (One may keep an aspect ratio: a box as
479
+ * wide as something other than its content is as tall as that makes it.)
480
+ *
481
+ * What is left of its answer is a height, which a pass measuring widths
482
+ * never reads, and the layouts after the pass run in another config version
483
+ * (`measuringExactly`), which yoga answers by measuring every leaf again.
484
+ */
485
+ export function markUnreadLeaves(node, out) {
486
+ const yoga = node.yoga;
487
+ const direction = yoga.getFlexDirection();
488
+ if (
489
+ (direction !== Yoga.FLEX_DIRECTION_COLUMN &&
490
+ direction !== Yoga.FLEX_DIRECTION_COLUMN_REVERSE) ||
491
+ yoga.getFlexWrap() !== Yoga.WRAP_NO_WRAP
492
+ ) {
493
+ // a row's items are read, and a column that wraps is as wide as its lines
494
+ return;
495
+ }
496
+ const height = heightOf(yoga);
497
+ if (height === 'unknown') return;
498
+ for (const child of node.children) {
499
+ if (!child.yoga || child.isWindow || child.style.display === 'none') {
500
+ continue;
501
+ }
502
+ if (child.style.position === 'absolute') {
503
+ // a height that follows this column's follows what is in it
504
+ if (height === 'content' && heightFollowsParent(child.yoga)) return;
505
+ } else if (
506
+ height === 'own' &&
507
+ // what the pass lets grow or give way (`setMeasuringShrink`)
508
+ (child.yoga.getFlexGrow() > 0 || namesOwnFloor(child, 'width'))
509
+ ) {
510
+ return;
511
+ }
512
+ }
513
+ const items = yoga.getAlignItems();
514
+ for (const child of node.children) {
515
+ if (!inFlow(child) || child._host !== null) continue;
516
+ if (!widthNotItsOwn(child.yoga, items)) continue;
517
+ if (child._measureFn) {
518
+ child._widthUnread = true;
519
+ out.push(child);
520
+ } else if (child.children.length !== 0) {
521
+ markUnreadLeaves(child, out);
522
+ }
523
+ }
524
+ }
525
+
526
+ /** The edges a horizontal margin can be set through. */
527
+ const ACROSS_EDGES = [
528
+ Yoga.EDGE_LEFT,
529
+ Yoga.EDGE_RIGHT,
530
+ Yoga.EDGE_START,
531
+ Yoga.EDGE_END,
532
+ Yoga.EDGE_HORIZONTAL,
533
+ Yoga.EDGE_ALL,
534
+ ];
535
+
536
+ /** Whether an in-flow child of a column is as wide as something other than
537
+ * its content: a width it names, or the column's, which it stretches to
538
+ * unless an alignment or an `auto` margin says otherwise. */
539
+ function widthNotItsOwn(yoga, items) {
540
+ const width = yoga.getWidth().unit;
541
+ if (width === Yoga.UNIT_POINT || width === Yoga.UNIT_PERCENT) return true;
542
+ const self = yoga.getAlignSelf();
543
+ if ((self === Yoga.ALIGN_AUTO ? items : self) !== Yoga.ALIGN_STRETCH) {
544
+ return false;
545
+ }
546
+ for (const edge of ACROSS_EDGES) {
547
+ if (yoga.getMargin(edge).unit === Yoga.UNIT_AUTO) return false;
548
+ }
549
+ return true;
550
+ }
551
+
552
+ /**
553
+ * Where a column's height comes from, down a pass with none on offer: its
554
+ * `'content'`, when it names nothing that bounds it; its `'own'`, when all it
555
+ * names are numbers — a height, a basis (a height, down a column), a floor or
556
+ * a ceiling; and `'unknown'` for a share of something (a percentage), which
557
+ * a pass with no height on offer resolves as it likes.
558
+ */
559
+ function heightOf(yoga) {
560
+ let own = false;
561
+ for (const value of [
562
+ yoga.getHeight(),
563
+ yoga.getFlexBasis(),
564
+ yoga.getMinHeight(),
565
+ yoga.getMaxHeight(),
566
+ ]) {
567
+ if (value.unit === Yoga.UNIT_PERCENT) return 'unknown';
568
+ if (value.unit === Yoga.UNIT_POINT) own = true;
569
+ }
570
+ return own ? 'own' : 'content';
571
+ }
572
+
573
+ /** Whether a box out of flow takes its height from its parent's: pinned to
574
+ * both edges, or a share of it. */
575
+ function heightFollowsParent(yoga) {
576
+ const pinned = (edge) => yoga.getPosition(edge).unit !== Yoga.UNIT_UNDEFINED;
577
+ const both = pinned(Yoga.EDGE_VERTICAL) || pinned(Yoga.EDGE_ALL);
578
+ if (both || (pinned(Yoga.EDGE_TOP) && pinned(Yoga.EDGE_BOTTOM))) return true;
579
+ return [yoga.getHeight(), yoga.getMinHeight(), yoga.getMaxHeight()].some(
580
+ (value) => value.unit === Yoga.UNIT_PERCENT,
581
+ );
582
+ }
583
+
448
584
  /** …and back to the layout everything else is run from. */
449
585
  export function restoreShrink(shrunk) {
450
586
  for (const child of shrunk) {
@@ -540,8 +676,15 @@ export function restoreWidths(frozen) {
540
676
  * one's: a scroll pane contributes nothing to the floor above it and still
541
677
  * needs floors written *inside* it, or the column of rows it holds would
542
678
  * shrink to the viewport and there would be nothing left to scroll.
679
+ *
680
+ * `read` false is a node whose own extent nobody reads (`markUnreadLeaves`
681
+ * says which those are), so neither are those of its children in flow: the
682
+ * walk goes on through them to the rows inside, whose items' floors are
683
+ * written, and measures nothing on the way. An extent left unmeasured is
684
+ * asked for when it starts to count — its parent turned into a row, which
685
+ * leaves the parent dirty (`collectFloorStale`).
543
686
  */
544
- export function contentSpan(node, axis, intrinsic, root) {
687
+ export function contentSpan(node, axis, intrinsic, root, read = true) {
545
688
  const yoga = node.yoga;
546
689
  const horizontal = axis === 'width';
547
690
  const own = horizontal ? yoga.getComputedWidth() : yoga.getComputedHeight();
@@ -564,6 +707,11 @@ export function contentSpan(node, axis, intrinsic, root) {
564
707
  // by itself, and a <text> span has no box of its own
565
708
  if (!child.yoga || child.isWindow) continue;
566
709
  if (child.style.display === 'none') continue;
710
+ if (!read && !receivesFloor(child, axis)) {
711
+ if (child._host === null)
712
+ contentSpan(child, axis, intrinsic, root, false);
713
+ continue;
714
+ }
567
715
  const laidOut = horizontal
568
716
  ? child.yoga.getComputedWidth()
569
717
  : child.yoga.getComputedHeight();
@@ -600,6 +748,8 @@ export function contentSpan(node, axis, intrinsic, root) {
600
748
  start = Math.min(start, at);
601
749
  end = Math.max(end, at + extent);
602
750
  }
751
+ // a span nobody reads, and — for a leaf — a measurement nobody needs
752
+ if (!read) return 0;
603
753
  if (start === Infinity) {
604
754
  // A leaf: nothing inside to look at, and what the pass did to it may
605
755
  // have been a stretch rather than a measurement. So it is asked again.
@@ -9,6 +9,7 @@ import {
9
9
  captureLeafHeights,
10
10
  collectFloorStale,
11
11
  columnHeightSpan,
12
+ markUnreadLeaves,
12
13
  declaresOwnMinimum,
13
14
  dirtyOutside,
14
15
  floorBoundaryOf,
@@ -41,6 +42,10 @@ import {
41
42
  // REACT_X11_NO_SCROLL_BLIT gives.
42
43
  const NO_SCOPED_FLOORS = process.env.REACT_X11_NO_SCOPED_FLOORS === '1';
43
44
 
45
+ // …and for the width pass reading only the extents a floor is written from
46
+ // (`markUnreadLeaves`): with it set, every leaf is shaped at no width again.
47
+ const NO_UNREAD_WIDTHS = process.env.REACT_X11_NO_UNREAD_WIDTHS === '1';
48
+
44
49
  /** Take a node's extents off, as `collectFloorStale` does a dirty child's:
45
50
  * both are measured again, and the minimum yoga holds may no longer be
46
51
  * the one the floors wrote. */
@@ -64,8 +69,13 @@ export class WindowSize {
64
69
  * they are when nothing has looked yet this frame, and a leaf the probe
65
70
  * finds moved has its floor taken off and the pass run again, since a
66
71
  * floor still on a node being measured would be read back as content.
72
+ *
73
+ * `wantRoot` is whether the root's own width is wanted — a window whose
74
+ * minimum is its content's (`_measureMinimum`). Otherwise the width pass
75
+ * reads only the extents a floor is written from, and the leaves nothing
76
+ * reads are not shaped (`markUnreadLeaves`).
67
77
  */
68
- _measureContentSpans(axis, forWidth, probe = false) {
78
+ _measureContentSpans(axis, forWidth, probe = false, wantRoot = false) {
69
79
  this._sweepLayoutHosts();
70
80
  const yoga = this.yoga;
71
81
  const dir = this._rootDirection;
@@ -79,9 +89,24 @@ export class WindowSize {
79
89
  if (this._layoutHosts.size !== 0) this._measureHostChildWidths();
80
90
  const shrunk = [];
81
91
  setMeasuringShrink(this, axis, shrunk);
92
+ // A layout host reads its own extent and its children's, and a root
93
+ // with a ceiling is as wide as its content up to it: both read what
94
+ // nothing else here does, so both are measured whole.
95
+ const read =
96
+ wantRoot ||
97
+ NO_UNREAD_WIDTHS ||
98
+ this._readEveryWidth ||
99
+ this._layoutHosts.size !== 0 ||
100
+ yoga.getMaxWidth().unit !== Yoga.UNIT_UNDEFINED;
101
+ const unread = [];
102
+ if (!read) markUnreadLeaves(this, unread);
82
103
  this._layoutPasses += 1;
83
- yoga.calculateLayout(0, undefined, dir);
84
- const span = contentSpan(this, axis, null, this);
104
+ try {
105
+ yoga.calculateLayout(0, undefined, dir);
106
+ } finally {
107
+ for (const leaf of unread) leaf._widthUnread = false;
108
+ }
109
+ const span = contentSpan(this, axis, null, this, read);
85
110
  restoreShrink(shrunk);
86
111
  return span;
87
112
  }
@@ -429,9 +454,9 @@ export class WindowSize {
429
454
  * (a stale extent writes the style's own minimum), which is what keeps a
430
455
  * floor from ratcheting: read back as content, it could only ever grow.
431
456
  */
432
- _measureWidthFloors() {
457
+ _measureWidthFloors(wantRoot = false) {
433
458
  this._writeFloors('width');
434
- const span = this._measureContentSpans('width');
459
+ const span = this._measureContentSpans('width', undefined, false, wantRoot);
435
460
  this._writeFloors('width');
436
461
  return span;
437
462
  }
@@ -452,7 +477,7 @@ export class WindowSize {
452
477
  return measuringExactly(() =>
453
478
  Math.ceil(
454
479
  axis === 'width'
455
- ? this._measureWidthFloors()
480
+ ? this._measureWidthFloors(true)
456
481
  : this._measureHeightFloors(forWidth, true),
457
482
  ),
458
483
  );
@@ -282,6 +282,9 @@ export class WindowNode extends Scrollable(Node) {
282
282
  // (spine.js), and the spines this frame's measurement runs on
283
283
  this._floorsOtherSources = new Set();
284
284
  this._floorsUnscoped = false;
285
+ // every width extent measured, read or not (`REACT_X11_NO_UNREAD_WIDTHS`,
286
+ // per window): what a test holds the width pass that skips them to
287
+ this._readEveryWidth = false;
285
288
  this._floorsScopeNow = null;
286
289
  this._floorsSpineNow = null;
287
290
  // whether the next layout walk must visit every box (`invalidate`)