react-x11 2.5.0 → 2.6.1
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 +4 -3
- package/src/Reconciler.js +6 -0
- package/src/cocoa/app.js +240 -31
- package/src/cocoa/context2d.js +287 -26
- package/src/cocoa/panehost.js +20 -2
- package/src/cocoa/panewindow.js +61 -9
- package/src/cocoa/presenter.js +191 -16
- package/src/cocoa/surface.js +9 -3
- package/src/cocoa/window.js +123 -13
- package/src/events.js +23 -5
- package/src/index.d.ts +25 -0
- package/src/nodes.js +849 -138
- package/src/paintcache.js +71 -11
- package/src/screens.js +39 -4
- package/src/svgnodes.js +4 -3
- package/src/types/elements.d.ts +13 -0
package/src/nodes.js
CHANGED
|
@@ -250,6 +250,10 @@ const NO_DAMAGE = Symbol('no-damage');
|
|
|
250
250
|
// against it.
|
|
251
251
|
const DAMAGE_SLOP = 1;
|
|
252
252
|
|
|
253
|
+
/** Stale bounds are undebuggable, and every other cache here has an escape
|
|
254
|
+
* hatch — see NO_SCROLL_BLIT and the paint cache's DISABLED. */
|
|
255
|
+
const NO_BOUNDS_CACHE = process.env.REACT_X11_NO_BOUNDS_CACHE === '1';
|
|
256
|
+
|
|
253
257
|
// While a bounded frame's layout pass runs, every node whose absolute rect
|
|
254
258
|
// comes out different reports its old and new rects here (each already
|
|
255
259
|
// inflated by that node's own paint reach) — which is what lets a layout
|
|
@@ -279,6 +283,7 @@ const INVALIDATE_REASONS = new Set([
|
|
|
279
283
|
'outline', // …and the same for an outline a style swap took away
|
|
280
284
|
'theme', // a theme/token change restyled a subtree
|
|
281
285
|
'direction', // the reading direction moved: sides, glyph order, bar edge
|
|
286
|
+
'scale', // a `scale` prop zoomed a subtree: every length in it moved
|
|
282
287
|
'animation', // a transition frame
|
|
283
288
|
'scroll', // scrollTo/scrollBy/scrollIntoView, textarea/textinput panning
|
|
284
289
|
'text', // text content, input value or caret editing
|
|
@@ -405,7 +410,7 @@ function insetRect(rect, by) {
|
|
|
405
410
|
}
|
|
406
411
|
|
|
407
412
|
/** The overlap of two rects, or null when they have none. */
|
|
408
|
-
function intersectRects(a, b) {
|
|
413
|
+
export function intersectRects(a, b) {
|
|
409
414
|
const x = Math.max(a.x, b.x);
|
|
410
415
|
const y = Math.max(a.y, b.y);
|
|
411
416
|
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
@@ -548,13 +553,13 @@ function coalesceRects(rects) {
|
|
|
548
553
|
* neighbours go first and far-apart rects last. That merge can itself overlap a
|
|
549
554
|
* third rect, so the result is coalesced again.
|
|
550
555
|
*/
|
|
551
|
-
function addDamageRect(rects, add) {
|
|
556
|
+
export function addDamageRect(rects, add, cap = MAX_DAMAGE_RECTS) {
|
|
552
557
|
let out = coalesceRects(
|
|
553
558
|
(rects ?? []).concat([
|
|
554
559
|
{ x: add.x, y: add.y, width: add.width, height: add.height },
|
|
555
560
|
]),
|
|
556
561
|
);
|
|
557
|
-
while (out.length >
|
|
562
|
+
while (out.length > cap) {
|
|
558
563
|
let bestI = 0;
|
|
559
564
|
let bestJ = 1;
|
|
560
565
|
let bestWaste = Infinity;
|
|
@@ -587,7 +592,7 @@ function addDamageRect(rects, add) {
|
|
|
587
592
|
* answers cover every claimed pixel, so this is a cost decision and never a
|
|
588
593
|
* correctness one.
|
|
589
594
|
*/
|
|
590
|
-
function damageToPaint(rects) {
|
|
595
|
+
export function damageToPaint(rects) {
|
|
591
596
|
if (rects.length < 2) return rects;
|
|
592
597
|
const box = rectsBounds(rects);
|
|
593
598
|
let sum = 0;
|
|
@@ -946,6 +951,9 @@ function assertWindowSize(props, kind) {
|
|
|
946
951
|
* pass run with no height on offer the row came out at nothing, taking its
|
|
947
952
|
* leaves down with it. A container in that position is recovered by looking
|
|
948
953
|
* inside it; a leaf has nothing inside, which is what this is for.
|
|
954
|
+
*
|
|
955
|
+
* Only the leaves the measurement is going to ask about: the walk goes
|
|
956
|
+
* through the nodes whose height extent is stale and no others.
|
|
949
957
|
*/
|
|
950
958
|
function captureLeafHeights(node, out) {
|
|
951
959
|
let leaf = true;
|
|
@@ -953,7 +961,7 @@ function captureLeafHeights(node, out) {
|
|
|
953
961
|
if (!child.yoga || child.isWindow) continue;
|
|
954
962
|
if (child.style.display === 'none') continue;
|
|
955
963
|
if (child.style.position !== 'absolute') leaf = false;
|
|
956
|
-
captureLeafHeights(child, out);
|
|
964
|
+
if (child._floorH === undefined) captureLeafHeights(child, out);
|
|
957
965
|
}
|
|
958
966
|
if (leaf) out.set(node, node.yoga.getComputedHeight());
|
|
959
967
|
}
|
|
@@ -991,7 +999,7 @@ function namesOwnFloor(node, axis) {
|
|
|
991
999
|
* answer, so there is no need to look inside it. Everything that names a
|
|
992
1000
|
* floor, plus the two other ways a style can bound itself: a **size**, which
|
|
993
1001
|
* a min-content measurement here keeps rather than shrinking past (see
|
|
994
|
-
* `
|
|
1002
|
+
* `writeFloors`), and a **ceiling**, since CSS clamps the content
|
|
995
1003
|
* suggestion by the specified `max-*` too.
|
|
996
1004
|
*/
|
|
997
1005
|
function declaresOwnMinimum(node, axis) {
|
|
@@ -1024,6 +1032,144 @@ const mainAxisOf = (node) => {
|
|
|
1024
1032
|
: 'height';
|
|
1025
1033
|
};
|
|
1026
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* Whether `child` is one the floors are **written on** along `axis`: a flex
|
|
1037
|
+
* item on its container's main axis whose author left the minimum to the
|
|
1038
|
+
* content. The same test `writeFloors` applies, asked ahead of time — it is
|
|
1039
|
+
* what decides whether a stale extent is one anybody will read.
|
|
1040
|
+
*/
|
|
1041
|
+
function receivesFloor(child, axis) {
|
|
1042
|
+
const parent = child.parent;
|
|
1043
|
+
if (!parent || mainAxisOf(parent) !== axis || !inFlow(child)) return false;
|
|
1044
|
+
const own = axis === 'width' ? 'minWidth' : 'minHeight';
|
|
1045
|
+
return typeof child.style[own] !== 'number';
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Find what the last measurement can no longer answer for, before a layout
|
|
1050
|
+
* pass clears the evidence.
|
|
1051
|
+
*
|
|
1052
|
+
* The floors are content, and yoga already keeps the exact record of which
|
|
1053
|
+
* content moved: a style setter that changed something, a child that came
|
|
1054
|
+
* or went, a text that asked to be re-measured all mark their node dirty
|
|
1055
|
+
* and every node above it, and the next `calculateLayout` clears the lot.
|
|
1056
|
+
* So this walks the dirty part of the tree — and only that part, since a
|
|
1057
|
+
* clean node has clean children — and takes the cached extents off every
|
|
1058
|
+
* node it finds (`_floorW`/`_floorH`), which is what "stale" means from
|
|
1059
|
+
* here on. The dirty nodes are listed for `writeFloors`: they are the
|
|
1060
|
+
* nodes whose children's floors can have moved — as is, later, every node
|
|
1061
|
+
* `contentSpan` measures a child of, since a window's natural-size
|
|
1062
|
+
* measurement (`_measure`) clears yoga's record before the first floors
|
|
1063
|
+
* pass, and a scroll pane's rows measured through a clean scroll pane still
|
|
1064
|
+
* need their floors written.
|
|
1065
|
+
*
|
|
1066
|
+
* `found` records whether any stale node is one a floor is written on: if
|
|
1067
|
+
* none is, no measurement is owed on that axis at all, whatever changed —
|
|
1068
|
+
* the extents that moved are ones nobody reads. An extent nobody read
|
|
1069
|
+
* stays unmeasured, on a clean node, for as long as nobody does; it is
|
|
1070
|
+
* asked for at the one moment it can start to matter, which is when its
|
|
1071
|
+
* parent changes (a column that turns into a row), and its parent is dirty
|
|
1072
|
+
* then. So the children of a dirty node are all looked at, and only the
|
|
1073
|
+
* dirty ones are descended into.
|
|
1074
|
+
*
|
|
1075
|
+
* Until the first floors pass has settled both axes (`sweep`), the walk
|
|
1076
|
+
* also goes down through nodes that were never measured, dirty or not: a
|
|
1077
|
+
* window's natural-size measurement (`_measure`) lays the tree out before
|
|
1078
|
+
* the first floors pass and clears yoga's record on the way, and a scroll
|
|
1079
|
+
* pane that names its own minimum receives no floor while the rows inside
|
|
1080
|
+
* it do. After that pass every reachable node has an extent on any axis
|
|
1081
|
+
* that had a floor to write, and the dirty path is the whole story.
|
|
1082
|
+
*
|
|
1083
|
+
* A `display: 'none'` subtree is marked and left: it takes part in no
|
|
1084
|
+
* layout, and a change inside it is still there when it is shown again,
|
|
1085
|
+
* because yoga clears a hidden node's own flag but never its children's.
|
|
1086
|
+
*/
|
|
1087
|
+
function collectFloorStale(node, stale, found, sweep) {
|
|
1088
|
+
for (const child of node.children) {
|
|
1089
|
+
if (!child.yoga || child.isWindow) continue;
|
|
1090
|
+
const dirty = child.yoga.isDirty();
|
|
1091
|
+
if (dirty) {
|
|
1092
|
+
child._floorW = undefined;
|
|
1093
|
+
child._floorH = undefined;
|
|
1094
|
+
// the minimum yoga holds may not be ours any more: a style change on
|
|
1095
|
+
// this node went through `applyLayoutStyle`, which writes the
|
|
1096
|
+
// author's minimum over whatever floor was there
|
|
1097
|
+
child._floorMinW = null;
|
|
1098
|
+
child._floorMinH = null;
|
|
1099
|
+
}
|
|
1100
|
+
if (child.style.display === 'none') continue;
|
|
1101
|
+
if (child._floorW === undefined && receivesFloor(child, 'width')) {
|
|
1102
|
+
found.width = true;
|
|
1103
|
+
}
|
|
1104
|
+
if (child._floorH === undefined && receivesFloor(child, 'height')) {
|
|
1105
|
+
found.height = true;
|
|
1106
|
+
}
|
|
1107
|
+
if (dirty) stale.add(child);
|
|
1108
|
+
else if (
|
|
1109
|
+
!sweep ||
|
|
1110
|
+
(child._floorW !== undefined && child._floorH !== undefined)
|
|
1111
|
+
) {
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
collectFloorStale(child, stale, found, sweep);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* After a pass that settled the widths: which of the height floors, each a
|
|
1120
|
+
* height *for a width*, were measured for a width their node no longer has.
|
|
1121
|
+
*
|
|
1122
|
+
* Walks down through every node whose width moved — a clean subtree whose
|
|
1123
|
+
* root is still the width it was measured at holds no surprises, so the
|
|
1124
|
+
* walk stops there — and asks each leaf it reaches the one question that
|
|
1125
|
+
* matters, in JavaScript rather than through a layout pass: is your height
|
|
1126
|
+
* at this width the height you had at the old one? A paragraph that still
|
|
1127
|
+
* fits on its line says no, and so does every unwrapped label in a grid
|
|
1128
|
+
* whose cells just moved a pixel, which is what makes a relayout of a large
|
|
1129
|
+
* tree one pass instead of four. A leaf that wraps differently is marked
|
|
1130
|
+
* stale with everything above it, and `hit.owed` says whether any of those
|
|
1131
|
+
* is a node a floor is written on.
|
|
1132
|
+
*
|
|
1133
|
+
* Widths are compared with a pixel of slack: the measuring passes run with
|
|
1134
|
+
* the pixel grid off and the real one with it on, so the same layout reads
|
|
1135
|
+
* a fraction apart between them.
|
|
1136
|
+
*/
|
|
1137
|
+
function probeHeightFloors(node, root, hit) {
|
|
1138
|
+
for (const child of node.children) {
|
|
1139
|
+
if (!child.yoga || child.isWindow) continue;
|
|
1140
|
+
if (child.style.display === 'none') continue;
|
|
1141
|
+
const stale = child._floorH === undefined;
|
|
1142
|
+
const width = child.yoga.getComputedWidth();
|
|
1143
|
+
const at = child._floorAtW;
|
|
1144
|
+
if (!stale && at !== undefined && Math.abs(width - at) < 1) continue;
|
|
1145
|
+
if (child._measureFn) {
|
|
1146
|
+
if (
|
|
1147
|
+
!stale &&
|
|
1148
|
+
at !== undefined &&
|
|
1149
|
+
child._heightForWidth(width) !== child._heightForWidth(at)
|
|
1150
|
+
) {
|
|
1151
|
+
markHeightStale(child, root, hit);
|
|
1152
|
+
}
|
|
1153
|
+
} else {
|
|
1154
|
+
probeHeightFloors(child, root, hit);
|
|
1155
|
+
}
|
|
1156
|
+
// whatever the extent is, it is the extent for this width now — a
|
|
1157
|
+
// stale one is about to be measured here, a clean one was just checked
|
|
1158
|
+
child._floorAtW = width;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** A leaf whose height moved takes every extent above it with it. */
|
|
1163
|
+
function markHeightStale(node, root, hit) {
|
|
1164
|
+
hit.marked = true;
|
|
1165
|
+
for (let n = node; n && n !== root; n = n.parent) {
|
|
1166
|
+
if (n._floorH === undefined) continue;
|
|
1167
|
+
n._floorH = undefined;
|
|
1168
|
+
root._floorsStale.add(n);
|
|
1169
|
+
if (receivesFloor(n, 'height')) hit.owed = true;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1027
1173
|
/**
|
|
1028
1174
|
* Put the tree in the state a **min-content** measurement means: a node that
|
|
1029
1175
|
* has said how small it can be is let go all the way down to it, and a node
|
|
@@ -1034,27 +1180,38 @@ const mainAxisOf = (node) => {
|
|
|
1034
1180
|
* own `flexShrink: 0` default. Now that the default is CSS's `1` (#249) the
|
|
1035
1181
|
* pass has to be told, or every node would shrink to nothing and answer that
|
|
1036
1182
|
* the content needs no room — which is true of no content anywhere.
|
|
1183
|
+
*
|
|
1184
|
+
* Every child of a node being measured is told, so that the node's own
|
|
1185
|
+
* layout is the one it always was; below a child whose extent is still
|
|
1186
|
+
* good nothing is, since nothing in there is read. `out` collects what was
|
|
1187
|
+
* written so `restoreShrink` can put back exactly that.
|
|
1037
1188
|
*/
|
|
1038
|
-
function setMeasuringShrink(node, axis) {
|
|
1189
|
+
function setMeasuringShrink(node, axis, out) {
|
|
1039
1190
|
for (const child of node.children) {
|
|
1040
1191
|
if (!child.yoga || child.isWindow) continue;
|
|
1041
|
-
child.yoga.setFlexShrink(namesOwnFloor(child, axis) ? 1 : 0);
|
|
1042
1192
|
// …and not into a `display: 'none'` subtree, which the measurement does
|
|
1043
|
-
// not read and
|
|
1193
|
+
// not read and the floors are not written back through
|
|
1044
1194
|
if (child.style.display === 'none') continue;
|
|
1045
|
-
|
|
1195
|
+
const shrink = namesOwnFloor(child, axis) ? 1 : 0;
|
|
1196
|
+
if ((child.style.flexShrink ?? 1) !== shrink) {
|
|
1197
|
+
child.yoga.setFlexShrink(shrink);
|
|
1198
|
+
out.push(child);
|
|
1199
|
+
}
|
|
1200
|
+
if (floorStale(child, axis)) setMeasuringShrink(child, axis, out);
|
|
1046
1201
|
}
|
|
1047
1202
|
}
|
|
1048
1203
|
|
|
1049
1204
|
/** …and back to the layout everything else is run from. */
|
|
1050
|
-
function restoreShrink(
|
|
1051
|
-
for (const child of
|
|
1052
|
-
if (!child.yoga || child.isWindow) continue;
|
|
1205
|
+
function restoreShrink(shrunk) {
|
|
1206
|
+
for (const child of shrunk) {
|
|
1053
1207
|
child.yoga.setFlexShrink(child.style.flexShrink ?? 1);
|
|
1054
|
-
restoreShrink(child);
|
|
1055
1208
|
}
|
|
1056
1209
|
}
|
|
1057
1210
|
|
|
1211
|
+
/** Whether `node`'s extent along `axis` has to be measured again. */
|
|
1212
|
+
const floorStale = (node, axis) =>
|
|
1213
|
+
(axis === 'width' ? node._floorW : node._floorH) === undefined;
|
|
1214
|
+
|
|
1058
1215
|
/**
|
|
1059
1216
|
* Pin every box under `node` at the width the pass just settled it at, so
|
|
1060
1217
|
* that the collapse which follows can only take **height** away.
|
|
@@ -1080,27 +1237,25 @@ function restoreShrink(node) {
|
|
|
1080
1237
|
* else the pass is doing, and its children are laid out inside that. It costs
|
|
1081
1238
|
* nothing either: every leaf is offered the width it was already measured at,
|
|
1082
1239
|
* so the paragraphs the first pass shaped come back out of the layout cache.
|
|
1240
|
+
*
|
|
1241
|
+
* Pinned as far as the measurement reads, like the shrink: the children of
|
|
1242
|
+
* every node whose extent is being measured, and no further.
|
|
1083
1243
|
*/
|
|
1084
|
-
function freezeWidths(node) {
|
|
1244
|
+
function freezeWidths(node, out) {
|
|
1085
1245
|
for (const child of node.children) {
|
|
1086
1246
|
if (!child.yoga || child.isWindow) continue;
|
|
1087
1247
|
// as in `setMeasuringShrink`: a `display: 'none'` subtree was not laid
|
|
1088
1248
|
// out, so there is no width in there to keep
|
|
1089
1249
|
if (child.style.display === 'none') continue;
|
|
1090
1250
|
child.yoga.setWidth(child.yoga.getComputedWidth());
|
|
1091
|
-
|
|
1251
|
+
out.push(child);
|
|
1252
|
+
if (child._floorH === undefined) freezeWidths(child, out);
|
|
1092
1253
|
}
|
|
1093
1254
|
}
|
|
1094
1255
|
|
|
1095
|
-
/** …and back to the width the style asks for
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
for (const child of node.children) {
|
|
1099
|
-
if (!child.yoga || child.isWindow) continue;
|
|
1100
|
-
if (child.style.display === 'none') continue;
|
|
1101
|
-
child.yoga.setWidth(child.style.width);
|
|
1102
|
-
restoreWidths(child);
|
|
1103
|
-
}
|
|
1256
|
+
/** …and back to the width the style asks for, on exactly what was pinned. */
|
|
1257
|
+
function restoreWidths(frozen) {
|
|
1258
|
+
for (const child of frozen) child.yoga.setWidth(child.style.width);
|
|
1104
1259
|
}
|
|
1105
1260
|
|
|
1106
1261
|
/**
|
|
@@ -1124,16 +1279,23 @@ function restoreWidths(node) {
|
|
|
1124
1279
|
* needs it. A container that came out at nothing is recovered by looking
|
|
1125
1280
|
* inside it; a leaf has nothing inside, so it has to be remembered.
|
|
1126
1281
|
*
|
|
1127
|
-
*
|
|
1128
|
-
*
|
|
1129
|
-
* so one pass and one walk give the whole tree its floors (#249)
|
|
1130
|
-
* a measurement per node
|
|
1131
|
-
*
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
*
|
|
1282
|
+
* What every node contributes to the box around it — which is exactly that
|
|
1283
|
+
* node's automatic minimum size — is written onto the node (`_floorW`,
|
|
1284
|
+
* `_floorH`), so one pass and one walk give the whole tree its floors (#249)
|
|
1285
|
+
* instead of a measurement per node, and so that the next measurement can
|
|
1286
|
+
* **read it back instead of looking again**: a child whose extent is still
|
|
1287
|
+
* there is taken at that number, and nothing under it is visited. The
|
|
1288
|
+
* extent is a function of the subtree alone — its content and its styles —
|
|
1289
|
+
* which is what makes the cache honest: `collectFloorStale` takes it off
|
|
1290
|
+
* every node whose subtree changed, and for a height the width it was
|
|
1291
|
+
* measured at is kept beside it (`_floorAtW`) for `probeHeightFloors` to
|
|
1292
|
+
* check. The recursion is therefore over the stale children only, and
|
|
1293
|
+
* still through the ones whose own content does not count towards this
|
|
1294
|
+
* one's: a scroll pane contributes nothing to the floor above it and still
|
|
1295
|
+
* needs floors written *inside* it, or the column of rows it holds would
|
|
1296
|
+
* shrink to the viewport and there would be nothing left to scroll.
|
|
1135
1297
|
*/
|
|
1136
|
-
function contentSpan(node, axis, intrinsic,
|
|
1298
|
+
function contentSpan(node, axis, intrinsic, root) {
|
|
1137
1299
|
const yoga = node.yoga;
|
|
1138
1300
|
const horizontal = axis === 'width';
|
|
1139
1301
|
const own = horizontal ? yoga.getComputedWidth() : yoga.getComputedHeight();
|
|
@@ -1153,19 +1315,31 @@ function contentSpan(node, axis, intrinsic, out) {
|
|
|
1153
1315
|
// by itself, and a <text> span has no box of its own
|
|
1154
1316
|
if (!child.yoga || child.isWindow) continue;
|
|
1155
1317
|
if (child.style.display === 'none') continue;
|
|
1156
|
-
const span = contentSpan(child, axis, intrinsic, out);
|
|
1157
1318
|
const laidOut = horizontal
|
|
1158
1319
|
? child.yoga.getComputedWidth()
|
|
1159
1320
|
: child.yoga.getComputedHeight();
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1321
|
+
let extent = horizontal ? child._floorW : child._floorH;
|
|
1322
|
+
if (extent === undefined) {
|
|
1323
|
+
const span = contentSpan(child, axis, intrinsic, root);
|
|
1324
|
+
// What the child needs from this box. A node that has said how small
|
|
1325
|
+
// it can be is taken at its word — the measuring pass already let it
|
|
1326
|
+
// shrink to exactly that — and anything else is asked what is inside
|
|
1327
|
+
// it. Its laid-out size is deliberately *not* a floor under that
|
|
1328
|
+
// answer: nothing shrank in this pass, so a box that measures its own
|
|
1329
|
+
// content is sitting at its **max**-content size, which is the width
|
|
1330
|
+
// a label would like to be rather than the width it can be squeezed
|
|
1331
|
+
// to.
|
|
1332
|
+
extent = declaresOwnMinimum(child, axis) ? laidOut : span;
|
|
1333
|
+
if (horizontal) child._floorW = extent;
|
|
1334
|
+
else {
|
|
1335
|
+
child._floorH = extent;
|
|
1336
|
+
child._floorAtW = child.yoga.getComputedWidth();
|
|
1337
|
+
}
|
|
1338
|
+
root._floorsMeasured += 1;
|
|
1339
|
+
// a fresh extent is a floor to write, whether or not this node was
|
|
1340
|
+
// dirty: see `collectFloorStale`
|
|
1341
|
+
root._floorsStale.add(node);
|
|
1342
|
+
}
|
|
1169
1343
|
if (child.style.position === 'absolute') continue;
|
|
1170
1344
|
const at =
|
|
1171
1345
|
(horizontal
|
|
@@ -1211,9 +1385,8 @@ function contentSpan(node, axis, intrinsic, out) {
|
|
|
1211
1385
|
}
|
|
1212
1386
|
|
|
1213
1387
|
/**
|
|
1214
|
-
* Write CSS's **automatic minimum size** onto
|
|
1215
|
-
*
|
|
1216
|
-
* and restore the `flexShrink` the measurement borrowed on the way past.
|
|
1388
|
+
* Write CSS's **automatic minimum size** onto the flex items in `node`: a
|
|
1389
|
+
* floor of the extent each needs, along the axis its container lays out on.
|
|
1217
1390
|
*
|
|
1218
1391
|
* This is the other half of `flexShrink` defaulting to `1` (#249), and
|
|
1219
1392
|
* neither half is any good without the other. Yoga implements the shrink and
|
|
@@ -1226,7 +1399,10 @@ function contentSpan(node, axis, intrinsic, out) {
|
|
|
1226
1399
|
*
|
|
1227
1400
|
* Only the **main** axis, as in CSS: shrinking happens along the axis the
|
|
1228
1401
|
* container packs on, and on the other one an item is stretched or fits its
|
|
1229
|
-
* content either way.
|
|
1402
|
+
* content either way. On the other axis the child gets its style's minimum
|
|
1403
|
+
* back — which is what takes a floor off a child whose container turned
|
|
1404
|
+
* from a column into a row, since the floor it needs now is on the other
|
|
1405
|
+
* axis.
|
|
1230
1406
|
*
|
|
1231
1407
|
* Where this deliberately parts company with CSS is a node that named a
|
|
1232
1408
|
* size: CSS floors that at `min(the size, the content)`, so a `height: 40`
|
|
@@ -1238,10 +1414,14 @@ function contentSpan(node, axis, intrinsic, out) {
|
|
|
1238
1414
|
* that was named is a size that is kept**, and `minHeight: 0` is how an
|
|
1239
1415
|
* author says otherwise.
|
|
1240
1416
|
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1417
|
+
* One node's children, from the extents on them (`contentSpan`): the
|
|
1418
|
+
* callers run it over every node whose children's extents may have moved,
|
|
1419
|
+
* and nothing else — a floor that did not change is not written again, so a
|
|
1420
|
+
* clean subtree is neither visited nor dirtied. What was written is kept
|
|
1421
|
+
* (`_floorMinW`/`_floorMinH`) so that the next write can tell; a stale
|
|
1422
|
+
* extent writes the style's own minimum, which is how a floor comes off a
|
|
1423
|
+
* node that is about to be measured, and which is why measuring a node
|
|
1424
|
+
* that carries one cannot read it back as content the tree cannot give up.
|
|
1245
1425
|
*
|
|
1246
1426
|
* A floor is written **unrounded**, and the measurement it came from ran
|
|
1247
1427
|
* with the pixel grid off (`measuringExactly`) for the reason given there:
|
|
@@ -1256,28 +1436,29 @@ function contentSpan(node, axis, intrinsic, out) {
|
|
|
1256
1436
|
* tall (issue #411). Whole pixels cancel exactly, which is why the text
|
|
1257
1437
|
* measures here answer in them (`TextNode._trim`).
|
|
1258
1438
|
*/
|
|
1259
|
-
function
|
|
1439
|
+
function writeFloors(node, axis) {
|
|
1440
|
+
const horizontal = axis === 'width';
|
|
1260
1441
|
const axisIsMain = mainAxisOf(node) === axis;
|
|
1261
|
-
const own =
|
|
1442
|
+
const own = horizontal ? 'minWidth' : 'minHeight';
|
|
1262
1443
|
for (const child of node.children) {
|
|
1263
1444
|
if (!child.yoga || child.isWindow) continue;
|
|
1264
|
-
|
|
1265
|
-
if (child.style.display === 'none') continue;
|
|
1445
|
+
let floor;
|
|
1266
1446
|
// A floor of 0 is what yoga does anyway, and an author who named their
|
|
1267
1447
|
// own `minWidth`/`minHeight` has already answered — overwriting it would
|
|
1268
1448
|
// put a measurement of ours above a number they wrote.
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
) {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1449
|
+
if (axisIsMain && inFlow(child) && typeof child.style[own] !== 'number') {
|
|
1450
|
+
const extent = horizontal ? child._floorW : child._floorH;
|
|
1451
|
+
if (extent > 0) floor = extent;
|
|
1452
|
+
}
|
|
1453
|
+
if ((horizontal ? child._floorMinW : child._floorMinH) === floor) continue;
|
|
1454
|
+
const value = floor ?? child.style[own];
|
|
1455
|
+
if (horizontal) {
|
|
1456
|
+
child.yoga.setMinWidth(value);
|
|
1457
|
+
child._floorMinW = floor;
|
|
1458
|
+
} else {
|
|
1459
|
+
child.yoga.setMinHeight(value);
|
|
1460
|
+
child._floorMinH = floor;
|
|
1279
1461
|
}
|
|
1280
|
-
writeContentFloors(child, axis, mins, floored);
|
|
1281
1462
|
}
|
|
1282
1463
|
}
|
|
1283
1464
|
|
|
@@ -1432,6 +1613,46 @@ const WINDOW_SEMANTIC_NAMES = new Set([
|
|
|
1432
1613
|
* are not: an element that wrote `widthMode === 0` would be pinned to
|
|
1433
1614
|
* yoga's ABI through us, which is exactly what the seam exists to stop.
|
|
1434
1615
|
*/
|
|
1616
|
+
/**
|
|
1617
|
+
* Text smaller than this many logical pixels is painted as a strip in its
|
|
1618
|
+
* ink instead of as glyphs (`TextNode._paintsStrip`). Six is under the
|
|
1619
|
+
* smallest size any UI sets on purpose and over what a zoomed-out view
|
|
1620
|
+
* shrinks its labels to; `createRoot({ textStripBelow })` is the seam.
|
|
1621
|
+
*/
|
|
1622
|
+
export const TEXT_STRIP_BELOW = 6;
|
|
1623
|
+
// The band a strip covers, in ems around the baseline — the x-height and a
|
|
1624
|
+
// little of the ascenders above it, the descenders below — and the share of
|
|
1625
|
+
// the ink it is painted at, which is roughly how much of that band small
|
|
1626
|
+
// text actually inks.
|
|
1627
|
+
const STRIP_ABOVE_BASELINE = 0.6;
|
|
1628
|
+
const STRIP_BELOW_BASELINE = 0.1;
|
|
1629
|
+
const STRIP_COVERAGE = 0.45;
|
|
1630
|
+
|
|
1631
|
+
const textStripBelow = new WeakMap();
|
|
1632
|
+
|
|
1633
|
+
/** The size under which this connection's text is painted as strips. */
|
|
1634
|
+
export function setTextStripBelow(app, below) {
|
|
1635
|
+
if (below === undefined) return;
|
|
1636
|
+
if (typeof below !== 'number' || !(below >= 0)) {
|
|
1637
|
+
throw new Error(
|
|
1638
|
+
`react-x11: createRoot({ textStripBelow: ${JSON.stringify(below)} }) ` +
|
|
1639
|
+
'— a size in logical pixels, or 0 to paint glyphs at every size.',
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
textStripBelow.set(app, below);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// `REACT_X11_TEXT_STRIP_BELOW` is the same line for a process that cannot
|
|
1646
|
+
// reach `createRoot` — a bench comparing the strip against glyphs, an app
|
|
1647
|
+
// run under a harness — read once
|
|
1648
|
+
const textStripBelowEnv = Number(process.env.REACT_X11_TEXT_STRIP_BELOW);
|
|
1649
|
+
|
|
1650
|
+
const textStripBelowFor = (app) =>
|
|
1651
|
+
textStripBelow.get(app) ??
|
|
1652
|
+
(Number.isFinite(textStripBelowEnv) && textStripBelowEnv >= 0
|
|
1653
|
+
? textStripBelowEnv
|
|
1654
|
+
: TEXT_STRIP_BELOW);
|
|
1655
|
+
|
|
1435
1656
|
const MEASURE_MODES = [];
|
|
1436
1657
|
MEASURE_MODES[Yoga.MEASURE_MODE_UNDEFINED] = 'unconstrained';
|
|
1437
1658
|
MEASURE_MODES[Yoga.MEASURE_MODE_EXACTLY] = 'exactly';
|
|
@@ -1502,6 +1723,33 @@ function shallowEqual(a, b) {
|
|
|
1502
1723
|
return ka.length === kb.length && ka.every((k) => a[k] === b[k]);
|
|
1503
1724
|
}
|
|
1504
1725
|
|
|
1726
|
+
/**
|
|
1727
|
+
* A node's own `scale` prop as a factor: what it multiplies the scale it
|
|
1728
|
+
* inherits by (`Node.scale`). Absent means 1, which is every node in a tree
|
|
1729
|
+
* that never mentions it.
|
|
1730
|
+
*
|
|
1731
|
+
* A zero, a negative or a NaN is a mistake rather than a design — it would
|
|
1732
|
+
* lay the subtree out at nothing, or at infinity — and in development it
|
|
1733
|
+
* says so where the mistake is, rather than as a blank pane three frames
|
|
1734
|
+
* later. Production falls back to 1 for the same reason a bad token keeps
|
|
1735
|
+
* the property dropped: a GUI that carries on is worth more than one that
|
|
1736
|
+
* dies on a fraction somebody divided by.
|
|
1737
|
+
*/
|
|
1738
|
+
function scaleFactorOf(props, kind) {
|
|
1739
|
+
const own = props?.scale;
|
|
1740
|
+
if (own === undefined) return 1;
|
|
1741
|
+
if (typeof own === 'number' && Number.isFinite(own) && own > 0) return own;
|
|
1742
|
+
if (DEV) {
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
`react-x11: <${kind} scale={${JSON.stringify(own)}}> — a subtree ` +
|
|
1745
|
+
'scale is a positive number, the factor this subtree is zoomed by ' +
|
|
1746
|
+
'(2 draws it twice the size, 0.5 half), and leaving it out means 1. ' +
|
|
1747
|
+
'See docs/scale.md, "A subtree of its own".',
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
return 1;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1505
1753
|
/** The half of `Node._joinsYoga` that is about the child alone — a real X
|
|
1506
1754
|
* window (`<window>`, `<popup>`) or a node built without a box at all (a text
|
|
1507
1755
|
* chunk) sits outside whatever parent it lands in. This is what
|
|
@@ -1515,16 +1763,93 @@ export class Node {
|
|
|
1515
1763
|
}
|
|
1516
1764
|
|
|
1517
1765
|
/**
|
|
1518
|
-
* Device pixels per logical pixel for this node
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
1766
|
+
* Device pixels per logical pixel **for this node** — the display scale
|
|
1767
|
+
* `createRoot` resolved (src/scale.js), times every `scale` prop between
|
|
1768
|
+
* this node and its window. `this.style` and `this.abs` are already
|
|
1769
|
+
* device pixels; this is for the values that never pass through a style —
|
|
1770
|
+
* a paint constant like the caret's width, or an event coordinate on its
|
|
1771
|
+
* way back to logical. A registered element that draws with its own
|
|
1772
|
+
* constants multiplies them by this.
|
|
1773
|
+
*
|
|
1774
|
+
* The `scale` prop is CSS `zoom`, not a transform: it multiplies the
|
|
1775
|
+
* inherited factor, and the node's *own* style scales with it. So
|
|
1776
|
+
* everything downstream of the style funnel follows with no second
|
|
1777
|
+
* mechanism — yoga lays out the scaled numbers like any others, paint
|
|
1778
|
+
* reads the scaled style, the caret and the scrollbar read this getter,
|
|
1779
|
+
* and text is shaped at the size it will be drawn at rather than
|
|
1780
|
+
* rasterized once and stretched (docs/scale.md, "A subtree of its own").
|
|
1781
|
+
*
|
|
1782
|
+
* **A real X window is its own root.** `<window>` and `<popup>` geometry
|
|
1783
|
+
* is the server's, in the display's pixels — `scaleWindowGeometry` reads
|
|
1784
|
+
* `scaleOf(app)` directly and a WM sees no zoom — so the cascade stops
|
|
1785
|
+
* there and a menu opened from a zoomed card comes up at the app's own
|
|
1786
|
+
* size. Everything else inherits, including `<glarea>` and `<foreign>`,
|
|
1787
|
+
* whose boxes are laid out by the parent like any other child's.
|
|
1788
|
+
*
|
|
1789
|
+
* Cached per node, dropped by `_rescaleSubtree` — the same contract
|
|
1790
|
+
* `theme` and `direction` have, for the same reason.
|
|
1525
1791
|
*/
|
|
1526
1792
|
get scale() {
|
|
1527
|
-
|
|
1793
|
+
if (this._scaleCache !== undefined) return this._scaleCache;
|
|
1794
|
+
if (this.isWindow) return (this._scaleCache = scaleOf(this.app));
|
|
1795
|
+
const base = this.parent ? this.parent.scale : scaleOf(this.app);
|
|
1796
|
+
return (this._scaleCache = base * scaleFactorOf(this.props, this.kind));
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* The effective scale of this node moved — its own `scale` prop changed,
|
|
1801
|
+
* or it was attached under an ancestor whose scale is not the one it
|
|
1802
|
+
* resolved against while detached.
|
|
1803
|
+
*
|
|
1804
|
+
* Everything below inherits, so the whole subtree is restyled; the early
|
|
1805
|
+
* out is the answer not having moved, which is what makes the call at
|
|
1806
|
+
* each of the attach sites free in the overwhelmingly common case of a
|
|
1807
|
+
* tree with no `scale` prop in it at all.
|
|
1808
|
+
*
|
|
1809
|
+
* `mounting` is `insertBefore` attaching a subtree that has never been in
|
|
1810
|
+
* the tree, and means the same thing it means to `_themeChanged`: resolve
|
|
1811
|
+
* everything, claim nothing (issue #402). A node that has never painted
|
|
1812
|
+
* has no stale pixels to cover, and where it lands is claimed by the
|
|
1813
|
+
* child-list protocol.
|
|
1814
|
+
*/
|
|
1815
|
+
_rescaleSubtree(mounting = false) {
|
|
1816
|
+
if (!this._rescaleMoved()) return;
|
|
1817
|
+
// One claim for the whole walk: `_invalidateLayout` bounds the subtree
|
|
1818
|
+
// as it stands and queues the after-layout claim, so the per-node
|
|
1819
|
+
// repeats the recursion below would make are the same rect over again.
|
|
1820
|
+
if (!mounting) this._invalidateLayout('scale');
|
|
1821
|
+
this._rescaled(mounting);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/** Drop the cached scale and re-ask; true when the answer moved. The
|
|
1825
|
+
* getter is the only place the rule lives, so a `<window>` — which
|
|
1826
|
+
* resolves the display scale whatever it is written inside — answers
|
|
1827
|
+
* false here and the walk stops at it. */
|
|
1828
|
+
_rescaleMoved() {
|
|
1829
|
+
const before = this._scaleCache;
|
|
1830
|
+
this._scaleCache = undefined;
|
|
1831
|
+
return this.scale !== before;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/** …the walk itself, for a node whose scale is already known to have
|
|
1835
|
+
* moved. Restyle in the new unit, then carry it down: an ordinary
|
|
1836
|
+
* descendant's scale is a product of this one, so it moved too. */
|
|
1837
|
+
_rescaled(mounting) {
|
|
1838
|
+
// both hold a size in device pixels at the old scale
|
|
1839
|
+
this._textBase = undefined;
|
|
1840
|
+
this._textScaled = null;
|
|
1841
|
+
const prevStyle = this.style;
|
|
1842
|
+
const style = this._syncStyle(this.props, mounting);
|
|
1843
|
+
if (this.yoga && style !== prevStyle) {
|
|
1844
|
+
applyLayoutStyle(this.yoga, style, prevStyle);
|
|
1845
|
+
}
|
|
1846
|
+
if (localTextStyleChanged(style, prevStyle)) this._textContentChanged();
|
|
1847
|
+
// its own claims are bounded, so this runs on a mount too — the same
|
|
1848
|
+
// rule `_themeChanged` follows
|
|
1849
|
+
this._retext();
|
|
1850
|
+
for (const child of this.children) {
|
|
1851
|
+
if (child._rescaleMoved()) child._rescaled(mounting);
|
|
1852
|
+
}
|
|
1528
1853
|
}
|
|
1529
1854
|
|
|
1530
1855
|
constructor(kind, props, app, { yoga = true } = {}) {
|
|
@@ -1547,6 +1872,20 @@ export class Node {
|
|
|
1547
1872
|
// that a second mutation reuses the rect instead of walking the subtree
|
|
1548
1873
|
// again. Lives exactly as long as membership in `root._reflowed`.
|
|
1549
1874
|
this._reflowBefore = null;
|
|
1875
|
+
// The automatic minimum size (#249), cached per node: what this node
|
|
1876
|
+
// contributes to the box around it on each axis (`contentSpan`), the
|
|
1877
|
+
// width the height was measured for (`probeHeightFloors`), and the
|
|
1878
|
+
// floor yoga currently holds from us on each axis (`writeFloors`).
|
|
1879
|
+
// `undefined` on an extent means it has to be measured again; `null`
|
|
1880
|
+
// on a written floor means yoga's minimum may not be ours any more.
|
|
1881
|
+
this._floorW = undefined;
|
|
1882
|
+
this._floorH = undefined;
|
|
1883
|
+
this._floorAtW = undefined;
|
|
1884
|
+
this._floorMinW = undefined;
|
|
1885
|
+
this._floorMinH = undefined;
|
|
1886
|
+
// the width mode yoga last measured this leaf in with no height on
|
|
1887
|
+
// offer — the question `_heightForWidth` repeats
|
|
1888
|
+
this._floorMeasureMode = null;
|
|
1550
1889
|
this.root = null; // owning WindowNode once attached
|
|
1551
1890
|
this.hidden = false;
|
|
1552
1891
|
this.destroyed = false;
|
|
@@ -1579,6 +1918,11 @@ export class Node {
|
|
|
1579
1918
|
// inherits. Undefined means "never asked", which is load-bearing — see
|
|
1580
1919
|
// `_retext`
|
|
1581
1920
|
this._resolvedText = undefined;
|
|
1921
|
+
// `inheritedTextStyle`'s cache at a *scale boundary* — a node whose
|
|
1922
|
+
// `scale` prop puts it in a different unit from its parent, so the
|
|
1923
|
+
// device size it inherits has to be re-expressed. Null everywhere else,
|
|
1924
|
+
// which is every node in a tree with no `scale` prop in it.
|
|
1925
|
+
this._textScaled = null;
|
|
1582
1926
|
// `direction`'s cache, same contract as `_resolvedText`'s: undefined means
|
|
1583
1927
|
// "never asked", which is what lets `_redirectSubtree` stop at a node
|
|
1584
1928
|
// nothing below has resolved through
|
|
@@ -1762,6 +2106,10 @@ export class Node {
|
|
|
1762
2106
|
this.style = this._anim?.size
|
|
1763
2107
|
? { ...target, ...this._animatedValues() }
|
|
1764
2108
|
: target;
|
|
2109
|
+
// The paint reach reads the style now in force — a shadow's spread, an
|
|
2110
|
+
// outline's width — so it is dropped on every swap, here, before the
|
|
2111
|
+
// old-extent claims below measure the new reach against the old one.
|
|
2112
|
+
this._clearPaintBounds();
|
|
1765
2113
|
// `hitSlop` feeds the cached hit reach and `overflow` decides where its
|
|
1766
2114
|
// invalidation walks stop, so a swap that changes either clears here —
|
|
1767
2115
|
// the one funnel every style path goes through. Animation ticks never
|
|
@@ -2014,6 +2362,9 @@ export class Node {
|
|
|
2014
2362
|
setStyleState(name, on) {
|
|
2015
2363
|
if (this.states[name] === on) return;
|
|
2016
2364
|
this.states[name] = on;
|
|
2365
|
+
// the focus ring's reach is read off `:focus-visible` whether or not
|
|
2366
|
+
// the node has a state block of its own (`_outlineExtent`)
|
|
2367
|
+
this._clearPaintBounds();
|
|
2017
2368
|
if (!this._stateful || this.destroyed) return;
|
|
2018
2369
|
const next = scaleResolvedStyle(
|
|
2019
2370
|
resolveComputedStyle(resolveStyleStates(this._baseStyle, this.states)),
|
|
@@ -2194,7 +2545,26 @@ export class Node {
|
|
|
2194
2545
|
* `insertBefore` re-resolves the subtree against the real one.
|
|
2195
2546
|
*/
|
|
2196
2547
|
get inheritedTextStyle() {
|
|
2197
|
-
if (this.parent)
|
|
2548
|
+
if (this.parent) {
|
|
2549
|
+
const inherited = this.parent.resolvedTextStyle();
|
|
2550
|
+
// A **scale boundary** — this node's `scale` prop, or a `<popup>`
|
|
2551
|
+
// written inside a zoomed subtree, which goes back to the display's
|
|
2552
|
+
// own unit. What comes down the cascade is already device pixels at
|
|
2553
|
+
// the parent's scale (that is the point of resolving the theme's size
|
|
2554
|
+
// once, at the root), so re-expressing it here is the only place a
|
|
2555
|
+
// second multiply is right: a `fontSize: 14` theme inside a
|
|
2556
|
+
// `scale={2}` box is 28 logical, 28 device at 1x, and the descendants
|
|
2557
|
+
// below inherit that without compounding it again.
|
|
2558
|
+
const from = this.parent.scale;
|
|
2559
|
+
const to = this.scale;
|
|
2560
|
+
if (from === to) return inherited;
|
|
2561
|
+
const size = (inherited.size * to) / from;
|
|
2562
|
+
const cached = this._textScaled;
|
|
2563
|
+
if (cached?.from !== inherited || cached.style.size !== size) {
|
|
2564
|
+
this._textScaled = { from: inherited, style: { ...inherited, size } };
|
|
2565
|
+
}
|
|
2566
|
+
return this._textScaled.style;
|
|
2567
|
+
}
|
|
2198
2568
|
const theme = this.theme;
|
|
2199
2569
|
const color = theme.text;
|
|
2200
2570
|
// A palette can reach a node as a bare `theme` **prop** rather than a
|
|
@@ -2469,6 +2839,24 @@ export class Node {
|
|
|
2469
2839
|
this.yoga.setMeasureFunc(measure);
|
|
2470
2840
|
}
|
|
2471
2841
|
|
|
2842
|
+
/**
|
|
2843
|
+
* The height this leaf takes at `width` with nothing bounding its height
|
|
2844
|
+
* — the answer yoga gets from the measuring pass that settles the height
|
|
2845
|
+
* floors, asked directly. `probeHeightFloors` asks it twice, for the width
|
|
2846
|
+
* a leaf was measured at and the one it has now, to find out whether a
|
|
2847
|
+
* relayout that moved the leaf changed what it needs; a paragraph answers
|
|
2848
|
+
* from its layout cache, and the elements whose height is not a function
|
|
2849
|
+
* of their width at all answer at once.
|
|
2850
|
+
*/
|
|
2851
|
+
_heightForWidth(width) {
|
|
2852
|
+
return this.measureContent({
|
|
2853
|
+
width,
|
|
2854
|
+
height: Infinity,
|
|
2855
|
+
widthMode: this._floorMeasureMode ?? 'at-most',
|
|
2856
|
+
heightMode: 'unconstrained',
|
|
2857
|
+
})?.height;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2472
2860
|
/**
|
|
2473
2861
|
* Hand `measureContent` to layout, translated: the modes arrive as words,
|
|
2474
2862
|
* an axis with no bound arrives as `Infinity` rather than as yoga's null,
|
|
@@ -2477,6 +2865,9 @@ export class Node {
|
|
|
2477
2865
|
*/
|
|
2478
2866
|
_useMeasureContent() {
|
|
2479
2867
|
this._setMeasureFunc((width, widthMode, height, heightMode) => {
|
|
2868
|
+
if (heightMode === Yoga.MEASURE_MODE_UNDEFINED) {
|
|
2869
|
+
this._floorMeasureMode = MEASURE_MODES[widthMode];
|
|
2870
|
+
}
|
|
2480
2871
|
const size = this.measureContent({
|
|
2481
2872
|
width: measureOffer(width, widthMode),
|
|
2482
2873
|
height: measureOffer(height, heightMode),
|
|
@@ -2626,6 +3017,9 @@ export class Node {
|
|
|
2626
3017
|
// it can see its ancestors now, so any token in its style can resolve.
|
|
2627
3018
|
// With no theme anywhere there is nothing to resolve and nothing to walk
|
|
2628
3019
|
if (this.theme || child.props.theme) child._themeChanged(mounting);
|
|
3020
|
+
// …and the same for the scale: a subtree styled while detached resolved
|
|
3021
|
+
// against the app's, and only now can see the `scale` props above it.
|
|
3022
|
+
child._rescaleSubtree(mounting);
|
|
2629
3023
|
this._textContentChanged();
|
|
2630
3024
|
this._childListChanged(before);
|
|
2631
3025
|
a11yHooks.attached?.(this, child);
|
|
@@ -2818,6 +3212,11 @@ export class Node {
|
|
|
2818
3212
|
const themeChanged = newProps.theme !== prev.theme;
|
|
2819
3213
|
this.props = newProps;
|
|
2820
3214
|
if (themeChanged) this._themeChanged();
|
|
3215
|
+
// Ahead of the `_syncStyle` below, because the funnel that runs
|
|
3216
|
+
// multiplies by `this.scale` and this is what makes it read the new
|
|
3217
|
+
// factor. The walk restyles this node too, so the call after it hits
|
|
3218
|
+
// the identity check and costs nothing twice.
|
|
3219
|
+
if (newProps.scale !== prev.scale) this._rescaleSubtree();
|
|
2821
3220
|
const style = this._syncStyle(newProps);
|
|
2822
3221
|
let layoutChanged = false;
|
|
2823
3222
|
// hoisted styles hit the identity check and skip the whole update
|
|
@@ -3249,6 +3648,9 @@ export class Node {
|
|
|
3249
3648
|
* invalid, whose own clear walked the rest of the way up.
|
|
3250
3649
|
*/
|
|
3251
3650
|
_clearHitBounds() {
|
|
3651
|
+
// what moves a hit reach moves the paint reach — first, because the
|
|
3652
|
+
// walk below returns from wherever it finds an ancestor already clear
|
|
3653
|
+
this._clearPaintBounds();
|
|
3252
3654
|
this._hitBoundsCache = null;
|
|
3253
3655
|
for (let n = this.parent; n; n = n.parent) {
|
|
3254
3656
|
if (n.clipsChildren() || n._hitBoundsCache === null) return;
|
|
@@ -3392,6 +3794,13 @@ export class Node {
|
|
|
3392
3794
|
const abs = this.abs;
|
|
3393
3795
|
abs.x += dx;
|
|
3394
3796
|
abs.y += dy;
|
|
3797
|
+
// the paint reach rides along the same way — unless it *is* `abs`,
|
|
3798
|
+
// which just moved
|
|
3799
|
+
const p = this._paintBoundsCache;
|
|
3800
|
+
if (p && p !== abs) {
|
|
3801
|
+
p.x += dx;
|
|
3802
|
+
p.y += dy;
|
|
3803
|
+
}
|
|
3395
3804
|
const b = this._hitBoundsCache;
|
|
3396
3805
|
if (b) {
|
|
3397
3806
|
b.left += dx;
|
|
@@ -3449,6 +3858,10 @@ export class Node {
|
|
|
3449
3858
|
// a layout change may grow what an enclosing scroll pane has to scroll,
|
|
3450
3859
|
// through a route yoga never sees (issue #405)
|
|
3451
3860
|
if (layoutChanged) this._markScrollMeasureDirty();
|
|
3861
|
+
// a node that says its appearance changed may have changed how far it
|
|
3862
|
+
// reaches — a shadow, an outline, a scene element's ink — so its cached
|
|
3863
|
+
// paint reach goes with the claim
|
|
3864
|
+
if (damage === this || damage === null) this._clearPaintBounds();
|
|
3452
3865
|
this.root?.invalidate(layoutChanged, damage, reason);
|
|
3453
3866
|
}
|
|
3454
3867
|
|
|
@@ -3682,16 +4095,44 @@ export class Node {
|
|
|
3682
4095
|
* almost nothing.
|
|
3683
4096
|
*/
|
|
3684
4097
|
_subtreeBounds() {
|
|
4098
|
+
// Cached like the hit reach (`_hitBounds`), and for the same reason: a
|
|
4099
|
+
// bounded frame asks every subtree on the way to its rect whether it
|
|
4100
|
+
// reaches in, and answering by walking the subtree made a one-cell
|
|
4101
|
+
// repaint cost the whole tree — a millisecond at four thousand nodes,
|
|
4102
|
+
// five at fourteen thousand, every frame. The cache is dropped up the
|
|
4103
|
+
// chain by whatever changes a reach: a rect assigned by layout, a
|
|
4104
|
+
// child list mutation (`_clearHitBounds`), and every change a node
|
|
4105
|
+
// announces about itself (`invalidate`, `setStyleState`) — so a stale
|
|
4106
|
+
// answer would need a change nobody announced, which is already a
|
|
4107
|
+
// repaint bug.
|
|
4108
|
+
const cached = this._paintBoundsCache;
|
|
4109
|
+
if (cached && !NO_BOUNDS_CACHE) return cached;
|
|
3685
4110
|
let bounds = this._ownPaintBounds();
|
|
3686
|
-
if (this.clipsChildren())
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
4111
|
+
if (!this.clipsChildren()) {
|
|
4112
|
+
for (const child of this.children) {
|
|
4113
|
+
if (child.isWindow || !child.yoga || child.hidden) continue;
|
|
4114
|
+
if (child.style?.display === 'none') continue;
|
|
4115
|
+
bounds = unionRect(bounds, child._subtreeBounds());
|
|
4116
|
+
}
|
|
3691
4117
|
}
|
|
4118
|
+
this._paintBoundsCache = bounds;
|
|
3692
4119
|
return bounds;
|
|
3693
4120
|
}
|
|
3694
4121
|
|
|
4122
|
+
/**
|
|
4123
|
+
* This node's paint reach changed: drop the cached union here and up the
|
|
4124
|
+
* chain, stopping where `_clearHitBounds` stops and for the same reasons
|
|
4125
|
+
* — a clipping ancestor's reach is its own rect, and an ancestor already
|
|
4126
|
+
* cleared has cleared the rest of the way up.
|
|
4127
|
+
*/
|
|
4128
|
+
_clearPaintBounds() {
|
|
4129
|
+
this._paintBoundsCache = null;
|
|
4130
|
+
for (let n = this.parent; n; n = n.parent) {
|
|
4131
|
+
if (n.clipsChildren() || n._paintBoundsCache === null) return;
|
|
4132
|
+
n._paintBoundsCache = null;
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
|
|
3695
4136
|
/**
|
|
3696
4137
|
* This node's own rect, grown by anything it draws outside it — the
|
|
3697
4138
|
* outline and the shadow, the only two. Per node rather than once at the
|
|
@@ -4443,11 +4884,17 @@ export class Node {
|
|
|
4443
4884
|
* format, // 'argb32', or 'a8' for coverage that gets tinted
|
|
4444
4885
|
* tint, // the colour an 'a8' surface is painted through
|
|
4445
4886
|
* }
|
|
4446
|
-
* paintCached(ctx, box) -> void // draw at the origin of `box`
|
|
4887
|
+
* paintCached(ctx, box, ink) -> void // draw at the origin of `box`
|
|
4447
4888
|
*
|
|
4448
4889
|
* Returning null opts out for this frame, which is the right answer
|
|
4449
4890
|
* whenever the paint depends on something the key cannot see.
|
|
4450
4891
|
*
|
|
4892
|
+
* `ink` is the colour a mono drawing — one that asked for `'a8'` — must
|
|
4893
|
+
* paint in: white where the surface is coverage and the tint arrives at
|
|
4894
|
+
* blit time, the tint itself on a backend without coverage surfaces,
|
|
4895
|
+
* where the cache bakes the colour into an argb32 entry and puts it in
|
|
4896
|
+
* the key (src/paintcache.js). A multi-colour drawing ignores it.
|
|
4897
|
+
*
|
|
4451
4898
|
* **The key is the entire correctness surface.** It must name every input
|
|
4452
4899
|
* `paintCached` reads, derived from the same values `applyProps` compares
|
|
4453
4900
|
* so the two cannot drift. Never cache a paint that depends on state
|
|
@@ -4494,6 +4941,11 @@ export class Node {
|
|
|
4494
4941
|
}
|
|
4495
4942
|
|
|
4496
4943
|
_paintChildren(ctx) {
|
|
4944
|
+
// A retained presenter replays a node's `paint` into a visual of that
|
|
4945
|
+
// node's own — its children have visuals of their own, so it sets this
|
|
4946
|
+
// for the duration of the call (src/cocoa/presenter.js, `paintSelf`).
|
|
4947
|
+
// Never set on the X11 or surface paths, where a frame is one walk.
|
|
4948
|
+
if (this._ownPaintOnly) return;
|
|
4497
4949
|
const order = this.paintOrder();
|
|
4498
4950
|
if (order.length === 0) return;
|
|
4499
4951
|
const clip = this.clipsChildren() && this._childrenCanOverflow();
|
|
@@ -4995,7 +5447,7 @@ export class TextNode extends Node {
|
|
|
4995
5447
|
* that should have cancelled to zero; a fraction that is not exact in
|
|
4996
5448
|
* binary leaves a rounding residue there instead, and dividing by it laid
|
|
4997
5449
|
* the section titles of `examples/configurator` out 5.6 billion pixels
|
|
4998
|
-
* tall. See `
|
|
5450
|
+
* tall. See `writeFloors`, which is the other end of it.
|
|
4999
5451
|
*/
|
|
5000
5452
|
_trim(layout) {
|
|
5001
5453
|
if (this.style.textBoxTrim !== 'cap-alphabetic') return null;
|
|
@@ -5081,9 +5533,60 @@ export class TextNode extends Node {
|
|
|
5081
5533
|
const placed = this._placedLayout();
|
|
5082
5534
|
if (!placed) return;
|
|
5083
5535
|
this._paintSelection(ctx);
|
|
5536
|
+
if (this._paintsStrip()) {
|
|
5537
|
+
this._paintStrip(ctx, placed);
|
|
5538
|
+
return;
|
|
5539
|
+
}
|
|
5084
5540
|
placed.layout.draw(ctx, placed.x, placed.y);
|
|
5085
5541
|
}
|
|
5086
5542
|
|
|
5543
|
+
/**
|
|
5544
|
+
* Whether this paragraph is too small to read, and is painted as a strip
|
|
5545
|
+
* where its lines are instead of as glyphs (`_paintStrip`).
|
|
5546
|
+
*
|
|
5547
|
+
* A zoomed-out view — a minimap, a graph at a tenth of its size, a grid
|
|
5548
|
+
* of five thousand cells — is a screen full of labels nobody can read,
|
|
5549
|
+
* each of which costs a `CTLineDraw` or a glyph-run composite at exactly
|
|
5550
|
+
* the price of a legible one. Below a legible size a label is a smudge
|
|
5551
|
+
* of its ink, and a strip of that ink at the coverage of small text is
|
|
5552
|
+
* the same smudge for one fill. The size is in logical pixels: what is
|
|
5553
|
+
* legible is a physical question, and a 5px label is the same size on a
|
|
5554
|
+
* 2x panel as on a 1x monitor. `textStripBelow` on `createRoot` moves
|
|
5555
|
+
* the line, and `0` keeps glyphs at every size.
|
|
5556
|
+
*/
|
|
5557
|
+
_paintsStrip() {
|
|
5558
|
+
const below = textStripBelowFor(this.app);
|
|
5559
|
+
return below > 0 && this.resolvedTextStyle().size / this.scale < below;
|
|
5560
|
+
}
|
|
5561
|
+
|
|
5562
|
+
/**
|
|
5563
|
+
* One rectangle per line, over the band the letters sit in — from a
|
|
5564
|
+
* little above the x-height down past the baseline — in the ink at a
|
|
5565
|
+
* coverage that reads the way small text does: solid ink would be a bar,
|
|
5566
|
+
* and a paragraph is mostly white space at any size.
|
|
5567
|
+
*/
|
|
5568
|
+
_paintStrip(ctx, { layout, x, y }) {
|
|
5569
|
+
const lines = layout.lines;
|
|
5570
|
+
if (!lines?.length) return;
|
|
5571
|
+
const style = this.resolvedTextStyle();
|
|
5572
|
+
const em = style.size;
|
|
5573
|
+
const ink = cssColorStraight(style.color);
|
|
5574
|
+
if (!ink) return;
|
|
5575
|
+
const rects = [];
|
|
5576
|
+
for (const line of lines) {
|
|
5577
|
+
if (!(line.width > 0)) continue;
|
|
5578
|
+
rects.push(
|
|
5579
|
+
x + line.x,
|
|
5580
|
+
y + line.baseline - em * STRIP_ABOVE_BASELINE,
|
|
5581
|
+
line.width,
|
|
5582
|
+
em * (STRIP_ABOVE_BASELINE + STRIP_BELOW_BASELINE),
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
if (!rects.length) return;
|
|
5586
|
+
ctx.fillStyle = `rgba(${Math.round(ink[0] * 255)}, ${Math.round(ink[1] * 255)}, ${Math.round(ink[2] * 255)}, ${ink[3] * STRIP_COVERAGE})`;
|
|
5587
|
+
ctx.fillRects(rects);
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5087
5590
|
/** The band under the glyphs, when a document selection reaches this
|
|
5088
5591
|
* paragraph. Drawn from the same accessors a registered element would use,
|
|
5089
5592
|
* so the built-in and the custom surface cannot drift apart. */
|
|
@@ -6419,7 +6922,7 @@ export class CanvasNode extends Node {
|
|
|
6419
6922
|
};
|
|
6420
6923
|
}
|
|
6421
6924
|
|
|
6422
|
-
paintCached(ctx, box) {
|
|
6925
|
+
paintCached(ctx, box, ink = '#ffffff') {
|
|
6423
6926
|
const onDraw = this.props.onDraw;
|
|
6424
6927
|
if (typeof onDraw !== 'function') return;
|
|
6425
6928
|
ctx.save();
|
|
@@ -6428,8 +6931,9 @@ export class CanvasNode extends Node {
|
|
|
6428
6931
|
ctx.clip();
|
|
6429
6932
|
ctx.translate(box.x, box.y);
|
|
6430
6933
|
// Into a coverage surface only the alpha of a paint survives and the
|
|
6431
|
-
// tint arrives at blit time, so any opaque colour renders the same mask
|
|
6432
|
-
|
|
6934
|
+
// tint arrives at blit time, so any opaque colour renders the same mask
|
|
6935
|
+
// — and the cache says white then, or the tint where it bakes colour.
|
|
6936
|
+
if (this.props.mono) this._presetMono(ctx, ink);
|
|
6433
6937
|
try {
|
|
6434
6938
|
onDraw(ctx, {
|
|
6435
6939
|
width: box.width,
|
|
@@ -8509,12 +9013,21 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
8509
9013
|
// it moves (see _sendSizeHints for why the size is part of it)
|
|
8510
9014
|
this._sentHints = null;
|
|
8511
9015
|
this._sentHintsAt = null;
|
|
8512
|
-
// The automatic minimum size (#249): the
|
|
8513
|
-
//
|
|
8514
|
-
//
|
|
8515
|
-
|
|
9016
|
+
// The automatic minimum size (#249): whether the floors are still the
|
|
9017
|
+
// answer, the width the height half of them was measured for, the nodes
|
|
9018
|
+
// this frame found stale (`collectFloorStale`), and two counters the
|
|
9019
|
+
// tests read — layout passes over the root and nodes measured.
|
|
8516
9020
|
this._floorsDirty = true;
|
|
8517
9021
|
this._floorsWidth = null;
|
|
9022
|
+
this._floorsStale = new Set();
|
|
9023
|
+
// whether the first floors pass has run (`collectFloorStale`'s sweep)
|
|
9024
|
+
this._floorsSwept = false;
|
|
9025
|
+
this._floorsMeasured = 0;
|
|
9026
|
+
this._layoutPasses = 0;
|
|
9027
|
+
// whether the tree's CONTENT moved since the floors were measured — a
|
|
9028
|
+
// resize alone does not, which is what lets a live resize defer them
|
|
9029
|
+
this._floorsContentDirty = true;
|
|
9030
|
+
this._floorsCatchUp = false;
|
|
8518
9031
|
}
|
|
8519
9032
|
|
|
8520
9033
|
/**
|
|
@@ -8567,7 +9080,19 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
8567
9080
|
this.invalidate(true, null, 'direction');
|
|
8568
9081
|
}
|
|
8569
9082
|
|
|
8570
|
-
|
|
9083
|
+
/**
|
|
9084
|
+
* One measuring pass over the tree, read back into the extents of every
|
|
9085
|
+
* node still to be measured (`contentSpan`), and the root's own span
|
|
9086
|
+
* returned — the number a `minWidth="auto"` window sends as its hint.
|
|
9087
|
+
*
|
|
9088
|
+
* `forWidth` is the width the heights are measured for; `probe` says
|
|
9089
|
+
* whether the widths that pass settles are still to be checked against
|
|
9090
|
+
* the ones the height floors were measured at (`probeHeightFloors`) —
|
|
9091
|
+
* they are when nothing has looked yet this frame, and a leaf the probe
|
|
9092
|
+
* finds moved has its floor taken off and the pass run again, since a
|
|
9093
|
+
* floor still on a node being measured would be read back as content.
|
|
9094
|
+
*/
|
|
9095
|
+
_measureContentSpans(axis, forWidth, probe = false) {
|
|
8571
9096
|
const yoga = this.yoga;
|
|
8572
9097
|
const dir = this._rootDirection;
|
|
8573
9098
|
// The root carries whatever size the last pass pinned on it, and an
|
|
@@ -8575,9 +9100,13 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
8575
9100
|
yoga.setWidth(undefined);
|
|
8576
9101
|
yoga.setHeight(undefined);
|
|
8577
9102
|
if (axis === 'width') {
|
|
8578
|
-
|
|
9103
|
+
const shrunk = [];
|
|
9104
|
+
setMeasuringShrink(this, axis, shrunk);
|
|
9105
|
+
this._layoutPasses += 1;
|
|
8579
9106
|
yoga.calculateLayout(0, undefined, dir);
|
|
8580
|
-
|
|
9107
|
+
const span = contentSpan(this, axis, null, this);
|
|
9108
|
+
restoreShrink(shrunk);
|
|
9109
|
+
return span;
|
|
8581
9110
|
}
|
|
8582
9111
|
// Height takes two passes. The first is the tree at its real width with
|
|
8583
9112
|
// no bound on the height, which is where every leaf reports the height
|
|
@@ -8588,36 +9117,108 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
8588
9117
|
// stretches: those are the ones the map above puts back. The widths the
|
|
8589
9118
|
// first pass settled are held across the second (`freezeWidths`), which
|
|
8590
9119
|
// is the only thing keeping it a collapse rather than a second opinion.
|
|
9120
|
+
this._layoutPasses += 1;
|
|
8591
9121
|
yoga.calculateLayout(forWidth, undefined, dir);
|
|
9122
|
+
if (probe && this._probeHeightFloors().marked) {
|
|
9123
|
+
this._writeFloors('height');
|
|
9124
|
+
this._layoutPasses += 1;
|
|
9125
|
+
yoga.calculateLayout(forWidth, undefined, dir);
|
|
9126
|
+
}
|
|
8592
9127
|
const intrinsic = new Map();
|
|
8593
9128
|
captureLeafHeights(this, intrinsic);
|
|
8594
|
-
|
|
8595
|
-
|
|
9129
|
+
const frozen = [];
|
|
9130
|
+
freezeWidths(this, frozen);
|
|
9131
|
+
const shrunk = [];
|
|
9132
|
+
setMeasuringShrink(this, axis, shrunk);
|
|
9133
|
+
this._layoutPasses += 1;
|
|
8596
9134
|
yoga.calculateLayout(forWidth, 0, dir);
|
|
8597
|
-
const span = contentSpan(this, axis, intrinsic,
|
|
8598
|
-
restoreWidths(
|
|
9135
|
+
const span = contentSpan(this, axis, intrinsic, this);
|
|
9136
|
+
restoreWidths(frozen);
|
|
9137
|
+
restoreShrink(shrunk);
|
|
9138
|
+
return span;
|
|
9139
|
+
}
|
|
9140
|
+
|
|
9141
|
+
/**
|
|
9142
|
+
* What the last measurement can no longer answer for, taken off the
|
|
9143
|
+
* nodes and listed (`collectFloorStale`). Run ahead of anything that lays
|
|
9144
|
+
* the tree out, since a pass clears yoga's record of what changed — and
|
|
9145
|
+
* cheap enough to run twice in a frame, because the second walk finds the
|
|
9146
|
+
* marks the first one left.
|
|
9147
|
+
*/
|
|
9148
|
+
_collectFloorStale() {
|
|
9149
|
+
const found = { width: false, height: false };
|
|
9150
|
+
this._floorsStale.clear();
|
|
9151
|
+
// the root's own children are written from here too, and its direction
|
|
9152
|
+
// can move like any node's
|
|
9153
|
+
this._floorsStale.add(this);
|
|
9154
|
+
collectFloorStale(this, this._floorsStale, found, !this._floorsSwept);
|
|
9155
|
+
return found;
|
|
9156
|
+
}
|
|
9157
|
+
|
|
9158
|
+
/** The floors on the children of every node found stale, from the extents
|
|
9159
|
+
* they carry — `writeFloors` for each. */
|
|
9160
|
+
_writeFloors(axis) {
|
|
9161
|
+
for (const node of this._floorsStale) {
|
|
9162
|
+
if (!node.destroyed) writeFloors(node, axis);
|
|
9163
|
+
}
|
|
9164
|
+
}
|
|
9165
|
+
|
|
9166
|
+
/** `probeHeightFloors` over this window's tree. */
|
|
9167
|
+
_probeHeightFloors() {
|
|
9168
|
+
const hit = { marked: false, owed: false };
|
|
9169
|
+
probeHeightFloors(this, this, hit);
|
|
9170
|
+
return hit;
|
|
9171
|
+
}
|
|
9172
|
+
|
|
9173
|
+
/**
|
|
9174
|
+
* Measure the width extents that are stale and write the width floors
|
|
9175
|
+
* from them. The floors on the nodes about to be measured come off first
|
|
9176
|
+
* (a stale extent writes the style's own minimum), which is what keeps a
|
|
9177
|
+
* floor from ratcheting: read back as content, it could only ever grow.
|
|
9178
|
+
*/
|
|
9179
|
+
_measureWidthFloors() {
|
|
9180
|
+
this._writeFloors('width');
|
|
9181
|
+
const span = this._measureContentSpans('width');
|
|
9182
|
+
this._writeFloors('width');
|
|
9183
|
+
return span;
|
|
9184
|
+
}
|
|
9185
|
+
|
|
9186
|
+
/** The same for the heights, at `forWidth`. */
|
|
9187
|
+
_measureHeightFloors(forWidth, probe) {
|
|
9188
|
+
this._writeFloors('height');
|
|
9189
|
+
const span = this._measureContentSpans('height', forWidth, probe);
|
|
9190
|
+
this._writeFloors('height');
|
|
8599
9191
|
return span;
|
|
8600
9192
|
}
|
|
8601
9193
|
|
|
8602
9194
|
_measureMinimum(axis, forWidth) {
|
|
8603
|
-
// Measured from the styles alone
|
|
8604
|
-
//
|
|
8605
|
-
//
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
9195
|
+
// Measured from the styles alone: the stale nodes' own floors come off
|
|
9196
|
+
// in the measurement, and a clean node's extent was measured the same
|
|
9197
|
+
// way before it was floored.
|
|
9198
|
+
this._collectFloorStale();
|
|
9199
|
+
return measuringExactly(() =>
|
|
9200
|
+
Math.ceil(
|
|
9201
|
+
axis === 'width'
|
|
9202
|
+
? this._measureWidthFloors()
|
|
9203
|
+
: this._measureHeightFloors(forWidth, true),
|
|
9204
|
+
),
|
|
8610
9205
|
);
|
|
8611
|
-
|
|
8612
|
-
|
|
9206
|
+
}
|
|
9207
|
+
|
|
9208
|
+
/** The real layout pass: the tree at the window's size, on the pixel grid. */
|
|
9209
|
+
_layoutRoot(width, height) {
|
|
9210
|
+
this._layoutPasses += 1;
|
|
9211
|
+
this.yoga.setWidth(width);
|
|
9212
|
+
this.yoga.setHeight(height);
|
|
9213
|
+
this.yoga.calculateLayout(width, height, this._rootDirection);
|
|
8613
9214
|
}
|
|
8614
9215
|
|
|
8615
9216
|
/**
|
|
8616
9217
|
* Give every flex item in this window's tree the floor CSS calls its
|
|
8617
9218
|
* automatic minimum size, so that `flexShrink`'s default of `1` squeezes a
|
|
8618
9219
|
* row into the space it has without squeezing its contents out of
|
|
8619
|
-
* existence. See `
|
|
8620
|
-
* halves are needed.
|
|
9220
|
+
* existence, and lay the tree out with them. See `writeFloors` for what
|
|
9221
|
+
* that means and why both halves are needed.
|
|
8621
9222
|
*
|
|
8622
9223
|
* Two measurements, in this order because they depend that way round: the
|
|
8623
9224
|
* widths from a pass with no room on offer at all, then — with those floors
|
|
@@ -8626,34 +9227,113 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
8626
9227
|
*
|
|
8627
9228
|
* Nothing about this is per frame: the floors are content, so they survive
|
|
8628
9229
|
* every frame that did not change any (`_floorsDirty`), which is what keeps
|
|
8629
|
-
* a wheel notch to the one layout pass it always was.
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
9230
|
+
* a wheel notch to the one layout pass it always was. And nothing about
|
|
9231
|
+
* it is per node either: every node keeps the extent it was last measured
|
|
9232
|
+
* at, and a measurement re-reads only the nodes whose subtree changed
|
|
9233
|
+
* (`collectFloorStale`), taking the rest at the number they carry. So a
|
|
9234
|
+
* padding change on a container measures the container and nothing
|
|
9235
|
+
* below it, a row that mounts measures itself alone, and a colour change
|
|
9236
|
+
* measures nothing.
|
|
9237
|
+
*
|
|
9238
|
+
* The passes are paid for only where a floor is going to be **written**
|
|
9239
|
+
* from what they find. The width pass runs when a stale node is one on a
|
|
9240
|
+
* row's main axis; and the heights are settled the other way round — the
|
|
9241
|
+
* real layout runs first, `probeHeightFloors` walks the nodes it moved
|
|
9242
|
+
* and asks each leaf whether its height at its new width is the height
|
|
9243
|
+
* it had, and only if one says otherwise (or content changed under a
|
|
9244
|
+
* node a floor is written on) do the two height passes run and the
|
|
9245
|
+
* layout with them. A relayout of a large tree whose labels all still
|
|
9246
|
+
* fit — a panel toggle, a theme switch, a resize that wraps nothing — is
|
|
9247
|
+
* one pass over yoga where it was four.
|
|
9248
|
+
*/
|
|
9249
|
+
_applyContentFloors(width, height) {
|
|
9250
|
+
const found = this._collectFloorStale();
|
|
8634
9251
|
measuringExactly(() => {
|
|
8635
|
-
|
|
8636
|
-
this.
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
this.
|
|
8640
|
-
writeContentFloors(this, 'height', heights, this._floored);
|
|
9252
|
+
if (found.width) this._measureWidthFloors();
|
|
9253
|
+
else this._writeFloors('width');
|
|
9254
|
+
// from the extents on hand; a stale one writes the style's minimum,
|
|
9255
|
+
// which is the floor coming off ahead of its measurement below
|
|
9256
|
+
this._writeFloors('height');
|
|
8641
9257
|
});
|
|
9258
|
+
let heights = found.height;
|
|
9259
|
+
let probed = false;
|
|
9260
|
+
if (!heights) {
|
|
9261
|
+
this._layoutRoot(width, height);
|
|
9262
|
+
heights = this._probeHeightFloors().owed;
|
|
9263
|
+
probed = true;
|
|
9264
|
+
}
|
|
9265
|
+
if (heights) {
|
|
9266
|
+
measuringExactly(() => this._measureHeightFloors(width, !probed));
|
|
9267
|
+
this._layoutRoot(width, height);
|
|
9268
|
+
}
|
|
8642
9269
|
this._floorsDirty = false;
|
|
9270
|
+
this._floorsContentDirty = false;
|
|
9271
|
+
this._floorsSwept = true;
|
|
8643
9272
|
this._floorsWidth = width;
|
|
8644
9273
|
}
|
|
8645
9274
|
|
|
8646
|
-
/**
|
|
8647
|
-
*
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
9275
|
+
/**
|
|
9276
|
+
* Answer a live resize with the floors already in hand, and measure fresh
|
|
9277
|
+
* ones once the drag is over.
|
|
9278
|
+
*
|
|
9279
|
+
* The floors were half of a relayout on a large tree — three extra layout
|
|
9280
|
+
* passes and their walks, measured at 21 of a 44ms frame on 3,600 nodes
|
|
9281
|
+
* (`npm run bench:presenters -- --scenario=layout`) before they were
|
|
9282
|
+
* measured incrementally — and a resize is the one layout change they
|
|
9283
|
+
* cannot follow at input rate: AppKit's resize loop calls the frame for
|
|
9284
|
+
* every pointer move, from inside the event, and the next move waits for
|
|
9285
|
+
* the frame. So a tick of a drag lays the tree out against the floors the
|
|
9286
|
+
* last measurement left, which are exact along the main axis (a
|
|
9287
|
+
* min-content width is content, and the content did not move) and a
|
|
9288
|
+
* frame stale for wrapped text along the other, and the frame after the
|
|
9289
|
+
* release measures once and lays out again — "answer the input, then
|
|
9290
|
+
* catch up". Only while the window says it is being resized live
|
|
9291
|
+
* (`liveResizing`, the Cocoa window's reading of AppKit's flag; an X
|
|
9292
|
+
* window has no such thing and takes the measured path every time), only
|
|
9293
|
+
* when the floors exist to reuse, and never over a content change the
|
|
9294
|
+
* floors have not seen — a row that mounted mid-drag has no floor at all,
|
|
9295
|
+
* and no floor is the collapse #249 exists to prevent.
|
|
9296
|
+
*/
|
|
9297
|
+
_deferContentFloors(width) {
|
|
9298
|
+
// nothing to measure: `_applyContentFloors` returns at once, and a
|
|
9299
|
+
// catch-up frame would owe nothing
|
|
9300
|
+
if (!this._floorsDirty && this._floorsWidth === width) return false;
|
|
9301
|
+
return (
|
|
9302
|
+
this.window?.liveResizing === true &&
|
|
9303
|
+
this._floorsWidth != null &&
|
|
9304
|
+
!this._floorsContentDirty
|
|
9305
|
+
);
|
|
9306
|
+
}
|
|
9307
|
+
|
|
9308
|
+
/**
|
|
9309
|
+
* The frame a deferred measurement owes: a full relayout with fresh
|
|
9310
|
+
* floors, run on the first frame tick after the live resize ends. One at
|
|
9311
|
+
* a time — a drag is many ticks, and the catch-up waits for the last of
|
|
9312
|
+
* them rather than following each.
|
|
9313
|
+
*/
|
|
9314
|
+
_scheduleFloorsCatchUp() {
|
|
9315
|
+
if (this._floorsCatchUp) return;
|
|
9316
|
+
this._floorsCatchUp = true;
|
|
9317
|
+
const schedule =
|
|
9318
|
+
typeof this.window?.requestAnimationFrame === 'function'
|
|
9319
|
+
? (cb) => this.window.requestAnimationFrame(cb)
|
|
9320
|
+
: (cb) => setImmediate(cb);
|
|
9321
|
+
const run = () => {
|
|
9322
|
+
if (this.destroyed || !this.window) {
|
|
9323
|
+
this._floorsCatchUp = false;
|
|
9324
|
+
return;
|
|
9325
|
+
}
|
|
9326
|
+
// still dragging (a pump tick inside a pause of the drag): wait on
|
|
9327
|
+
if (this.window.liveResizing) {
|
|
9328
|
+
schedule(run);
|
|
9329
|
+
return;
|
|
9330
|
+
}
|
|
9331
|
+
this._floorsCatchUp = false;
|
|
9332
|
+
this._floorsDirty = true;
|
|
9333
|
+
this.invalidate(true, null, 'resize');
|
|
9334
|
+
this.flush();
|
|
9335
|
+
};
|
|
9336
|
+
schedule(run);
|
|
8657
9337
|
}
|
|
8658
9338
|
|
|
8659
9339
|
/**
|
|
@@ -10536,6 +11216,9 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10536
11216
|
// offset applied during `absolutize` and leaves every yoga node exactly
|
|
10537
11217
|
// as it was, at input rate, on the biggest trees in any app.
|
|
10538
11218
|
if (reason !== 'scroll') this._floorsDirty = true;
|
|
11219
|
+
if (reason !== 'scroll' && reason !== 'resize') {
|
|
11220
|
+
this._floorsContentDirty = true;
|
|
11221
|
+
}
|
|
10539
11222
|
}
|
|
10540
11223
|
// A layout change with no bound named repaints everything, because a
|
|
10541
11224
|
// reflow can move any node and one that moved leaves stale pixels at a
|
|
@@ -10627,7 +11310,7 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10627
11310
|
// as a list of rects rather than one box around them all, so two changes
|
|
10628
11311
|
// at opposite corners of the window no longer repaint everything
|
|
10629
11312
|
// between them.
|
|
10630
|
-
this._damage = addDamageRect(this._damage, bounds);
|
|
11313
|
+
this._damage = addDamageRect(this._damage, bounds, this._damageRectCap());
|
|
10631
11314
|
}
|
|
10632
11315
|
this.needsPaint = true;
|
|
10633
11316
|
// Recorded before the `_scheduled` gate, not inside it: the debt is
|
|
@@ -10673,7 +11356,12 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10673
11356
|
// A window that realized after a loop registered has one now
|
|
10674
11357
|
if (this._loopNodes.size && !this._loopWatch) this._watchLoops();
|
|
10675
11358
|
this._advanceAnimations(now());
|
|
10676
|
-
if (this.needsLayout)
|
|
11359
|
+
if (this.needsLayout) {
|
|
11360
|
+
// before `_refit` lays anything out: a pass clears yoga's record of
|
|
11361
|
+
// which subtrees changed, and the floors are measured from that record
|
|
11362
|
+
this._collectFloorStale();
|
|
11363
|
+
this._refit();
|
|
11364
|
+
}
|
|
10677
11365
|
const width = this.window.width ?? this._requestedSize?.width ?? 0;
|
|
10678
11366
|
const height = this.window.height ?? this._requestedSize?.height ?? 0;
|
|
10679
11367
|
let layoutMoved = false;
|
|
@@ -10687,24 +11375,30 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10687
11375
|
// ledger's rect moves with the shift (issue #398).
|
|
10688
11376
|
this._laidOut = true;
|
|
10689
11377
|
this._resolveSizeQueries(width, height);
|
|
10690
|
-
this.
|
|
10691
|
-
|
|
10692
|
-
this.
|
|
10693
|
-
|
|
11378
|
+
if (!this._floorsDirty && this._floorsWidth === width) {
|
|
11379
|
+
this._layoutRoot(width, height);
|
|
11380
|
+
} else if (this._deferContentFloors(width)) {
|
|
11381
|
+
this._scheduleFloorsCatchUp();
|
|
11382
|
+
this._layoutRoot(width, height);
|
|
11383
|
+
} else {
|
|
11384
|
+
this._applyContentFloors(width, height);
|
|
11385
|
+
}
|
|
10694
11386
|
this.abs = { x: 0, y: 0, width, height };
|
|
10695
11387
|
this._placed = true;
|
|
10696
11388
|
// the root's rect is written here, not through _assignAbs, so its
|
|
10697
11389
|
// cached hit reach is dropped here too (children bubble their own)
|
|
10698
11390
|
this._hitBoundsCache = null;
|
|
11391
|
+
this._paintBoundsCache = null;
|
|
10699
11392
|
// A bounded frame watches the walk: whatever this pass actually moved
|
|
10700
11393
|
// claims its old and new rects through the sink, and the frame stays
|
|
10701
11394
|
// a few rects instead of the whole window. An unbounded frame skips
|
|
10702
11395
|
// the bookkeeping — it repaints everything anyway.
|
|
10703
11396
|
if (this._damage !== FULL_DAMAGE) {
|
|
11397
|
+
const cap = this._damageRectCap();
|
|
10704
11398
|
layoutDiffSink = (rect) => {
|
|
10705
11399
|
if (this._damage === FULL_DAMAGE) return;
|
|
10706
11400
|
layoutMoved = true;
|
|
10707
|
-
this._damage = addDamageRect(this._damage, rect);
|
|
11401
|
+
this._damage = addDamageRect(this._damage, rect, cap);
|
|
10708
11402
|
};
|
|
10709
11403
|
}
|
|
10710
11404
|
try {
|
|
@@ -10736,7 +11430,11 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
10736
11430
|
if (sv && !sv._recordBlitClaim(after)) {
|
|
10737
11431
|
sv._pendingBlitFrom = BLIT_POISONED;
|
|
10738
11432
|
}
|
|
10739
|
-
this._damage = addDamageRect(
|
|
11433
|
+
this._damage = addDamageRect(
|
|
11434
|
+
this._damage,
|
|
11435
|
+
after,
|
|
11436
|
+
this._damageRectCap(),
|
|
11437
|
+
);
|
|
10740
11438
|
}
|
|
10741
11439
|
this._reflowed.clear();
|
|
10742
11440
|
} else if (this._reflowed.size) {
|
|
@@ -11458,6 +12156,19 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
11458
12156
|
* has to clear the flag, so it degrades to a full repaint rather than
|
|
11459
12157
|
* painting nothing.
|
|
11460
12158
|
*/
|
|
12159
|
+
/**
|
|
12160
|
+
* How many rects a frame's damage may hold before `addDamageRect` merges
|
|
12161
|
+
* the closest pair. Four on X11 (`MAX_DAMAGE_RECTS`), where every pass
|
|
12162
|
+
* costs the server a clip mask; a backend whose pass is a client-side
|
|
12163
|
+
* clip and a culled walk says so on its window (`damageRectCap`) and
|
|
12164
|
+
* keeps more of them — a clock, a graph and a status row ticking in one
|
|
12165
|
+
* frame stay three small rects instead of the box around all three.
|
|
12166
|
+
*/
|
|
12167
|
+
_damageRectCap() {
|
|
12168
|
+
const cap = this.window?.damageRectCap;
|
|
12169
|
+
return Number.isInteger(cap) && cap > 0 ? cap : MAX_DAMAGE_RECTS;
|
|
12170
|
+
}
|
|
12171
|
+
|
|
11461
12172
|
_takeDamage(width, height) {
|
|
11462
12173
|
const damage = this._damage;
|
|
11463
12174
|
this._damage = null;
|