gesso-core 0.4.0 → 0.4.2
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 +67 -0
- package/dist/{CanvasSurface-BR7hECHm.js → CanvasSurface-BMOmE_dD.js} +812 -99
- package/dist/CanvasSurface-BMOmE_dD.js.map +1 -0
- package/dist/{index-BzXn0Bci.d.ts → index-BiN3WdTh.d.ts} +357 -25
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +13 -1
- package/dist/testing.js +76 -7
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/CanvasSurface-BR7hECHm.js.map +0 -1
|
@@ -4519,6 +4519,41 @@ const UiProperties = {
|
|
|
4519
4519
|
affects: T
|
|
4520
4520
|
}),
|
|
4521
4521
|
/**
|
|
4522
|
+
* Another node whose scroll offset this scroll container takes, on
|
|
4523
|
+
* the axes `scrollWithAxis` names: a line-number gutter beside an
|
|
4524
|
+
* editable's text, a spreadsheet's row header beside its grid.
|
|
4525
|
+
*
|
|
4526
|
+
* Taken in the same layout pass that settles the other node's
|
|
4527
|
+
* offset, so the two are painted in step on every frame, whatever
|
|
4528
|
+
* scrolled the other one — a wheel, a scrollbar, the caret, a clamp
|
|
4529
|
+
* after text was deleted. Binding `scrollY` to what `scrollPosition`
|
|
4530
|
+
* reports cannot do that: the report arrives after the frame is laid
|
|
4531
|
+
* out, and the follower is a frame behind.
|
|
4532
|
+
*
|
|
4533
|
+
* The follower's own `scrollX`/`scrollY` are ignored on a followed
|
|
4534
|
+
* axis, it shows no scrollbars, and a wheel over it scrolls the node
|
|
4535
|
+
* it follows. The other node may be any scroll container or an
|
|
4536
|
+
* editable, which scrolls its own text; the offset is clamped to the
|
|
4537
|
+
* follower's own range, so give it content as tall as the text.
|
|
4538
|
+
*
|
|
4539
|
+
* The follower may be `overflow: 'hidden'`, which is otherwise never
|
|
4540
|
+
* scrolled: following makes it scrolled, and nothing but the leader
|
|
4541
|
+
* moves it.
|
|
4542
|
+
*/
|
|
4543
|
+
scrollWith: defineProperty({
|
|
4544
|
+
name: "scrollWith",
|
|
4545
|
+
defaultValue: void 0,
|
|
4546
|
+
inherited: false,
|
|
4547
|
+
affects: 36
|
|
4548
|
+
}),
|
|
4549
|
+
/** Which axes `scrollWith` follows: 'both' (default), 'x' or 'y'. */
|
|
4550
|
+
scrollWithAxis: defineProperty({
|
|
4551
|
+
name: "scrollWithAxis",
|
|
4552
|
+
defaultValue: void 0,
|
|
4553
|
+
inherited: false,
|
|
4554
|
+
affects: T
|
|
4555
|
+
}),
|
|
4556
|
+
/**
|
|
4522
4557
|
* What happens to a wheel delta this node cannot use.
|
|
4523
4558
|
*
|
|
4524
4559
|
* Named after the CSS property it means the same thing as.
|
|
@@ -4536,6 +4571,12 @@ const UiProperties = {
|
|
|
4536
4571
|
* wants the default, or the article stops scrolling whenever the
|
|
4537
4572
|
* cursor crosses the canvas.
|
|
4538
4573
|
*
|
|
4574
|
+
* Unlike the CSS property it is honoured on any node, not only a
|
|
4575
|
+
* scroll container. An app's root is rarely one, and neither is a
|
|
4576
|
+
* canvas that pans and zooms on the wheel itself — which wants
|
|
4577
|
+
* `'contain'` so a ctrl-wheel, or a trackpad pinch, zooms the canvas
|
|
4578
|
+
* and not the page around it.
|
|
4579
|
+
*
|
|
4539
4580
|
* Like `scrollBehavior`, this is read by the input layer and
|
|
4540
4581
|
* affects nothing laid out or painted.
|
|
4541
4582
|
*/
|
|
@@ -8635,6 +8676,30 @@ const SCROLLBAR_MIN_THUMB = 24;
|
|
|
8635
8676
|
/** Distance from the end edge within which the pointer reveals the bar. */
|
|
8636
8677
|
const SCROLLBAR_HOVER_ZONE = 16;
|
|
8637
8678
|
/**
|
|
8679
|
+
* The node a `scrollWith` container follows, or null for one that
|
|
8680
|
+
* scrolls on its own. A follower has no scrollbar of its own, and what
|
|
8681
|
+
* would scroll it scrolls this instead.
|
|
8682
|
+
*/
|
|
8683
|
+
function scrollLeaderOf(node) {
|
|
8684
|
+
const leader = node.properties.get("scrollWith");
|
|
8685
|
+
return leader !== void 0 && leader !== null && leader !== node ? leader : null;
|
|
8686
|
+
}
|
|
8687
|
+
/**
|
|
8688
|
+
* The furthest a record's content can be scrolled along an axis.
|
|
8689
|
+
*
|
|
8690
|
+
* The one answer the layout engine's clamp, the scrollbars and the
|
|
8691
|
+
* scroll sinks all read, so a wheel stops exactly where the caret
|
|
8692
|
+
* would. A field may go one caret width past its text, so that a
|
|
8693
|
+
* caret at the very end is inside the box and not on the clipped
|
|
8694
|
+
* edge; a field wide enough for its text gets no allowance, or the
|
|
8695
|
+
* line would shift by a pixel the moment the caret reached the end.
|
|
8696
|
+
*/
|
|
8697
|
+
function scrollRange(rec, axis) {
|
|
8698
|
+
const overflow = axis === "y" ? rec.contentHeight - rec.height : rec.contentWidth - rec.width;
|
|
8699
|
+
if (!(overflow > 0)) return 0;
|
|
8700
|
+
return rec.scrollsText ? overflow + 1 : overflow;
|
|
8701
|
+
}
|
|
8702
|
+
/**
|
|
8638
8703
|
* The scrollbars a scroll container shows: one per axis whose content
|
|
8639
8704
|
* overflows. Renderers draw exactly these rectangles and the hit
|
|
8640
8705
|
* tester grabs them, so what is seen is what is dragged.
|
|
@@ -8655,7 +8720,7 @@ function thumbFor(rec, axis) {
|
|
|
8655
8720
|
const track = viewport - 4;
|
|
8656
8721
|
const length = Math.min(track, Math.max(24, track * viewport / content));
|
|
8657
8722
|
const travel = Math.max(0, track - length);
|
|
8658
|
-
const maxScroll =
|
|
8723
|
+
const maxScroll = scrollRange(rec, axis);
|
|
8659
8724
|
const offset = axis === "y" ? rec.scrollY : rec.scrollX;
|
|
8660
8725
|
const along = 2 + (maxScroll > 0 ? travel * offset / maxScroll : 0);
|
|
8661
8726
|
return {
|
|
@@ -8975,7 +9040,7 @@ var UiHitTester = class {
|
|
|
8975
9040
|
const py = this.point.y - rec.stickyOffsetY;
|
|
8976
9041
|
if (rec.clips) {
|
|
8977
9042
|
if (px < rec.x || px >= rec.x + rec.width || py < rec.y || py >= rec.y + rec.height) return false;
|
|
8978
|
-
if (rec.scrollable && this.hitScrollbar(node, rec, px, py)) return true;
|
|
9043
|
+
if ((rec.scrollable || rec.textScrollbars) && scrollLeaderOf(node) === null && this.hitScrollbar(node, rec, px, py)) return true;
|
|
8979
9044
|
if (this.zoneOnly) {
|
|
8980
9045
|
const shiftX = rec.scrollX;
|
|
8981
9046
|
const shiftY = rec.scrollY;
|
|
@@ -9050,8 +9115,7 @@ var UiHitTester = class {
|
|
|
9050
9115
|
}
|
|
9051
9116
|
}
|
|
9052
9117
|
if (bar === null) return false;
|
|
9053
|
-
|
|
9054
|
-
if (!onThumb && !visible && !this.zoneOnly) return false;
|
|
9118
|
+
if (!(rec.scrollbarVisibleUntil > (typeof performance !== "undefined" ? performance.now() : Date.now())) && !this.zoneOnly && (!onThumb || !rec.scrollable)) return false;
|
|
9055
9119
|
this.recordHit(node, rec, px, py);
|
|
9056
9120
|
this.result.scrollbar = {
|
|
9057
9121
|
axis,
|
|
@@ -9967,6 +10031,15 @@ function hasScrollRoom(offset, max, delta) {
|
|
|
9967
10031
|
return delta > 0 ? offset < max - SCROLL_EPSILON : offset > SCROLL_EPSILON;
|
|
9968
10032
|
}
|
|
9969
10033
|
/** Whether this node keeps overscroll rather than chaining it outwards. */
|
|
10034
|
+
/**
|
|
10035
|
+
* Whether a node keeps the wheel deltas nothing below it used.
|
|
10036
|
+
*
|
|
10037
|
+
* Honoured on any node, where CSS's `overscroll-behavior` applies only
|
|
10038
|
+
* to scroll containers. The property is how an app says "no wheel over
|
|
10039
|
+
* me reaches the page", and the nodes that most need to say it — an
|
|
10040
|
+
* app's root, a canvas that pans and zooms on the wheel itself — are
|
|
10041
|
+
* the ones that are not scroll containers.
|
|
10042
|
+
*/
|
|
9970
10043
|
function containsOverscroll(node) {
|
|
9971
10044
|
return node !== null && node.getProperty("overscrollBehavior") === "contain";
|
|
9972
10045
|
}
|
|
@@ -10099,16 +10172,17 @@ var UiWheelController = class {
|
|
|
10099
10172
|
let left = false;
|
|
10100
10173
|
let right = false;
|
|
10101
10174
|
for (let node = target; node !== null; node = node.parent) {
|
|
10102
|
-
|
|
10103
|
-
const state = this.scrollSink.containerState(
|
|
10104
|
-
if (state
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10175
|
+
const scrolled = scrollLeaderOf(node) ?? node;
|
|
10176
|
+
const state = takesWheel(node) ? this.scrollSink.containerState(scrolled) : void 0;
|
|
10177
|
+
if (state !== void 0) {
|
|
10178
|
+
left ||= hasScrollRoom(state.scrollX, state.maxScrollX, -1);
|
|
10179
|
+
right ||= hasScrollRoom(state.scrollX, state.maxScrollX, 1);
|
|
10180
|
+
up ||= hasScrollRoom(state.scrollY, state.maxScrollY, -1);
|
|
10181
|
+
down ||= hasScrollRoom(state.scrollY, state.maxScrollY, 1);
|
|
10182
|
+
}
|
|
10109
10183
|
if (containsOverscroll(node)) return EVERYTHING_SCROLLABLE;
|
|
10110
10184
|
}
|
|
10111
|
-
if (
|
|
10185
|
+
if (this.appContainsOverscroll(target)) return EVERYTHING_SCROLLABLE;
|
|
10112
10186
|
return {
|
|
10113
10187
|
up,
|
|
10114
10188
|
down,
|
|
@@ -10147,13 +10221,19 @@ var UiWheelController = class {
|
|
|
10147
10221
|
*/
|
|
10148
10222
|
scrollChain(target, event, deltaX, deltaY, deltaMode, wheelDeltaY) {
|
|
10149
10223
|
for (let node = target; node !== null; node = node.parent) {
|
|
10150
|
-
|
|
10151
|
-
const state = this.scrollSink.containerState(
|
|
10152
|
-
if (state === void 0)
|
|
10224
|
+
const scrolled = scrollLeaderOf(node) ?? node;
|
|
10225
|
+
const state = takesWheel(node) ? this.scrollSink.containerState(scrolled) : void 0;
|
|
10226
|
+
if (state === void 0) {
|
|
10227
|
+
if (containsOverscroll(node)) {
|
|
10228
|
+
event.markConsumed();
|
|
10229
|
+
return;
|
|
10230
|
+
}
|
|
10231
|
+
continue;
|
|
10232
|
+
}
|
|
10153
10233
|
const takesX = deltaX !== 0 && hasScrollRoom(state.scrollX, state.maxScrollX, deltaX);
|
|
10154
10234
|
const takesY = deltaY !== 0 && hasScrollRoom(state.scrollY, state.maxScrollY, deltaY);
|
|
10155
10235
|
if (takesX || takesY) {
|
|
10156
|
-
this.applyDelta(
|
|
10236
|
+
this.applyDelta(scrolled, state, takesX ? deltaX : 0, takesY ? deltaY : 0, deltaMode, behaviorFor(scrolled, deltaMode, wheelDeltaY));
|
|
10157
10237
|
event.markConsumed();
|
|
10158
10238
|
return;
|
|
10159
10239
|
}
|
|
@@ -10162,7 +10242,22 @@ var UiWheelController = class {
|
|
|
10162
10242
|
return;
|
|
10163
10243
|
}
|
|
10164
10244
|
}
|
|
10165
|
-
if (
|
|
10245
|
+
if (this.appContainsOverscroll(target)) event.markConsumed();
|
|
10246
|
+
}
|
|
10247
|
+
/**
|
|
10248
|
+
* Whether the application's root asked to keep every wheel.
|
|
10249
|
+
*
|
|
10250
|
+
* Asked of `rootNode` first, because in a mounted app the tree's
|
|
10251
|
+
* topmost node is not the application's: the runtime wraps the app
|
|
10252
|
+
* in a stack beside its overlay layer, and `contain` set on the app's
|
|
10253
|
+
* own root — which is where the property's documentation says to set
|
|
10254
|
+
* it — was on a node `rootOf` never reached. A node in the overlay
|
|
10255
|
+
* layer is not under the app's root either, so the chain walk alone
|
|
10256
|
+
* would miss it too. `rootOf` is kept for a host that passes no
|
|
10257
|
+
* root, where the topmost node is the only root there is.
|
|
10258
|
+
*/
|
|
10259
|
+
appContainsOverscroll(target) {
|
|
10260
|
+
return containsOverscroll(this.rootNode?.() ?? null) || containsOverscroll(rootOf(target));
|
|
10166
10261
|
}
|
|
10167
10262
|
applyDelta(container, state, deltaX, deltaY, deltaMode, behavior) {
|
|
10168
10263
|
this.scrollSink.scrollBy(container, deltaX === 0 ? 0 : this.toPixels(deltaX, deltaMode, state.viewportWidth), deltaY === 0 ? 0 : this.toPixels(deltaY, deltaMode, state.viewportHeight), behavior);
|
|
@@ -10193,6 +10288,17 @@ function behaviorFor(container, deltaMode, wheelDeltaY) {
|
|
|
10193
10288
|
if (container.getProperty("scrollBehavior") === "instant") return "instant";
|
|
10194
10289
|
return isNotchedWheel(deltaMode, wheelDeltaY) ? "smooth" : "instant";
|
|
10195
10290
|
}
|
|
10291
|
+
/**
|
|
10292
|
+
* Whether the wheel scrolls this node: a scroll container, an
|
|
10293
|
+
* editable, which scrolls its own text, or a `scrollWith` follower,
|
|
10294
|
+
* whose wheel goes to the node it follows.
|
|
10295
|
+
*
|
|
10296
|
+
* Only the wheel. A finger on a field places a caret or selects, and a
|
|
10297
|
+
* scrollbar drag is found by hit test, so neither asks this.
|
|
10298
|
+
*/
|
|
10299
|
+
function takesWheel(node) {
|
|
10300
|
+
return isScrollContainer(node) || node.type === "editable-text" || scrollLeaderOf(node) !== null;
|
|
10301
|
+
}
|
|
10196
10302
|
/** A ScrollView, or any node with overflow 'scroll' or 'auto'. */
|
|
10197
10303
|
function isScrollContainer(node) {
|
|
10198
10304
|
if (node.type === "scroll-view") return true;
|
|
@@ -10426,6 +10532,11 @@ const VELOCITY_SAMPLES = 8;
|
|
|
10426
10532
|
* long press raises nothing: it has a secondary button, and the
|
|
10427
10533
|
* pointer controller dispatches the event from that instead.
|
|
10428
10534
|
*
|
|
10535
|
+
* A mouse long press does not cost the Click either, unless its
|
|
10536
|
+
* LongPress was `preventDefault()`ed or it went on to a Drag: a button
|
|
10537
|
+
* held down a moment before letting go is a slow click, and dropping
|
|
10538
|
+
* it made toolbar buttons seem to ignore a deliberate press.
|
|
10539
|
+
*
|
|
10429
10540
|
* ## Two contacts
|
|
10430
10541
|
*
|
|
10431
10542
|
* Every contact the controller sees is fed to a `UiPinchRecognizer`,
|
|
@@ -10505,6 +10616,7 @@ var UiGestureRecognizer = class {
|
|
|
10505
10616
|
return;
|
|
10506
10617
|
}
|
|
10507
10618
|
if (this.state === "longPressed") {
|
|
10619
|
+
if (!this.claimedPress && Math.hypot(event.x - this.startX, event.y - this.startY) <= this.pressSlop()) return;
|
|
10508
10620
|
this.state = "dragging";
|
|
10509
10621
|
this.claimedPress = true;
|
|
10510
10622
|
this.dispatch("dragstart", target, this.startX, this.startY, event);
|
|
@@ -10559,8 +10671,10 @@ var UiGestureRecognizer = class {
|
|
|
10559
10671
|
this.timer = null;
|
|
10560
10672
|
if (this.state !== "pressing") return;
|
|
10561
10673
|
this.state = "longPressed";
|
|
10562
|
-
|
|
10674
|
+
const mouse = down.pointer.kind === "mouse";
|
|
10675
|
+
this.claimedPress = !mouse;
|
|
10563
10676
|
const held = this.dispatch("longpress", target, this.startX, this.startY, down);
|
|
10677
|
+
if (mouse && held.defaultPrevented) this.claimedPress = true;
|
|
10564
10678
|
if (this.contextMenuOnLongPress && down.pointer.kind !== "mouse" && !held.defaultPrevented) this.dispatcher.dispatch(new UiPointerEvent("contextmenu", this.startX, this.startY, down.buttons, down.modifiers, down.pointer), target);
|
|
10565
10679
|
}, this.longPressDelay);
|
|
10566
10680
|
}
|
|
@@ -12356,6 +12470,23 @@ var LayoutRecord = class {
|
|
|
12356
12470
|
clips = false;
|
|
12357
12471
|
scrollable = false;
|
|
12358
12472
|
/**
|
|
12473
|
+
* An editable, which scrolls its own text inside its box.
|
|
12474
|
+
*
|
|
12475
|
+
* Not `scrollable`: it has no children to translate and its offset
|
|
12476
|
+
* follows the caret, so every path that treats a scroll container's
|
|
12477
|
+
* content as children leaves it alone. What it shares with one is
|
|
12478
|
+
* the offset and its clamp — see `scrollRange` — and, when it is
|
|
12479
|
+
* multiline, the overlay scrollbars.
|
|
12480
|
+
*/
|
|
12481
|
+
scrollsText = false;
|
|
12482
|
+
/**
|
|
12483
|
+
* A multiline editable, which shows the overlay scrollbars a scroll
|
|
12484
|
+
* container does over its own text. A single-line field scrolls
|
|
12485
|
+
* sideways too, but a bar under one line of text reads as part of
|
|
12486
|
+
* the control rather than a hint that there is more of it.
|
|
12487
|
+
*/
|
|
12488
|
+
textScrollbars = false;
|
|
12489
|
+
/**
|
|
12359
12490
|
* A scroll container that reads right to left, so its overlay
|
|
12360
12491
|
* scrollbar hangs on the left edge and the band that reveals it is
|
|
12361
12492
|
* on the left too.
|
|
@@ -12621,6 +12752,8 @@ var LayoutRecord = class {
|
|
|
12621
12752
|
this.liftBoundary = false;
|
|
12622
12753
|
this.clips = false;
|
|
12623
12754
|
this.scrollable = false;
|
|
12755
|
+
this.scrollsText = false;
|
|
12756
|
+
this.textScrollbars = false;
|
|
12624
12757
|
this.mirrored = false;
|
|
12625
12758
|
this.sticky = false;
|
|
12626
12759
|
this.stickyOffsetX = 0;
|
|
@@ -12786,7 +12919,7 @@ function accumulatedOffsetTo(node, records, out) {
|
|
|
12786
12919
|
}
|
|
12787
12920
|
//#endregion
|
|
12788
12921
|
//#region src/layout/LayoutExplanation.ts
|
|
12789
|
-
const EPSILON = 1e-6;
|
|
12922
|
+
const EPSILON$1 = 1e-6;
|
|
12790
12923
|
/**
|
|
12791
12924
|
* Turns the facts about one axis into the rule that decided it and the
|
|
12792
12925
|
* sentences that say so.
|
|
@@ -12801,7 +12934,7 @@ function buildAxisExplanation(f) {
|
|
|
12801
12934
|
if (f.customParent !== void 0 && !f.isRoot) {
|
|
12802
12935
|
decidedBy = "custom";
|
|
12803
12936
|
reasons.push(`the '${f.customParent}' layout on ${f.parentLabel} measured it at ${px(f.measured)}`);
|
|
12804
|
-
if (Math.abs(f.final - f.measured) > EPSILON) reasons.push(`and placed it at ${px(f.final)}`);
|
|
12937
|
+
if (Math.abs(f.final - f.measured) > EPSILON$1) reasons.push(`and placed it at ${px(f.final)}`);
|
|
12805
12938
|
return {
|
|
12806
12939
|
axis: f.axis,
|
|
12807
12940
|
content: f.content,
|
|
@@ -12840,7 +12973,7 @@ function buildAxisExplanation(f) {
|
|
|
12840
12973
|
const to = f.effectiveMin > f.parentMin ? f.ownMin : f.ownMax;
|
|
12841
12974
|
reasons.push(`clamped by its own ${clamp} ${px(to ?? f.effectiveMin)} → ${px(f.effectiveMin)}`);
|
|
12842
12975
|
}
|
|
12843
|
-
if (f.content > f.effectiveMin + EPSILON) reasons.push(`its content would need ${px(f.content)}; the extra ${px(f.content - f.effectiveMin)} ${f.clipsContent ? "is clipped" : "overflows"}`);
|
|
12976
|
+
if (f.content > f.effectiveMin + EPSILON$1) reasons.push(`its content would need ${px(f.content)}; the extra ${px(f.content - f.effectiveMin)} ${f.clipsContent ? "is clipped" : "overflows"}`);
|
|
12844
12977
|
} else if (f.explicit !== void 0) {
|
|
12845
12978
|
decidedBy = "explicit";
|
|
12846
12979
|
reasons.push(`${f.axis}: ${describeLength(f.explicitRaw)} (explicit) → ${px(f.explicit)}`);
|
|
@@ -12850,29 +12983,29 @@ function buildAxisExplanation(f) {
|
|
|
12850
12983
|
const to = f.effectiveMin > f.explicit ? f.ownMin : f.ownMax;
|
|
12851
12984
|
reasons.push(`clamped by ${clamp} ${px(to ?? f.effectiveMin)} → ${px(f.effectiveMin)}`);
|
|
12852
12985
|
}
|
|
12853
|
-
if (isFinite(f.parentMax) && f.effectiveMin > f.parentMax + EPSILON) reasons.push(`wider than the ${px(f.parentMax)} available: it overflows ${f.parentLabel}`);
|
|
12986
|
+
if (isFinite(f.parentMax) && f.effectiveMin > f.parentMax + EPSILON$1) reasons.push(`wider than the ${px(f.parentMax)} available: it overflows ${f.parentLabel}`);
|
|
12854
12987
|
} else {
|
|
12855
12988
|
reasons.push(contentReason(f));
|
|
12856
12989
|
if (f.flex !== void 0 && !f.flex.scroller) reasons.push(unflexedReason(f.flex));
|
|
12857
|
-
if (f.aspectRatio !== void 0 && Math.abs(f.content - f.measured) > EPSILON) {
|
|
12990
|
+
if (f.aspectRatio !== void 0 && Math.abs(f.content - f.measured) > EPSILON$1) {
|
|
12858
12991
|
decidedBy = "aspect-ratio";
|
|
12859
12992
|
reasons.push(`aspectRatio ${formatNumber(f.aspectRatio)} derives it from the other axis → ${px(f.measured)}`);
|
|
12860
12993
|
}
|
|
12861
|
-
if (f.content < f.effectiveMin - EPSILON) {
|
|
12862
|
-
if (f.ownMin !== void 0 && f.ownMin >= f.effectiveMin - EPSILON) {
|
|
12994
|
+
if (f.content < f.effectiveMin - EPSILON$1) {
|
|
12995
|
+
if (f.ownMin !== void 0 && f.ownMin >= f.effectiveMin - EPSILON$1) {
|
|
12863
12996
|
decidedBy = "min";
|
|
12864
12997
|
reasons.push(`raised to ${minProp} ${px(f.ownMin)}`);
|
|
12865
12998
|
} else {
|
|
12866
12999
|
decidedBy = "min";
|
|
12867
13000
|
reasons.push(`raised to the minimum ${f.parentLabel} asked for: ${px(f.parentMin)}`);
|
|
12868
13001
|
}
|
|
12869
|
-
} else if (f.ownMax !== void 0 && f.content > f.ownMax + EPSILON && f.measured <= f.ownMax + EPSILON) {
|
|
13002
|
+
} else if (f.ownMax !== void 0 && f.content > f.ownMax + EPSILON$1 && f.measured <= f.ownMax + EPSILON$1) {
|
|
12870
13003
|
decidedBy = "max";
|
|
12871
13004
|
reasons.push(`capped by ${maxProp} ${px(f.ownMax)}`);
|
|
12872
13005
|
}
|
|
12873
|
-
if (isFinite(f.parentMax) && f.measured > f.parentMax + EPSILON) reasons.push(`the ${px(f.parentMax)} available was only a bound, not a size: the extra ${px(f.measured - f.parentMax)} ${f.clipsContent ? "is clipped" : "overflows"} ${f.parentLabel}`);
|
|
13006
|
+
if (isFinite(f.parentMax) && f.measured > f.parentMax + EPSILON$1) reasons.push(`the ${px(f.parentMax)} available was only a bound, not a size: the extra ${px(f.measured - f.parentMax)} ${f.clipsContent ? "is clipped" : "overflows"} ${f.parentLabel}`);
|
|
12874
13007
|
}
|
|
12875
|
-
if (Math.abs(f.final - f.measured) > EPSILON) reasons.push(`placed at ${px(f.final)} by ${f.parentLabel} (measured ${px(f.measured)})`);
|
|
13008
|
+
if (Math.abs(f.final - f.measured) > EPSILON$1) reasons.push(`placed at ${px(f.final)} by ${f.parentLabel} (measured ${px(f.measured)})`);
|
|
12876
13009
|
return {
|
|
12877
13010
|
axis: f.axis,
|
|
12878
13011
|
content: f.content,
|
|
@@ -12890,8 +13023,8 @@ function buildAxisExplanation(f) {
|
|
|
12890
13023
|
function flexReasons(f, flex, reasons) {
|
|
12891
13024
|
const px = formatNumber;
|
|
12892
13025
|
const final = f.parentMin;
|
|
12893
|
-
const grew = final > flex.base + EPSILON;
|
|
12894
|
-
const shrank = final < flex.base - EPSILON;
|
|
13026
|
+
const grew = final > flex.base + EPSILON$1;
|
|
13027
|
+
const shrank = final < flex.base - EPSILON$1;
|
|
12895
13028
|
if (!grew && !shrank) {
|
|
12896
13029
|
if (f.explicit !== void 0) {
|
|
12897
13030
|
reasons.push(`${f.axis}: ${describeLength(f.explicitRaw)} (explicit) → ${px(f.explicit)}`);
|
|
@@ -12911,10 +13044,10 @@ function flexReasons(f, flex, reasons) {
|
|
|
12911
13044
|
reasons.push(`flex item of ${flex.containerLabel}: base ${px(flex.base)} from ${baseSource}`);
|
|
12912
13045
|
if (grew) {
|
|
12913
13046
|
reasons.push(`grew to ${px(final)} (flexGrow ${px(flex.grow)} takes a share of the free space)`);
|
|
12914
|
-
if (Math.abs(final - flex.max) < EPSILON && isFinite(flex.max)) reasons.push(`stopped at ${f.axis === "width" ? "maxWidth" : "maxHeight"} ${px(flex.max)}`);
|
|
13047
|
+
if (Math.abs(final - flex.max) < EPSILON$1 && isFinite(flex.max)) reasons.push(`stopped at ${f.axis === "width" ? "maxWidth" : "maxHeight"} ${px(flex.max)}`);
|
|
12915
13048
|
} else {
|
|
12916
13049
|
reasons.push(`shrank to ${px(final)} (flexShrink ${px(flex.shrink)}: the items' base sizes exceed the content box of ${flex.containerLabel}, so they give up space)`);
|
|
12917
|
-
if (Math.abs(final - flex.min) < EPSILON) reasons.push(minimumReason(f, flex));
|
|
13050
|
+
if (Math.abs(final - flex.min) < EPSILON$1) reasons.push(minimumReason(f, flex));
|
|
12918
13051
|
}
|
|
12919
13052
|
return "flex";
|
|
12920
13053
|
}
|
|
@@ -12932,7 +13065,7 @@ function minimumReason(f, flex) {
|
|
|
12932
13065
|
if (!flex.minAuto) return `stopped at ${minProp} ${px(flex.min)}`;
|
|
12933
13066
|
if (flex.min === 0 && f.clipsContent) return `its automatic minimum is 0: a scroll container or clipped text has none, so it may shrink to nothing`;
|
|
12934
13067
|
if (flex.min === 0 && f.contentMin === 0) return `its automatic minimum is 0: the content has no minimum ${f.axis}`;
|
|
12935
|
-
if (f.explicit !== void 0 && Math.abs(flex.min - f.explicit) < EPSILON) return `stopped at its automatic minimum ${px(flex.min)}, its explicit ${f.axis} (set ${minProp}: 0 to allow smaller)`;
|
|
13068
|
+
if (f.explicit !== void 0 && Math.abs(flex.min - f.explicit) < EPSILON$1) return `stopped at its automatic minimum ${px(flex.min)}, its explicit ${f.axis} (set ${minProp}: 0 to allow smaller)`;
|
|
12936
13069
|
return `stopped at its automatic minimum ${px(flex.min)} (the content's min-content ${f.axis}; set ${minProp}: 0 to allow smaller)`;
|
|
12937
13070
|
}
|
|
12938
13071
|
function contentReason(f) {
|
|
@@ -13344,6 +13477,23 @@ var LayoutEngine = class {
|
|
|
13344
13477
|
}
|
|
13345
13478
|
return this.records.get(node);
|
|
13346
13479
|
}
|
|
13480
|
+
/**
|
|
13481
|
+
* A number that changes whenever a box may have moved: every box
|
|
13482
|
+
* written, every pass that did any work, every sticky shift and every
|
|
13483
|
+
* node newly given a transform. It is the counter subtree bounds are
|
|
13484
|
+
* kept honest by, read out for a renderer.
|
|
13485
|
+
*
|
|
13486
|
+
* What it deliberately does not count is a scroll offset on its own.
|
|
13487
|
+
* A scroll container's descendants keep their pre-scroll records, so
|
|
13488
|
+
* a frame that only scrolled leaves this alone, and that is what lets
|
|
13489
|
+
* Canvas2D tell a frame that scrolled a list from one that moved it.
|
|
13490
|
+
* The answer is conservative the other way: a pass that did work and
|
|
13491
|
+
* moved nothing still counts, because proving it moved nothing would
|
|
13492
|
+
* cost the walk the counter exists to avoid.
|
|
13493
|
+
*/
|
|
13494
|
+
get geometryVersion() {
|
|
13495
|
+
return this.layoutVersion;
|
|
13496
|
+
}
|
|
13347
13497
|
/** The node the last `layout` / `layoutForFrame` started from, if any. */
|
|
13348
13498
|
get root() {
|
|
13349
13499
|
return this.layoutRoot;
|
|
@@ -14032,6 +14182,11 @@ var LayoutEngine = class {
|
|
|
14032
14182
|
scrollContainers() {
|
|
14033
14183
|
return this.scrollNodes;
|
|
14034
14184
|
}
|
|
14185
|
+
/** Everything that can show a scrollbar: scroll containers, then fields. */
|
|
14186
|
+
*scrollbarNodes() {
|
|
14187
|
+
yield* this.scrollNodes;
|
|
14188
|
+
yield* this.textScrollNodes;
|
|
14189
|
+
}
|
|
14035
14190
|
/**
|
|
14036
14191
|
* When the next scrollbar changes appearance (starts fading or
|
|
14037
14192
|
* disappears), or undefined when none is showing. Lets a host schedule
|
|
@@ -14039,9 +14194,9 @@ var LayoutEngine = class {
|
|
|
14039
14194
|
*/
|
|
14040
14195
|
nextScrollbarChange(now) {
|
|
14041
14196
|
let next;
|
|
14042
|
-
for (const node of this.
|
|
14197
|
+
for (const node of this.scrollbarNodes()) {
|
|
14043
14198
|
const rec = this.records.get(node);
|
|
14044
|
-
if (rec === void 0 || !rec.scrollable || rec.scrollbarVisibleUntil <= now) continue;
|
|
14199
|
+
if (rec === void 0 || !(rec.scrollable || rec.textScrollbars) || rec.scrollbarVisibleUntil <= now) continue;
|
|
14045
14200
|
if (rec.contentWidth <= rec.width && rec.contentHeight <= rec.height) continue;
|
|
14046
14201
|
const fadeStart = rec.scrollbarVisibleUntil - 350;
|
|
14047
14202
|
const candidate = now < fadeStart ? fadeStart : now + 16;
|
|
@@ -15823,38 +15978,52 @@ var LayoutEngine = class {
|
|
|
15823
15978
|
}
|
|
15824
15979
|
}
|
|
15825
15980
|
applyScroll() {
|
|
15826
|
-
|
|
15981
|
+
let followers;
|
|
15982
|
+
for (const node of this.scrollNodes) {
|
|
15983
|
+
if (scrollLeaderOf(node) !== null) {
|
|
15984
|
+
(followers ??= []).push(node);
|
|
15985
|
+
continue;
|
|
15986
|
+
}
|
|
15987
|
+
this.applyScrollOffset(node, false);
|
|
15988
|
+
}
|
|
15827
15989
|
for (const node of this.textScrollNodes) this.applyScrollOffset(node, true);
|
|
15990
|
+
if (followers !== void 0) for (const node of followers) this.applyFollowedScroll(node, scrollLeaderOf(node));
|
|
15828
15991
|
this.applySticky();
|
|
15829
15992
|
}
|
|
15830
15993
|
/**
|
|
15994
|
+
* A `scrollWith` container's offset: the leader's, on the axes it
|
|
15995
|
+
* follows, clamped to the follower's own range. No scrollbar shows
|
|
15996
|
+
* for it, since what is being scrolled is the leader.
|
|
15997
|
+
*/
|
|
15998
|
+
applyFollowedScroll(node, leader) {
|
|
15999
|
+
const rec = this.record(node);
|
|
16000
|
+
const source = this.records.get(leader);
|
|
16001
|
+
const axis = node.properties.get("scrollWithAxis") ?? "both";
|
|
16002
|
+
const followsX = axis !== "y";
|
|
16003
|
+
const followsY = axis !== "x";
|
|
16004
|
+
const rawX = followsX ? source?.scrollX ?? 0 : this.numberProp(node, "scrollX") ?? 0;
|
|
16005
|
+
const rawY = followsY ? source?.scrollY ?? 0 : this.numberProp(node, "scrollY") ?? 0;
|
|
16006
|
+
rec.scrollX = this.clamp(rawX, 0, scrollRange(rec, "x"));
|
|
16007
|
+
rec.scrollY = this.clamp(rawY, 0, scrollRange(rec, "y"));
|
|
16008
|
+
rec.transformDirty = false;
|
|
16009
|
+
}
|
|
16010
|
+
/**
|
|
15831
16011
|
* Brings a record's scroll offset up to date with its `scrollX` and
|
|
15832
|
-
* `scrollY` properties, clamped to what there is to scroll
|
|
15833
|
-
* every pass, so an offset written against
|
|
15834
|
-
* whose text has since been deleted, a
|
|
15835
|
-
* back into range on its own.
|
|
15836
|
-
*
|
|
15837
|
-
* A field that scrolls may go one caret width further than its text,
|
|
15838
|
-
* so that a caret at the very end is inside the box and not on its
|
|
15839
|
-
* edge, where the clip would take it. A field wide enough for its
|
|
15840
|
-
* text does not: the allowance would shift the line by a pixel the
|
|
15841
|
-
* moment the caret reached the end.
|
|
16012
|
+
* `scrollY` properties, clamped to what there is to scroll (see
|
|
16013
|
+
* `scrollRange`). This runs every pass, so an offset written against
|
|
16014
|
+
* a larger content — a field whose text has since been deleted, a
|
|
16015
|
+
* list that lost rows — comes back into range on its own.
|
|
15842
16016
|
*/
|
|
15843
16017
|
applyScrollOffset(node, field) {
|
|
15844
16018
|
const rec = this.record(node);
|
|
15845
16019
|
const rawX = this.numberProp(node, "scrollX") ?? 0;
|
|
15846
16020
|
const rawY = this.numberProp(node, "scrollY") ?? 0;
|
|
15847
|
-
const
|
|
15848
|
-
const
|
|
15849
|
-
const overflowY = rec.contentHeight - rec.height;
|
|
15850
|
-
const maxX = overflowX > 0 ? overflowX + caret : 0;
|
|
15851
|
-
const maxY = overflowY > 0 ? overflowY + caret : 0;
|
|
15852
|
-
const scrollX = this.clamp(rawX, 0, maxX);
|
|
15853
|
-
const scrollY = this.clamp(rawY, 0, maxY);
|
|
16021
|
+
const scrollX = this.clamp(rawX, 0, scrollRange(rec, "x"));
|
|
16022
|
+
const scrollY = this.clamp(rawY, 0, scrollRange(rec, "y"));
|
|
15854
16023
|
if (scrollX !== rec.scrollX || scrollY !== rec.scrollY) {
|
|
15855
16024
|
rec.scrollX = scrollX;
|
|
15856
16025
|
rec.scrollY = scrollY;
|
|
15857
|
-
if (!field) rec.scrollbarVisibleUntil = this.now() + SCROLLBAR_LINGER_MS;
|
|
16026
|
+
if (!field || rec.textScrollbars) rec.scrollbarVisibleUntil = this.now() + SCROLLBAR_LINGER_MS;
|
|
15858
16027
|
}
|
|
15859
16028
|
rec.transformDirty = false;
|
|
15860
16029
|
}
|
|
@@ -16066,8 +16235,10 @@ var LayoutEngine = class {
|
|
|
16066
16235
|
rec.sticky = position === "sticky";
|
|
16067
16236
|
rec.positioned = rec.absolute || rec.sticky || position === "relative";
|
|
16068
16237
|
const overflow = props.get("overflow");
|
|
16069
|
-
rec.scrollable = node.type === "scroll-view" || overflow === "scroll" || overflow === "auto";
|
|
16070
|
-
rec.
|
|
16238
|
+
rec.scrollable = node.type === "scroll-view" || overflow === "scroll" || overflow === "auto" || overflow === "hidden" && scrollLeaderOf(node) !== null;
|
|
16239
|
+
rec.scrollsText = node.type === "editable-text";
|
|
16240
|
+
rec.textScrollbars = rec.scrollsText && props.get("multiline") === true;
|
|
16241
|
+
rec.mirrored = (rec.scrollable || rec.textScrollbars) && this.startEdge(node) === "right";
|
|
16071
16242
|
rec.clips = rec.scrollable || overflow === "hidden" || node.type === "editable-text";
|
|
16072
16243
|
const insetH = this.lengthProp(node, "inset", base.width);
|
|
16073
16244
|
const insetV = this.lengthProp(node, "inset", base.height);
|
|
@@ -17485,6 +17656,14 @@ const MAX_PICTURE_SIDE = 8192;
|
|
|
17485
17656
|
* is what remains: that both place the same picture in the same box,
|
|
17486
17657
|
* at the same point in paint order, under the same clip and opacity.
|
|
17487
17658
|
*
|
|
17659
|
+
* **A picture that changes every frame is the exception**, on Canvas2D:
|
|
17660
|
+
* `draw` replays its new recording straight onto the frame and makes a
|
|
17661
|
+
* bitmap only on the first frame its inputs hold still. The parity
|
|
17662
|
+
* claim is kept where it is made — both backends draw the same bitmap of
|
|
17663
|
+
* a settled picture, and a changing one is the same recording through
|
|
17664
|
+
* the same `PaintTarget` — and a canvas whose layers move every frame
|
|
17665
|
+
* stops paying for a bitmap per layer per frame that nobody draws twice.
|
|
17666
|
+
*
|
|
17488
17667
|
* **The cost follows the change.** A frame that draws a painted node
|
|
17489
17668
|
* whose inputs, box, scale and theme are unchanged makes no recording
|
|
17490
17669
|
* and no bitmap; it draws the one already here. The key holds the
|
|
@@ -17504,7 +17683,8 @@ var PaintPictureCache = class {
|
|
|
17504
17683
|
stats = {
|
|
17505
17684
|
recorded: 0,
|
|
17506
17685
|
rasterized: 0,
|
|
17507
|
-
resolved: 0
|
|
17686
|
+
resolved: 0,
|
|
17687
|
+
direct: 0
|
|
17508
17688
|
};
|
|
17509
17689
|
constructor(createCanvas = offscreenPaintCanvas) {
|
|
17510
17690
|
this.createCanvas = createCanvas;
|
|
@@ -17517,6 +17697,7 @@ var PaintPictureCache = class {
|
|
|
17517
17697
|
this.stats.recorded = 0;
|
|
17518
17698
|
this.stats.rasterized = 0;
|
|
17519
17699
|
this.stats.resolved = 0;
|
|
17700
|
+
this.stats.direct = 0;
|
|
17520
17701
|
}
|
|
17521
17702
|
/**
|
|
17522
17703
|
* The node's picture at this box and device scale, made if the
|
|
@@ -17528,39 +17709,107 @@ var PaintPictureCache = class {
|
|
|
17528
17709
|
*/
|
|
17529
17710
|
pictureFor(node, rec, scale) {
|
|
17530
17711
|
this.stats.resolved++;
|
|
17531
|
-
const
|
|
17532
|
-
|
|
17533
|
-
if (paint === void 0 && path === void 0) return;
|
|
17534
|
-
if (!(rec.width > 0) || !(rec.height > 0)) return;
|
|
17535
|
-
const clipPath = node.properties.get("clipPath");
|
|
17536
|
-
const blur = node.properties.get("blur");
|
|
17712
|
+
const key = keyFor(node, rec, scale);
|
|
17713
|
+
if (key === null) return;
|
|
17537
17714
|
const slot = this.slots.get(node);
|
|
17538
|
-
if (slot !== void 0 && slot
|
|
17539
|
-
const
|
|
17540
|
-
|
|
17541
|
-
|
|
17542
|
-
|
|
17543
|
-
|
|
17544
|
-
|
|
17545
|
-
|
|
17546
|
-
|
|
17547
|
-
|
|
17548
|
-
|
|
17549
|
-
|
|
17550
|
-
|
|
17551
|
-
|
|
17552
|
-
|
|
17715
|
+
if (slot !== void 0 && matches(slot, key)) return this.pictureOf(node, slot, key.box) ?? void 0;
|
|
17716
|
+
const recording = this.record(key.paint, key.path, key.clipPath, key.blur, key.box);
|
|
17717
|
+
const next = this.remember(node, slot, key, recording);
|
|
17718
|
+
return this.pictureOf(node, next, key.box) ?? void 0;
|
|
17719
|
+
}
|
|
17720
|
+
/**
|
|
17721
|
+
* Draws a node's picture onto a Canvas2D frame, making a bitmap only
|
|
17722
|
+
* when one will be drawn more than once.
|
|
17723
|
+
*
|
|
17724
|
+
* `pictureFor` rasterises every new recording into a bitmap, which
|
|
17725
|
+
* pays for itself when the picture is drawn again — a chart that
|
|
17726
|
+
* changed once and is panned for a minute. It does not when the
|
|
17727
|
+
* picture changes every frame: the bitmap is drawn once and thrown
|
|
17728
|
+
* away, and the frame has paid for a canvas cleared, a separate
|
|
17729
|
+
* flush, and a composite of every pixel of it. gessologic's canvas
|
|
17730
|
+
* redraws a layer of live wires per tile per frame, and in software
|
|
17731
|
+
* rendering `transferToImageBitmap` alone was a quarter of the render
|
|
17732
|
+
* worker's time.
|
|
17733
|
+
*
|
|
17734
|
+
* So a recording made because the painter's inputs changed is
|
|
17735
|
+
* replayed straight onto the frame's context, clipped to the node's
|
|
17736
|
+
* box, and no bitmap is made. Once it has held still for
|
|
17737
|
+
* `SETTLE_FRAMES` frames it makes the bitmap and draws that from there
|
|
17738
|
+
* on, exactly as `pictureFor` would. A picture that changes every
|
|
17739
|
+
* frame, or nearly every frame, is never rasterised; one that settles
|
|
17740
|
+
* is rasterised once, a few frames late.
|
|
17741
|
+
* A picture seen for the first time, or at a new size, is rasterised
|
|
17742
|
+
* at once, because it most likely holds still.
|
|
17743
|
+
*
|
|
17744
|
+
* Canvas2D only. WebGPU has no path pipeline to replay onto, which is
|
|
17745
|
+
* the reason the cache exists (see the class comment), and asks
|
|
17746
|
+
* `pictureFor`. The two backends then differ only in when the pixels
|
|
17747
|
+
* of a changing picture are made, not in which: both come from the
|
|
17748
|
+
* same recording through the same `PaintTarget`.
|
|
17749
|
+
*/
|
|
17750
|
+
draw(node, rec, scale, ctx) {
|
|
17751
|
+
this.stats.resolved++;
|
|
17752
|
+
const key = keyFor(node, rec, scale);
|
|
17753
|
+
if (key === null) return;
|
|
17754
|
+
const slot = this.slots.get(node);
|
|
17755
|
+
if (slot !== void 0 && matches(slot, key)) {
|
|
17756
|
+
if (!slot.rasterized && slot.settling > 0) {
|
|
17757
|
+
slot.settling--;
|
|
17758
|
+
this.replay(node, slot.recording, rec, ctx);
|
|
17759
|
+
return;
|
|
17760
|
+
}
|
|
17761
|
+
const picture = this.pictureOf(node, slot, key.box);
|
|
17762
|
+
if (picture !== null) ctx.drawImage(picture, rec.x, rec.y, rec.width, rec.height);
|
|
17763
|
+
return;
|
|
17764
|
+
}
|
|
17765
|
+
const recording = this.record(key.paint, key.path, key.clipPath, key.blur, key.box);
|
|
17766
|
+
const next = this.remember(node, slot, key, recording);
|
|
17767
|
+
if (recording.ops.length === 0) return;
|
|
17768
|
+
if (slot === void 0 || !contentChanged(slot, key)) {
|
|
17769
|
+
const picture = this.pictureOf(node, next, key.box);
|
|
17770
|
+
if (picture !== null) ctx.drawImage(picture, rec.x, rec.y, rec.width, rec.height);
|
|
17771
|
+
return;
|
|
17772
|
+
}
|
|
17773
|
+
next.settling = 3;
|
|
17774
|
+
this.replay(node, recording, rec, ctx);
|
|
17775
|
+
}
|
|
17776
|
+
/** A recording drawn straight onto the frame, clipped to the box its bitmap would fill. */
|
|
17777
|
+
replay(node, recording, rec, ctx) {
|
|
17778
|
+
this.stats.direct++;
|
|
17779
|
+
ctx.save();
|
|
17780
|
+
ctx.translate(rec.x, rec.y);
|
|
17781
|
+
ctx.beginPath();
|
|
17782
|
+
ctx.rect(0, 0, rec.width, rec.height);
|
|
17783
|
+
ctx.clip();
|
|
17784
|
+
replayPaint(recording, new PaintTarget(ctx, resolverFor(node)));
|
|
17785
|
+
ctx.restore();
|
|
17786
|
+
}
|
|
17787
|
+
/** A slot's bitmap, made now if it has not been. */
|
|
17788
|
+
pictureOf(node, slot, box) {
|
|
17789
|
+
if (!slot.rasterized) {
|
|
17790
|
+
slot.picture = this.rasterize(node, slot.recording, box);
|
|
17791
|
+
slot.rasterized = true;
|
|
17792
|
+
}
|
|
17793
|
+
return slot.picture;
|
|
17794
|
+
}
|
|
17795
|
+
remember(node, previous, key, recording) {
|
|
17796
|
+
previous?.picture?.close?.();
|
|
17797
|
+
const slot = {
|
|
17798
|
+
picture: null,
|
|
17799
|
+
rasterized: false,
|
|
17800
|
+
settling: 0,
|
|
17553
17801
|
recording,
|
|
17554
|
-
paint,
|
|
17555
|
-
path,
|
|
17556
|
-
clipPath,
|
|
17557
|
-
blur,
|
|
17802
|
+
paint: key.paint,
|
|
17803
|
+
path: key.path,
|
|
17804
|
+
clipPath: key.clipPath,
|
|
17805
|
+
blur: key.blur,
|
|
17558
17806
|
environment: node.environment,
|
|
17559
|
-
width:
|
|
17560
|
-
height:
|
|
17561
|
-
scale
|
|
17562
|
-
}
|
|
17563
|
-
|
|
17807
|
+
width: key.box.width,
|
|
17808
|
+
height: key.box.height,
|
|
17809
|
+
scale: key.box.scale
|
|
17810
|
+
};
|
|
17811
|
+
this.slots.set(node, slot);
|
|
17812
|
+
return slot;
|
|
17564
17813
|
}
|
|
17565
17814
|
/**
|
|
17566
17815
|
* The recording a node's picture was made from.
|
|
@@ -17607,6 +17856,36 @@ var PaintPictureCache = class {
|
|
|
17607
17856
|
* make it possible for them not to.
|
|
17608
17857
|
*/
|
|
17609
17858
|
const paintPictures = new PaintPictureCache();
|
|
17859
|
+
/** The key for a node's picture, or null when it paints nothing or its box has no area. */
|
|
17860
|
+
function keyFor(node, rec, scale) {
|
|
17861
|
+
const paint = node.properties.get("paint");
|
|
17862
|
+
const path = node.properties.get("path");
|
|
17863
|
+
if (paint === void 0 && path === void 0) return null;
|
|
17864
|
+
if (!(rec.width > 0) || !(rec.height > 0)) return null;
|
|
17865
|
+
return {
|
|
17866
|
+
paint,
|
|
17867
|
+
path,
|
|
17868
|
+
clipPath: node.properties.get("clipPath"),
|
|
17869
|
+
blur: node.properties.get("blur"),
|
|
17870
|
+
environment: node.environment,
|
|
17871
|
+
box: {
|
|
17872
|
+
width: rec.width,
|
|
17873
|
+
height: rec.height,
|
|
17874
|
+
paddingTop: rec.paddingTop,
|
|
17875
|
+
paddingRight: rec.paddingRight,
|
|
17876
|
+
paddingBottom: rec.paddingBottom,
|
|
17877
|
+
paddingLeft: rec.paddingLeft,
|
|
17878
|
+
scale
|
|
17879
|
+
}
|
|
17880
|
+
};
|
|
17881
|
+
}
|
|
17882
|
+
/** Whether what the painter draws changed, as against where or at what size it is drawn. */
|
|
17883
|
+
function contentChanged(slot, key) {
|
|
17884
|
+
return slot.clipPath !== key.clipPath || slot.blur !== key.blur || !paintValuesEqual(slot.paint, key.paint) || !pathValuesEqual(slot.path, key.path);
|
|
17885
|
+
}
|
|
17886
|
+
function matches(slot, key) {
|
|
17887
|
+
return slot.width === key.box.width && slot.height === key.box.height && slot.scale === key.box.scale && slot.environment === key.environment && slot.clipPath === key.clipPath && slot.blur === key.blur && paintValuesEqual(slot.paint, key.paint) && pathValuesEqual(slot.path, key.path);
|
|
17888
|
+
}
|
|
17610
17889
|
/** A painter's colour values, against the node's own theme. */
|
|
17611
17890
|
function resolverFor(node) {
|
|
17612
17891
|
return {
|
|
@@ -17669,6 +17948,310 @@ function offscreenPaintCanvas(width, height) {
|
|
|
17669
17948
|
};
|
|
17670
17949
|
}
|
|
17671
17950
|
//#endregion
|
|
17951
|
+
//#region src/rendering/canvas2d/ScrollLayers.ts
|
|
17952
|
+
/**
|
|
17953
|
+
* An `OffscreenCanvas` where there is one. Where there is not, no
|
|
17954
|
+
* layer is made and every frame is drawn directly, which is the
|
|
17955
|
+
* behaviour a host without one had anyway.
|
|
17956
|
+
*/
|
|
17957
|
+
function offscreenLayerCanvas(width, height) {
|
|
17958
|
+
if (typeof OffscreenCanvas !== "function") return null;
|
|
17959
|
+
return new OffscreenCanvas(width, height);
|
|
17960
|
+
}
|
|
17961
|
+
/**
|
|
17962
|
+
* How far from a whole device pixel a translation may be and still be
|
|
17963
|
+
* treated as whole. Floating point makes a scroll of ten pixels come
|
|
17964
|
+
* out as 9.999999999998 often enough that exact equality would refuse
|
|
17965
|
+
* nearly every pan; a thousandth of a pixel is invisible, and the
|
|
17966
|
+
* layer's offset is kept pixel-true (see `shift`) so the error never
|
|
17967
|
+
* accumulates past it.
|
|
17968
|
+
*/
|
|
17969
|
+
const EPSILON = .001;
|
|
17970
|
+
/**
|
|
17971
|
+
* The most layers held at once. Each costs two canvases the size of its
|
|
17972
|
+
* viewport; a screen with more scroll containers scrolling at once than
|
|
17973
|
+
* this is not one a layer per container would help.
|
|
17974
|
+
*/
|
|
17975
|
+
const MAX_LAYERS = 4;
|
|
17976
|
+
var ScrollLayerCache = class {
|
|
17977
|
+
createCanvas;
|
|
17978
|
+
settleFrames;
|
|
17979
|
+
layers = /* @__PURE__ */ new Map();
|
|
17980
|
+
/**
|
|
17981
|
+
* Containers whose last layer drew something that changes without
|
|
17982
|
+
* telling anyone — a caret, a fading scrollbar, a video. Building
|
|
17983
|
+
* another on the next scroll would only throw it away again, so none
|
|
17984
|
+
* is built until something in or above the subtree is dirtied, which
|
|
17985
|
+
* is how every one of those ends.
|
|
17986
|
+
*/
|
|
17987
|
+
refused = /* @__PURE__ */ new Set();
|
|
17988
|
+
geometryVersion = NaN;
|
|
17989
|
+
dpr = 0;
|
|
17990
|
+
surfaceWidth = 0;
|
|
17991
|
+
surfaceHeight = 0;
|
|
17992
|
+
frame = null;
|
|
17993
|
+
/** Whether this frame changed nothing a layer could not absorb: no layout, children, content or environment. */
|
|
17994
|
+
calm = false;
|
|
17995
|
+
/** Nodes this frame dirtied, and every ancestor of each, less a container that only scrolled. */
|
|
17996
|
+
disturbed = /* @__PURE__ */ new Set();
|
|
17997
|
+
/** Consecutive frames each container without a layer has scrolled quietly. See `SCROLL_LAYER_SETTLE_FRAMES`. */
|
|
17998
|
+
streaks = /* @__PURE__ */ new WeakMap();
|
|
17999
|
+
/**
|
|
18000
|
+
* Between `beginFrame` and `endFrame`. Still true at the next
|
|
18001
|
+
* `beginFrame` means the last frame threw part way through, perhaps
|
|
18002
|
+
* inside a layer's walk, and whatever the layers hold is suspect.
|
|
18003
|
+
*/
|
|
18004
|
+
open = false;
|
|
18005
|
+
stats = {
|
|
18006
|
+
built: 0,
|
|
18007
|
+
composited: 0,
|
|
18008
|
+
shifted: 0,
|
|
18009
|
+
dropped: 0
|
|
18010
|
+
};
|
|
18011
|
+
constructor(createCanvas, settleFrames = 3) {
|
|
18012
|
+
this.createCanvas = createCanvas;
|
|
18013
|
+
this.settleFrames = settleFrames;
|
|
18014
|
+
}
|
|
18015
|
+
/** Whether layers may be used on this frame; false until `beginFrame` says so. */
|
|
18016
|
+
get active() {
|
|
18017
|
+
return this.frame !== null;
|
|
18018
|
+
}
|
|
18019
|
+
resetStats() {
|
|
18020
|
+
this.stats.built = 0;
|
|
18021
|
+
this.stats.composited = 0;
|
|
18022
|
+
this.stats.shifted = 0;
|
|
18023
|
+
this.stats.dropped = 0;
|
|
18024
|
+
}
|
|
18025
|
+
/**
|
|
18026
|
+
* Settles which layers this frame's changes leave standing, before
|
|
18027
|
+
* anything is drawn. Every rule here drops; none keeps. A frame the
|
|
18028
|
+
* caller could not vouch for drops them all.
|
|
18029
|
+
*/
|
|
18030
|
+
beginFrame(changes, dpr, surfaceWidth, surfaceHeight) {
|
|
18031
|
+
const interrupted = this.open;
|
|
18032
|
+
this.open = true;
|
|
18033
|
+
this.disturbed.clear();
|
|
18034
|
+
this.calm = changes !== void 0;
|
|
18035
|
+
for (const [node, flags] of changes?.frame.entries() ?? []) {
|
|
18036
|
+
if ((flags & -355) !== 0) {
|
|
18037
|
+
this.calm = false;
|
|
18038
|
+
break;
|
|
18039
|
+
}
|
|
18040
|
+
for (let current = (flags & 32) !== 0 ? node.parent : node; current !== null; current = current.parent) this.disturbed.add(current);
|
|
18041
|
+
}
|
|
18042
|
+
if (interrupted || changes === void 0 || dpr !== this.dpr || surfaceWidth !== this.surfaceWidth || surfaceHeight !== this.surfaceHeight || changes.geometryVersion !== this.geometryVersion) {
|
|
18043
|
+
this.dropAll();
|
|
18044
|
+
this.dpr = dpr;
|
|
18045
|
+
this.surfaceWidth = surfaceWidth;
|
|
18046
|
+
this.surfaceHeight = surfaceHeight;
|
|
18047
|
+
this.geometryVersion = changes?.geometryVersion ?? NaN;
|
|
18048
|
+
this.frame = changes?.frame ?? null;
|
|
18049
|
+
return;
|
|
18050
|
+
}
|
|
18051
|
+
const frame = changes.frame;
|
|
18052
|
+
this.frame = frame;
|
|
18053
|
+
if (this.layers.size === 0 && this.refused.size === 0) return;
|
|
18054
|
+
for (const [node, flags] of frame.entries()) {
|
|
18055
|
+
if ((flags & -355) !== 0) {
|
|
18056
|
+
this.dropAll();
|
|
18057
|
+
return;
|
|
18058
|
+
}
|
|
18059
|
+
const scrolledOnly = flags === 32;
|
|
18060
|
+
for (let current = scrolledOnly ? node.parent : node; current !== null; current = current.parent) this.forget(current);
|
|
18061
|
+
}
|
|
18062
|
+
for (const node of [...this.layers.keys(), ...this.refused]) for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
18063
|
+
const flags = frame.dirtyFlagsFor(ancestor);
|
|
18064
|
+
if (flags !== 0 && flags !== 32) {
|
|
18065
|
+
this.forget(node);
|
|
18066
|
+
break;
|
|
18067
|
+
}
|
|
18068
|
+
}
|
|
18069
|
+
}
|
|
18070
|
+
/** Drops the layers the frame did not composite: their container was culled, removed, or drawn directly. */
|
|
18071
|
+
endFrame() {
|
|
18072
|
+
this.open = false;
|
|
18073
|
+
for (const [node, layer] of this.layers) if (!layer.used) this.drop(node);
|
|
18074
|
+
else layer.used = false;
|
|
18075
|
+
}
|
|
18076
|
+
get(node) {
|
|
18077
|
+
return this.layers.get(node);
|
|
18078
|
+
}
|
|
18079
|
+
/**
|
|
18080
|
+
* Whether a container with no layer should get one now: it has
|
|
18081
|
+
* scrolled on the last few frames running with nothing else in or
|
|
18082
|
+
* above it changing, which is the evidence a layer would be kept, and
|
|
18083
|
+
* it has not just shown it holds something a layer cannot.
|
|
18084
|
+
*/
|
|
18085
|
+
wants(node) {
|
|
18086
|
+
const frame = this.frame;
|
|
18087
|
+
if (frame === null || this.layers.size >= MAX_LAYERS || this.refused.has(node)) return false;
|
|
18088
|
+
if (!this.calm || this.disturbed.has(node) || dirtyAbove(frame, node)) {
|
|
18089
|
+
this.streaks.delete(node);
|
|
18090
|
+
return false;
|
|
18091
|
+
}
|
|
18092
|
+
if ((frame.dirtyFlagsFor(node) & 32) === 0) return false;
|
|
18093
|
+
const streak = (this.streaks.get(node) ?? 0) + 1;
|
|
18094
|
+
if (streak < this.settleFrames) {
|
|
18095
|
+
this.streaks.set(node, streak);
|
|
18096
|
+
return false;
|
|
18097
|
+
}
|
|
18098
|
+
this.streaks.delete(node);
|
|
18099
|
+
return true;
|
|
18100
|
+
}
|
|
18101
|
+
/** A new layer for the placement, or null where no canvas could be had. */
|
|
18102
|
+
create(node, placement, rec) {
|
|
18103
|
+
const front = this.createCanvas(placement.width, placement.height);
|
|
18104
|
+
const back = front === null ? null : this.createCanvas(placement.width, placement.height);
|
|
18105
|
+
const frontContext = front?.getContext("2d");
|
|
18106
|
+
const backContext = back?.getContext("2d");
|
|
18107
|
+
if (front === null || back === null || !frontContext || !backContext) return null;
|
|
18108
|
+
const layer = {
|
|
18109
|
+
front,
|
|
18110
|
+
frontContext,
|
|
18111
|
+
back,
|
|
18112
|
+
backContext,
|
|
18113
|
+
width: placement.width,
|
|
18114
|
+
height: placement.height,
|
|
18115
|
+
offsetX: placement.offsetX,
|
|
18116
|
+
offsetY: placement.offsetY,
|
|
18117
|
+
dpr: this.dpr,
|
|
18118
|
+
scrollX: rec.scrollX,
|
|
18119
|
+
scrollY: rec.scrollY,
|
|
18120
|
+
used: true
|
|
18121
|
+
};
|
|
18122
|
+
this.layers.set(node, layer);
|
|
18123
|
+
this.stats.built++;
|
|
18124
|
+
return layer;
|
|
18125
|
+
}
|
|
18126
|
+
/**
|
|
18127
|
+
* Whether a layer's pixels would land where this frame's direct draw
|
|
18128
|
+
* would put them: the same size, the same fraction of a pixel.
|
|
18129
|
+
*/
|
|
18130
|
+
fits(layer, placement) {
|
|
18131
|
+
return !contextLost(layer.frontContext) && !contextLost(layer.backContext) && layer.width === placement.width && layer.height === placement.height && layer.dpr === this.dpr && Math.abs(layer.offsetX - placement.offsetX) < EPSILON && Math.abs(layer.offsetY - placement.offsetY) < EPSILON;
|
|
18132
|
+
}
|
|
18133
|
+
/**
|
|
18134
|
+
* Moves the layer's pixels to follow the record's scroll offset, and
|
|
18135
|
+
* returns the strips that came into view and need painting — none
|
|
18136
|
+
* when it did not scroll, the whole layer when it scrolled further
|
|
18137
|
+
* than it is wide. Null when the scroll was not a whole number of
|
|
18138
|
+
* device pixels, which a copy cannot follow without resampling.
|
|
18139
|
+
*/
|
|
18140
|
+
shift(layer, rec) {
|
|
18141
|
+
const exactX = (layer.scrollX - rec.scrollX) * layer.dpr;
|
|
18142
|
+
const exactY = (layer.scrollY - rec.scrollY) * layer.dpr;
|
|
18143
|
+
const dx = Math.round(exactX);
|
|
18144
|
+
const dy = Math.round(exactY);
|
|
18145
|
+
if (Math.abs(exactX - dx) > EPSILON || Math.abs(exactY - dy) > EPSILON) return null;
|
|
18146
|
+
if (dx === 0 && dy === 0) return [];
|
|
18147
|
+
layer.scrollX -= dx / layer.dpr;
|
|
18148
|
+
layer.scrollY -= dy / layer.dpr;
|
|
18149
|
+
const { width, height } = layer;
|
|
18150
|
+
if (Math.abs(dx) >= width || Math.abs(dy) >= height) {
|
|
18151
|
+
clearLayer(layer.frontContext, width, height);
|
|
18152
|
+
return [{
|
|
18153
|
+
x: 0,
|
|
18154
|
+
y: 0,
|
|
18155
|
+
width,
|
|
18156
|
+
height
|
|
18157
|
+
}];
|
|
18158
|
+
}
|
|
18159
|
+
const back = layer.backContext;
|
|
18160
|
+
clearLayer(back, width, height);
|
|
18161
|
+
back.drawImage(layer.front, dx, dy, width, height);
|
|
18162
|
+
[layer.front, layer.back] = [layer.back, layer.front];
|
|
18163
|
+
[layer.frontContext, layer.backContext] = [layer.backContext, layer.frontContext];
|
|
18164
|
+
this.stats.shifted++;
|
|
18165
|
+
const strips = [];
|
|
18166
|
+
if (dx !== 0) strips.push({
|
|
18167
|
+
x: dx > 0 ? 0 : width + dx,
|
|
18168
|
+
y: 0,
|
|
18169
|
+
width: Math.abs(dx),
|
|
18170
|
+
height
|
|
18171
|
+
});
|
|
18172
|
+
if (dy !== 0) strips.push({
|
|
18173
|
+
x: dx > 0 ? dx : 0,
|
|
18174
|
+
y: dy > 0 ? 0 : height + dy,
|
|
18175
|
+
width: width - Math.abs(dx),
|
|
18176
|
+
height: Math.abs(dy)
|
|
18177
|
+
});
|
|
18178
|
+
return strips;
|
|
18179
|
+
}
|
|
18180
|
+
/** Stops building layers for a container until its subtree is next dirtied. */
|
|
18181
|
+
refuse(node) {
|
|
18182
|
+
this.drop(node);
|
|
18183
|
+
this.refused.add(node);
|
|
18184
|
+
}
|
|
18185
|
+
drop(node) {
|
|
18186
|
+
const layer = this.layers.get(node);
|
|
18187
|
+
if (layer === void 0) return;
|
|
18188
|
+
this.layers.delete(node);
|
|
18189
|
+
this.stats.dropped++;
|
|
18190
|
+
layer.front.width = 0;
|
|
18191
|
+
layer.back.width = 0;
|
|
18192
|
+
}
|
|
18193
|
+
dropAll() {
|
|
18194
|
+
for (const node of this.layers.keys()) this.drop(node);
|
|
18195
|
+
this.refused.clear();
|
|
18196
|
+
}
|
|
18197
|
+
forget(node) {
|
|
18198
|
+
this.drop(node);
|
|
18199
|
+
this.refused.delete(node);
|
|
18200
|
+
}
|
|
18201
|
+
};
|
|
18202
|
+
/** Whether an ancestor was dirtied by more than its own scroll: inherited paint reaches the subtree without saying so. */
|
|
18203
|
+
function dirtyAbove(frame, node) {
|
|
18204
|
+
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
18205
|
+
const flags = frame.dirtyFlagsFor(ancestor);
|
|
18206
|
+
if (flags !== 0 && flags !== 32) return true;
|
|
18207
|
+
}
|
|
18208
|
+
return false;
|
|
18209
|
+
}
|
|
18210
|
+
/**
|
|
18211
|
+
* Where a scroll container's viewport lands in device pixels, or null
|
|
18212
|
+
* when a layer could not reproduce the direct draw exactly: no
|
|
18213
|
+
* transform to read, a rotation, skew or scale above the container, an
|
|
18214
|
+
* opacity in force, or a viewport too large to be worth two copies.
|
|
18215
|
+
*
|
|
18216
|
+
* The layer covers the whole pixels the viewport touches. Its content
|
|
18217
|
+
* is drawn with the same fractional translation the frame has, so a
|
|
18218
|
+
* node lands in the layer at the same fraction of a pixel it would
|
|
18219
|
+
* have on the frame, and the layer is then copied onto whole pixels
|
|
18220
|
+
* with no resampling at all.
|
|
18221
|
+
*/
|
|
18222
|
+
function placeLayer(ctx, rec, dpr, surfaceWidth, surfaceHeight) {
|
|
18223
|
+
const m = ctx.getTransform?.();
|
|
18224
|
+
if (m === void 0 || ctx.globalAlpha !== 1) return null;
|
|
18225
|
+
if (Math.abs(m.b) > EPSILON || Math.abs(m.c) > EPSILON || m.a !== dpr || m.d !== dpr) return null;
|
|
18226
|
+
const left = dpr * rec.x + m.e;
|
|
18227
|
+
const top = dpr * rec.y + m.f;
|
|
18228
|
+
const x = Math.floor(left + EPSILON);
|
|
18229
|
+
const y = Math.floor(top + EPSILON);
|
|
18230
|
+
const width = Math.ceil(left + dpr * rec.width - EPSILON) - x;
|
|
18231
|
+
const height = Math.ceil(top + dpr * rec.height - EPSILON) - y;
|
|
18232
|
+
if (width <= 0 || height <= 0 || width * height > surfaceWidth * surfaceHeight) return null;
|
|
18233
|
+
return {
|
|
18234
|
+
x,
|
|
18235
|
+
y,
|
|
18236
|
+
width,
|
|
18237
|
+
height,
|
|
18238
|
+
offsetX: m.e - x,
|
|
18239
|
+
offsetY: m.f - y
|
|
18240
|
+
};
|
|
18241
|
+
}
|
|
18242
|
+
/**
|
|
18243
|
+
* Whether the browser threw a layer's backing store away — a GPU reset
|
|
18244
|
+
* takes 2D canvases with it — which would leave a layer that composites
|
|
18245
|
+
* as nothing. Optional on the context, as on older engines.
|
|
18246
|
+
*/
|
|
18247
|
+
function contextLost(ctx) {
|
|
18248
|
+
return ctx.isContextLost?.() === true;
|
|
18249
|
+
}
|
|
18250
|
+
function clearLayer(ctx, width, height) {
|
|
18251
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
18252
|
+
ctx.clearRect(0, 0, width, height);
|
|
18253
|
+
}
|
|
18254
|
+
//#endregion
|
|
17672
18255
|
//#region src/rendering/canvas2d/Canvas2DRenderer.ts
|
|
17673
18256
|
/**
|
|
17674
18257
|
* Canvas2D rendering backend.
|
|
@@ -17701,6 +18284,9 @@ function offscreenPaintCanvas(width, height) {
|
|
|
17701
18284
|
* The frame strategy is a full clear + full traversal. Off-screen
|
|
17702
18285
|
* subtrees are culled (while no ancestor transform is active) so
|
|
17703
18286
|
* scrolling large lists only issues draws for the visible window.
|
|
18287
|
+
* The one exception is a scroll container's content on a frame that
|
|
18288
|
+
* only scrolled it, which is copied from the last frame's pixels and
|
|
18289
|
+
* has only the strip that came into view drawn (`ScrollLayers.ts`).
|
|
17704
18290
|
*/
|
|
17705
18291
|
var Canvas2DRenderer = class {
|
|
17706
18292
|
options;
|
|
@@ -17754,8 +18340,32 @@ var Canvas2DRenderer = class {
|
|
|
17754
18340
|
* Drained after the walk; see `render`.
|
|
17755
18341
|
*/
|
|
17756
18342
|
liftedPass = [];
|
|
18343
|
+
/** Scroll containers' content held as pixels; see `ScrollLayers.ts`. */
|
|
18344
|
+
layers;
|
|
18345
|
+
/**
|
|
18346
|
+
* How deep inside a layer's walk the traversal is. A layer is drawn
|
|
18347
|
+
* onto the frame, never onto another layer: an inner container that
|
|
18348
|
+
* scrolls dirties the outer one's subtree anyway, so a layer inside
|
|
18349
|
+
* a layer would be rebuilt every frame the inner one was useful.
|
|
18350
|
+
*/
|
|
18351
|
+
layerDepth = 0;
|
|
18352
|
+
/**
|
|
18353
|
+
* Bumped whenever something is drawn whose pixels change without the
|
|
18354
|
+
* frame's changes saying so — a caret, a scrollbar still fading, a
|
|
18355
|
+
* video frame. A layer's walk compares it before and after, and does
|
|
18356
|
+
* not keep what it drew if it moved.
|
|
18357
|
+
*/
|
|
18358
|
+
volatileDraws = 0;
|
|
17757
18359
|
constructor(options) {
|
|
17758
18360
|
this.options = options;
|
|
18361
|
+
this.layers = new ScrollLayerCache(options.createLayerCanvas ?? offscreenLayerCanvas, options.scrollLayerSettleFrames);
|
|
18362
|
+
}
|
|
18363
|
+
/** What the scroll layers did since the last reset. For budget specs. */
|
|
18364
|
+
get scrollLayerStats() {
|
|
18365
|
+
return this.layers.stats;
|
|
18366
|
+
}
|
|
18367
|
+
resetScrollLayerStats() {
|
|
18368
|
+
this.layers.resetStats();
|
|
17759
18369
|
}
|
|
17760
18370
|
get surface() {
|
|
17761
18371
|
return this.options.surface;
|
|
@@ -17769,10 +18379,12 @@ var Canvas2DRenderer = class {
|
|
|
17769
18379
|
}
|
|
17770
18380
|
resize(width, height, dpr) {
|
|
17771
18381
|
this.surface.setLogicalSize(width, height, dpr);
|
|
18382
|
+
this.layers.dropAll();
|
|
17772
18383
|
}
|
|
17773
18384
|
dispose() {
|
|
17774
18385
|
this.disposed = true;
|
|
17775
18386
|
this.scaledImages.dispose();
|
|
18387
|
+
this.layers.dropAll();
|
|
17776
18388
|
}
|
|
17777
18389
|
render(root, context) {
|
|
17778
18390
|
const ctx = this.surface.getContext2D();
|
|
@@ -17784,9 +18396,12 @@ var Canvas2DRenderer = class {
|
|
|
17784
18396
|
this.cullHeight = this.surface.logicalHeight;
|
|
17785
18397
|
this.liftedPass.length = 0;
|
|
17786
18398
|
this.paintOwner = null;
|
|
18399
|
+
this.layerDepth = 0;
|
|
18400
|
+
this.layers.beginFrame(this.options.scrollLayers === false ? void 0 : context.changes, this.surface.dpr, this.surface.physicalWidth, this.surface.physicalHeight);
|
|
17787
18401
|
this.renderNode(root, context, ctx, true, false);
|
|
17788
18402
|
this.renderLifted(context, ctx);
|
|
17789
18403
|
if (context.overlay !== void 0 && context.overlay.length > 0) drawOverlayShapes(ctx, context.overlay);
|
|
18404
|
+
this.layers.endFrame();
|
|
17790
18405
|
}
|
|
17791
18406
|
/**
|
|
17792
18407
|
* Folds a node's style into the shared scratch and records that it
|
|
@@ -17837,13 +18452,15 @@ var Canvas2DRenderer = class {
|
|
|
17837
18452
|
ctx.clip();
|
|
17838
18453
|
this.pushCull(rec);
|
|
17839
18454
|
}
|
|
17840
|
-
if (rec.scrollable) {
|
|
17841
|
-
ctx.save();
|
|
17842
|
-
ctx.translate(-rec.scrollX, -rec.scrollY);
|
|
17843
|
-
}
|
|
17844
18455
|
const liftedBefore = this.liftedPass.length;
|
|
17845
|
-
this.
|
|
17846
|
-
|
|
18456
|
+
if (!rec.scrollable || !this.renderScrolledThroughLayer(node, rec, context, ctx)) {
|
|
18457
|
+
if (rec.scrollable) {
|
|
18458
|
+
ctx.save();
|
|
18459
|
+
ctx.translate(-rec.scrollX, -rec.scrollY);
|
|
18460
|
+
}
|
|
18461
|
+
this.renderChildren(node, context, ctx, cull && !paint.hasTransform && !sticky);
|
|
18462
|
+
if (rec.scrollable) ctx.restore();
|
|
18463
|
+
}
|
|
17847
18464
|
if (rec.clips) this.popCull();
|
|
17848
18465
|
if (hasText) {
|
|
17849
18466
|
if (this.paintOwner !== node) this.resolvePaint(node);
|
|
@@ -17852,7 +18469,7 @@ var Canvas2DRenderer = class {
|
|
|
17852
18469
|
if (rec.liftBoundary && this.liftedPass.length > liftedBefore) this.renderLifted(context, ctx, liftedBefore, node);
|
|
17853
18470
|
if (rec.clips) ctx.restore();
|
|
17854
18471
|
if (decorations !== null && hasDecorationPhase(decorations, true)) this.paintDecorations(ctx, node, rec, this.paintOwner === node ? this.paint : this.resolvePaint(node), decorations, true);
|
|
17855
|
-
if (rec.scrollable) this.paintScrollbars(ctx, rec, context.now ?? performance.now());
|
|
18472
|
+
if (rec.scrollable || rec.textScrollbars) this.paintScrollbars(ctx, rec, context.now ?? performance.now());
|
|
17856
18473
|
while (saves > 0) {
|
|
17857
18474
|
ctx.restore();
|
|
17858
18475
|
saves--;
|
|
@@ -17942,6 +18559,7 @@ var Canvas2DRenderer = class {
|
|
|
17942
18559
|
const video = paint.video;
|
|
17943
18560
|
const source = video?.frame ?? paint.image;
|
|
17944
18561
|
if (source === void 0 || source === null) return;
|
|
18562
|
+
if (video !== void 0) this.volatileDraws++;
|
|
17945
18563
|
const size = video === void 0 ? {
|
|
17946
18564
|
width: paint.image.width,
|
|
17947
18565
|
height: paint.image.height
|
|
@@ -17979,9 +18597,7 @@ var Canvas2DRenderer = class {
|
|
|
17979
18597
|
* story for a painted node.
|
|
17980
18598
|
*/
|
|
17981
18599
|
paintPicture(ctx, node, rec) {
|
|
17982
|
-
|
|
17983
|
-
if (picture === void 0) return;
|
|
17984
|
-
ctx.drawImage(picture, rec.x, rec.y, rec.width, rec.height);
|
|
18600
|
+
paintPictures.draw(node, rec, this.surface.dpr, ctx);
|
|
17985
18601
|
}
|
|
17986
18602
|
/**
|
|
17987
18603
|
* The image to draw for a destination `width` x `height` in the
|
|
@@ -18106,6 +18722,7 @@ var Canvas2DRenderer = class {
|
|
|
18106
18722
|
*/
|
|
18107
18723
|
paintEditable(ctx, paint, context) {
|
|
18108
18724
|
const model = paint.editor;
|
|
18725
|
+
if (model.focused) this.volatileDraws++;
|
|
18109
18726
|
const layout = new EditableLayout(model, this.contentBox, paint, context.text);
|
|
18110
18727
|
fillTextRects(ctx, textRunBackgrounds(layout.lines, paint));
|
|
18111
18728
|
if (model.focused && !model.collapsed) {
|
|
@@ -18131,6 +18748,101 @@ var Canvas2DRenderer = class {
|
|
|
18131
18748
|
ctx.fillRect(Math.round(caret.x), caret.y, 1, caret.height);
|
|
18132
18749
|
}
|
|
18133
18750
|
}
|
|
18751
|
+
/**
|
|
18752
|
+
* Draws a scroll container's children through its layer, and returns
|
|
18753
|
+
* whether it did. False means the caller draws them directly, as it
|
|
18754
|
+
* always used to; that is the answer to every doubt below.
|
|
18755
|
+
*
|
|
18756
|
+
* Called where the direct path would translate by the scroll offset,
|
|
18757
|
+
* so the container's clip is open and its background and border are
|
|
18758
|
+
* already drawn. The layer is copied onto the frame under that clip,
|
|
18759
|
+
* which is how a rounded viewport stays rounded.
|
|
18760
|
+
*/
|
|
18761
|
+
renderScrolledThroughLayer(node, rec, context, ctx) {
|
|
18762
|
+
const layers = this.layers;
|
|
18763
|
+
if (!layers.active || this.layerDepth > 0) return false;
|
|
18764
|
+
const dpr = this.surface.dpr;
|
|
18765
|
+
const width = this.surface.physicalWidth;
|
|
18766
|
+
const height = this.surface.physicalHeight;
|
|
18767
|
+
const placement = rec.clips && node.type !== "editable-text" && node.firstChild !== null ? placeLayer(ctx, rec, dpr, width, height) : null;
|
|
18768
|
+
let layer = layers.get(node);
|
|
18769
|
+
if (placement === null) {
|
|
18770
|
+
layers.drop(node);
|
|
18771
|
+
return false;
|
|
18772
|
+
}
|
|
18773
|
+
if (layer !== void 0 && !layers.fits(layer, placement)) {
|
|
18774
|
+
layers.drop(node);
|
|
18775
|
+
layer = void 0;
|
|
18776
|
+
}
|
|
18777
|
+
const volatileBefore = this.volatileDraws;
|
|
18778
|
+
const liftedBefore = this.liftedPass.length;
|
|
18779
|
+
if (layer === void 0) {
|
|
18780
|
+
if (!layers.wants(node)) return false;
|
|
18781
|
+
const created = layers.create(node, placement, rec);
|
|
18782
|
+
if (created === null) return false;
|
|
18783
|
+
layer = created;
|
|
18784
|
+
this.paintLayer(layer, null, node, rec, context);
|
|
18785
|
+
} else {
|
|
18786
|
+
const strips = layers.shift(layer, rec);
|
|
18787
|
+
if (strips === null) {
|
|
18788
|
+
layers.drop(node);
|
|
18789
|
+
return false;
|
|
18790
|
+
}
|
|
18791
|
+
for (const strip of strips) this.paintLayer(layer, strip, node, rec, context);
|
|
18792
|
+
}
|
|
18793
|
+
layer.used = true;
|
|
18794
|
+
layers.stats.composited++;
|
|
18795
|
+
ctx.save();
|
|
18796
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
18797
|
+
ctx.drawImage(layer.front, placement.x, placement.y, layer.width, layer.height);
|
|
18798
|
+
ctx.restore();
|
|
18799
|
+
if (this.volatileDraws !== volatileBefore || this.liftedPass.length !== liftedBefore) layers.refuse(node);
|
|
18800
|
+
return true;
|
|
18801
|
+
}
|
|
18802
|
+
/**
|
|
18803
|
+
* Draws the container's children into its layer: the whole of it
|
|
18804
|
+
* when `strip` is null, otherwise only the strip a scroll exposed.
|
|
18805
|
+
*
|
|
18806
|
+
* A strip is clipped, not culled. The walk visits exactly the nodes a
|
|
18807
|
+
* direct draw of the viewport would, and the clip keeps their pixels
|
|
18808
|
+
* inside the strip; culling to the strip instead would miss a shadow,
|
|
18809
|
+
* a focus ring or overflowing text reaching into it from a node whose
|
|
18810
|
+
* box does not, and leave a seam where a direct draw has none. What a
|
|
18811
|
+
* strip saves is raster, which is the cost it exists for; the walk
|
|
18812
|
+
* costs what a direct draw's walk would.
|
|
18813
|
+
*/
|
|
18814
|
+
paintLayer(layer, strip, node, rec, context) {
|
|
18815
|
+
const lctx = layer.frontContext;
|
|
18816
|
+
lctx.save();
|
|
18817
|
+
if (strip !== null) {
|
|
18818
|
+
lctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
18819
|
+
lctx.beginPath();
|
|
18820
|
+
lctx.rect(strip.x, strip.y, strip.width, strip.height);
|
|
18821
|
+
lctx.clip();
|
|
18822
|
+
}
|
|
18823
|
+
const dpr = layer.dpr;
|
|
18824
|
+
lctx.setTransform(dpr, 0, 0, dpr, layer.offsetX, layer.offsetY);
|
|
18825
|
+
lctx.translate(-layer.scrollX, -layer.scrollY);
|
|
18826
|
+
const savedCullX = this.cullX;
|
|
18827
|
+
const savedCullY = this.cullY;
|
|
18828
|
+
const savedCullWidth = this.cullWidth;
|
|
18829
|
+
const savedCullHeight = this.cullHeight;
|
|
18830
|
+
this.cullX = rec.x + layer.scrollX;
|
|
18831
|
+
this.cullY = rec.y + layer.scrollY;
|
|
18832
|
+
this.cullWidth = rec.width;
|
|
18833
|
+
this.cullHeight = rec.height;
|
|
18834
|
+
this.layerDepth++;
|
|
18835
|
+
try {
|
|
18836
|
+
this.renderChildren(node, context, lctx, true);
|
|
18837
|
+
} finally {
|
|
18838
|
+
this.layerDepth--;
|
|
18839
|
+
this.cullX = savedCullX;
|
|
18840
|
+
this.cullY = savedCullY;
|
|
18841
|
+
this.cullWidth = savedCullWidth;
|
|
18842
|
+
this.cullHeight = savedCullHeight;
|
|
18843
|
+
lctx.restore();
|
|
18844
|
+
}
|
|
18845
|
+
}
|
|
18134
18846
|
applyTransform(ctx, rec, paint) {
|
|
18135
18847
|
const transform = paint.transform;
|
|
18136
18848
|
const originX = rec.x + transform.x;
|
|
@@ -18159,6 +18871,7 @@ var Canvas2DRenderer = class {
|
|
|
18159
18871
|
paintScrollbars(ctx, rec, now) {
|
|
18160
18872
|
const remaining = rec.scrollbarVisibleUntil - now;
|
|
18161
18873
|
if (remaining <= 0) return;
|
|
18874
|
+
this.volatileDraws++;
|
|
18162
18875
|
ctx.fillStyle = `rgba(128,128,128,${(Math.min(1, remaining / 350) * .55).toFixed(3)})`;
|
|
18163
18876
|
const { vertical, horizontal } = scrollbarThumbs(rec);
|
|
18164
18877
|
for (const bar of [vertical, horizontal]) {
|
|
@@ -18300,6 +19013,6 @@ function createCanvasSurface(host) {
|
|
|
18300
19013
|
return new CanvasSurface(host);
|
|
18301
19014
|
}
|
|
18302
19015
|
//#endregion
|
|
18303
|
-
export { prepareInputSurface as $,
|
|
19016
|
+
export { prepareInputSurface as $, UiNodeType as $a, isTypographyRole as $i, offsetForVerticalMove as $n, isMinMaxTrack as $r, DEFAULT_FONT_FAMILY as $t, accumulatedOffsetTo as A, writeDeclaredProperty as Aa, hasThemeExtension as Ai, linkOf as An, statesEqual as Ar, ELLIPSIS as At, parseFlexDirection as B, UiFocusEvent as Ba, defaultSpacing as Bi, isMultiline as Bn, transform as Br, drawText as Bt, describeLength as C, findEnvironmentKey as Ca, unknownContainerSize as Ci, isNodeInert as Cn, propertyValuesEqualByName as Cr, paragraphGeometry as Ct, formatNumber as D, clearOverrideProperty as Da, withContrast as Di, clearLinkHover as Dn, isUiRole as Dr, CharacterCountTextMeasurer as Dt, formatExplanation as E, DirtyNodeSet as Ea, themesEqual as Ei, resolveCursor as En, UI_SEMANTIC_STATES as Er, wordRangeIn as Et, constraintsEqual as F, visualState as Fa, contrastRatio as Fi, EDITOR_PROP as Fn, inputsEqual as Fr, isPrintable as Ft, MainAxisAlignment as G, UiPinchEvent as Ga, shadowsEqual as Gi, isVideoSurface as Gn, gradientsEqual as Gr, spanOf as Gt, sizeGridTracks as H, UiInputEvent as Ha, spacingEqual as Hi, nextCaretToggle as Hn, transformsEqual as Hr, layoutTextLines as Ht, tightenConstraints as I, visualStatesEqual as Ia, highContrastColors as Ii, caretVisibleAt as In, paintValuesEqual as Ir, EditableLayout as It, parseMainAxisAlignment as J, UiTextChangeEvent as Ja, boxShadowsEqual as Ji, lineIndexAtY as Jn, validateGradient as Jr, textRunDecorations as Jt, parseAlignContent as K, UiPointerEvent as Ka, boxShadow as Ki, videoFrameSize as Kn, linearGradient as Kr, textMeasureRequest as Kt, MAX_MEASURES_PER_CHILD as L, MOUSE_POINTER as La, raiseContrast as Li, editorFor as Ln, pathValuesEqual as Lr, applyCanvasTextStyle as Lt, LayoutRecord as M, UiVisualState as Ma, themeExtension as Mi, setLinkHover as Mn, validateStates as Mr, proportionalFontMetrics as Mt, Constraints as N, defaultVisualState as Na, themeExtensionsEqual as Ni, CARET_BLINK_MS as Nn, UI_SUBGRID_AXES as Nr, commandForKey as Nt, labelNode as O, overrideSources as Oa, withDensity as Oi, hasTextLinks as On, isUiSemanticState as Or, ParagraphTextMeasurer as Ot, clampSize as P, hasVisualState as Pa, withThemeExtension as Pi, CARET_WIDTH as Pn, validateSubgrid as Pr, detectEditingPlatform as Pt, pointerDeviceOf as Q, wheelDeltaYOf as Qa, defaultTypography as Qi, offsetAtX as Qn, isFrLength as Qr, registerFontStack as Qt, isLayoutProtocol as R, UiBeforeInputEvent as Ra, relativeLuminance as Ri, editorOf as Rn, UiTransforms as Rr, buildFontString as Rt, SCROLLBAR_LINGER_MS as S, createEnvironmentKey as Sa, containerBands as Si, isNodeHitTestable as Sn, propertyIsInherited as Sr, offsetAtPointIn as St, formatConstraints as T, UiNode as Ta, lightTheme as Ti, isNodeTabStop as Tn, UI_ROLES as Tr, selectionRectsIn as Tt, AlignContent as U, UiKeyboardEvent as Ua, spacingSteps as Ui, syncEditorValue as Un, MAX_GRADIENT_STOPS as Ur, runCanvasStyleInto as Ut, placeGridItems as V, UiGestureEvent as Va, scaleSpacing as Vi, isReadOnly as Vn, transformIsIdentity as Vr, fillTextRects as Vt, CrossAxisAlignment as W, UiPasteEvent as Wa, defaultShadows as Wi, resolveColorValue as Wn, gradientPaint as Wr, runFontString as Wt, UiPlatformAdapter as X, UiWheelEvent as Xa, isShapeName as Xi, lineLimit as Xn, fr as Xr, clearFontStacks as Xt, CanvasPlatformSurface as Y, UiWheelDeltaMode as Ya, defaultShapes as Yi, lineIndexForOffset as Yn, auto as Yr, bumpFontStack as Yt, capturePointer as Z, noKeyModifiers as Za, shapesEqual as Zi, offsetAtPoint as Zn, isAutoLength as Zr, fontStackFor as Zt, LABEL_PADDING_X as _, colorsEqual as _a, insetsEqual as _i, selectableTextNodes as _n, findPropertyDefinition as _r, scrollbarThumb as _t, PaintPictureCache as a, spanAtOffset as aa, UiBorderRadiuses as ai, createPaintState as an, nextGraphemeEnd as ar, UiWheelController as at, LayoutEngine as b, rgb8 as ba, UiContainerSizeSource as bi, setSelectionRange as bn, inheritedPropertyNames as br, UiInputDispatcher as bt, EMPTY_RECORDING as c, textSpansEqual as ca, borderRadiusEqual as ci, normalizeVerticalAlign as cn, previousWordStart as cr, UiPointerController as ct, tracePathData as d, darkColors as da, normalizeBorderRadius as di, TEXT_MATCHES_PROP as dn, resolveNumber as dr, SCROLLBAR_HOVER_ZONE as dt, typographyEqual as ea, isPercentLength as ei, DEFAULT_FONT_SIZE as en, DirtyFlags as eo, selectionRects as er, touchActionFor as et, ScaledImageCache as f, lightColors as fa, uniformBorderRadius as fi, clearMatchRanges as fn, resolveProperty as fr, SCROLLBAR_INSET as ft, paintsAfterChildren as g, colorValuesEqual as ga, UiInsetRegistry as gi, clearSelectionRange as gn, closestPropertyName as gr, scrollRange as gt, hasDecorationPhase as h, colorToRgba as ha, UiEnvironmentKeys as hi, TEXT_SELECTION_PROP as hn, UiGraph as hr, scrollLeaderOf as ht, traceRoundedRect as i, resolvedSpansOf as ia, resolveLength as ii, computeObjectFitRect as in, lineStartAt as ir, UiPinchRecognizer as it, contentOffset as j, writeOverrideProperty as ja, noThemeExtensions as ji, linkRunAtPointIn as jn, validateRole as jr, layoutParagraph as jt, withOverrideSource as k, resetOverrideWarnings as ka, defineThemeExtension as ki, linkHoverOf as kn, normalizeStates as kr, spannedRunsFor as kt, PaintRecorder as l, textStylesEqual as la, borderRadiusIsZero as li, resolvePaintState as ln, wordRangeAt as lr, UiFocusManager as lt, decorationRect as m, colorToHex as ma, propertyValuesEqual as mi, setMatchRanges as mn, resolveString as mr, SCROLLBAR_THICKNESS as mt, createCanvasSurface as n, editableSpansOf as na, percent as ni, DEFAULT_TEXT_COLOR as nn, graphemeBoundaries as nr, UiTouchScroller as nt, paintPictures as o, spannedTextOf as oa, borderRadius as oi, isPaintVisible as on, nextWordEnd as or, isScrollContainer as ot, decorationColor as p, UiBasicColors as pa, defineProperty as pi, matchRangesOf as pn, resolvePropertyByName as pr, SCROLLBAR_MIN_THUMB as pt, parseCrossAxisAlignment as q, UiSelectionChangeEvent as qa, boxShadowArraysEqual as qi, caretRectFor as qn, radialGradient as qr, textRunBackgrounds as qt, Canvas2DRenderer as r, flattenTextSpans as ra, repeat as ri, colorToCss as rn, lineEndAt as rr, UiGestureRecognizer as rt, PaintTarget as s, textContentOf as sa, borderRadiusCorners as si, normalizeTextAlign as sn, previousGraphemeStart as sr, UiKeyboardController as st, CanvasSurface as t, defaultTextStyle as ta, minmax as ti, DEFAULT_FONT_WEIGHT as tn, UiBinding as to, EditableTextModel as tr, UiEditingController as tt, replayPaint as u, colorsEqualPalette as ua, borderRadiusValuesEqual as ui, DEFAULT_LINE_HEIGHT_FACTOR as un, resolveBoolean as ur, UiHitTester as ut, drawOverlayShapes as v, normalizeColor as va, noInsets as vi, selectableTextOf as vn, getPropertyNames as vr, scrollbarThumbs as vt, describeOverrides as w, UiEnvironment as wa, darkTheme as wi, isNodeSelectable as wn, UiProperties as wr, paragraphGeometryFrom as wt, SCROLLBAR_FADE_MS as x, rgba as xa, bandOf as xi, isNodeFocusable as xn, propertyEffects as xr, hasDrawnText as xt, labelOrigin as y, parseColor as ya, observeViewportInsets as yi, selectionRangeOf as yn, inheritedPropertyFlags as yr, scrollbarZoneAt as yt, FlexDirection as z, UiEventType as za, densityFactors as zi, isEditableNode as zn, parseTransform as zr, createCanvasTextStyle as zt };
|
|
18304
19017
|
|
|
18305
|
-
//# sourceMappingURL=CanvasSurface-
|
|
19018
|
+
//# sourceMappingURL=CanvasSurface-BMOmE_dD.js.map
|