gesso-core 0.6.19 → 0.6.20
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/CHANGELOG.md +14 -0
- package/dist/{CanvasSurface-tAao5BLh.js → CanvasSurface-VUJRrcXl.js} +586 -80
- package/dist/CanvasSurface-VUJRrcXl.js.map +1 -0
- package/dist/{index-8yhrkCnb.d.ts → index-lzNLHD-1.d.ts} +241 -25
- package/dist/index.d.ts +1 -1
- package/dist/index.js +248 -9
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +1 -1
- package/dist/CanvasSurface-tAao5BLh.js.map +0 -1
|
@@ -804,6 +804,10 @@ var DirtyNodeSet = class {
|
|
|
804
804
|
isEmpty() {
|
|
805
805
|
return this.nodes.size === 0;
|
|
806
806
|
}
|
|
807
|
+
/** The nodes in the set, without draining it. Not to be changed while walked. */
|
|
808
|
+
values() {
|
|
809
|
+
return this.nodes.values();
|
|
810
|
+
}
|
|
807
811
|
/**
|
|
808
812
|
* Whether any node in the set carries one of `flags`, without
|
|
809
813
|
* draining it.
|
|
@@ -3919,6 +3923,19 @@ const UiProperties = {
|
|
|
3919
3923
|
inherited: false,
|
|
3920
3924
|
affects: 6
|
|
3921
3925
|
}),
|
|
3926
|
+
/**
|
|
3927
|
+
* Whether a scroll container shows its overlay scrollbars: \`'auto'\`
|
|
3928
|
+
* (the default) while scrolling, \`'none'\` never, and then they cannot
|
|
3929
|
+
* be grabbed either. For a container scrolled by something else, as a
|
|
3930
|
+
* picture is moved inside a window onto it, where a bar would claim
|
|
3931
|
+
* the person could scroll it.
|
|
3932
|
+
*/
|
|
3933
|
+
scrollbars: defineProperty({
|
|
3934
|
+
name: "scrollbars",
|
|
3935
|
+
defaultValue: void 0,
|
|
3936
|
+
inherited: false,
|
|
3937
|
+
affects: 6
|
|
3938
|
+
}),
|
|
3922
3939
|
inset: defineProperty({
|
|
3923
3940
|
name: "inset",
|
|
3924
3941
|
defaultValue: void 0,
|
|
@@ -5161,6 +5178,36 @@ var UiGraph = class {
|
|
|
5161
5178
|
this.nodeRemovedListener?.(node);
|
|
5162
5179
|
for (let i = removed.length - 1; i > 0; i--) this.detachNode(removed[i]);
|
|
5163
5180
|
}
|
|
5181
|
+
/**
|
|
5182
|
+
* Moves a child before \`reference\` among its own siblings, or to the
|
|
5183
|
+
* end when that is null. Its parent is unchanged, and so is everything
|
|
5184
|
+
* it inherits, so its subtree is not visited: a lazy list recycling its
|
|
5185
|
+
* rows moves one from end to end on every frame of a scroll, and
|
|
5186
|
+
* re-inheriting walked the whole row each time.
|
|
5187
|
+
*/
|
|
5188
|
+
moveWithin(child, reference) {
|
|
5189
|
+
const parent = child.parent;
|
|
5190
|
+
if (parent === null) throw new Error(`Node '${child.id}' has no parent to move within.`);
|
|
5191
|
+
if (reference !== null && reference.parent !== parent) throw new Error(`Reference node '${reference.id}' is not a sibling of '${child.id}'.`);
|
|
5192
|
+
if (child === reference || child.nextSibling === reference) return;
|
|
5193
|
+
this.detachNode(child);
|
|
5194
|
+
parent.childOrderVersion++;
|
|
5195
|
+
child.parent = parent;
|
|
5196
|
+
if (reference === null) {
|
|
5197
|
+
const last = parent.lastChild;
|
|
5198
|
+
child.previousSibling = last;
|
|
5199
|
+
if (last !== null) last.nextSibling = child;
|
|
5200
|
+
else parent.firstChild = child;
|
|
5201
|
+
parent.lastChild = child;
|
|
5202
|
+
return;
|
|
5203
|
+
}
|
|
5204
|
+
const previous = reference.previousSibling;
|
|
5205
|
+
if (previous !== null) previous.nextSibling = child;
|
|
5206
|
+
else parent.firstChild = child;
|
|
5207
|
+
child.previousSibling = previous;
|
|
5208
|
+
child.nextSibling = reference;
|
|
5209
|
+
reference.previousSibling = child;
|
|
5210
|
+
}
|
|
5164
5211
|
detachNode(node) {
|
|
5165
5212
|
const parent = node.parent;
|
|
5166
5213
|
if (!parent) return;
|
|
@@ -9379,6 +9426,7 @@ function scrollbarThumb(rec, axis) {
|
|
|
9379
9426
|
return thumbFor(rec, axis);
|
|
9380
9427
|
}
|
|
9381
9428
|
function thumbFor(rec, axis) {
|
|
9429
|
+
if (rec.hidesScrollbars) return null;
|
|
9382
9430
|
const viewport = axis === "y" ? rec.height : rec.width;
|
|
9383
9431
|
const content = axis === "y" ? rec.contentHeight : rec.contentWidth;
|
|
9384
9432
|
if (!(content > viewport) || viewport <= 0) return null;
|
|
@@ -10930,6 +10978,11 @@ var UiWheelController = class {
|
|
|
10930
10978
|
* wheel of a burst land correctly even though the first could not.
|
|
10931
10979
|
*/
|
|
10932
10980
|
lastWheelTarget = null;
|
|
10981
|
+
/**
|
|
10982
|
+
* The container the wheel last scrolled, for a frame's report of where
|
|
10983
|
+
* scrolling put it. Null until the wheel has scrolled something.
|
|
10984
|
+
*/
|
|
10985
|
+
lastScrolled = null;
|
|
10933
10986
|
wheel(x, y, deltaX, deltaY, modifiers = noKeyModifiers(), deltaMode = 0, wheelDeltaY, at) {
|
|
10934
10987
|
const target = this.hitTester.hitTest(x, y)?.node ?? null;
|
|
10935
10988
|
this.lastWheelTarget = target;
|
|
@@ -11082,7 +11135,8 @@ var UiWheelController = class {
|
|
|
11082
11135
|
dirX: 0,
|
|
11083
11136
|
dirY: 0,
|
|
11084
11137
|
quiet: 0,
|
|
11085
|
-
fresh: false
|
|
11138
|
+
fresh: false,
|
|
11139
|
+
fitted: false
|
|
11086
11140
|
};
|
|
11087
11141
|
this.pacing.set(container, paced);
|
|
11088
11142
|
}
|
|
@@ -11098,9 +11152,11 @@ var UiWheelController = class {
|
|
|
11098
11152
|
});
|
|
11099
11153
|
while (paced.samples.length > 2 && paced.samples[0].t < this.eventTime - VELOCITY_WINDOW_MS) paced.samples.shift();
|
|
11100
11154
|
this.scrollSink.scrollBy(container, 0, 0, "instant");
|
|
11155
|
+
this.lastScrolled = container;
|
|
11101
11156
|
return;
|
|
11102
11157
|
}
|
|
11103
11158
|
this.scrollSink.scrollBy(container, dx, dy, behavior);
|
|
11159
|
+
this.lastScrolled = container;
|
|
11104
11160
|
}
|
|
11105
11161
|
/**
|
|
11106
11162
|
* A container's state as a step should judge it: during a paced gesture,
|
|
@@ -11128,16 +11184,15 @@ var UiWheelController = class {
|
|
|
11128
11184
|
* comes a varying time after the last step: applied as they arrive, a
|
|
11129
11185
|
* steady flick moves the page by uneven amounts, which reads as
|
|
11130
11186
|
* judder. Evening that out by waiting for steps puts the page behind
|
|
11131
|
-
* the hand. So this does what a browser does for its own scrolling:
|
|
11132
|
-
*
|
|
11133
|
-
*
|
|
11134
|
-
*
|
|
11135
|
-
* even, and nothing trails.
|
|
11187
|
+
* the hand. So this does what a browser does for its own scrolling: it
|
|
11188
|
+
* resamples the steps at the frame's time, reading a line fitted through
|
|
11189
|
+
* the last `VELOCITY_WINDOW_MS` of them. Frames come out even, and
|
|
11190
|
+
* nothing trails.
|
|
11136
11191
|
*
|
|
11137
|
-
* A gesture's first step, with no
|
|
11138
|
-
* steps stop, the
|
|
11139
|
-
*
|
|
11140
|
-
*
|
|
11192
|
+
* A gesture's first step, with no line yet, goes as it is. Once the
|
|
11193
|
+
* steps stop, the page eases over a few frames to exactly where they add
|
|
11194
|
+
* up to; a trackpad's momentum slows to nothing first, so there is next
|
|
11195
|
+
* to nothing to take up.
|
|
11141
11196
|
*
|
|
11142
11197
|
* True while a gesture is still being paced, so the host keeps frames
|
|
11143
11198
|
* coming until it has settled.
|
|
@@ -11157,14 +11212,38 @@ var UiWheelController = class {
|
|
|
11157
11212
|
let y = paced.totalY;
|
|
11158
11213
|
const samples = paced.samples;
|
|
11159
11214
|
const last = samples[samples.length - 1];
|
|
11160
|
-
|
|
11161
|
-
for (let k = samples.length - 2; k >= 0 && last !== void 0 && samples[k].t >= last.t - RECENT_MS; k--) first = samples[k];
|
|
11215
|
+
const first = samples[0];
|
|
11162
11216
|
const since = time === void 0 || last === void 0 ? -1 : time - last.t;
|
|
11163
|
-
|
|
11164
|
-
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
|
|
11217
|
+
const at = time === void 0 ? void 0 : time - RESAMPLE_MS;
|
|
11218
|
+
if (at !== void 0 && last !== void 0 && first !== void 0 && since >= -1 && at - last.t < STALE_MS && samples.length >= MIN_FIT_STEPS && last.t - first.t >= 2) {
|
|
11219
|
+
let meanT = 0;
|
|
11220
|
+
let meanX = 0;
|
|
11221
|
+
let meanY = 0;
|
|
11222
|
+
for (const sample of samples) {
|
|
11223
|
+
meanT += sample.t;
|
|
11224
|
+
meanX += sample.x;
|
|
11225
|
+
meanY += sample.y;
|
|
11226
|
+
}
|
|
11227
|
+
meanT /= samples.length;
|
|
11228
|
+
meanX /= samples.length;
|
|
11229
|
+
meanY /= samples.length;
|
|
11230
|
+
let varT = 0;
|
|
11231
|
+
let covX = 0;
|
|
11232
|
+
let covY = 0;
|
|
11233
|
+
for (const sample of samples) {
|
|
11234
|
+
const dt = sample.t - meanT;
|
|
11235
|
+
varT += dt * dt;
|
|
11236
|
+
covX += dt * (sample.x - meanX);
|
|
11237
|
+
covY += dt * (sample.y - meanY);
|
|
11238
|
+
}
|
|
11239
|
+
x = meanX + covX / varT * (at - meanT);
|
|
11240
|
+
y = meanY + covY / varT * (at - meanT);
|
|
11241
|
+
paced.fitted = true;
|
|
11242
|
+
} else if (paced.fitted) {
|
|
11243
|
+
const drawnX = paced.lastX - paced.baseX;
|
|
11244
|
+
const drawnY = paced.lastY - paced.baseY;
|
|
11245
|
+
x = Math.abs(x - drawnX) < SETTLED_PX ? x : drawnX + (x - drawnX) * SETTLE_RATE;
|
|
11246
|
+
y = Math.abs(y - drawnY) < SETTLED_PX ? y : drawnY + (y - drawnY) * SETTLE_RATE;
|
|
11168
11247
|
}
|
|
11169
11248
|
const state = this.scrollSink.containerState(container);
|
|
11170
11249
|
if (state === void 0) {
|
|
@@ -11190,7 +11269,7 @@ var UiWheelController = class {
|
|
|
11190
11269
|
if (dx !== 0 || dy !== 0) this.scrollSink.scrollBy(container, dx, dy, "instant");
|
|
11191
11270
|
paced.lastX = toX;
|
|
11192
11271
|
paced.lastY = toY;
|
|
11193
|
-
if (toX === Math.min(Math.max(paced.baseX + paced.totalX, 0), state.maxScrollX) && toY === Math.min(Math.max(paced.baseY + paced.totalY, 0), state.maxScrollY) && paced.quiet >= QUIET_FRAMES) this.pacing.delete(container);
|
|
11272
|
+
if (toX === Math.min(Math.max(paced.baseX + paced.totalX, 0), state.maxScrollX) && toY === Math.min(Math.max(paced.baseY + paced.totalY, 0), state.maxScrollY) && paced.quiet >= QUIET_FRAMES && (since < 0 || since >= STALE_MS)) this.pacing.delete(container);
|
|
11194
11273
|
}
|
|
11195
11274
|
return this.pacing.size > 0;
|
|
11196
11275
|
}
|
|
@@ -11244,18 +11323,29 @@ function isScrollContainer(node) {
|
|
|
11244
11323
|
* hand from a trackpad to a mouse.
|
|
11245
11324
|
*/
|
|
11246
11325
|
const PRECISE_MEMORY_MS = 400;
|
|
11247
|
-
/** How far back the velocity is read: a few of a trackpad's steps. */
|
|
11248
|
-
const VELOCITY_WINDOW_MS = 40;
|
|
11249
|
-
/** How long after the last step its velocity is still trusted. */
|
|
11250
|
-
const STALE_MS = 40;
|
|
11251
11326
|
/**
|
|
11252
|
-
*
|
|
11253
|
-
*
|
|
11254
|
-
*
|
|
11327
|
+
* How far back the steps are fitted: five or six of a trackpad's. Less, and
|
|
11328
|
+
* the line follows each step's rounding; more, and it lags a flick that is
|
|
11329
|
+
* speeding up or slowing down. Measured on a real flick on a Mac, 48 ms left
|
|
11330
|
+
* a sixth of the frame-to-frame variation of the last-few-steps velocity,
|
|
11331
|
+
* with no frame standing still and the page no further behind the hand.
|
|
11255
11332
|
*/
|
|
11256
|
-
const
|
|
11257
|
-
/** How
|
|
11258
|
-
const
|
|
11333
|
+
const VELOCITY_WINDOW_MS = 48;
|
|
11334
|
+
/** How long after the last step its line is still trusted. */
|
|
11335
|
+
const STALE_MS = 40;
|
|
11336
|
+
/**
|
|
11337
|
+
* How long before the frame the line is read. Each millisecond is one more
|
|
11338
|
+
* of lag and less of a guess: measured on recorded flicks, 12 left the
|
|
11339
|
+
* worst frames a third as uneven as reading at the frame, and the page
|
|
11340
|
+
* about 11 ms behind the hand, against 15 for steps applied as they come.
|
|
11341
|
+
*/
|
|
11342
|
+
const RESAMPLE_MS = 12;
|
|
11343
|
+
/** Steps a line is fitted through: two are a line through their rounding. */
|
|
11344
|
+
const MIN_FIT_STEPS = 3;
|
|
11345
|
+
/** How much of the way to where the steps add up to a frame goes once they stop. */
|
|
11346
|
+
const SETTLE_RATE = .35;
|
|
11347
|
+
/** Close enough to where the steps add up to, to go the rest at once. */
|
|
11348
|
+
const SETTLED_PX = .25;
|
|
11259
11349
|
/** The display's interval before one has been measured. */
|
|
11260
11350
|
const DEFAULT_FRAME_MS = 1e3 / 60;
|
|
11261
11351
|
/** Quiet frames, once settled, after which a gesture is over. */
|
|
@@ -13985,6 +14075,8 @@ var LayoutRecord = class {
|
|
|
13985
14075
|
* multiline, the overlay scrollbars.
|
|
13986
14076
|
*/
|
|
13987
14077
|
scrollsText = false;
|
|
14078
|
+
/** A scroll container set to show no scrollbars (\`scrollbars: 'none'\`). */
|
|
14079
|
+
hidesScrollbars = false;
|
|
13988
14080
|
/**
|
|
13989
14081
|
* A multiline editable, which shows the overlay scrollbars a scroll
|
|
13990
14082
|
* container does over its own text. A single-line field scrolls
|
|
@@ -14247,6 +14339,7 @@ var LayoutRecord = class {
|
|
|
14247
14339
|
this.paintReachBottom = 0;
|
|
14248
14340
|
this.scrollable = false;
|
|
14249
14341
|
this.scrollsText = false;
|
|
14342
|
+
this.hidesScrollbars = false;
|
|
14250
14343
|
this.textScrollbars = false;
|
|
14251
14344
|
this.mirrored = false;
|
|
14252
14345
|
this.sticky = false;
|
|
@@ -18266,6 +18359,7 @@ var LayoutEngine = class {
|
|
|
18266
18359
|
const overflow = props.get("overflow");
|
|
18267
18360
|
rec.scrollable = node.type === "scroll-view" || overflow === "scroll" || overflow === "auto" || overflow === "hidden" && scrollLeaderOf(node) !== null;
|
|
18268
18361
|
rec.scrollsText = node.type === "editable-text";
|
|
18362
|
+
rec.hidesScrollbars = props.get("scrollbars") === "none";
|
|
18269
18363
|
rec.textScrollbars = rec.scrollsText && props.get("multiline") === true;
|
|
18270
18364
|
rec.mirrored = (rec.scrollable || rec.textScrollbars) && this.startEdge(node) === "right";
|
|
18271
18365
|
rec.clips = rec.scrollable || overflow === "hidden" || node.type === "editable-text";
|
|
@@ -20441,15 +20535,10 @@ var ScrollLayerCache = class {
|
|
|
20441
20535
|
* is how every one of those ends.
|
|
20442
20536
|
*/
|
|
20443
20537
|
refused = /* @__PURE__ */ new Set();
|
|
20444
|
-
geometryVersion = NaN;
|
|
20445
20538
|
dpr = 0;
|
|
20446
20539
|
surfaceWidth = 0;
|
|
20447
20540
|
surfaceHeight = 0;
|
|
20448
20541
|
frame = null;
|
|
20449
|
-
/** Whether this frame changed nothing a layer could not absorb: no layout, children, content or environment. */
|
|
20450
|
-
calm = false;
|
|
20451
|
-
/** Nodes this frame dirtied, and every ancestor of each, less a container that only scrolled. */
|
|
20452
|
-
disturbed = /* @__PURE__ */ new Set();
|
|
20453
20542
|
/** Consecutive frames each container without a layer has scrolled quietly. See `SCROLL_LAYER_SETTLE_FRAMES`. */
|
|
20454
20543
|
streaks = /* @__PURE__ */ new WeakMap();
|
|
20455
20544
|
/**
|
|
@@ -20481,46 +20570,26 @@ var ScrollLayerCache = class {
|
|
|
20481
20570
|
/**
|
|
20482
20571
|
* Settles which layers this frame's changes leave standing, before
|
|
20483
20572
|
* anything is drawn. Every rule here drops; none keeps. A frame the
|
|
20484
|
-
* caller could not vouch for drops them all.
|
|
20573
|
+
* caller could not vouch for drops them all. What changed inside a
|
|
20574
|
+
* container is not settled here but drawn over: see \`damage\`.
|
|
20485
20575
|
*/
|
|
20486
20576
|
beginFrame(changes, dpr, surfaceWidth, surfaceHeight) {
|
|
20487
20577
|
const interrupted = this.open;
|
|
20488
20578
|
this.open = true;
|
|
20489
|
-
this.
|
|
20490
|
-
this.calm = changes !== void 0;
|
|
20491
|
-
for (const [node, flags] of changes?.frame.entries() ?? []) {
|
|
20492
|
-
if ((flags & -355) !== 0) {
|
|
20493
|
-
this.calm = false;
|
|
20494
|
-
break;
|
|
20495
|
-
}
|
|
20496
|
-
for (let current = (flags & 32) !== 0 ? node.parent : node; current !== null; current = current.parent) this.disturbed.add(current);
|
|
20497
|
-
}
|
|
20498
|
-
if (interrupted || changes === void 0 || dpr !== this.dpr || surfaceWidth !== this.surfaceWidth || surfaceHeight !== this.surfaceHeight || changes.geometryVersion !== this.geometryVersion) {
|
|
20579
|
+
if (interrupted || changes === void 0 || dpr !== this.dpr || surfaceWidth !== this.surfaceWidth || surfaceHeight !== this.surfaceHeight) {
|
|
20499
20580
|
this.dropAll();
|
|
20500
20581
|
this.dpr = dpr;
|
|
20501
20582
|
this.surfaceWidth = surfaceWidth;
|
|
20502
20583
|
this.surfaceHeight = surfaceHeight;
|
|
20503
|
-
this.geometryVersion = changes?.geometryVersion ?? NaN;
|
|
20504
20584
|
this.frame = changes?.frame ?? null;
|
|
20505
20585
|
return;
|
|
20506
20586
|
}
|
|
20507
20587
|
const frame = changes.frame;
|
|
20508
20588
|
this.frame = frame;
|
|
20509
|
-
|
|
20589
|
+
for (const node of this.layers.keys()) if (dirtyAbove(frame, node)) this.forget(node);
|
|
20510
20590
|
for (const [node, flags] of frame.entries()) {
|
|
20511
|
-
if (
|
|
20512
|
-
|
|
20513
|
-
return;
|
|
20514
|
-
}
|
|
20515
|
-
const scrolledOnly = flags === 32;
|
|
20516
|
-
for (let current = scrolledOnly ? node.parent : node; current !== null; current = current.parent) this.forget(current);
|
|
20517
|
-
}
|
|
20518
|
-
for (const node of [...this.layers.keys(), ...this.refused]) for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
20519
|
-
const flags = frame.dirtyFlagsFor(ancestor);
|
|
20520
|
-
if (flags !== 0 && flags !== 32) {
|
|
20521
|
-
this.forget(node);
|
|
20522
|
-
break;
|
|
20523
|
-
}
|
|
20591
|
+
if (flags === 32) continue;
|
|
20592
|
+
for (const refused of this.refused) if (node === refused || contains(node, refused) || contains(refused, node)) this.refused.delete(refused);
|
|
20524
20593
|
}
|
|
20525
20594
|
}
|
|
20526
20595
|
/** Drops the layers the frame did not composite: their container was culled, removed, or drawn directly. */
|
|
@@ -20534,14 +20603,16 @@ var ScrollLayerCache = class {
|
|
|
20534
20603
|
}
|
|
20535
20604
|
/**
|
|
20536
20605
|
* Whether a container with no layer should get one now: it has
|
|
20537
|
-
* scrolled on the last few frames running with nothing
|
|
20538
|
-
*
|
|
20539
|
-
*
|
|
20606
|
+
* scrolled on the last few frames running with nothing above it
|
|
20607
|
+
* changing, which is the evidence a layer would be kept, and it has
|
|
20608
|
+
* not just shown it holds something a layer cannot. What changes
|
|
20609
|
+
* inside it is absorbed item by item (see \`damage\`), so a lazy list
|
|
20610
|
+
* mounting rows as it scrolls keeps its layer.
|
|
20540
20611
|
*/
|
|
20541
20612
|
wants(node) {
|
|
20542
20613
|
const frame = this.frame;
|
|
20543
20614
|
if (frame === null || this.layers.size >= MAX_LAYERS || this.refused.has(node)) return false;
|
|
20544
|
-
if (
|
|
20615
|
+
if (dirtyAbove(frame, node)) {
|
|
20545
20616
|
this.streaks.delete(node);
|
|
20546
20617
|
return false;
|
|
20547
20618
|
}
|
|
@@ -20571,9 +20642,10 @@ var ScrollLayerCache = class {
|
|
|
20571
20642
|
offsetX: placement.offsetX,
|
|
20572
20643
|
offsetY: placement.offsetY,
|
|
20573
20644
|
dpr: this.dpr,
|
|
20574
|
-
scrollX: rec.scrollX,
|
|
20575
|
-
scrollY: rec.scrollY,
|
|
20576
|
-
used: true
|
|
20645
|
+
scrollX: snapScroll(rec.scrollX, this.dpr),
|
|
20646
|
+
scrollY: snapScroll(rec.scrollY, this.dpr),
|
|
20647
|
+
used: true,
|
|
20648
|
+
items: /* @__PURE__ */ new Map()
|
|
20577
20649
|
};
|
|
20578
20650
|
this.layers.set(node, layer);
|
|
20579
20651
|
this.stats.built++;
|
|
@@ -20594,8 +20666,8 @@ var ScrollLayerCache = class {
|
|
|
20594
20666
|
* device pixels, which a copy cannot follow without resampling.
|
|
20595
20667
|
*/
|
|
20596
20668
|
shift(layer, rec) {
|
|
20597
|
-
const exactX = (layer.scrollX - rec.scrollX) * layer.dpr;
|
|
20598
|
-
const exactY = (layer.scrollY - rec.scrollY) * layer.dpr;
|
|
20669
|
+
const exactX = (layer.scrollX - snapScroll(rec.scrollX, layer.dpr)) * layer.dpr;
|
|
20670
|
+
const exactY = (layer.scrollY - snapScroll(rec.scrollY, layer.dpr)) * layer.dpr;
|
|
20599
20671
|
const dx = Math.round(exactX);
|
|
20600
20672
|
const dy = Math.round(exactY);
|
|
20601
20673
|
if (Math.abs(exactX - dx) > EPSILON || Math.abs(exactY - dy) > EPSILON) return null;
|
|
@@ -20633,6 +20705,79 @@ var ScrollLayerCache = class {
|
|
|
20633
20705
|
});
|
|
20634
20706
|
return strips;
|
|
20635
20707
|
}
|
|
20708
|
+
/**
|
|
20709
|
+
* The parts of a layer this frame's changes inside the container left
|
|
20710
|
+
* wrong, in the layer's pixels, or \`'all'\` when that is most of it.
|
|
20711
|
+
* Called after \`shift\`, so the layer's offset is the frame's.
|
|
20712
|
+
*
|
|
20713
|
+
* Judged item by item, an item being one of the container's children
|
|
20714
|
+
* (through any fragments): a lazy list's row, say. An item added,
|
|
20715
|
+
* removed or moved is wrong where it was and where it is; an item with
|
|
20716
|
+
* anything changed inside it is wrong where it is. Whatever none of
|
|
20717
|
+
* that touches is right already, which in a lazy list scrolling is
|
|
20718
|
+
* everything on screen: the rows it mounts and unmounts are in its
|
|
20719
|
+
* overscan, and the spacers that stand for the rest only grow and
|
|
20720
|
+
* shrink off the top and bottom.
|
|
20721
|
+
*/
|
|
20722
|
+
damage(layer, container, layout) {
|
|
20723
|
+
const frame = this.frame;
|
|
20724
|
+
if (frame === null) return "all";
|
|
20725
|
+
const boxes = [];
|
|
20726
|
+
for (const [node, flags] of frame.entries()) {
|
|
20727
|
+
if (node === container) {
|
|
20728
|
+
if ((flags & 128) !== 0) return "all";
|
|
20729
|
+
continue;
|
|
20730
|
+
}
|
|
20731
|
+
const item = itemHolding(container, node);
|
|
20732
|
+
if (item === null) continue;
|
|
20733
|
+
const was = layer.items.get(item);
|
|
20734
|
+
const now = boxOf(layout, item);
|
|
20735
|
+
if (was !== void 0) boxes.push(was);
|
|
20736
|
+
if (now !== void 0 && (was === void 0 || !sameBox(was, now))) boxes.push(now);
|
|
20737
|
+
}
|
|
20738
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20739
|
+
for (const item of itemsOf(container)) {
|
|
20740
|
+
seen.add(item);
|
|
20741
|
+
const now = boxOf(layout, item);
|
|
20742
|
+
const was = layer.items.get(item);
|
|
20743
|
+
if (now === void 0) continue;
|
|
20744
|
+
if (was === void 0) boxes.push(now);
|
|
20745
|
+
else if (!sameBox(was, now)) boxes.push(was, now);
|
|
20746
|
+
}
|
|
20747
|
+
for (const [item, was] of layer.items) if (!seen.has(item)) boxes.push(was);
|
|
20748
|
+
const rects = [];
|
|
20749
|
+
let area = 0;
|
|
20750
|
+
for (const box of boxes) {
|
|
20751
|
+
const rect = this.toLayer(layer, box);
|
|
20752
|
+
if (rect !== null && !rects.some((other) => sameBox(other, rect))) {
|
|
20753
|
+
rects.push(rect);
|
|
20754
|
+
area += rect.width * rect.height;
|
|
20755
|
+
}
|
|
20756
|
+
}
|
|
20757
|
+
return area > layer.width * layer.height * MOSTLY ? "all" : rects;
|
|
20758
|
+
}
|
|
20759
|
+
/** Notes where the container's items are, for the next frame's \`damage\`. */
|
|
20760
|
+
remember(layer, container, layout) {
|
|
20761
|
+
layer.items.clear();
|
|
20762
|
+
for (const item of itemsOf(container)) {
|
|
20763
|
+
const box = boxOf(layout, item);
|
|
20764
|
+
if (box !== void 0) layer.items.set(item, box);
|
|
20765
|
+
}
|
|
20766
|
+
}
|
|
20767
|
+
/** An item's box in the layer's pixels, widened for what paints past it, or null off the layer. */
|
|
20768
|
+
toLayer(layer, box) {
|
|
20769
|
+
const dpr = layer.dpr;
|
|
20770
|
+
const left = Math.max(0, Math.floor(layer.offsetX + dpr * (box.x - layer.scrollX - 8)));
|
|
20771
|
+
const top = Math.max(0, Math.floor(layer.offsetY + dpr * (box.y - layer.scrollY - 8)));
|
|
20772
|
+
const right = Math.min(layer.width, Math.ceil(layer.offsetX + dpr * (box.x + box.width - layer.scrollX + 8)));
|
|
20773
|
+
const bottom = Math.min(layer.height, Math.ceil(layer.offsetY + dpr * (box.y + box.height - layer.scrollY + 8)));
|
|
20774
|
+
return right > left && bottom > top ? {
|
|
20775
|
+
x: left,
|
|
20776
|
+
y: top,
|
|
20777
|
+
width: right - left,
|
|
20778
|
+
height: bottom - top
|
|
20779
|
+
} : null;
|
|
20780
|
+
}
|
|
20636
20781
|
/** Stops building layers for a container until its subtree is next dirtied. */
|
|
20637
20782
|
refuse(node) {
|
|
20638
20783
|
this.drop(node);
|
|
@@ -20655,14 +20800,63 @@ var ScrollLayerCache = class {
|
|
|
20655
20800
|
this.refused.delete(node);
|
|
20656
20801
|
}
|
|
20657
20802
|
};
|
|
20658
|
-
/**
|
|
20803
|
+
/**
|
|
20804
|
+
* Whether an ancestor's properties or environment changed. An inherited
|
|
20805
|
+
* value set on an ancestor marks every descendant that reads it as well
|
|
20806
|
+
* (the graph re-provides it), so what it changes inside the container
|
|
20807
|
+
* is damage like any other; this keeps the old caution for the rest.
|
|
20808
|
+
* An ancestor marked for paint alone is the runtime asking for a frame
|
|
20809
|
+
* (a scrollbar fading, an inspector outline), which nothing inside the
|
|
20810
|
+
* container hears; one laid out again either leaves the container where
|
|
20811
|
+
* it was or moves it, which \`fits\` hears about; one whose children
|
|
20812
|
+
* changed has changed nothing inside this one. The last two happen on
|
|
20813
|
+
* every frame a lazy list mounts a row, since a layout need is marked
|
|
20814
|
+
* all the way up.
|
|
20815
|
+
*/
|
|
20659
20816
|
function dirtyAbove(frame, node) {
|
|
20660
|
-
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent)
|
|
20661
|
-
|
|
20662
|
-
|
|
20663
|
-
|
|
20817
|
+
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) if ((frame.dirtyFlagsFor(ancestor) & INHERITED) !== 0) return true;
|
|
20818
|
+
return false;
|
|
20819
|
+
}
|
|
20820
|
+
/** What an ancestor's dirt can say about how its descendants look. */
|
|
20821
|
+
const INHERITED = 192;
|
|
20822
|
+
/** Whether \`ancestor\` is above \`node\` in the tree. */
|
|
20823
|
+
function contains(ancestor, node) {
|
|
20824
|
+
for (let current = node.parent; current !== null; current = current.parent) if (current === ancestor) return true;
|
|
20664
20825
|
return false;
|
|
20665
20826
|
}
|
|
20827
|
+
/** A container's items: its children, and the children of any fragment among them, in order. */
|
|
20828
|
+
function itemsOf(container) {
|
|
20829
|
+
const items = [];
|
|
20830
|
+
const visit = (parent) => {
|
|
20831
|
+
for (let child = parent.firstChild; child !== null; child = child.nextSibling) if (child.type === "fragment") visit(child);
|
|
20832
|
+
else items.push(child);
|
|
20833
|
+
};
|
|
20834
|
+
visit(container);
|
|
20835
|
+
return items;
|
|
20836
|
+
}
|
|
20837
|
+
/** The item of a container that holds a node, the node itself if it is one, or null outside the container or above its items. */
|
|
20838
|
+
function itemHolding(container, node) {
|
|
20839
|
+
let item = null;
|
|
20840
|
+
for (let current = node; current !== null; current = current.parent) {
|
|
20841
|
+
if (current === container) return item;
|
|
20842
|
+
if (current.type !== "fragment") item = current;
|
|
20843
|
+
}
|
|
20844
|
+
return null;
|
|
20845
|
+
}
|
|
20846
|
+
function sameBox(a, b) {
|
|
20847
|
+
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
|
20848
|
+
}
|
|
20849
|
+
function boxOf(layout, node) {
|
|
20850
|
+
const rec = layout.recordFor(node);
|
|
20851
|
+
return rec === void 0 ? void 0 : {
|
|
20852
|
+
x: rec.x,
|
|
20853
|
+
y: rec.y,
|
|
20854
|
+
width: rec.width,
|
|
20855
|
+
height: rec.height
|
|
20856
|
+
};
|
|
20857
|
+
}
|
|
20858
|
+
/** Damage over this share of a layer repaints all of it in one walk. */
|
|
20859
|
+
const MOSTLY = .5;
|
|
20666
20860
|
/**
|
|
20667
20861
|
* Where a scroll container's viewport lands in device pixels, or null
|
|
20668
20862
|
* when a layer could not reproduce the direct draw exactly: no
|
|
@@ -20708,6 +20902,273 @@ function clearLayer(ctx, width, height) {
|
|
|
20708
20902
|
ctx.clearRect(0, 0, width, height);
|
|
20709
20903
|
}
|
|
20710
20904
|
//#endregion
|
|
20905
|
+
//#region src/rendering/canvas2d/TextRasterCache.ts
|
|
20906
|
+
/**
|
|
20907
|
+
* Text kept as pixels, so a frame that only moved it copies it rather
|
|
20908
|
+
* than drawing its glyphs again.
|
|
20909
|
+
*
|
|
20910
|
+
* **Why.** Drawing glyphs is most of what a frame of text costs:
|
|
20911
|
+
* profiled while flicking through a file in Easel, `fillText` was 1,064
|
|
20912
|
+
* of the renderer's 1,985 ms. A scroll moves every line on screen and
|
|
20913
|
+
* changes none of them, so every one of those glyphs was drawn again
|
|
20914
|
+
* only to land a few pixels from where it was. In WKWebView a frame of
|
|
20915
|
+
* that cost 4 to 7 ms, which is more than a 165 Hz refresh holds.
|
|
20916
|
+
*
|
|
20917
|
+
* **What is kept.** What `drawText` draws for one node, drawn once into
|
|
20918
|
+
* a canvas of its own at the device scale and fraction of a pixel it
|
|
20919
|
+
* lands on, and copied onto the frame from then on. The copy lands on
|
|
20920
|
+
* whole device pixels, so the pixels are the ones a direct draw at that
|
|
20921
|
+
* fraction would have made, and a scroll, which moves by whole device
|
|
20922
|
+
* pixels, keeps the fraction.
|
|
20923
|
+
*
|
|
20924
|
+
* **When it is drawn again.** When anything `drawText` reads changes:
|
|
20925
|
+
* the text, its runs, its font, its colour, its box, the scale, or the
|
|
20926
|
+
* fraction of a pixel it sits at. Each is compared every frame, which is
|
|
20927
|
+
* a few comparisons; a run list or a colour that has not changed is the
|
|
20928
|
+
* same object it was.
|
|
20929
|
+
*
|
|
20930
|
+
* **What it is not for.** Text that changes on most frames (a clock, a
|
|
20931
|
+
* counter, an animated colour) would be drawn twice a frame, into the
|
|
20932
|
+
* cache and out of it. A node redrawn on most of its recent frames is
|
|
20933
|
+
* drawn directly instead, until it has been still for a while.
|
|
20934
|
+
*/
|
|
20935
|
+
var TextRasterCache = class {
|
|
20936
|
+
createCanvas;
|
|
20937
|
+
entries = /* @__PURE__ */ new Map();
|
|
20938
|
+
frame = 0;
|
|
20939
|
+
pixels = 0;
|
|
20940
|
+
/** Draws served from the cache and drawn into it, since the last reset. For budget specs. */
|
|
20941
|
+
stats = {
|
|
20942
|
+
copied: 0,
|
|
20943
|
+
drawn: 0,
|
|
20944
|
+
direct: 0
|
|
20945
|
+
};
|
|
20946
|
+
constructor(createCanvas) {
|
|
20947
|
+
this.createCanvas = createCanvas;
|
|
20948
|
+
}
|
|
20949
|
+
beginFrame() {
|
|
20950
|
+
this.frame++;
|
|
20951
|
+
}
|
|
20952
|
+
/** Lets go of text not drawn lately: removed, scrolled far away, or hidden. */
|
|
20953
|
+
endFrame() {
|
|
20954
|
+
for (const [node, entry] of this.entries) if (this.frame - entry.used > IDLE_FRAMES) this.drop(node, entry);
|
|
20955
|
+
}
|
|
20956
|
+
dropAll() {
|
|
20957
|
+
for (const [node, entry] of this.entries) this.drop(node, entry);
|
|
20958
|
+
}
|
|
20959
|
+
resetStats() {
|
|
20960
|
+
this.stats.copied = 0;
|
|
20961
|
+
this.stats.drawn = 0;
|
|
20962
|
+
this.stats.direct = 0;
|
|
20963
|
+
}
|
|
20964
|
+
/**
|
|
20965
|
+
* Draws a node's text from the cache, drawing it into the cache first
|
|
20966
|
+
* when what it holds is out of date. False when the text should be
|
|
20967
|
+
* drawn directly: a context that cannot say where it draws, one that
|
|
20968
|
+
* rotates or skews, text too large to keep, or text that changes too
|
|
20969
|
+
* often to be worth keeping.
|
|
20970
|
+
*/
|
|
20971
|
+
draw(ctx, node, box, paint, measurer) {
|
|
20972
|
+
const m = ctx.getTransform?.();
|
|
20973
|
+
if (m === void 0 || m.b !== 0 || m.c !== 0 || !(m.a > 0) || !(m.d > 0)) return false;
|
|
20974
|
+
let entry = this.entries.get(node);
|
|
20975
|
+
if (entry !== void 0 && entry.directUntil > this.frame) {
|
|
20976
|
+
entry.used = this.frame;
|
|
20977
|
+
this.stats.direct++;
|
|
20978
|
+
return false;
|
|
20979
|
+
}
|
|
20980
|
+
let extent = entry !== void 0 && entry.drawnFrom(paint, box, m.a, m.d) ? entry.extent : null;
|
|
20981
|
+
if (extent === null) {
|
|
20982
|
+
extent = inkExtent(box, paint, measurer);
|
|
20983
|
+
if (extent === null) return false;
|
|
20984
|
+
}
|
|
20985
|
+
const left = m.a * (box.x + extent.left) + m.e;
|
|
20986
|
+
const top = m.d * (box.y + extent.top) + m.f;
|
|
20987
|
+
const x = Math.floor(left);
|
|
20988
|
+
const y = Math.floor(top);
|
|
20989
|
+
const width = Math.ceil(m.a * (extent.right - extent.left) + (left - x)) + 1;
|
|
20990
|
+
const height = Math.ceil(m.d * (extent.bottom - extent.top) + (top - y)) + 1;
|
|
20991
|
+
if (width * height > MAX_ENTRY_PIXELS) return false;
|
|
20992
|
+
const offsetX = m.e - x;
|
|
20993
|
+
const offsetY = m.f - y;
|
|
20994
|
+
let raster = entry ?? null;
|
|
20995
|
+
if (raster === null || !raster.holds(paint, box, m.a, m.d, offsetX, offsetY, width, height)) {
|
|
20996
|
+
raster = this.redraw(node, entry, paint, box, measurer, m.a, m.d, offsetX, offsetY, width, height, extent);
|
|
20997
|
+
if (raster === null) return false;
|
|
20998
|
+
}
|
|
20999
|
+
raster.used = this.frame;
|
|
21000
|
+
ctx.save();
|
|
21001
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
21002
|
+
ctx.drawImage(raster.canvas, x, y, raster.canvasWidth, raster.canvasHeight);
|
|
21003
|
+
ctx.restore();
|
|
21004
|
+
this.stats.copied++;
|
|
21005
|
+
return true;
|
|
21006
|
+
}
|
|
21007
|
+
redraw(node, previous, paint, box, measurer, scaleX, scaleY, offsetX, offsetY, width, height, extent) {
|
|
21008
|
+
let entry = previous;
|
|
21009
|
+
if (entry !== void 0) {
|
|
21010
|
+
entry.churn = this.frame - entry.drawnAt <= CHURN_FRAMES ? entry.churn + 1 : 0;
|
|
21011
|
+
if (entry.churn >= CHURN_LIMIT) {
|
|
21012
|
+
entry.churn = 0;
|
|
21013
|
+
entry.directUntil = this.frame + DIRECT_FRAMES;
|
|
21014
|
+
entry.used = this.frame;
|
|
21015
|
+
this.stats.direct++;
|
|
21016
|
+
return null;
|
|
21017
|
+
}
|
|
21018
|
+
}
|
|
21019
|
+
if (entry === void 0 || entry.canvasWidth < width || entry.canvasHeight < height) {
|
|
21020
|
+
const canvasWidth = Math.max(width, entry?.canvasWidth ?? 0);
|
|
21021
|
+
const canvasHeight = Math.max(height, entry?.canvasHeight ?? 0);
|
|
21022
|
+
const canvas = this.createCanvas(canvasWidth, canvasHeight);
|
|
21023
|
+
const context = canvas?.getContext("2d");
|
|
21024
|
+
if (canvas === null || !context) return null;
|
|
21025
|
+
this.make(canvasWidth * canvasHeight);
|
|
21026
|
+
if (entry !== void 0) {
|
|
21027
|
+
this.pixels -= entry.canvasWidth * entry.canvasHeight;
|
|
21028
|
+
entry.canvas = canvas;
|
|
21029
|
+
entry.context = context;
|
|
21030
|
+
entry.canvasWidth = canvasWidth;
|
|
21031
|
+
entry.canvasHeight = canvasHeight;
|
|
21032
|
+
} else {
|
|
21033
|
+
entry = new TextRaster(canvas, context, canvasWidth, canvasHeight);
|
|
21034
|
+
this.entries.set(node, entry);
|
|
21035
|
+
}
|
|
21036
|
+
this.pixels += canvasWidth * canvasHeight;
|
|
21037
|
+
}
|
|
21038
|
+
const context = entry.context;
|
|
21039
|
+
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
21040
|
+
context.clearRect(0, 0, entry.canvasWidth, entry.canvasHeight);
|
|
21041
|
+
context.setTransform(scaleX, 0, 0, scaleY, offsetX, offsetY);
|
|
21042
|
+
context.globalAlpha = 1;
|
|
21043
|
+
drawText(context, box, paint, measurer);
|
|
21044
|
+
entry.remember(paint, box, scaleX, scaleY, offsetX, offsetY, width, height);
|
|
21045
|
+
entry.extent = extent;
|
|
21046
|
+
entry.drawnAt = this.frame;
|
|
21047
|
+
this.stats.drawn++;
|
|
21048
|
+
return entry;
|
|
21049
|
+
}
|
|
21050
|
+
/** Makes room for `needed` more pixels, letting go of what was drawn least recently. */
|
|
21051
|
+
make(needed) {
|
|
21052
|
+
if (this.pixels + needed <= MAX_PIXELS) return;
|
|
21053
|
+
const byAge = [...this.entries].sort((a, b) => a[1].used - b[1].used);
|
|
21054
|
+
for (const [node, entry] of byAge) {
|
|
21055
|
+
if (this.pixels + needed <= MAX_PIXELS || entry.used === this.frame) break;
|
|
21056
|
+
this.drop(node, entry);
|
|
21057
|
+
}
|
|
21058
|
+
}
|
|
21059
|
+
drop(node, entry) {
|
|
21060
|
+
this.entries.delete(node);
|
|
21061
|
+
this.pixels -= entry.canvasWidth * entry.canvasHeight;
|
|
21062
|
+
entry.canvas.width = 0;
|
|
21063
|
+
entry.canvas.height = 0;
|
|
21064
|
+
}
|
|
21065
|
+
};
|
|
21066
|
+
/** One node's text as pixels, and what it was drawn from. */
|
|
21067
|
+
var TextRaster = class {
|
|
21068
|
+
canvas;
|
|
21069
|
+
context;
|
|
21070
|
+
canvasWidth;
|
|
21071
|
+
canvasHeight;
|
|
21072
|
+
used = 0;
|
|
21073
|
+
drawnAt = 0;
|
|
21074
|
+
churn = 0;
|
|
21075
|
+
directUntil = 0;
|
|
21076
|
+
/** Where the ink reaches, from the box's corner, in layout pixels. */
|
|
21077
|
+
extent = null;
|
|
21078
|
+
text;
|
|
21079
|
+
spans;
|
|
21080
|
+
font = "";
|
|
21081
|
+
color = "";
|
|
21082
|
+
style = "";
|
|
21083
|
+
linkHover = 0;
|
|
21084
|
+
boxWidth = 0;
|
|
21085
|
+
boxHeight = 0;
|
|
21086
|
+
scaleX = 0;
|
|
21087
|
+
scaleY = 0;
|
|
21088
|
+
offsetX = 0;
|
|
21089
|
+
offsetY = 0;
|
|
21090
|
+
width = 0;
|
|
21091
|
+
height = 0;
|
|
21092
|
+
constructor(canvas, context, canvasWidth, canvasHeight) {
|
|
21093
|
+
this.canvas = canvas;
|
|
21094
|
+
this.context = context;
|
|
21095
|
+
this.canvasWidth = canvasWidth;
|
|
21096
|
+
this.canvasHeight = canvasHeight;
|
|
21097
|
+
}
|
|
21098
|
+
holds(paint, box, scaleX, scaleY, offsetX, offsetY, width, height) {
|
|
21099
|
+
return this.drawnFrom(paint, box, scaleX, scaleY) && Math.abs(this.offsetX - offsetX) < SAME_PLACE && Math.abs(this.offsetY - offsetY) < SAME_PLACE && this.width === width && this.height === height;
|
|
21100
|
+
}
|
|
21101
|
+
/** Whether the pixels are of this text, in this style, at this size and scale, wherever they are. */
|
|
21102
|
+
drawnFrom(paint, box, scaleX, scaleY) {
|
|
21103
|
+
return this.text === paint.text && this.spans === paint.spans && this.linkHover === paint.linkHover && this.boxWidth === box.width && this.boxHeight === box.height && this.scaleX === scaleX && this.scaleY === scaleY && this.font === fontOf(paint) && this.color === colorToCss(paint.textColor) && this.style === styleOf(paint);
|
|
21104
|
+
}
|
|
21105
|
+
remember(paint, box, scaleX, scaleY, offsetX, offsetY, width, height) {
|
|
21106
|
+
this.text = paint.text;
|
|
21107
|
+
this.spans = paint.spans;
|
|
21108
|
+
this.linkHover = paint.linkHover;
|
|
21109
|
+
this.boxWidth = box.width;
|
|
21110
|
+
this.boxHeight = box.height;
|
|
21111
|
+
this.scaleX = scaleX;
|
|
21112
|
+
this.scaleY = scaleY;
|
|
21113
|
+
this.offsetX = offsetX;
|
|
21114
|
+
this.offsetY = offsetY;
|
|
21115
|
+
this.width = width;
|
|
21116
|
+
this.height = height;
|
|
21117
|
+
this.font = fontOf(paint);
|
|
21118
|
+
this.color = colorToCss(paint.textColor);
|
|
21119
|
+
this.style = styleOf(paint);
|
|
21120
|
+
}
|
|
21121
|
+
};
|
|
21122
|
+
/**
|
|
21123
|
+
* The lines' extent, and a margin for what glyphs reach past their line
|
|
21124
|
+
* box (an italic's lean, a descender below a tight line height, an
|
|
21125
|
+
* underline). Null for text that draws nothing.
|
|
21126
|
+
*/
|
|
21127
|
+
function inkExtent(box, paint, measurer) {
|
|
21128
|
+
const placements = layoutTextLines(box, paint, measurer);
|
|
21129
|
+
if (placements.length === 0) return null;
|
|
21130
|
+
let left = Infinity;
|
|
21131
|
+
let top = Infinity;
|
|
21132
|
+
let right = -Infinity;
|
|
21133
|
+
let bottom = -Infinity;
|
|
21134
|
+
for (const line of placements) {
|
|
21135
|
+
left = Math.min(left, line.x);
|
|
21136
|
+
top = Math.min(top, line.y);
|
|
21137
|
+
right = Math.max(right, line.x + line.width);
|
|
21138
|
+
bottom = Math.max(bottom, line.y + line.height);
|
|
21139
|
+
}
|
|
21140
|
+
const margin = Math.ceil(paint.fontSize * INK_MARGIN) + 2;
|
|
21141
|
+
return {
|
|
21142
|
+
left: left - box.x - margin,
|
|
21143
|
+
top: top - box.y - margin,
|
|
21144
|
+
right: right - box.x + margin,
|
|
21145
|
+
bottom: bottom - box.y + margin
|
|
21146
|
+
};
|
|
21147
|
+
}
|
|
21148
|
+
function fontOf(paint) {
|
|
21149
|
+
return `${paint.fontStyle} ${paint.fontVariant} ${paint.fontWeight} ${paint.fontStretch} ${paint.fontSize}/${paint.lineHeight} ${paint.fontFamily}`;
|
|
21150
|
+
}
|
|
21151
|
+
/** Everything else `drawText` reads that places or decorates the glyphs. */
|
|
21152
|
+
function styleOf(paint) {
|
|
21153
|
+
return `${paint.letterSpacing}|${paint.fontKerning}|${paint.textDecoration}|${paint.textAlign}|${paint.verticalAlign}|${paint.textWrap}|${paint.maxLines}|${paint.textOverflow}|${paint.rtl}`;
|
|
21154
|
+
}
|
|
21155
|
+
/** How far past its line box, as a share of the font size, a glyph's ink may reach. */
|
|
21156
|
+
const INK_MARGIN = .5;
|
|
21157
|
+
/** Within this much of a device pixel, text is at the same place. */
|
|
21158
|
+
const SAME_PLACE = 1 / 256;
|
|
21159
|
+
/** The largest single piece of text kept: a few screens of a long paragraph. */
|
|
21160
|
+
const MAX_ENTRY_PIXELS = 4e6;
|
|
21161
|
+
/** Everything kept, at most: about 128 MB. */
|
|
21162
|
+
const MAX_PIXELS = 32e6;
|
|
21163
|
+
/** Frames text is kept for without being drawn: about two seconds at 60 Hz. */
|
|
21164
|
+
const IDLE_FRAMES = 120;
|
|
21165
|
+
/** Drawn again within this many frames of the last time counts toward churn. */
|
|
21166
|
+
const CHURN_FRAMES = 2;
|
|
21167
|
+
/** Times in a row text may churn before it is drawn directly. */
|
|
21168
|
+
const CHURN_LIMIT = 3;
|
|
21169
|
+
/** Frames churning text is drawn directly for before the cache tries it again. */
|
|
21170
|
+
const DIRECT_FRAMES = 60;
|
|
21171
|
+
//#endregion
|
|
20711
21172
|
//#region src/rendering/canvas2d/Canvas2DRenderer.ts
|
|
20712
21173
|
/**
|
|
20713
21174
|
* Canvas2D rendering backend.
|
|
@@ -20819,6 +21280,13 @@ var Canvas2DRenderer = class {
|
|
|
20819
21280
|
constructor(options) {
|
|
20820
21281
|
this.options = options;
|
|
20821
21282
|
this.layers = new ScrollLayerCache(options.createLayerCanvas ?? offscreenLayerCanvas, options.scrollLayerSettleFrames);
|
|
21283
|
+
this.textCache = options.textCache === false ? null : new TextRasterCache(options.createLayerCanvas ?? offscreenLayerCanvas);
|
|
21284
|
+
}
|
|
21285
|
+
/** Text kept as pixels; see `TextRasterCache.ts`. */
|
|
21286
|
+
textCache;
|
|
21287
|
+
/** What the text cache did since the last reset, or null with it off. For budget specs. */
|
|
21288
|
+
get textCacheStats() {
|
|
21289
|
+
return this.textCache?.stats ?? null;
|
|
20822
21290
|
}
|
|
20823
21291
|
/** What the scroll layers did since the last reset. For budget specs. */
|
|
20824
21292
|
get scrollLayerStats() {
|
|
@@ -20844,11 +21312,13 @@ var Canvas2DRenderer = class {
|
|
|
20844
21312
|
/** A restored scroll layer reads as healthy and holds nothing. */
|
|
20845
21313
|
surfaceRestored() {
|
|
20846
21314
|
this.layers.dropAll();
|
|
21315
|
+
this.textCache?.dropAll();
|
|
20847
21316
|
}
|
|
20848
21317
|
dispose() {
|
|
20849
21318
|
this.disposed = true;
|
|
20850
21319
|
this.scaledImages.dispose();
|
|
20851
21320
|
this.layers.dropAll();
|
|
21321
|
+
this.textCache?.dropAll();
|
|
20852
21322
|
}
|
|
20853
21323
|
render(root, context) {
|
|
20854
21324
|
const raw = this.surface.getContext2D();
|
|
@@ -20862,11 +21332,13 @@ var Canvas2DRenderer = class {
|
|
|
20862
21332
|
this.liftedPass.length = 0;
|
|
20863
21333
|
this.paintOwner = null;
|
|
20864
21334
|
this.layerDepth = 0;
|
|
21335
|
+
this.textCache?.beginFrame();
|
|
20865
21336
|
this.layers.beginFrame(this.options.scrollLayers === false ? void 0 : context.changes, this.surface.dpr, this.surface.physicalWidth, this.surface.physicalHeight);
|
|
20866
21337
|
this.renderNode(root, context, ctx, true, false);
|
|
20867
21338
|
this.renderLifted(context, ctx);
|
|
20868
21339
|
if (context.overlay !== void 0 && context.overlay.length > 0) drawOverlayShapes(ctx, context.overlay);
|
|
20869
21340
|
this.layers.endFrame();
|
|
21341
|
+
this.textCache?.endFrame();
|
|
20870
21342
|
}
|
|
20871
21343
|
/**
|
|
20872
21344
|
* Folds a node's style into the shared scratch and records that it
|
|
@@ -20931,7 +21403,7 @@ var Canvas2DRenderer = class {
|
|
|
20931
21403
|
if (rec.clips) this.popCull();
|
|
20932
21404
|
if (hasText) {
|
|
20933
21405
|
if (this.paintOwner !== node) this.resolvePaint(node);
|
|
20934
|
-
this.paintContent(ctx, rec, this.paint, context);
|
|
21406
|
+
this.paintContent(ctx, node, rec, this.paint, context);
|
|
20935
21407
|
}
|
|
20936
21408
|
if (rec.liftBoundary && this.liftedPass.length > liftedBefore) this.renderLifted(context, ctx, liftedBefore, node);
|
|
20937
21409
|
if (rec.clips) ctx.restore();
|
|
@@ -21205,7 +21677,7 @@ var Canvas2DRenderer = class {
|
|
|
21205
21677
|
} else ctx.strokeRect(x, y, width, height);
|
|
21206
21678
|
}
|
|
21207
21679
|
}
|
|
21208
|
-
paintContent(ctx, rec, paint, context) {
|
|
21680
|
+
paintContent(ctx, node, rec, paint, context) {
|
|
21209
21681
|
if (paint.text !== void 0) {
|
|
21210
21682
|
this.contentBox.x = rec.x + rec.paddingLeft - rec.scrollX;
|
|
21211
21683
|
this.contentBox.y = rec.y + rec.paddingTop - rec.scrollY;
|
|
@@ -21219,6 +21691,7 @@ var Canvas2DRenderer = class {
|
|
|
21219
21691
|
this.paintHighlightedText(ctx, paint, context);
|
|
21220
21692
|
return;
|
|
21221
21693
|
}
|
|
21694
|
+
if (this.textCache?.draw(ctx, node, this.contentBox, paint, context.text) === true) return;
|
|
21222
21695
|
drawText(ctx, this.contentBox, paint, context.text);
|
|
21223
21696
|
}
|
|
21224
21697
|
}
|
|
@@ -21320,8 +21793,21 @@ var Canvas2DRenderer = class {
|
|
|
21320
21793
|
layers.drop(node);
|
|
21321
21794
|
return false;
|
|
21322
21795
|
}
|
|
21323
|
-
|
|
21796
|
+
const damage = layers.damage(layer, node, context.layout);
|
|
21797
|
+
const wrong = damage === "all" ? [{
|
|
21798
|
+
x: 0,
|
|
21799
|
+
y: 0,
|
|
21800
|
+
width: layer.width,
|
|
21801
|
+
height: layer.height
|
|
21802
|
+
}] : damage;
|
|
21803
|
+
if (wrong.length > 0) {
|
|
21804
|
+
const front = layer.frontContext;
|
|
21805
|
+
front.setTransform(1, 0, 0, 1, 0, 0);
|
|
21806
|
+
for (const rect of wrong) front.clearRect(rect.x, rect.y, rect.width, rect.height);
|
|
21807
|
+
}
|
|
21808
|
+
if (strips.length > 0 || wrong.length > 0) this.paintLayer(layer, [...strips, ...wrong], node, rec, context);
|
|
21324
21809
|
}
|
|
21810
|
+
layers.remember(layer, node, context.layout);
|
|
21325
21811
|
layer.used = true;
|
|
21326
21812
|
layers.stats.composited++;
|
|
21327
21813
|
ctx.save();
|
|
@@ -21343,13 +21829,13 @@ var Canvas2DRenderer = class {
|
|
|
21343
21829
|
* strip saves is raster, which is the cost it exists for; the walk
|
|
21344
21830
|
* costs what a direct draw's walk would.
|
|
21345
21831
|
*/
|
|
21346
|
-
paintLayer(layer,
|
|
21832
|
+
paintLayer(layer, strips, node, rec, context) {
|
|
21347
21833
|
const lctx = new PreciseContext(layer.frontContext);
|
|
21348
21834
|
lctx.save();
|
|
21349
|
-
if (
|
|
21835
|
+
if (strips !== null) {
|
|
21350
21836
|
lctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
21351
21837
|
lctx.beginPath();
|
|
21352
|
-
lctx.rect(strip.x, strip.y, strip.width, strip.height);
|
|
21838
|
+
for (const strip of strips) lctx.rect(strip.x, strip.y, strip.width, strip.height);
|
|
21353
21839
|
lctx.clip();
|
|
21354
21840
|
}
|
|
21355
21841
|
const dpr = layer.dpr;
|
|
@@ -21363,6 +21849,26 @@ var Canvas2DRenderer = class {
|
|
|
21363
21849
|
this.cullY = rec.y + layer.scrollY;
|
|
21364
21850
|
this.cullWidth = rec.width;
|
|
21365
21851
|
this.cullHeight = rec.height;
|
|
21852
|
+
if (strips !== null && strips.length > 0) {
|
|
21853
|
+
let left = Infinity;
|
|
21854
|
+
let top = Infinity;
|
|
21855
|
+
let right = -Infinity;
|
|
21856
|
+
let bottom = -Infinity;
|
|
21857
|
+
for (const strip of strips) {
|
|
21858
|
+
left = Math.min(left, strip.x);
|
|
21859
|
+
top = Math.min(top, strip.y);
|
|
21860
|
+
right = Math.max(right, strip.x + strip.width);
|
|
21861
|
+
bottom = Math.max(bottom, strip.y + strip.height);
|
|
21862
|
+
}
|
|
21863
|
+
const x0 = Math.max(this.cullX, (left - layer.offsetX) / dpr + layer.scrollX - 8);
|
|
21864
|
+
const y0 = Math.max(this.cullY, (top - layer.offsetY) / dpr + layer.scrollY - 8);
|
|
21865
|
+
const x1 = Math.min(this.cullX + this.cullWidth, (right - layer.offsetX) / dpr + layer.scrollX + 8);
|
|
21866
|
+
const y1 = Math.min(this.cullY + this.cullHeight, (bottom - layer.offsetY) / dpr + layer.scrollY + 8);
|
|
21867
|
+
this.cullX = x0;
|
|
21868
|
+
this.cullY = y0;
|
|
21869
|
+
this.cullWidth = Math.max(0, x1 - x0);
|
|
21870
|
+
this.cullHeight = Math.max(0, y1 - y0);
|
|
21871
|
+
}
|
|
21366
21872
|
this.layerDepth++;
|
|
21367
21873
|
try {
|
|
21368
21874
|
this.renderChildren(node, context, lctx, true);
|
|
@@ -21573,4 +22079,4 @@ function createCanvasSurface(host) {
|
|
|
21573
22079
|
//#endregion
|
|
21574
22080
|
export { isImeKey as $, MOUSE_POINTER as $a, spacingEqual as $i, ELLIPSIS as $n, transformsEqual as $r, registerFontStack as $t, withOverrideSource as A, colorToHex as Aa, observeViewportInsets as Ai, clearLinkHover as An, inheritedPropertyNames as Ar, adjacentField as At, FlexDirection as B, UiEnvironment as Ba, defineThemeExtension as Bi, editorFor as Bn, normalizeStates as Br, createCanvasTextStyle as Bt, SCROLLBAR_LINGER_MS as C, textContentOf as Ca, uniformBorderRadius as Ci, isNodeInert as Cn, resolveProperty as Cr, hasDrawnText as Ct, formatExplanation as D, darkColors as Da, UiInsetRegistry as Di, isSubtreeInert as Dn, findPropertyDefinition as Dr, selectionRectsIn as Dt, formatConstraints as E, colorsEqualPalette as Ea, UiEnvironmentKeys as Ei, isPressable as En, closestPropertyName as Er, paragraphGeometryFrom as Et, clampSize as F, parseColor as Fa, darkTheme as Fi, setLinkHover as Fn, UiProperties as Fr, detectEditingPlatform as Ft, CrossAxisAlignment as G, resetOverrideWarnings as Ga, withThemeExtension as Gi, nextCaretToggle as Gn, validateSubgrid as Gr, runFontString as Gt, placeGridItems as H, DirtyNodeSet as Ha, noThemeExtensions as Hi, isEditableNode as Hn, validateRole as Hr, fillTextRects as Ht, constraintsEqual as I, rgb8 as Ia, lightTheme as Ii, CARET_BLINK_MS as In, UI_ROLES as Ir, isPrintable as It, parseCrossAxisAlignment as J, UiVisualState as Ja, raiseContrast as Ji, isVideoSurface as Jn, pathValuesEqual as Jr, textRunBackgrounds as Jt, MainAxisAlignment as K, writeDeclaredProperty as Ka, contrastRatio as Ki, syncEditorValue as Kn, inputsEqual as Kr, spanOf as Kt, tightenConstraints as L, rgba as La, themesEqual as Li, CARET_WIDTH as Ln, UI_SEMANTIC_STATES as Lr, EditableLayout as Lt, contentOffset as M, colorValuesEqual as Ma, bandOf as Mi, linkHoverOf as Mn, propertyIsInherited as Mr, edgeField as Mt, LayoutRecord as N, colorsEqual as Na, containerBands as Ni, linkOf as Nn, propertyNarrowers as Nr, editingGroupOf as Nt, formatNumber as O, lightColors as Oa, insetsEqual as Oi, pointerEventsOf as On, getPropertyNames as Or, wordRangeIn as Ot, Constraints as P, normalizeColor as Pa, unknownContainerSize as Pi, linkRunAtPointIn as Pn, propertyValuesEqualByName as Pr, commandForKey as Pt, capturePointer as Q, visualStatesEqual as Qa, scaleSpacing as Qi, spannedRunsFor as Qn, transformIsIdentity as Qr, fontStackFor as Qt, MAX_MEASURES_PER_CHILD as R, createEnvironmentKey as Ra, withContrast as Ri, EDITOR_PROP as Rn, isUiRole as Rr, applyCanvasTextStyle as Rt, SCROLLBAR_FADE_MS as S, spansDifferOnlyInPaint as Sa, normalizeBorderRadius as Si, isNodeHitTestable as Sn, resolveNumber as Sr, UiInputDispatcher as St, describeOverrides as T, textStylesEqual as Ta, propertyValuesEqual as Ti, isNodeTabStop as Tn, resolveString as Tr, paragraphGeometry as Tt, sizeGridTracks as U, clearOverrideProperty as Ua, themeExtension as Ui, isMultiline as Un, validateStates as Ur, layoutTextLines as Ut, parseFlexDirection as V, UiNode as Va, hasThemeExtension as Vi, editorOf as Vn, statesEqual as Vr, drawText as Vt, AlignContent as W, overrideSources as Wa, themeExtensionsEqual as Wi, isReadOnly as Wn, UI_SUBGRID_AXES as Wr, runCanvasStyleInto as Wt, CanvasPlatformSurface as X, hasVisualState as Xa, densityFactors as Xi, CharacterCountTextMeasurer as Xn, parseTransform as Xr, bumpFontStack as Xt, parseMainAxisAlignment as Y, defaultVisualState as Ya, relativeLuminance as Yi, videoFrameSize as Yn, UiTransforms as Yr, textRunDecorations as Yt, UiPlatformAdapter as Z, visualState as Za, defaultSpacing as Zi, ParagraphTextMeasurer as Zn, transform as Zr, clearFontStacks as Zt, LABEL_PADDING_X as _, flattenTextSpans as _a, borderRadius as _i, selectableTextNodes as _n, DirtyFlags as _o, previousWordStart as _r, scrollLeaderOf as _t, PaintPictureCache as a, boxShadowArraysEqual as aa, validateGradient as ai, computeObjectFitRect as an, UiInputEvent as ao, lineLimit as ar, UiGestureRecognizer as at, snapScroll as b, spanAtOffset as ba, borderRadiusIsZero as bi, setSelectionRange as bn, DEFAULT_LINE_HEIGHT_FACTOR as br, scrollbarThumbs as bt, EMPTY_RECORDING as c, boxShadowsEqual as ca, isAutoLength as ci, normalizeTextAlign as cn, UiPinchEvent as co, offsetForVerticalMove as cr, isScrollContainer as ct, tracePathData as d, shapesEqual as da, isPercentLength as di, TEXT_MATCHES_PROP as dn, UiTextChangeEvent as do, graphemeBoundaries as dr, UiFocusManager as dt, spacingSteps as ea, MAX_GRADIENT_STOPS as ei, DEFAULT_FONT_FAMILY as en, UiBeforeInputEvent as eo, layoutParagraph as er, pointerDeviceOf as et, ScaledImageCache as f, defaultTypography as fa, minmax as fi, clearMatchRanges as fn, UiWheelDeltaMode as fo, lineEndAt as fr, UiHitTester as ft, paintsAfterChildren as g, editableSpansOf as ga, UiBorderRadiuses as gi, clearSelectionRange as gn, UiNodeType as go, previousGraphemeStart as gr, SCROLLBAR_THICKNESS as gt, hasDecorationPhase as h, defaultTextStyle as ha, resolveLength as hi, TEXT_SELECTION_PROP as hn, wheelDeltaYOf as ho, nextWordEnd as hr, SCROLLBAR_MIN_THUMB as ht, traceRoundedRect as i, boxShadow as ia, radialGradient as ii, colorToCss as in, UiGestureEvent as io, lineIndexForOffset as ir, UiTouchScroller as it, accumulatedOffsetTo as j, colorToRgba as ja, UiContainerSizeSource as ji, hasTextLinks as jn, propertyEffects as jr, comparePositions as jt, labelNode as k, UiBasicColors as ka, noInsets as ki, resolveCursor as kn, inheritedPropertyFlags as kr, textInputOf as kt, PaintRecorder as l, defaultShapes as la, isFrLength as li, normalizeVerticalAlign as ln, UiPointerEvent as lo, selectionRects as lr, UiKeyboardController as lt, decorationRect as m, typographyEqual as ma, repeat as mi, setMatchRanges as mn, noKeyModifiers as mo, nextGraphemeEnd as mr, SCROLLBAR_INSET as mt, createCanvasSurface as n, shadowsEqual as na, gradientsEqual as ni, DEFAULT_FONT_WEIGHT as nn, UiEventType as no, caretRectFor as nr, touchActionFor as nt, paintPictures as o, boxShadowRadius as oa, auto as oi, createPaintState as on, UiKeyboardEvent as oo, offsetAtPoint as or, UiPinchRecognizer as ot, decorationColor as p, isTypographyRole as pa, percent as pi, matchRangesOf as pn, UiWheelEvent as po, lineStartAt as pr, SCROLLBAR_HOVER_ZONE as pt, parseAlignContent as q, writeOverrideProperty as qa, highContrastColors as qi, resolveColorValue as qn, paintValuesEqual as qr, textMeasureRequest as qt, Canvas2DRenderer as r, BOX_SHADOW_BLUR_REACH as ra, linearGradient as ri, DEFAULT_TEXT_COLOR as rn, UiFocusEvent as ro, lineIndexAtY as rr, UiEditingController as rt, PaintTarget as s, boxShadowReach as sa, fr as si, isPaintVisible as sn, UiPasteEvent as so, offsetAtX as sr, UiWheelController as st, CanvasSurface as t, defaultShadows as ta, gradientPaint as ti, DEFAULT_FONT_SIZE as tn, UiCompositionEvent as to, proportionalFontMetrics as tr, prepareInputSurface as tt, replayPaint as u, isShapeName as ua, isMinMaxTrack as ui, resolvePaintState as un, UiSelectionChangeEvent as uo, EditableTextModel as ur, UiPointerController as ut, drawOverlayShapes as v, hiddenRangesOf as va, borderRadiusCorners as vi, selectableTextOf as vn, UiBinding as vo, wordRangeAt as vr, scrollRange as vt, describeLength as w, textSpansEqual as wa, defineProperty as wi, isNodeSelectable as wn, resolvePropertyByName as wr, offsetAtPointIn as wt, LayoutEngine as x, spannedTextOf as xa, borderRadiusValuesEqual as xi, isNodeFocusable as xn, resolveBoolean as xr, scrollbarZoneAt as xt, labelOrigin as y, resolvedSpansOf as ya, borderRadiusEqual as yi, selectionRangeOf as yn, UiGraph as yr, scrollbarThumb as yt, isLayoutProtocol as z, findEnvironmentKey as za, withDensity as zi, caretVisibleAt as zn, isUiSemanticState as zr, buildFontString as zt };
|
|
21575
22081
|
|
|
21576
|
-
//# sourceMappingURL=CanvasSurface-
|
|
22082
|
+
//# sourceMappingURL=CanvasSurface-VUJRrcXl.js.map
|