react-x11 2.22.5 → 2.22.7
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 +2 -2
- package/src/cocoa/fonts.js +127 -9
- package/src/cocoa/window.js +113 -26
- package/src/nodes/layout.js +7 -0
- package/src/nodes/node.js +3 -0
- package/src/nodes/window/floors.js +151 -1
- package/src/nodes/window/size.js +31 -6
- package/src/nodes/window/window.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.22.
|
|
3
|
+
"version": "2.22.7",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"yoga-layout": "^3.2.1"
|
|
109
109
|
},
|
|
110
110
|
"optionalDependencies": {
|
|
111
|
-
"@windowkit/appkit": "^0.
|
|
111
|
+
"@windowkit/appkit": "^0.14.0",
|
|
112
112
|
"@windowkit/wayland": "^3.1.1",
|
|
113
113
|
"@windowkit/win32": "^0.0.5",
|
|
114
114
|
"dbus-native": "^0.15.1",
|
package/src/cocoa/fonts.js
CHANGED
|
@@ -102,11 +102,53 @@ function parseColor(color) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/** UTF-16 offset of each code point boundary, plus the end. */
|
|
105
|
+
/** Where each code point of `text` starts, in code units — or null when
|
|
106
|
+
* the two are the same index, which is all text with no surrogate pair in
|
|
107
|
+
* it. A table per paragraph was a walk of every character of a document,
|
|
108
|
+
* every time it was laid out. */
|
|
105
109
|
function codeUnitOffsets(text) {
|
|
110
|
+
if (!SURROGATE.test(text)) return null;
|
|
106
111
|
const offsets = [0];
|
|
107
112
|
for (const ch of text) offsets.push(offsets[offsets.length - 1] + ch.length);
|
|
108
113
|
return offsets;
|
|
109
114
|
}
|
|
115
|
+
const SURROGATE = /[\uD800-\uDFFF]/;
|
|
116
|
+
const NO_GEOMETRY = new Float64Array(0);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A layout's lines from the bridge's packed geometry (`createLayout`'s
|
|
120
|
+
* `packed`): ten numbers a line, five a run, into the objects the
|
|
121
|
+
* unpacked form has, key for key. A bridge that answered neither form
|
|
122
|
+
* laid out no lines.
|
|
123
|
+
*/
|
|
124
|
+
function unpackLines(lineData = NO_GEOMETRY, runData = NO_GEOMETRY) {
|
|
125
|
+
const lines = new Array(lineData.length / 10);
|
|
126
|
+
for (let i = 0, l = 0, r = 0; i < lineData.length; i += 10, l++) {
|
|
127
|
+
const runs = new Array(lineData[i + 9]);
|
|
128
|
+
for (let k = 0; k < runs.length; k++, r += 5) {
|
|
129
|
+
runs[k] = {
|
|
130
|
+
x: runData[r],
|
|
131
|
+
width: runData[r + 1],
|
|
132
|
+
start: runData[r + 2],
|
|
133
|
+
end: runData[r + 3],
|
|
134
|
+
rtl: runData[r + 4] === 1,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
lines[l] = {
|
|
138
|
+
x: lineData[i],
|
|
139
|
+
y: lineData[i + 1],
|
|
140
|
+
width: lineData[i + 2],
|
|
141
|
+
height: lineData[i + 3],
|
|
142
|
+
baseline: lineData[i + 4],
|
|
143
|
+
ascent: lineData[i + 5],
|
|
144
|
+
descent: lineData[i + 6],
|
|
145
|
+
start: lineData[i + 7],
|
|
146
|
+
end: lineData[i + 8],
|
|
147
|
+
runs,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
110
152
|
|
|
111
153
|
class CocoaTextLayout {
|
|
112
154
|
constructor(native, raw, text) {
|
|
@@ -116,16 +158,20 @@ class CocoaTextLayout {
|
|
|
116
158
|
this._cpToCu = codeUnitOffsets(text);
|
|
117
159
|
this.width = raw.width;
|
|
118
160
|
this.height = raw.height;
|
|
119
|
-
this.lines = raw.lines;
|
|
161
|
+
this.lines = raw.lines ?? unpackLines(raw.lineData, raw.runData);
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
_cuOf(cp) {
|
|
123
165
|
const t = this._cpToCu;
|
|
166
|
+
if (t === null) return Math.max(0, Math.min(cp, this._text.length));
|
|
124
167
|
return t[Math.max(0, Math.min(cp, t.length - 1))];
|
|
125
168
|
}
|
|
126
169
|
|
|
127
170
|
_cpOf(cu) {
|
|
128
171
|
const t = this._cpToCu;
|
|
172
|
+
if (t === null) {
|
|
173
|
+
return cu <= 0 ? 0 : Math.min(Math.ceil(cu), this._text.length);
|
|
174
|
+
}
|
|
129
175
|
// t is sorted; layouts are short, a linear walk is fine
|
|
130
176
|
for (let i = 0; i < t.length; i++) if (t[i] >= cu) return i;
|
|
131
177
|
return t.length - 1;
|
|
@@ -500,6 +546,10 @@ function spacedFeatures(features, spacing) {
|
|
|
500
546
|
: OPTIONAL_LIGATURES_OFF;
|
|
501
547
|
}
|
|
502
548
|
|
|
549
|
+
/** The text the kept typesetters may hold, in UTF-16 units: a document of
|
|
550
|
+
* about a megabyte, at 25 bytes a unit in CoreText. */
|
|
551
|
+
const KEPT_TYPESETTER_UNITS = 1 << 20;
|
|
552
|
+
|
|
503
553
|
export class CocoaFontManager {
|
|
504
554
|
/** @param native the @windowkit/appkit module; the tests hand in a fake */
|
|
505
555
|
constructor(native = loadNative()) {
|
|
@@ -513,6 +563,50 @@ export class CocoaFontManager {
|
|
|
513
563
|
this._sized = new Map(); // face key|size|variations -> CTFont handle
|
|
514
564
|
this._layouts = new Map(); // layout signature -> CocoaTextLayout (LRU)
|
|
515
565
|
this._faceByPs = new Map(); // PostScript name -> face CoreText chose itself
|
|
566
|
+
// paragraph -> its typesetter, kept to lay it out at another width
|
|
567
|
+
// (`_keptTypesetter`); LRU, bounded by the UTF-16 units they hold. Only
|
|
568
|
+
// a bridge that keeps them (`@windowkit/appkit` 0.14) has the verb.
|
|
569
|
+
this._keeps = typeof native.releaseTypesetter === 'function';
|
|
570
|
+
this._typesetters = new Map();
|
|
571
|
+
this._typesetterUnits = 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* A paragraph's typesetter, kept from a layout of the same text at any
|
|
576
|
+
* width. Two thirds of a layout is the text becoming glyphs — the
|
|
577
|
+
* attributed string, then CoreText's shaping — and a paragraph laid out
|
|
578
|
+
* at another width breaks the same glyphs into other lines: a window
|
|
579
|
+
* resize lays a document out at every width it passes through, and a
|
|
580
|
+
* paragraph's max-content and wrapped measurements are two layouts of
|
|
581
|
+
* one text. The cache holds `KEPT_TYPESETTER_UNITS` of text, about
|
|
582
|
+
* 25 MB, which is a long document; past that the paragraph laid out
|
|
583
|
+
* least recently is let go.
|
|
584
|
+
*/
|
|
585
|
+
_keptTypesetter(key) {
|
|
586
|
+
const kept = this._typesetters.get(key);
|
|
587
|
+
if (kept === undefined) return null;
|
|
588
|
+
this._typesetters.delete(key);
|
|
589
|
+
this._typesetters.set(key, kept);
|
|
590
|
+
return kept.handle;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
_keepTypesetter(key, handle, units) {
|
|
594
|
+
this._typesetters.set(key, { handle, units });
|
|
595
|
+
this._typesetterUnits += units;
|
|
596
|
+
while (this._typesetterUnits > KEPT_TYPESETTER_UNITS) {
|
|
597
|
+
const [oldest, entry] = this._typesetters.entries().next().value;
|
|
598
|
+
this._typesetters.delete(oldest);
|
|
599
|
+
this._typesetterUnits -= entry.units;
|
|
600
|
+
this._native.releaseTypesetter(entry.handle);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
_forgetTypesetters() {
|
|
605
|
+
for (const { handle } of this._typesetters.values()) {
|
|
606
|
+
this._native.releaseTypesetter(handle);
|
|
607
|
+
}
|
|
608
|
+
this._typesetters.clear();
|
|
609
|
+
this._typesetterUnits = 0;
|
|
516
610
|
}
|
|
517
611
|
|
|
518
612
|
/** A face the app loaded joins every fallback chain: what the faces that
|
|
@@ -941,6 +1035,7 @@ export class CocoaFontManager {
|
|
|
941
1035
|
this._faces.clear();
|
|
942
1036
|
this._sized.clear();
|
|
943
1037
|
this._layouts.clear();
|
|
1038
|
+
if (this._keeps) this._forgetTypesetters();
|
|
944
1039
|
this._forgetCovers();
|
|
945
1040
|
// `null` on purpose: src/fonts.js keeps the fontkit face it already
|
|
946
1041
|
// opened as the handle, which is the one whose metrics apps can read;
|
|
@@ -957,8 +1052,10 @@ export class CocoaFontManager {
|
|
|
957
1052
|
// Memoized: an immediate-mode caller (the fonts app's specimen canvas,
|
|
958
1053
|
// a hover pass) lays the same paragraph out every repaint, and a
|
|
959
1054
|
// CTFramesetter per pointer move is what sluggish feels like. The key
|
|
960
|
-
// is everything shaping reads
|
|
961
|
-
|
|
1055
|
+
// is everything shaping reads — the paragraph, `shape`, which is also
|
|
1056
|
+
// what its kept typesetter is found by — and then everything breaking
|
|
1057
|
+
// it into lines reads; ~64 entries covers a screenful.
|
|
1058
|
+
const shape = JSON.stringify([
|
|
962
1059
|
spans.map((sp) => [
|
|
963
1060
|
sp.text,
|
|
964
1061
|
sp.family ?? base.family,
|
|
@@ -971,13 +1068,17 @@ export class CocoaFontManager {
|
|
|
971
1068
|
sp.features ?? base.features ?? null,
|
|
972
1069
|
sp.letterSpacing ?? base.letterSpacing ?? 0,
|
|
973
1070
|
]),
|
|
974
|
-
options.maxWidth,
|
|
975
|
-
options.align,
|
|
976
|
-
options.lineHeight,
|
|
977
|
-
options.maxLines,
|
|
978
|
-
options.overflow,
|
|
979
1071
|
options.direction,
|
|
980
1072
|
]);
|
|
1073
|
+
const signature =
|
|
1074
|
+
shape +
|
|
1075
|
+
JSON.stringify([
|
|
1076
|
+
options.maxWidth,
|
|
1077
|
+
options.align,
|
|
1078
|
+
options.lineHeight,
|
|
1079
|
+
options.maxLines,
|
|
1080
|
+
options.overflow,
|
|
1081
|
+
]);
|
|
981
1082
|
const hit = this._layouts.get(signature);
|
|
982
1083
|
if (hit) {
|
|
983
1084
|
// refresh LRU position
|
|
@@ -1054,8 +1155,17 @@ export class CocoaFontManager {
|
|
|
1054
1155
|
: minContent
|
|
1055
1156
|
? brokenAtEveryOpportunity(nativeSpans)
|
|
1056
1157
|
: nativeSpans;
|
|
1158
|
+
// The paragraph's kept typesetter, where it has one. A min-content
|
|
1159
|
+
// measurement lays out other text — the paragraph broken at every
|
|
1160
|
+
// opportunity, and capped for an eliding one — and keeps its own.
|
|
1161
|
+
const shapeKey = probe
|
|
1162
|
+
? `${shape}|${maxLines}`
|
|
1163
|
+
: minContent
|
|
1164
|
+
? `${shape}|min`
|
|
1165
|
+
: shape;
|
|
1166
|
+
const typesetter = this._keeps ? this._keptTypesetter(shapeKey) : null;
|
|
1057
1167
|
const raw = this._native.createLayout({
|
|
1058
|
-
spans: laid,
|
|
1168
|
+
...(typesetter ? { typesetter } : { spans: laid }),
|
|
1059
1169
|
maxWidth:
|
|
1060
1170
|
Number.isFinite(maxWidth) && maxWidth > 0 ? maxWidth : undefined,
|
|
1061
1171
|
align: flushFor(align, direction),
|
|
@@ -1063,7 +1173,15 @@ export class CocoaFontManager {
|
|
|
1063
1173
|
maxLines: probe || !Number.isFinite(maxLines) ? undefined : maxLines,
|
|
1064
1174
|
ellipsis: !probe && overflow === 'ellipsis',
|
|
1065
1175
|
rtl: direction === 'rtl',
|
|
1176
|
+
// the geometry as two arrays rather than an object a line and a run:
|
|
1177
|
+
// a tenth of the calls across the bridge (`unpackLines`)
|
|
1178
|
+
...(this._keeps ? { keep: typesetter === null, packed: true } : null),
|
|
1066
1179
|
});
|
|
1180
|
+
if (raw.typesetter) {
|
|
1181
|
+
let units = 0;
|
|
1182
|
+
for (const span of laid) units += span.text.length;
|
|
1183
|
+
this._keepTypesetter(shapeKey, raw.typesetter, units);
|
|
1184
|
+
}
|
|
1067
1185
|
// the text the native actually laid out: a min-content measurement's
|
|
1068
1186
|
// line and run ranges are indices into the broken text, and the two have
|
|
1069
1187
|
// to agree for `indexAt`/`caretPosition` to mean anything at all
|
package/src/cocoa/window.js
CHANGED
|
@@ -50,6 +50,50 @@ function owe(owed, damage) {
|
|
|
50
50
|
return rects.length > OWED_RECTS_MAX ? 'full' : rects;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/** The band `scrollSurface` would write for a shift of `scroll` inside a
|
|
54
|
+
* `width` x `height` bitmap, clamped the way the native clamps the rect:
|
|
55
|
+
* rect ∩ (rect + delta), or null when nothing survives the shift. */
|
|
56
|
+
function survivingBand(scroll, width, height) {
|
|
57
|
+
const { x, y, dx, dy } = scroll;
|
|
58
|
+
if (!dx && !dy) return null;
|
|
59
|
+
const clamp = (v, hi) => (v < 0 ? 0 : v > hi ? hi : v);
|
|
60
|
+
const x0 = clamp(x, width);
|
|
61
|
+
const y0 = clamp(y, height);
|
|
62
|
+
const x1 = clamp(x + scroll.width, width);
|
|
63
|
+
const y1 = clamp(y + scroll.height, height);
|
|
64
|
+
const left = Math.max(x0, x0 + dx);
|
|
65
|
+
const top = Math.max(y0, y0 + dy);
|
|
66
|
+
const right = Math.min(x1, x1 + dx);
|
|
67
|
+
const bottom = Math.min(y1, y1 + dy);
|
|
68
|
+
if (right <= left || bottom <= top) return null;
|
|
69
|
+
return { x: left, y: top, width: right - left, height: bottom - top };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Flat `[x, y, w, h, …]` rects less the `[x, y, w, h]` hole: the rows above
|
|
73
|
+
* and below it full width, the pieces beside it its rows only. */
|
|
74
|
+
function aroundHole(rects, [hx, hy, hw, hh]) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (let i = 0; i + 3 < rects.length; i += 4) {
|
|
77
|
+
const x = rects[i];
|
|
78
|
+
const y = rects[i + 1];
|
|
79
|
+
const right = x + rects[i + 2];
|
|
80
|
+
const bottom = y + rects[i + 3];
|
|
81
|
+
const top = Math.max(y, hy);
|
|
82
|
+
const under = Math.min(bottom, hy + hh);
|
|
83
|
+
const left = Math.max(x, hx);
|
|
84
|
+
const beside = Math.min(right, hx + hw);
|
|
85
|
+
if (beside <= left || under <= top) {
|
|
86
|
+
out.push(x, y, right - x, bottom - y);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (top > y) out.push(x, y, right - x, top - y);
|
|
90
|
+
if (bottom > under) out.push(x, under, right - x, bottom - under);
|
|
91
|
+
if (left > x) out.push(x, top, left - x, under - top);
|
|
92
|
+
if (right > beside) out.push(beside, top, right - beside, under - top);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
53
97
|
export class CocoaWindow {
|
|
54
98
|
constructor(app, attributes = {}) {
|
|
55
99
|
this.app = app;
|
|
@@ -599,8 +643,18 @@ export class CocoaWindow {
|
|
|
599
643
|
* always drew into.
|
|
600
644
|
*
|
|
601
645
|
* Then what it missed, copied across from the frame on glass.
|
|
646
|
+
*
|
|
647
|
+
* When the frame's first draw is a scroll blit (`scrollRegion`), the band
|
|
648
|
+
* it moves comes across already moved: one copy from the frame on glass
|
|
649
|
+
* at the shift, where catching up and then shifting in place wrote the
|
|
650
|
+
* band twice — the catch-up owes it, since the frame before blitted too.
|
|
651
|
+
* The catch-up copies the rest. Every pixel ends as the two passes left
|
|
652
|
+
* it, the strips the shift exposes included, so what the frame repaints
|
|
653
|
+
* does not change. Answers whether the band moved, or undefined when
|
|
654
|
+
* there was no scroll to take or it has to move in place: a chain of
|
|
655
|
+
* one, a buffer that owes nothing, a bridge without `blitSurface`.
|
|
602
656
|
*/
|
|
603
|
-
_takeBack() {
|
|
657
|
+
_takeBack(scroll = null) {
|
|
604
658
|
const chain = this._chain;
|
|
605
659
|
let free = null;
|
|
606
660
|
let oldest = null;
|
|
@@ -639,10 +693,9 @@ export class CocoaWindow {
|
|
|
639
693
|
// re-syncs its sticky state off the generation
|
|
640
694
|
this._surfaceGen++;
|
|
641
695
|
this._native.surfaceLock(back.handle);
|
|
696
|
+
let moved;
|
|
642
697
|
if (back.owed && back !== chain.front) {
|
|
643
|
-
|
|
644
|
-
chain.front.handle,
|
|
645
|
-
back.handle,
|
|
698
|
+
let owed =
|
|
646
699
|
back.owed === 'full'
|
|
647
700
|
? null
|
|
648
701
|
: back.owed.flatMap((r) => [
|
|
@@ -650,10 +703,39 @@ export class CocoaWindow {
|
|
|
650
703
|
Math.floor(r.y),
|
|
651
704
|
Math.ceil(r.width) + 1,
|
|
652
705
|
Math.ceil(r.height) + 1,
|
|
653
|
-
])
|
|
654
|
-
)
|
|
706
|
+
]);
|
|
707
|
+
if (scroll && typeof this._native.blitSurface === 'function') {
|
|
708
|
+
const { width, height } = this._surfaceSize;
|
|
709
|
+
const band = survivingBand(scroll, width, height);
|
|
710
|
+
// the band and where it comes from are both inside the bitmap, so
|
|
711
|
+
// the copy is the whole band — anything else is a bridge that did
|
|
712
|
+
// not copy, and the band moves in place after the plain catch-up
|
|
713
|
+
const copied =
|
|
714
|
+
band &&
|
|
715
|
+
this._native.blitSurface(
|
|
716
|
+
chain.front.handle,
|
|
717
|
+
band.x - scroll.dx,
|
|
718
|
+
band.y - scroll.dy,
|
|
719
|
+
band.width,
|
|
720
|
+
band.height,
|
|
721
|
+
back.handle,
|
|
722
|
+
band.x,
|
|
723
|
+
band.y,
|
|
724
|
+
);
|
|
725
|
+
if (!band) {
|
|
726
|
+
moved = false;
|
|
727
|
+
} else if (Array.isArray(copied)) {
|
|
728
|
+
moved = true;
|
|
729
|
+
owed = aroundHole(owed ?? [0, 0, width, height], copied);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
// (an empty list is the whole bitmap to the native, not nothing)
|
|
733
|
+
if (owed === null || owed.length > 0) {
|
|
734
|
+
this._native.copySurfaceRegion(chain.front.handle, back.handle, owed);
|
|
735
|
+
}
|
|
655
736
|
}
|
|
656
737
|
back.owed = null;
|
|
738
|
+
return moved;
|
|
657
739
|
}
|
|
658
740
|
|
|
659
741
|
/**
|
|
@@ -832,18 +914,30 @@ export class CocoaWindow {
|
|
|
832
914
|
return false;
|
|
833
915
|
}
|
|
834
916
|
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
917
|
+
const x = Math.round(rect.x);
|
|
918
|
+
const y = Math.round(rect.y);
|
|
919
|
+
const width = Math.round(rect.width);
|
|
920
|
+
const height = Math.round(rect.height);
|
|
921
|
+
// The band moves in the buffer this frame draws into. When this is the
|
|
922
|
+
// frame's first draw, taking that buffer moves it (`_takeBack`);
|
|
923
|
+
// otherwise, or where that cannot, it is taken and caught up to the
|
|
924
|
+
// frame on glass first, and the band moves in place.
|
|
925
|
+
const chain = this._chain;
|
|
926
|
+
let moved =
|
|
927
|
+
chain && !chain.back
|
|
928
|
+
? this._takeBack({ x, y, width, height, dx, dy })
|
|
929
|
+
: undefined;
|
|
930
|
+
if (moved === undefined) {
|
|
931
|
+
moved = this._native.scrollSurface(
|
|
932
|
+
this._ensureSurface(),
|
|
933
|
+
x,
|
|
934
|
+
y,
|
|
935
|
+
width,
|
|
936
|
+
height,
|
|
937
|
+
dx,
|
|
938
|
+
dy,
|
|
939
|
+
);
|
|
940
|
+
}
|
|
847
941
|
if (moved) {
|
|
848
942
|
this._dirty = true;
|
|
849
943
|
// The band moved inside the BACK buffer only. After the flip every
|
|
@@ -851,14 +945,7 @@ export class CocoaWindow {
|
|
|
851
945
|
// is owed only covers what the flush painted — the strips the shift
|
|
852
946
|
// exposed — so the next frame would blit a band one frame stale.
|
|
853
947
|
// Record the shifted rect as painted, and the catch-up carries it.
|
|
854
|
-
this.noteFrameDamage([
|
|
855
|
-
{
|
|
856
|
-
x: Math.round(rect.x),
|
|
857
|
-
y: Math.round(rect.y),
|
|
858
|
-
width: Math.round(rect.width),
|
|
859
|
-
height: Math.round(rect.height),
|
|
860
|
-
},
|
|
861
|
-
]);
|
|
948
|
+
this.noteFrameDamage([{ x, y, width, height }]);
|
|
862
949
|
}
|
|
863
950
|
return Boolean(moved);
|
|
864
951
|
}
|
package/src/nodes/layout.js
CHANGED
|
@@ -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.
|
package/src/nodes/window/size.js
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
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`)
|