gesso-core 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/dist/{CanvasSurface-ifHkg0-g.js → CanvasSurface-CaNWsAlm.js} +776 -84
- package/dist/CanvasSurface-CaNWsAlm.js.map +1 -0
- package/dist/{index-B4o3nKt2.d.ts → index-CfUSbphd.d.ts} +353 -18
- package/dist/index.d.ts +2 -2
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +13 -1
- package/dist/testing.js +72 -1
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/CanvasSurface-ifHkg0-g.js.map +0 -1
|
@@ -188,6 +188,16 @@ let UiEventType = /* @__PURE__ */ function(UiEventType) {
|
|
|
188
188
|
*/
|
|
189
189
|
UiEventType["Paste"] = "paste";
|
|
190
190
|
UiEventType["Click"] = "click";
|
|
191
|
+
/**
|
|
192
|
+
* A second Click on the same node, soon enough after the first and
|
|
193
|
+
* close enough to it.
|
|
194
|
+
*
|
|
195
|
+
* Dispatched after that second Click rather than instead of it, as
|
|
196
|
+
* the DOM's `dblclick` is: a listener that answers a click has
|
|
197
|
+
* already answered both by the time this arrives, which is what lets
|
|
198
|
+
* a double click on a cell select it and then do more.
|
|
199
|
+
*/
|
|
200
|
+
UiEventType["DoubleClick"] = "doubleclick";
|
|
191
201
|
UiEventType["LongPress"] = "longpress";
|
|
192
202
|
UiEventType["DragStart"] = "dragstart";
|
|
193
203
|
UiEventType["DragMove"] = "dragmove";
|
|
@@ -3937,6 +3947,20 @@ const UiProperties = {
|
|
|
3937
3947
|
inherited: false,
|
|
3938
3948
|
affects: L
|
|
3939
3949
|
}),
|
|
3950
|
+
/**
|
|
3951
|
+
* A point to be placed beside, as though it were an anchor of no
|
|
3952
|
+
* size: where a context menu was asked for. Relative to the
|
|
3953
|
+
* containing block, like `top` and `left`, and with `placement` the
|
|
3954
|
+
* same flip and clamp an anchored node gets — so a menu opened near
|
|
3955
|
+
* the bottom edge opens upward instead of off the screen. Ignored
|
|
3956
|
+
* when `anchor` is set.
|
|
3957
|
+
*/
|
|
3958
|
+
anchorPoint: defineProperty({
|
|
3959
|
+
name: "anchorPoint",
|
|
3960
|
+
defaultValue: void 0,
|
|
3961
|
+
inherited: false,
|
|
3962
|
+
affects: L
|
|
3963
|
+
}),
|
|
3940
3964
|
/** Gap between an anchored node and its anchor. */
|
|
3941
3965
|
anchorOffset: defineProperty({
|
|
3942
3966
|
name: "anchorOffset",
|
|
@@ -4512,6 +4536,12 @@ const UiProperties = {
|
|
|
4512
4536
|
* wants the default, or the article stops scrolling whenever the
|
|
4513
4537
|
* cursor crosses the canvas.
|
|
4514
4538
|
*
|
|
4539
|
+
* Unlike the CSS property it is honoured on any node, not only a
|
|
4540
|
+
* scroll container. An app's root is rarely one, and neither is a
|
|
4541
|
+
* canvas that pans and zooms on the wheel itself — which wants
|
|
4542
|
+
* `'contain'` so a ctrl-wheel, or a trackpad pinch, zooms the canvas
|
|
4543
|
+
* and not the page around it.
|
|
4544
|
+
*
|
|
4515
4545
|
* Like `scrollBehavior`, this is read by the input layer and
|
|
4516
4546
|
* affects nothing laid out or painted.
|
|
4517
4547
|
*/
|
|
@@ -9435,6 +9465,15 @@ var UiPointerController = class {
|
|
|
9435
9465
|
onHoverChange;
|
|
9436
9466
|
editing;
|
|
9437
9467
|
selection;
|
|
9468
|
+
now;
|
|
9469
|
+
/**
|
|
9470
|
+
* The last Click, for telling whether the next one makes a pair.
|
|
9471
|
+
*
|
|
9472
|
+
* Only a Click counts, not a press: a press that became a drag, or
|
|
9473
|
+
* was cancelled, is not half of a double click, and a release that
|
|
9474
|
+
* followed one would otherwise be read as the second half of it.
|
|
9475
|
+
*/
|
|
9476
|
+
lastClick = null;
|
|
9438
9477
|
hoverNode = null;
|
|
9439
9478
|
downTarget = null;
|
|
9440
9479
|
downX = 0;
|
|
@@ -9461,6 +9500,7 @@ var UiPointerController = class {
|
|
|
9461
9500
|
this.onHoverChange = options.onHoverChange ?? null;
|
|
9462
9501
|
this.editing = options.editing;
|
|
9463
9502
|
this.selection = options.selection;
|
|
9503
|
+
this.now = options.now ?? defaultClock$2;
|
|
9464
9504
|
}
|
|
9465
9505
|
/** The node currently under the pointer, or null over empty space. */
|
|
9466
9506
|
/** Where the pointer was last seen, in canvas space; null before it has been. */
|
|
@@ -9597,12 +9637,39 @@ var UiPointerController = class {
|
|
|
9597
9637
|
if (dx <= slop && dy <= slop) {
|
|
9598
9638
|
const click = new UiPointerEvent("click", x, y, buttons, modifiers, pointer);
|
|
9599
9639
|
this.dispatcher.dispatch(click, target);
|
|
9600
|
-
|
|
9601
|
-
|
|
9640
|
+
this.countClick(target, x, y, buttons, modifiers, pointer);
|
|
9641
|
+
} else this.lastClick = null;
|
|
9642
|
+
} else this.lastClick = null;
|
|
9602
9643
|
this.releaseHover(x, y, modifiers, pointer);
|
|
9603
9644
|
return event;
|
|
9604
9645
|
}
|
|
9605
9646
|
/**
|
|
9647
|
+
* Pairs this Click with the one before it, and dispatches a
|
|
9648
|
+
* DoubleClick when they make a pair.
|
|
9649
|
+
*
|
|
9650
|
+
* A pair is two Clicks on the same node within `MULTI_CLICK_MS` and
|
|
9651
|
+
* `MULTI_CLICK_SLOP` of each other — the window the editing and
|
|
9652
|
+
* selection controllers already use for a word, so a double click
|
|
9653
|
+
* means the same thing on a label as it does in a field. The pair is
|
|
9654
|
+
* spent once it has fired: a third Click starts the next pair rather
|
|
9655
|
+
* than making a second DoubleClick out of the second and third.
|
|
9656
|
+
*/
|
|
9657
|
+
countClick(target, x, y, buttons, modifiers, pointer) {
|
|
9658
|
+
const now = this.now();
|
|
9659
|
+
const last = this.lastClick;
|
|
9660
|
+
if (!(last !== null && last.node === target && now - last.at <= MULTI_CLICK_MS$1 && Math.abs(last.x - x) <= MULTI_CLICK_SLOP$1 && Math.abs(last.y - y) <= MULTI_CLICK_SLOP$1)) {
|
|
9661
|
+
this.lastClick = {
|
|
9662
|
+
node: target,
|
|
9663
|
+
x,
|
|
9664
|
+
y,
|
|
9665
|
+
at: now
|
|
9666
|
+
};
|
|
9667
|
+
return;
|
|
9668
|
+
}
|
|
9669
|
+
this.lastClick = null;
|
|
9670
|
+
this.dispatcher.dispatch(new UiPointerEvent("doubleclick", x, y, buttons, modifiers, pointer), target);
|
|
9671
|
+
}
|
|
9672
|
+
/**
|
|
9606
9673
|
* Aborts the active press: dispatches PointerCancel to the pressed
|
|
9607
9674
|
* node and clears press state so no Click is synthesized.
|
|
9608
9675
|
*/
|
|
@@ -9754,6 +9821,12 @@ var UiPointerController = class {
|
|
|
9754
9821
|
function isSecondaryButton(buttons) {
|
|
9755
9822
|
return buttons === 2;
|
|
9756
9823
|
}
|
|
9824
|
+
/** How long, and how far, a second Click may be from the first and still pair with it. */
|
|
9825
|
+
const MULTI_CLICK_MS$1 = 500;
|
|
9826
|
+
const MULTI_CLICK_SLOP$1 = 4;
|
|
9827
|
+
function defaultClock$2() {
|
|
9828
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
9829
|
+
}
|
|
9757
9830
|
//#endregion
|
|
9758
9831
|
//#region src/input/UiKeyboardController.ts
|
|
9759
9832
|
/**
|
|
@@ -9808,13 +9881,19 @@ var UiKeyboardController = class {
|
|
|
9808
9881
|
this.find = options.find;
|
|
9809
9882
|
this.activation = options.activation;
|
|
9810
9883
|
}
|
|
9811
|
-
|
|
9884
|
+
/**
|
|
9885
|
+
* `textFollows` says whether the shell will send the key's text on its
|
|
9886
|
+
* own, as a `beforeinput`. False — a key that reached the canvas while
|
|
9887
|
+
* an editable held the focus — has the key inserted here, whatever the
|
|
9888
|
+
* text source; absent leaves it to the editing controller's setting.
|
|
9889
|
+
*/
|
|
9890
|
+
keyDown(key, modifiers = noKeyModifiers(), textFollows) {
|
|
9812
9891
|
if (!MODIFIER_KEYS.has(key)) this.focusManager.noteInput("keyboard");
|
|
9813
9892
|
const event = new UiKeyboardEvent("keydown", key, modifiers);
|
|
9814
9893
|
const focused = this.focusManager.focusedNode;
|
|
9815
9894
|
const target = focused ?? this.root();
|
|
9816
9895
|
this.dispatcher.dispatch(event, target);
|
|
9817
|
-
if (!event.defaultPrevented && focused !== null && this.editing !== void 0 && this.editing.handleKey(focused, key, modifiers)) {
|
|
9896
|
+
if (!event.defaultPrevented && focused !== null && this.editing !== void 0 && this.editing.handleKey(focused, key, modifiers, textFollows === false ? true : void 0)) {
|
|
9818
9897
|
event.preventDefault();
|
|
9819
9898
|
return event;
|
|
9820
9899
|
}
|
|
@@ -9894,6 +9973,15 @@ function hasScrollRoom(offset, max, delta) {
|
|
|
9894
9973
|
return delta > 0 ? offset < max - SCROLL_EPSILON : offset > SCROLL_EPSILON;
|
|
9895
9974
|
}
|
|
9896
9975
|
/** Whether this node keeps overscroll rather than chaining it outwards. */
|
|
9976
|
+
/**
|
|
9977
|
+
* Whether a node keeps the wheel deltas nothing below it used.
|
|
9978
|
+
*
|
|
9979
|
+
* Honoured on any node, where CSS's `overscroll-behavior` applies only
|
|
9980
|
+
* to scroll containers. The property is how an app says "no wheel over
|
|
9981
|
+
* me reaches the page", and the nodes that most need to say it — an
|
|
9982
|
+
* app's root, a canvas that pans and zooms on the wheel itself — are
|
|
9983
|
+
* the ones that are not scroll containers.
|
|
9984
|
+
*/
|
|
9897
9985
|
function containsOverscroll(node) {
|
|
9898
9986
|
return node !== null && node.getProperty("overscrollBehavior") === "contain";
|
|
9899
9987
|
}
|
|
@@ -10026,16 +10114,16 @@ var UiWheelController = class {
|
|
|
10026
10114
|
let left = false;
|
|
10027
10115
|
let right = false;
|
|
10028
10116
|
for (let node = target; node !== null; node = node.parent) {
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10117
|
+
const state = isScrollContainer(node) ? this.scrollSink.containerState(node) : void 0;
|
|
10118
|
+
if (state !== void 0) {
|
|
10119
|
+
left ||= hasScrollRoom(state.scrollX, state.maxScrollX, -1);
|
|
10120
|
+
right ||= hasScrollRoom(state.scrollX, state.maxScrollX, 1);
|
|
10121
|
+
up ||= hasScrollRoom(state.scrollY, state.maxScrollY, -1);
|
|
10122
|
+
down ||= hasScrollRoom(state.scrollY, state.maxScrollY, 1);
|
|
10123
|
+
}
|
|
10036
10124
|
if (containsOverscroll(node)) return EVERYTHING_SCROLLABLE;
|
|
10037
10125
|
}
|
|
10038
|
-
if (
|
|
10126
|
+
if (this.appContainsOverscroll(target)) return EVERYTHING_SCROLLABLE;
|
|
10039
10127
|
return {
|
|
10040
10128
|
up,
|
|
10041
10129
|
down,
|
|
@@ -10074,9 +10162,14 @@ var UiWheelController = class {
|
|
|
10074
10162
|
*/
|
|
10075
10163
|
scrollChain(target, event, deltaX, deltaY, deltaMode, wheelDeltaY) {
|
|
10076
10164
|
for (let node = target; node !== null; node = node.parent) {
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10165
|
+
const state = isScrollContainer(node) ? this.scrollSink.containerState(node) : void 0;
|
|
10166
|
+
if (state === void 0) {
|
|
10167
|
+
if (containsOverscroll(node)) {
|
|
10168
|
+
event.markConsumed();
|
|
10169
|
+
return;
|
|
10170
|
+
}
|
|
10171
|
+
continue;
|
|
10172
|
+
}
|
|
10080
10173
|
const takesX = deltaX !== 0 && hasScrollRoom(state.scrollX, state.maxScrollX, deltaX);
|
|
10081
10174
|
const takesY = deltaY !== 0 && hasScrollRoom(state.scrollY, state.maxScrollY, deltaY);
|
|
10082
10175
|
if (takesX || takesY) {
|
|
@@ -10089,7 +10182,22 @@ var UiWheelController = class {
|
|
|
10089
10182
|
return;
|
|
10090
10183
|
}
|
|
10091
10184
|
}
|
|
10092
|
-
if (
|
|
10185
|
+
if (this.appContainsOverscroll(target)) event.markConsumed();
|
|
10186
|
+
}
|
|
10187
|
+
/**
|
|
10188
|
+
* Whether the application's root asked to keep every wheel.
|
|
10189
|
+
*
|
|
10190
|
+
* Asked of `rootNode` first, because in a mounted app the tree's
|
|
10191
|
+
* topmost node is not the application's: the runtime wraps the app
|
|
10192
|
+
* in a stack beside its overlay layer, and `contain` set on the app's
|
|
10193
|
+
* own root — which is where the property's documentation says to set
|
|
10194
|
+
* it — was on a node `rootOf` never reached. A node in the overlay
|
|
10195
|
+
* layer is not under the app's root either, so the chain walk alone
|
|
10196
|
+
* would miss it too. `rootOf` is kept for a host that passes no
|
|
10197
|
+
* root, where the topmost node is the only root there is.
|
|
10198
|
+
*/
|
|
10199
|
+
appContainsOverscroll(target) {
|
|
10200
|
+
return containsOverscroll(this.rootNode?.() ?? null) || containsOverscroll(rootOf(target));
|
|
10093
10201
|
}
|
|
10094
10202
|
applyDelta(container, state, deltaX, deltaY, deltaMode, behavior) {
|
|
10095
10203
|
this.scrollSink.scrollBy(container, deltaX === 0 ? 0 : this.toPixels(deltaX, deltaMode, state.viewportWidth), deltaY === 0 ? 0 : this.toPixels(deltaY, deltaMode, state.viewportHeight), behavior);
|
|
@@ -10353,6 +10461,11 @@ const VELOCITY_SAMPLES = 8;
|
|
|
10353
10461
|
* long press raises nothing: it has a secondary button, and the
|
|
10354
10462
|
* pointer controller dispatches the event from that instead.
|
|
10355
10463
|
*
|
|
10464
|
+
* A mouse long press does not cost the Click either, unless its
|
|
10465
|
+
* LongPress was `preventDefault()`ed or it went on to a Drag: a button
|
|
10466
|
+
* held down a moment before letting go is a slow click, and dropping
|
|
10467
|
+
* it made toolbar buttons seem to ignore a deliberate press.
|
|
10468
|
+
*
|
|
10356
10469
|
* ## Two contacts
|
|
10357
10470
|
*
|
|
10358
10471
|
* Every contact the controller sees is fed to a `UiPinchRecognizer`,
|
|
@@ -10432,6 +10545,7 @@ var UiGestureRecognizer = class {
|
|
|
10432
10545
|
return;
|
|
10433
10546
|
}
|
|
10434
10547
|
if (this.state === "longPressed") {
|
|
10548
|
+
if (!this.claimedPress && Math.hypot(event.x - this.startX, event.y - this.startY) <= this.pressSlop()) return;
|
|
10435
10549
|
this.state = "dragging";
|
|
10436
10550
|
this.claimedPress = true;
|
|
10437
10551
|
this.dispatch("dragstart", target, this.startX, this.startY, event);
|
|
@@ -10486,8 +10600,10 @@ var UiGestureRecognizer = class {
|
|
|
10486
10600
|
this.timer = null;
|
|
10487
10601
|
if (this.state !== "pressing") return;
|
|
10488
10602
|
this.state = "longPressed";
|
|
10489
|
-
|
|
10603
|
+
const mouse = down.pointer.kind === "mouse";
|
|
10604
|
+
this.claimedPress = !mouse;
|
|
10490
10605
|
const held = this.dispatch("longpress", target, this.startX, this.startY, down);
|
|
10606
|
+
if (mouse && held.defaultPrevented) this.claimedPress = true;
|
|
10491
10607
|
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);
|
|
10492
10608
|
}, this.longPressDelay);
|
|
10493
10609
|
}
|
|
@@ -10808,9 +10924,10 @@ var UiEditingController = class {
|
|
|
10808
10924
|
* when the key was an editing command, so the caller can mark the
|
|
10809
10925
|
* event handled.
|
|
10810
10926
|
*/
|
|
10811
|
-
|
|
10927
|
+
/** `textFromKeys` overrides the controller's own setting for this one key; see `UiKeyboardController.keyDown`. */
|
|
10928
|
+
handleKey(node, key, modifiers, textFromKeys = this.textFromKeys) {
|
|
10812
10929
|
if (!isEditableNode(node)) return false;
|
|
10813
|
-
const command = commandForKey(key, modifiers, this.platform,
|
|
10930
|
+
const command = commandForKey(key, modifiers, this.platform, textFromKeys);
|
|
10814
10931
|
if (command === null) return false;
|
|
10815
10932
|
return this.execute(node, command);
|
|
10816
10933
|
}
|
|
@@ -12712,7 +12829,7 @@ function accumulatedOffsetTo(node, records, out) {
|
|
|
12712
12829
|
}
|
|
12713
12830
|
//#endregion
|
|
12714
12831
|
//#region src/layout/LayoutExplanation.ts
|
|
12715
|
-
const EPSILON = 1e-6;
|
|
12832
|
+
const EPSILON$1 = 1e-6;
|
|
12716
12833
|
/**
|
|
12717
12834
|
* Turns the facts about one axis into the rule that decided it and the
|
|
12718
12835
|
* sentences that say so.
|
|
@@ -12727,7 +12844,7 @@ function buildAxisExplanation(f) {
|
|
|
12727
12844
|
if (f.customParent !== void 0 && !f.isRoot) {
|
|
12728
12845
|
decidedBy = "custom";
|
|
12729
12846
|
reasons.push(`the '${f.customParent}' layout on ${f.parentLabel} measured it at ${px(f.measured)}`);
|
|
12730
|
-
if (Math.abs(f.final - f.measured) > EPSILON) reasons.push(`and placed it at ${px(f.final)}`);
|
|
12847
|
+
if (Math.abs(f.final - f.measured) > EPSILON$1) reasons.push(`and placed it at ${px(f.final)}`);
|
|
12731
12848
|
return {
|
|
12732
12849
|
axis: f.axis,
|
|
12733
12850
|
content: f.content,
|
|
@@ -12766,7 +12883,7 @@ function buildAxisExplanation(f) {
|
|
|
12766
12883
|
const to = f.effectiveMin > f.parentMin ? f.ownMin : f.ownMax;
|
|
12767
12884
|
reasons.push(`clamped by its own ${clamp} ${px(to ?? f.effectiveMin)} → ${px(f.effectiveMin)}`);
|
|
12768
12885
|
}
|
|
12769
|
-
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"}`);
|
|
12886
|
+
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"}`);
|
|
12770
12887
|
} else if (f.explicit !== void 0) {
|
|
12771
12888
|
decidedBy = "explicit";
|
|
12772
12889
|
reasons.push(`${f.axis}: ${describeLength(f.explicitRaw)} (explicit) → ${px(f.explicit)}`);
|
|
@@ -12776,29 +12893,29 @@ function buildAxisExplanation(f) {
|
|
|
12776
12893
|
const to = f.effectiveMin > f.explicit ? f.ownMin : f.ownMax;
|
|
12777
12894
|
reasons.push(`clamped by ${clamp} ${px(to ?? f.effectiveMin)} → ${px(f.effectiveMin)}`);
|
|
12778
12895
|
}
|
|
12779
|
-
if (isFinite(f.parentMax) && f.effectiveMin > f.parentMax + EPSILON) reasons.push(`wider than the ${px(f.parentMax)} available: it overflows ${f.parentLabel}`);
|
|
12896
|
+
if (isFinite(f.parentMax) && f.effectiveMin > f.parentMax + EPSILON$1) reasons.push(`wider than the ${px(f.parentMax)} available: it overflows ${f.parentLabel}`);
|
|
12780
12897
|
} else {
|
|
12781
12898
|
reasons.push(contentReason(f));
|
|
12782
12899
|
if (f.flex !== void 0 && !f.flex.scroller) reasons.push(unflexedReason(f.flex));
|
|
12783
|
-
if (f.aspectRatio !== void 0 && Math.abs(f.content - f.measured) > EPSILON) {
|
|
12900
|
+
if (f.aspectRatio !== void 0 && Math.abs(f.content - f.measured) > EPSILON$1) {
|
|
12784
12901
|
decidedBy = "aspect-ratio";
|
|
12785
12902
|
reasons.push(`aspectRatio ${formatNumber(f.aspectRatio)} derives it from the other axis → ${px(f.measured)}`);
|
|
12786
12903
|
}
|
|
12787
|
-
if (f.content < f.effectiveMin - EPSILON) {
|
|
12788
|
-
if (f.ownMin !== void 0 && f.ownMin >= f.effectiveMin - EPSILON) {
|
|
12904
|
+
if (f.content < f.effectiveMin - EPSILON$1) {
|
|
12905
|
+
if (f.ownMin !== void 0 && f.ownMin >= f.effectiveMin - EPSILON$1) {
|
|
12789
12906
|
decidedBy = "min";
|
|
12790
12907
|
reasons.push(`raised to ${minProp} ${px(f.ownMin)}`);
|
|
12791
12908
|
} else {
|
|
12792
12909
|
decidedBy = "min";
|
|
12793
12910
|
reasons.push(`raised to the minimum ${f.parentLabel} asked for: ${px(f.parentMin)}`);
|
|
12794
12911
|
}
|
|
12795
|
-
} else if (f.ownMax !== void 0 && f.content > f.ownMax + EPSILON && f.measured <= f.ownMax + EPSILON) {
|
|
12912
|
+
} else if (f.ownMax !== void 0 && f.content > f.ownMax + EPSILON$1 && f.measured <= f.ownMax + EPSILON$1) {
|
|
12796
12913
|
decidedBy = "max";
|
|
12797
12914
|
reasons.push(`capped by ${maxProp} ${px(f.ownMax)}`);
|
|
12798
12915
|
}
|
|
12799
|
-
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}`);
|
|
12916
|
+
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}`);
|
|
12800
12917
|
}
|
|
12801
|
-
if (Math.abs(f.final - f.measured) > EPSILON) reasons.push(`placed at ${px(f.final)} by ${f.parentLabel} (measured ${px(f.measured)})`);
|
|
12918
|
+
if (Math.abs(f.final - f.measured) > EPSILON$1) reasons.push(`placed at ${px(f.final)} by ${f.parentLabel} (measured ${px(f.measured)})`);
|
|
12802
12919
|
return {
|
|
12803
12920
|
axis: f.axis,
|
|
12804
12921
|
content: f.content,
|
|
@@ -12816,8 +12933,8 @@ function buildAxisExplanation(f) {
|
|
|
12816
12933
|
function flexReasons(f, flex, reasons) {
|
|
12817
12934
|
const px = formatNumber;
|
|
12818
12935
|
const final = f.parentMin;
|
|
12819
|
-
const grew = final > flex.base + EPSILON;
|
|
12820
|
-
const shrank = final < flex.base - EPSILON;
|
|
12936
|
+
const grew = final > flex.base + EPSILON$1;
|
|
12937
|
+
const shrank = final < flex.base - EPSILON$1;
|
|
12821
12938
|
if (!grew && !shrank) {
|
|
12822
12939
|
if (f.explicit !== void 0) {
|
|
12823
12940
|
reasons.push(`${f.axis}: ${describeLength(f.explicitRaw)} (explicit) → ${px(f.explicit)}`);
|
|
@@ -12837,10 +12954,10 @@ function flexReasons(f, flex, reasons) {
|
|
|
12837
12954
|
reasons.push(`flex item of ${flex.containerLabel}: base ${px(flex.base)} from ${baseSource}`);
|
|
12838
12955
|
if (grew) {
|
|
12839
12956
|
reasons.push(`grew to ${px(final)} (flexGrow ${px(flex.grow)} takes a share of the free space)`);
|
|
12840
|
-
if (Math.abs(final - flex.max) < EPSILON && isFinite(flex.max)) reasons.push(`stopped at ${f.axis === "width" ? "maxWidth" : "maxHeight"} ${px(flex.max)}`);
|
|
12957
|
+
if (Math.abs(final - flex.max) < EPSILON$1 && isFinite(flex.max)) reasons.push(`stopped at ${f.axis === "width" ? "maxWidth" : "maxHeight"} ${px(flex.max)}`);
|
|
12841
12958
|
} else {
|
|
12842
12959
|
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)`);
|
|
12843
|
-
if (Math.abs(final - flex.min) < EPSILON) reasons.push(minimumReason(f, flex));
|
|
12960
|
+
if (Math.abs(final - flex.min) < EPSILON$1) reasons.push(minimumReason(f, flex));
|
|
12844
12961
|
}
|
|
12845
12962
|
return "flex";
|
|
12846
12963
|
}
|
|
@@ -12858,7 +12975,7 @@ function minimumReason(f, flex) {
|
|
|
12858
12975
|
if (!flex.minAuto) return `stopped at ${minProp} ${px(flex.min)}`;
|
|
12859
12976
|
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`;
|
|
12860
12977
|
if (flex.min === 0 && f.contentMin === 0) return `its automatic minimum is 0: the content has no minimum ${f.axis}`;
|
|
12861
|
-
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)`;
|
|
12978
|
+
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)`;
|
|
12862
12979
|
return `stopped at its automatic minimum ${px(flex.min)} (the content's min-content ${f.axis}; set ${minProp}: 0 to allow smaller)`;
|
|
12863
12980
|
}
|
|
12864
12981
|
function contentReason(f) {
|
|
@@ -13270,6 +13387,23 @@ var LayoutEngine = class {
|
|
|
13270
13387
|
}
|
|
13271
13388
|
return this.records.get(node);
|
|
13272
13389
|
}
|
|
13390
|
+
/**
|
|
13391
|
+
* A number that changes whenever a box may have moved: every box
|
|
13392
|
+
* written, every pass that did any work, every sticky shift and every
|
|
13393
|
+
* node newly given a transform. It is the counter subtree bounds are
|
|
13394
|
+
* kept honest by, read out for a renderer.
|
|
13395
|
+
*
|
|
13396
|
+
* What it deliberately does not count is a scroll offset on its own.
|
|
13397
|
+
* A scroll container's descendants keep their pre-scroll records, so
|
|
13398
|
+
* a frame that only scrolled leaves this alone, and that is what lets
|
|
13399
|
+
* Canvas2D tell a frame that scrolled a list from one that moved it.
|
|
13400
|
+
* The answer is conservative the other way: a pass that did work and
|
|
13401
|
+
* moved nothing still counts, because proving it moved nothing would
|
|
13402
|
+
* cost the walk the counter exists to avoid.
|
|
13403
|
+
*/
|
|
13404
|
+
get geometryVersion() {
|
|
13405
|
+
return this.layoutVersion;
|
|
13406
|
+
}
|
|
13273
13407
|
/** The node the last `layout` / `layoutForFrame` started from, if any. */
|
|
13274
13408
|
get root() {
|
|
13275
13409
|
return this.layoutRoot;
|
|
@@ -15183,6 +15317,7 @@ var LayoutEngine = class {
|
|
|
15183
15317
|
};
|
|
15184
15318
|
this.resolveLayoutProps(child, cRec);
|
|
15185
15319
|
const anchor = child.properties.get("anchor");
|
|
15320
|
+
const point = child.properties.get("anchorPoint");
|
|
15186
15321
|
if (anchor !== void 0 && anchor !== null) {
|
|
15187
15322
|
this.anchoredNodes.add(child);
|
|
15188
15323
|
this.trackAnchor(child, anchor);
|
|
@@ -15191,6 +15326,12 @@ var LayoutEngine = class {
|
|
|
15191
15326
|
this.placeAnchored(child, cRec, anchorRec, anchor, block);
|
|
15192
15327
|
return;
|
|
15193
15328
|
}
|
|
15329
|
+
} else if (point !== void 0 && point !== null) {
|
|
15330
|
+
this.anchoredNodes.delete(child);
|
|
15331
|
+
this.trackAnchor(child, null);
|
|
15332
|
+
this.measure(child, new Constraints(0, Math.max(0, block.width), 0, Math.max(0, block.height)));
|
|
15333
|
+
this.placeBeside(child, cRec, block.x + point.x, block.y + point.y, 0, 0, block);
|
|
15334
|
+
return;
|
|
15194
15335
|
} else {
|
|
15195
15336
|
this.anchoredNodes.delete(child);
|
|
15196
15337
|
this.trackAnchor(child, null);
|
|
@@ -15199,9 +15340,11 @@ var LayoutEngine = class {
|
|
|
15199
15340
|
const marginV = cRec.marginTop + cRec.marginBottom;
|
|
15200
15341
|
const availableWidth = Math.max(0, block.width - (cRec.left ?? 0) - (cRec.right ?? 0) - marginH);
|
|
15201
15342
|
const availableHeight = Math.max(0, block.height - (cRec.top ?? 0) - (cRec.bottom ?? 0) - marginV);
|
|
15202
|
-
const
|
|
15203
|
-
const
|
|
15204
|
-
|
|
15343
|
+
const explicitWidth = this.lengthProp(child, "width", block.width) !== void 0;
|
|
15344
|
+
const explicitHeight = this.lengthProp(child, "height", block.height) !== void 0;
|
|
15345
|
+
const tightWidth = cRec.left !== void 0 && cRec.right !== void 0 && !explicitWidth;
|
|
15346
|
+
const tightHeight = cRec.top !== void 0 && cRec.bottom !== void 0 && !explicitHeight;
|
|
15347
|
+
this.measure(child, new Constraints(tightWidth ? availableWidth : 0, explicitWidth ? Infinity : availableWidth, tightHeight ? availableHeight : 0, explicitHeight ? Infinity : availableHeight));
|
|
15205
15348
|
const width = cRec.measuredWidth;
|
|
15206
15349
|
const height = cRec.measuredHeight;
|
|
15207
15350
|
let x;
|
|
@@ -15229,18 +15372,25 @@ var LayoutEngine = class {
|
|
|
15229
15372
|
*/
|
|
15230
15373
|
placeAnchored(child, cRec, anchorRec, anchor, block) {
|
|
15231
15374
|
this.measure(child, new Constraints(0, Math.max(0, block.width), 0, Math.max(0, block.height)));
|
|
15232
|
-
const width = cRec.measuredWidth;
|
|
15233
|
-
const height = cRec.measuredHeight;
|
|
15234
|
-
const gap = this.numberProp(child, "anchorOffset") ?? 0;
|
|
15235
|
-
const { side, align } = this.parsePlacement(child.properties.get("placement"));
|
|
15236
15375
|
const scrollAnchor = this.scrollOffsetOf(anchor);
|
|
15237
15376
|
const scrollChild = this.scrollOffsetOf(child);
|
|
15238
15377
|
const stickyAnchor = this.stickyOffsetOf(anchor);
|
|
15239
15378
|
const stickyChild = this.stickyOffsetOf(child);
|
|
15240
15379
|
const ax = anchorRec.x + stickyAnchor.x - scrollAnchor.x + scrollChild.x - stickyChild.x;
|
|
15241
15380
|
const ay = anchorRec.y + stickyAnchor.y - scrollAnchor.y + scrollChild.y - stickyChild.y;
|
|
15242
|
-
|
|
15243
|
-
|
|
15381
|
+
this.placeBeside(child, cRec, ax, ay, anchorRec.width, anchorRec.height, block);
|
|
15382
|
+
}
|
|
15383
|
+
/**
|
|
15384
|
+
* Places a measured node beside a box — an anchor's, or a point's of
|
|
15385
|
+
* no size — flipping to the opposite side when the named one would
|
|
15386
|
+
* overflow the block and the other has more room, and clamping along
|
|
15387
|
+
* the side to stay inside it.
|
|
15388
|
+
*/
|
|
15389
|
+
placeBeside(child, cRec, ax, ay, aw, ah, block) {
|
|
15390
|
+
const width = cRec.measuredWidth;
|
|
15391
|
+
const height = cRec.measuredHeight;
|
|
15392
|
+
const gap = this.numberProp(child, "anchorOffset") ?? 0;
|
|
15393
|
+
const { side, align } = this.parsePlacement(child.properties.get("placement"));
|
|
15244
15394
|
const vertical = side === "top" || side === "bottom";
|
|
15245
15395
|
let resolvedSide = side;
|
|
15246
15396
|
if (vertical) {
|
|
@@ -17395,6 +17545,14 @@ const MAX_PICTURE_SIDE = 8192;
|
|
|
17395
17545
|
* is what remains: that both place the same picture in the same box,
|
|
17396
17546
|
* at the same point in paint order, under the same clip and opacity.
|
|
17397
17547
|
*
|
|
17548
|
+
* **A picture that changes every frame is the exception**, on Canvas2D:
|
|
17549
|
+
* `draw` replays its new recording straight onto the frame and makes a
|
|
17550
|
+
* bitmap only on the first frame its inputs hold still. The parity
|
|
17551
|
+
* claim is kept where it is made — both backends draw the same bitmap of
|
|
17552
|
+
* a settled picture, and a changing one is the same recording through
|
|
17553
|
+
* the same `PaintTarget` — and a canvas whose layers move every frame
|
|
17554
|
+
* stops paying for a bitmap per layer per frame that nobody draws twice.
|
|
17555
|
+
*
|
|
17398
17556
|
* **The cost follows the change.** A frame that draws a painted node
|
|
17399
17557
|
* whose inputs, box, scale and theme are unchanged makes no recording
|
|
17400
17558
|
* and no bitmap; it draws the one already here. The key holds the
|
|
@@ -17414,7 +17572,8 @@ var PaintPictureCache = class {
|
|
|
17414
17572
|
stats = {
|
|
17415
17573
|
recorded: 0,
|
|
17416
17574
|
rasterized: 0,
|
|
17417
|
-
resolved: 0
|
|
17575
|
+
resolved: 0,
|
|
17576
|
+
direct: 0
|
|
17418
17577
|
};
|
|
17419
17578
|
constructor(createCanvas = offscreenPaintCanvas) {
|
|
17420
17579
|
this.createCanvas = createCanvas;
|
|
@@ -17427,6 +17586,7 @@ var PaintPictureCache = class {
|
|
|
17427
17586
|
this.stats.recorded = 0;
|
|
17428
17587
|
this.stats.rasterized = 0;
|
|
17429
17588
|
this.stats.resolved = 0;
|
|
17589
|
+
this.stats.direct = 0;
|
|
17430
17590
|
}
|
|
17431
17591
|
/**
|
|
17432
17592
|
* The node's picture at this box and device scale, made if the
|
|
@@ -17438,39 +17598,107 @@ var PaintPictureCache = class {
|
|
|
17438
17598
|
*/
|
|
17439
17599
|
pictureFor(node, rec, scale) {
|
|
17440
17600
|
this.stats.resolved++;
|
|
17441
|
-
const
|
|
17442
|
-
|
|
17443
|
-
if (paint === void 0 && path === void 0) return;
|
|
17444
|
-
if (!(rec.width > 0) || !(rec.height > 0)) return;
|
|
17445
|
-
const clipPath = node.properties.get("clipPath");
|
|
17446
|
-
const blur = node.properties.get("blur");
|
|
17601
|
+
const key = keyFor(node, rec, scale);
|
|
17602
|
+
if (key === null) return;
|
|
17447
17603
|
const slot = this.slots.get(node);
|
|
17448
|
-
if (slot !== void 0 && slot
|
|
17449
|
-
const
|
|
17450
|
-
|
|
17451
|
-
|
|
17452
|
-
|
|
17453
|
-
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
|
|
17458
|
-
|
|
17459
|
-
|
|
17460
|
-
|
|
17461
|
-
|
|
17462
|
-
|
|
17604
|
+
if (slot !== void 0 && matches(slot, key)) return this.pictureOf(node, slot, key.box) ?? void 0;
|
|
17605
|
+
const recording = this.record(key.paint, key.path, key.clipPath, key.blur, key.box);
|
|
17606
|
+
const next = this.remember(node, slot, key, recording);
|
|
17607
|
+
return this.pictureOf(node, next, key.box) ?? void 0;
|
|
17608
|
+
}
|
|
17609
|
+
/**
|
|
17610
|
+
* Draws a node's picture onto a Canvas2D frame, making a bitmap only
|
|
17611
|
+
* when one will be drawn more than once.
|
|
17612
|
+
*
|
|
17613
|
+
* `pictureFor` rasterises every new recording into a bitmap, which
|
|
17614
|
+
* pays for itself when the picture is drawn again — a chart that
|
|
17615
|
+
* changed once and is panned for a minute. It does not when the
|
|
17616
|
+
* picture changes every frame: the bitmap is drawn once and thrown
|
|
17617
|
+
* away, and the frame has paid for a canvas cleared, a separate
|
|
17618
|
+
* flush, and a composite of every pixel of it. gessologic's canvas
|
|
17619
|
+
* redraws a layer of live wires per tile per frame, and in software
|
|
17620
|
+
* rendering `transferToImageBitmap` alone was a quarter of the render
|
|
17621
|
+
* worker's time.
|
|
17622
|
+
*
|
|
17623
|
+
* So a recording made because the painter's inputs changed is
|
|
17624
|
+
* replayed straight onto the frame's context, clipped to the node's
|
|
17625
|
+
* box, and no bitmap is made. Once it has held still for
|
|
17626
|
+
* `SETTLE_FRAMES` frames it makes the bitmap and draws that from there
|
|
17627
|
+
* on, exactly as `pictureFor` would. A picture that changes every
|
|
17628
|
+
* frame, or nearly every frame, is never rasterised; one that settles
|
|
17629
|
+
* is rasterised once, a few frames late.
|
|
17630
|
+
* A picture seen for the first time, or at a new size, is rasterised
|
|
17631
|
+
* at once, because it most likely holds still.
|
|
17632
|
+
*
|
|
17633
|
+
* Canvas2D only. WebGPU has no path pipeline to replay onto, which is
|
|
17634
|
+
* the reason the cache exists (see the class comment), and asks
|
|
17635
|
+
* `pictureFor`. The two backends then differ only in when the pixels
|
|
17636
|
+
* of a changing picture are made, not in which: both come from the
|
|
17637
|
+
* same recording through the same `PaintTarget`.
|
|
17638
|
+
*/
|
|
17639
|
+
draw(node, rec, scale, ctx) {
|
|
17640
|
+
this.stats.resolved++;
|
|
17641
|
+
const key = keyFor(node, rec, scale);
|
|
17642
|
+
if (key === null) return;
|
|
17643
|
+
const slot = this.slots.get(node);
|
|
17644
|
+
if (slot !== void 0 && matches(slot, key)) {
|
|
17645
|
+
if (!slot.rasterized && slot.settling > 0) {
|
|
17646
|
+
slot.settling--;
|
|
17647
|
+
this.replay(node, slot.recording, rec, ctx);
|
|
17648
|
+
return;
|
|
17649
|
+
}
|
|
17650
|
+
const picture = this.pictureOf(node, slot, key.box);
|
|
17651
|
+
if (picture !== null) ctx.drawImage(picture, rec.x, rec.y, rec.width, rec.height);
|
|
17652
|
+
return;
|
|
17653
|
+
}
|
|
17654
|
+
const recording = this.record(key.paint, key.path, key.clipPath, key.blur, key.box);
|
|
17655
|
+
const next = this.remember(node, slot, key, recording);
|
|
17656
|
+
if (recording.ops.length === 0) return;
|
|
17657
|
+
if (slot === void 0 || !contentChanged(slot, key)) {
|
|
17658
|
+
const picture = this.pictureOf(node, next, key.box);
|
|
17659
|
+
if (picture !== null) ctx.drawImage(picture, rec.x, rec.y, rec.width, rec.height);
|
|
17660
|
+
return;
|
|
17661
|
+
}
|
|
17662
|
+
next.settling = 3;
|
|
17663
|
+
this.replay(node, recording, rec, ctx);
|
|
17664
|
+
}
|
|
17665
|
+
/** A recording drawn straight onto the frame, clipped to the box its bitmap would fill. */
|
|
17666
|
+
replay(node, recording, rec, ctx) {
|
|
17667
|
+
this.stats.direct++;
|
|
17668
|
+
ctx.save();
|
|
17669
|
+
ctx.translate(rec.x, rec.y);
|
|
17670
|
+
ctx.beginPath();
|
|
17671
|
+
ctx.rect(0, 0, rec.width, rec.height);
|
|
17672
|
+
ctx.clip();
|
|
17673
|
+
replayPaint(recording, new PaintTarget(ctx, resolverFor(node)));
|
|
17674
|
+
ctx.restore();
|
|
17675
|
+
}
|
|
17676
|
+
/** A slot's bitmap, made now if it has not been. */
|
|
17677
|
+
pictureOf(node, slot, box) {
|
|
17678
|
+
if (!slot.rasterized) {
|
|
17679
|
+
slot.picture = this.rasterize(node, slot.recording, box);
|
|
17680
|
+
slot.rasterized = true;
|
|
17681
|
+
}
|
|
17682
|
+
return slot.picture;
|
|
17683
|
+
}
|
|
17684
|
+
remember(node, previous, key, recording) {
|
|
17685
|
+
previous?.picture?.close?.();
|
|
17686
|
+
const slot = {
|
|
17687
|
+
picture: null,
|
|
17688
|
+
rasterized: false,
|
|
17689
|
+
settling: 0,
|
|
17463
17690
|
recording,
|
|
17464
|
-
paint,
|
|
17465
|
-
path,
|
|
17466
|
-
clipPath,
|
|
17467
|
-
blur,
|
|
17691
|
+
paint: key.paint,
|
|
17692
|
+
path: key.path,
|
|
17693
|
+
clipPath: key.clipPath,
|
|
17694
|
+
blur: key.blur,
|
|
17468
17695
|
environment: node.environment,
|
|
17469
|
-
width:
|
|
17470
|
-
height:
|
|
17471
|
-
scale
|
|
17472
|
-
}
|
|
17473
|
-
|
|
17696
|
+
width: key.box.width,
|
|
17697
|
+
height: key.box.height,
|
|
17698
|
+
scale: key.box.scale
|
|
17699
|
+
};
|
|
17700
|
+
this.slots.set(node, slot);
|
|
17701
|
+
return slot;
|
|
17474
17702
|
}
|
|
17475
17703
|
/**
|
|
17476
17704
|
* The recording a node's picture was made from.
|
|
@@ -17517,6 +17745,36 @@ var PaintPictureCache = class {
|
|
|
17517
17745
|
* make it possible for them not to.
|
|
17518
17746
|
*/
|
|
17519
17747
|
const paintPictures = new PaintPictureCache();
|
|
17748
|
+
/** The key for a node's picture, or null when it paints nothing or its box has no area. */
|
|
17749
|
+
function keyFor(node, rec, scale) {
|
|
17750
|
+
const paint = node.properties.get("paint");
|
|
17751
|
+
const path = node.properties.get("path");
|
|
17752
|
+
if (paint === void 0 && path === void 0) return null;
|
|
17753
|
+
if (!(rec.width > 0) || !(rec.height > 0)) return null;
|
|
17754
|
+
return {
|
|
17755
|
+
paint,
|
|
17756
|
+
path,
|
|
17757
|
+
clipPath: node.properties.get("clipPath"),
|
|
17758
|
+
blur: node.properties.get("blur"),
|
|
17759
|
+
environment: node.environment,
|
|
17760
|
+
box: {
|
|
17761
|
+
width: rec.width,
|
|
17762
|
+
height: rec.height,
|
|
17763
|
+
paddingTop: rec.paddingTop,
|
|
17764
|
+
paddingRight: rec.paddingRight,
|
|
17765
|
+
paddingBottom: rec.paddingBottom,
|
|
17766
|
+
paddingLeft: rec.paddingLeft,
|
|
17767
|
+
scale
|
|
17768
|
+
}
|
|
17769
|
+
};
|
|
17770
|
+
}
|
|
17771
|
+
/** Whether what the painter draws changed, as against where or at what size it is drawn. */
|
|
17772
|
+
function contentChanged(slot, key) {
|
|
17773
|
+
return slot.clipPath !== key.clipPath || slot.blur !== key.blur || !paintValuesEqual(slot.paint, key.paint) || !pathValuesEqual(slot.path, key.path);
|
|
17774
|
+
}
|
|
17775
|
+
function matches(slot, key) {
|
|
17776
|
+
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);
|
|
17777
|
+
}
|
|
17520
17778
|
/** A painter's colour values, against the node's own theme. */
|
|
17521
17779
|
function resolverFor(node) {
|
|
17522
17780
|
return {
|
|
@@ -17579,6 +17837,310 @@ function offscreenPaintCanvas(width, height) {
|
|
|
17579
17837
|
};
|
|
17580
17838
|
}
|
|
17581
17839
|
//#endregion
|
|
17840
|
+
//#region src/rendering/canvas2d/ScrollLayers.ts
|
|
17841
|
+
/**
|
|
17842
|
+
* An `OffscreenCanvas` where there is one. Where there is not, no
|
|
17843
|
+
* layer is made and every frame is drawn directly, which is the
|
|
17844
|
+
* behaviour a host without one had anyway.
|
|
17845
|
+
*/
|
|
17846
|
+
function offscreenLayerCanvas(width, height) {
|
|
17847
|
+
if (typeof OffscreenCanvas !== "function") return null;
|
|
17848
|
+
return new OffscreenCanvas(width, height);
|
|
17849
|
+
}
|
|
17850
|
+
/**
|
|
17851
|
+
* How far from a whole device pixel a translation may be and still be
|
|
17852
|
+
* treated as whole. Floating point makes a scroll of ten pixels come
|
|
17853
|
+
* out as 9.999999999998 often enough that exact equality would refuse
|
|
17854
|
+
* nearly every pan; a thousandth of a pixel is invisible, and the
|
|
17855
|
+
* layer's offset is kept pixel-true (see `shift`) so the error never
|
|
17856
|
+
* accumulates past it.
|
|
17857
|
+
*/
|
|
17858
|
+
const EPSILON = .001;
|
|
17859
|
+
/**
|
|
17860
|
+
* The most layers held at once. Each costs two canvases the size of its
|
|
17861
|
+
* viewport; a screen with more scroll containers scrolling at once than
|
|
17862
|
+
* this is not one a layer per container would help.
|
|
17863
|
+
*/
|
|
17864
|
+
const MAX_LAYERS = 4;
|
|
17865
|
+
var ScrollLayerCache = class {
|
|
17866
|
+
createCanvas;
|
|
17867
|
+
settleFrames;
|
|
17868
|
+
layers = /* @__PURE__ */ new Map();
|
|
17869
|
+
/**
|
|
17870
|
+
* Containers whose last layer drew something that changes without
|
|
17871
|
+
* telling anyone — a caret, a fading scrollbar, a video. Building
|
|
17872
|
+
* another on the next scroll would only throw it away again, so none
|
|
17873
|
+
* is built until something in or above the subtree is dirtied, which
|
|
17874
|
+
* is how every one of those ends.
|
|
17875
|
+
*/
|
|
17876
|
+
refused = /* @__PURE__ */ new Set();
|
|
17877
|
+
geometryVersion = NaN;
|
|
17878
|
+
dpr = 0;
|
|
17879
|
+
surfaceWidth = 0;
|
|
17880
|
+
surfaceHeight = 0;
|
|
17881
|
+
frame = null;
|
|
17882
|
+
/** Whether this frame changed nothing a layer could not absorb: no layout, children, content or environment. */
|
|
17883
|
+
calm = false;
|
|
17884
|
+
/** Nodes this frame dirtied, and every ancestor of each, less a container that only scrolled. */
|
|
17885
|
+
disturbed = /* @__PURE__ */ new Set();
|
|
17886
|
+
/** Consecutive frames each container without a layer has scrolled quietly. See `SCROLL_LAYER_SETTLE_FRAMES`. */
|
|
17887
|
+
streaks = /* @__PURE__ */ new WeakMap();
|
|
17888
|
+
/**
|
|
17889
|
+
* Between `beginFrame` and `endFrame`. Still true at the next
|
|
17890
|
+
* `beginFrame` means the last frame threw part way through, perhaps
|
|
17891
|
+
* inside a layer's walk, and whatever the layers hold is suspect.
|
|
17892
|
+
*/
|
|
17893
|
+
open = false;
|
|
17894
|
+
stats = {
|
|
17895
|
+
built: 0,
|
|
17896
|
+
composited: 0,
|
|
17897
|
+
shifted: 0,
|
|
17898
|
+
dropped: 0
|
|
17899
|
+
};
|
|
17900
|
+
constructor(createCanvas, settleFrames = 3) {
|
|
17901
|
+
this.createCanvas = createCanvas;
|
|
17902
|
+
this.settleFrames = settleFrames;
|
|
17903
|
+
}
|
|
17904
|
+
/** Whether layers may be used on this frame; false until `beginFrame` says so. */
|
|
17905
|
+
get active() {
|
|
17906
|
+
return this.frame !== null;
|
|
17907
|
+
}
|
|
17908
|
+
resetStats() {
|
|
17909
|
+
this.stats.built = 0;
|
|
17910
|
+
this.stats.composited = 0;
|
|
17911
|
+
this.stats.shifted = 0;
|
|
17912
|
+
this.stats.dropped = 0;
|
|
17913
|
+
}
|
|
17914
|
+
/**
|
|
17915
|
+
* Settles which layers this frame's changes leave standing, before
|
|
17916
|
+
* anything is drawn. Every rule here drops; none keeps. A frame the
|
|
17917
|
+
* caller could not vouch for drops them all.
|
|
17918
|
+
*/
|
|
17919
|
+
beginFrame(changes, dpr, surfaceWidth, surfaceHeight) {
|
|
17920
|
+
const interrupted = this.open;
|
|
17921
|
+
this.open = true;
|
|
17922
|
+
this.disturbed.clear();
|
|
17923
|
+
this.calm = changes !== void 0;
|
|
17924
|
+
for (const [node, flags] of changes?.frame.entries() ?? []) {
|
|
17925
|
+
if ((flags & -355) !== 0) {
|
|
17926
|
+
this.calm = false;
|
|
17927
|
+
break;
|
|
17928
|
+
}
|
|
17929
|
+
for (let current = (flags & 32) !== 0 ? node.parent : node; current !== null; current = current.parent) this.disturbed.add(current);
|
|
17930
|
+
}
|
|
17931
|
+
if (interrupted || changes === void 0 || dpr !== this.dpr || surfaceWidth !== this.surfaceWidth || surfaceHeight !== this.surfaceHeight || changes.geometryVersion !== this.geometryVersion) {
|
|
17932
|
+
this.dropAll();
|
|
17933
|
+
this.dpr = dpr;
|
|
17934
|
+
this.surfaceWidth = surfaceWidth;
|
|
17935
|
+
this.surfaceHeight = surfaceHeight;
|
|
17936
|
+
this.geometryVersion = changes?.geometryVersion ?? NaN;
|
|
17937
|
+
this.frame = changes?.frame ?? null;
|
|
17938
|
+
return;
|
|
17939
|
+
}
|
|
17940
|
+
const frame = changes.frame;
|
|
17941
|
+
this.frame = frame;
|
|
17942
|
+
if (this.layers.size === 0 && this.refused.size === 0) return;
|
|
17943
|
+
for (const [node, flags] of frame.entries()) {
|
|
17944
|
+
if ((flags & -355) !== 0) {
|
|
17945
|
+
this.dropAll();
|
|
17946
|
+
return;
|
|
17947
|
+
}
|
|
17948
|
+
const scrolledOnly = flags === 32;
|
|
17949
|
+
for (let current = scrolledOnly ? node.parent : node; current !== null; current = current.parent) this.forget(current);
|
|
17950
|
+
}
|
|
17951
|
+
for (const node of [...this.layers.keys(), ...this.refused]) for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
17952
|
+
const flags = frame.dirtyFlagsFor(ancestor);
|
|
17953
|
+
if (flags !== 0 && flags !== 32) {
|
|
17954
|
+
this.forget(node);
|
|
17955
|
+
break;
|
|
17956
|
+
}
|
|
17957
|
+
}
|
|
17958
|
+
}
|
|
17959
|
+
/** Drops the layers the frame did not composite: their container was culled, removed, or drawn directly. */
|
|
17960
|
+
endFrame() {
|
|
17961
|
+
this.open = false;
|
|
17962
|
+
for (const [node, layer] of this.layers) if (!layer.used) this.drop(node);
|
|
17963
|
+
else layer.used = false;
|
|
17964
|
+
}
|
|
17965
|
+
get(node) {
|
|
17966
|
+
return this.layers.get(node);
|
|
17967
|
+
}
|
|
17968
|
+
/**
|
|
17969
|
+
* Whether a container with no layer should get one now: it has
|
|
17970
|
+
* scrolled on the last few frames running with nothing else in or
|
|
17971
|
+
* above it changing, which is the evidence a layer would be kept, and
|
|
17972
|
+
* it has not just shown it holds something a layer cannot.
|
|
17973
|
+
*/
|
|
17974
|
+
wants(node) {
|
|
17975
|
+
const frame = this.frame;
|
|
17976
|
+
if (frame === null || this.layers.size >= MAX_LAYERS || this.refused.has(node)) return false;
|
|
17977
|
+
if (!this.calm || this.disturbed.has(node) || dirtyAbove(frame, node)) {
|
|
17978
|
+
this.streaks.delete(node);
|
|
17979
|
+
return false;
|
|
17980
|
+
}
|
|
17981
|
+
if ((frame.dirtyFlagsFor(node) & 32) === 0) return false;
|
|
17982
|
+
const streak = (this.streaks.get(node) ?? 0) + 1;
|
|
17983
|
+
if (streak < this.settleFrames) {
|
|
17984
|
+
this.streaks.set(node, streak);
|
|
17985
|
+
return false;
|
|
17986
|
+
}
|
|
17987
|
+
this.streaks.delete(node);
|
|
17988
|
+
return true;
|
|
17989
|
+
}
|
|
17990
|
+
/** A new layer for the placement, or null where no canvas could be had. */
|
|
17991
|
+
create(node, placement, rec) {
|
|
17992
|
+
const front = this.createCanvas(placement.width, placement.height);
|
|
17993
|
+
const back = front === null ? null : this.createCanvas(placement.width, placement.height);
|
|
17994
|
+
const frontContext = front?.getContext("2d");
|
|
17995
|
+
const backContext = back?.getContext("2d");
|
|
17996
|
+
if (front === null || back === null || !frontContext || !backContext) return null;
|
|
17997
|
+
const layer = {
|
|
17998
|
+
front,
|
|
17999
|
+
frontContext,
|
|
18000
|
+
back,
|
|
18001
|
+
backContext,
|
|
18002
|
+
width: placement.width,
|
|
18003
|
+
height: placement.height,
|
|
18004
|
+
offsetX: placement.offsetX,
|
|
18005
|
+
offsetY: placement.offsetY,
|
|
18006
|
+
dpr: this.dpr,
|
|
18007
|
+
scrollX: rec.scrollX,
|
|
18008
|
+
scrollY: rec.scrollY,
|
|
18009
|
+
used: true
|
|
18010
|
+
};
|
|
18011
|
+
this.layers.set(node, layer);
|
|
18012
|
+
this.stats.built++;
|
|
18013
|
+
return layer;
|
|
18014
|
+
}
|
|
18015
|
+
/**
|
|
18016
|
+
* Whether a layer's pixels would land where this frame's direct draw
|
|
18017
|
+
* would put them: the same size, the same fraction of a pixel.
|
|
18018
|
+
*/
|
|
18019
|
+
fits(layer, placement) {
|
|
18020
|
+
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;
|
|
18021
|
+
}
|
|
18022
|
+
/**
|
|
18023
|
+
* Moves the layer's pixels to follow the record's scroll offset, and
|
|
18024
|
+
* returns the strips that came into view and need painting — none
|
|
18025
|
+
* when it did not scroll, the whole layer when it scrolled further
|
|
18026
|
+
* than it is wide. Null when the scroll was not a whole number of
|
|
18027
|
+
* device pixels, which a copy cannot follow without resampling.
|
|
18028
|
+
*/
|
|
18029
|
+
shift(layer, rec) {
|
|
18030
|
+
const exactX = (layer.scrollX - rec.scrollX) * layer.dpr;
|
|
18031
|
+
const exactY = (layer.scrollY - rec.scrollY) * layer.dpr;
|
|
18032
|
+
const dx = Math.round(exactX);
|
|
18033
|
+
const dy = Math.round(exactY);
|
|
18034
|
+
if (Math.abs(exactX - dx) > EPSILON || Math.abs(exactY - dy) > EPSILON) return null;
|
|
18035
|
+
if (dx === 0 && dy === 0) return [];
|
|
18036
|
+
layer.scrollX -= dx / layer.dpr;
|
|
18037
|
+
layer.scrollY -= dy / layer.dpr;
|
|
18038
|
+
const { width, height } = layer;
|
|
18039
|
+
if (Math.abs(dx) >= width || Math.abs(dy) >= height) {
|
|
18040
|
+
clearLayer(layer.frontContext, width, height);
|
|
18041
|
+
return [{
|
|
18042
|
+
x: 0,
|
|
18043
|
+
y: 0,
|
|
18044
|
+
width,
|
|
18045
|
+
height
|
|
18046
|
+
}];
|
|
18047
|
+
}
|
|
18048
|
+
const back = layer.backContext;
|
|
18049
|
+
clearLayer(back, width, height);
|
|
18050
|
+
back.drawImage(layer.front, dx, dy, width, height);
|
|
18051
|
+
[layer.front, layer.back] = [layer.back, layer.front];
|
|
18052
|
+
[layer.frontContext, layer.backContext] = [layer.backContext, layer.frontContext];
|
|
18053
|
+
this.stats.shifted++;
|
|
18054
|
+
const strips = [];
|
|
18055
|
+
if (dx !== 0) strips.push({
|
|
18056
|
+
x: dx > 0 ? 0 : width + dx,
|
|
18057
|
+
y: 0,
|
|
18058
|
+
width: Math.abs(dx),
|
|
18059
|
+
height
|
|
18060
|
+
});
|
|
18061
|
+
if (dy !== 0) strips.push({
|
|
18062
|
+
x: dx > 0 ? dx : 0,
|
|
18063
|
+
y: dy > 0 ? 0 : height + dy,
|
|
18064
|
+
width: width - Math.abs(dx),
|
|
18065
|
+
height: Math.abs(dy)
|
|
18066
|
+
});
|
|
18067
|
+
return strips;
|
|
18068
|
+
}
|
|
18069
|
+
/** Stops building layers for a container until its subtree is next dirtied. */
|
|
18070
|
+
refuse(node) {
|
|
18071
|
+
this.drop(node);
|
|
18072
|
+
this.refused.add(node);
|
|
18073
|
+
}
|
|
18074
|
+
drop(node) {
|
|
18075
|
+
const layer = this.layers.get(node);
|
|
18076
|
+
if (layer === void 0) return;
|
|
18077
|
+
this.layers.delete(node);
|
|
18078
|
+
this.stats.dropped++;
|
|
18079
|
+
layer.front.width = 0;
|
|
18080
|
+
layer.back.width = 0;
|
|
18081
|
+
}
|
|
18082
|
+
dropAll() {
|
|
18083
|
+
for (const node of this.layers.keys()) this.drop(node);
|
|
18084
|
+
this.refused.clear();
|
|
18085
|
+
}
|
|
18086
|
+
forget(node) {
|
|
18087
|
+
this.drop(node);
|
|
18088
|
+
this.refused.delete(node);
|
|
18089
|
+
}
|
|
18090
|
+
};
|
|
18091
|
+
/** Whether an ancestor was dirtied by more than its own scroll: inherited paint reaches the subtree without saying so. */
|
|
18092
|
+
function dirtyAbove(frame, node) {
|
|
18093
|
+
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
18094
|
+
const flags = frame.dirtyFlagsFor(ancestor);
|
|
18095
|
+
if (flags !== 0 && flags !== 32) return true;
|
|
18096
|
+
}
|
|
18097
|
+
return false;
|
|
18098
|
+
}
|
|
18099
|
+
/**
|
|
18100
|
+
* Where a scroll container's viewport lands in device pixels, or null
|
|
18101
|
+
* when a layer could not reproduce the direct draw exactly: no
|
|
18102
|
+
* transform to read, a rotation, skew or scale above the container, an
|
|
18103
|
+
* opacity in force, or a viewport too large to be worth two copies.
|
|
18104
|
+
*
|
|
18105
|
+
* The layer covers the whole pixels the viewport touches. Its content
|
|
18106
|
+
* is drawn with the same fractional translation the frame has, so a
|
|
18107
|
+
* node lands in the layer at the same fraction of a pixel it would
|
|
18108
|
+
* have on the frame, and the layer is then copied onto whole pixels
|
|
18109
|
+
* with no resampling at all.
|
|
18110
|
+
*/
|
|
18111
|
+
function placeLayer(ctx, rec, dpr, surfaceWidth, surfaceHeight) {
|
|
18112
|
+
const m = ctx.getTransform?.();
|
|
18113
|
+
if (m === void 0 || ctx.globalAlpha !== 1) return null;
|
|
18114
|
+
if (Math.abs(m.b) > EPSILON || Math.abs(m.c) > EPSILON || m.a !== dpr || m.d !== dpr) return null;
|
|
18115
|
+
const left = dpr * rec.x + m.e;
|
|
18116
|
+
const top = dpr * rec.y + m.f;
|
|
18117
|
+
const x = Math.floor(left + EPSILON);
|
|
18118
|
+
const y = Math.floor(top + EPSILON);
|
|
18119
|
+
const width = Math.ceil(left + dpr * rec.width - EPSILON) - x;
|
|
18120
|
+
const height = Math.ceil(top + dpr * rec.height - EPSILON) - y;
|
|
18121
|
+
if (width <= 0 || height <= 0 || width * height > surfaceWidth * surfaceHeight) return null;
|
|
18122
|
+
return {
|
|
18123
|
+
x,
|
|
18124
|
+
y,
|
|
18125
|
+
width,
|
|
18126
|
+
height,
|
|
18127
|
+
offsetX: m.e - x,
|
|
18128
|
+
offsetY: m.f - y
|
|
18129
|
+
};
|
|
18130
|
+
}
|
|
18131
|
+
/**
|
|
18132
|
+
* Whether the browser threw a layer's backing store away — a GPU reset
|
|
18133
|
+
* takes 2D canvases with it — which would leave a layer that composites
|
|
18134
|
+
* as nothing. Optional on the context, as on older engines.
|
|
18135
|
+
*/
|
|
18136
|
+
function contextLost(ctx) {
|
|
18137
|
+
return ctx.isContextLost?.() === true;
|
|
18138
|
+
}
|
|
18139
|
+
function clearLayer(ctx, width, height) {
|
|
18140
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
18141
|
+
ctx.clearRect(0, 0, width, height);
|
|
18142
|
+
}
|
|
18143
|
+
//#endregion
|
|
17582
18144
|
//#region src/rendering/canvas2d/Canvas2DRenderer.ts
|
|
17583
18145
|
/**
|
|
17584
18146
|
* Canvas2D rendering backend.
|
|
@@ -17611,6 +18173,9 @@ function offscreenPaintCanvas(width, height) {
|
|
|
17611
18173
|
* The frame strategy is a full clear + full traversal. Off-screen
|
|
17612
18174
|
* subtrees are culled (while no ancestor transform is active) so
|
|
17613
18175
|
* scrolling large lists only issues draws for the visible window.
|
|
18176
|
+
* The one exception is a scroll container's content on a frame that
|
|
18177
|
+
* only scrolled it, which is copied from the last frame's pixels and
|
|
18178
|
+
* has only the strip that came into view drawn (`ScrollLayers.ts`).
|
|
17614
18179
|
*/
|
|
17615
18180
|
var Canvas2DRenderer = class {
|
|
17616
18181
|
options;
|
|
@@ -17664,8 +18229,32 @@ var Canvas2DRenderer = class {
|
|
|
17664
18229
|
* Drained after the walk; see `render`.
|
|
17665
18230
|
*/
|
|
17666
18231
|
liftedPass = [];
|
|
18232
|
+
/** Scroll containers' content held as pixels; see `ScrollLayers.ts`. */
|
|
18233
|
+
layers;
|
|
18234
|
+
/**
|
|
18235
|
+
* How deep inside a layer's walk the traversal is. A layer is drawn
|
|
18236
|
+
* onto the frame, never onto another layer: an inner container that
|
|
18237
|
+
* scrolls dirties the outer one's subtree anyway, so a layer inside
|
|
18238
|
+
* a layer would be rebuilt every frame the inner one was useful.
|
|
18239
|
+
*/
|
|
18240
|
+
layerDepth = 0;
|
|
18241
|
+
/**
|
|
18242
|
+
* Bumped whenever something is drawn whose pixels change without the
|
|
18243
|
+
* frame's changes saying so — a caret, a scrollbar still fading, a
|
|
18244
|
+
* video frame. A layer's walk compares it before and after, and does
|
|
18245
|
+
* not keep what it drew if it moved.
|
|
18246
|
+
*/
|
|
18247
|
+
volatileDraws = 0;
|
|
17667
18248
|
constructor(options) {
|
|
17668
18249
|
this.options = options;
|
|
18250
|
+
this.layers = new ScrollLayerCache(options.createLayerCanvas ?? offscreenLayerCanvas, options.scrollLayerSettleFrames);
|
|
18251
|
+
}
|
|
18252
|
+
/** What the scroll layers did since the last reset. For budget specs. */
|
|
18253
|
+
get scrollLayerStats() {
|
|
18254
|
+
return this.layers.stats;
|
|
18255
|
+
}
|
|
18256
|
+
resetScrollLayerStats() {
|
|
18257
|
+
this.layers.resetStats();
|
|
17669
18258
|
}
|
|
17670
18259
|
get surface() {
|
|
17671
18260
|
return this.options.surface;
|
|
@@ -17679,10 +18268,12 @@ var Canvas2DRenderer = class {
|
|
|
17679
18268
|
}
|
|
17680
18269
|
resize(width, height, dpr) {
|
|
17681
18270
|
this.surface.setLogicalSize(width, height, dpr);
|
|
18271
|
+
this.layers.dropAll();
|
|
17682
18272
|
}
|
|
17683
18273
|
dispose() {
|
|
17684
18274
|
this.disposed = true;
|
|
17685
18275
|
this.scaledImages.dispose();
|
|
18276
|
+
this.layers.dropAll();
|
|
17686
18277
|
}
|
|
17687
18278
|
render(root, context) {
|
|
17688
18279
|
const ctx = this.surface.getContext2D();
|
|
@@ -17694,9 +18285,12 @@ var Canvas2DRenderer = class {
|
|
|
17694
18285
|
this.cullHeight = this.surface.logicalHeight;
|
|
17695
18286
|
this.liftedPass.length = 0;
|
|
17696
18287
|
this.paintOwner = null;
|
|
18288
|
+
this.layerDepth = 0;
|
|
18289
|
+
this.layers.beginFrame(this.options.scrollLayers === false ? void 0 : context.changes, this.surface.dpr, this.surface.physicalWidth, this.surface.physicalHeight);
|
|
17697
18290
|
this.renderNode(root, context, ctx, true, false);
|
|
17698
18291
|
this.renderLifted(context, ctx);
|
|
17699
18292
|
if (context.overlay !== void 0 && context.overlay.length > 0) drawOverlayShapes(ctx, context.overlay);
|
|
18293
|
+
this.layers.endFrame();
|
|
17700
18294
|
}
|
|
17701
18295
|
/**
|
|
17702
18296
|
* Folds a node's style into the shared scratch and records that it
|
|
@@ -17747,13 +18341,15 @@ var Canvas2DRenderer = class {
|
|
|
17747
18341
|
ctx.clip();
|
|
17748
18342
|
this.pushCull(rec);
|
|
17749
18343
|
}
|
|
17750
|
-
if (rec.scrollable) {
|
|
17751
|
-
ctx.save();
|
|
17752
|
-
ctx.translate(-rec.scrollX, -rec.scrollY);
|
|
17753
|
-
}
|
|
17754
18344
|
const liftedBefore = this.liftedPass.length;
|
|
17755
|
-
this.
|
|
17756
|
-
|
|
18345
|
+
if (!rec.scrollable || !this.renderScrolledThroughLayer(node, rec, context, ctx)) {
|
|
18346
|
+
if (rec.scrollable) {
|
|
18347
|
+
ctx.save();
|
|
18348
|
+
ctx.translate(-rec.scrollX, -rec.scrollY);
|
|
18349
|
+
}
|
|
18350
|
+
this.renderChildren(node, context, ctx, cull && !paint.hasTransform && !sticky);
|
|
18351
|
+
if (rec.scrollable) ctx.restore();
|
|
18352
|
+
}
|
|
17757
18353
|
if (rec.clips) this.popCull();
|
|
17758
18354
|
if (hasText) {
|
|
17759
18355
|
if (this.paintOwner !== node) this.resolvePaint(node);
|
|
@@ -17852,6 +18448,7 @@ var Canvas2DRenderer = class {
|
|
|
17852
18448
|
const video = paint.video;
|
|
17853
18449
|
const source = video?.frame ?? paint.image;
|
|
17854
18450
|
if (source === void 0 || source === null) return;
|
|
18451
|
+
if (video !== void 0) this.volatileDraws++;
|
|
17855
18452
|
const size = video === void 0 ? {
|
|
17856
18453
|
width: paint.image.width,
|
|
17857
18454
|
height: paint.image.height
|
|
@@ -17889,9 +18486,7 @@ var Canvas2DRenderer = class {
|
|
|
17889
18486
|
* story for a painted node.
|
|
17890
18487
|
*/
|
|
17891
18488
|
paintPicture(ctx, node, rec) {
|
|
17892
|
-
|
|
17893
|
-
if (picture === void 0) return;
|
|
17894
|
-
ctx.drawImage(picture, rec.x, rec.y, rec.width, rec.height);
|
|
18489
|
+
paintPictures.draw(node, rec, this.surface.dpr, ctx);
|
|
17895
18490
|
}
|
|
17896
18491
|
/**
|
|
17897
18492
|
* The image to draw for a destination `width` x `height` in the
|
|
@@ -18016,6 +18611,7 @@ var Canvas2DRenderer = class {
|
|
|
18016
18611
|
*/
|
|
18017
18612
|
paintEditable(ctx, paint, context) {
|
|
18018
18613
|
const model = paint.editor;
|
|
18614
|
+
if (model.focused) this.volatileDraws++;
|
|
18019
18615
|
const layout = new EditableLayout(model, this.contentBox, paint, context.text);
|
|
18020
18616
|
fillTextRects(ctx, textRunBackgrounds(layout.lines, paint));
|
|
18021
18617
|
if (model.focused && !model.collapsed) {
|
|
@@ -18041,6 +18637,101 @@ var Canvas2DRenderer = class {
|
|
|
18041
18637
|
ctx.fillRect(Math.round(caret.x), caret.y, 1, caret.height);
|
|
18042
18638
|
}
|
|
18043
18639
|
}
|
|
18640
|
+
/**
|
|
18641
|
+
* Draws a scroll container's children through its layer, and returns
|
|
18642
|
+
* whether it did. False means the caller draws them directly, as it
|
|
18643
|
+
* always used to; that is the answer to every doubt below.
|
|
18644
|
+
*
|
|
18645
|
+
* Called where the direct path would translate by the scroll offset,
|
|
18646
|
+
* so the container's clip is open and its background and border are
|
|
18647
|
+
* already drawn. The layer is copied onto the frame under that clip,
|
|
18648
|
+
* which is how a rounded viewport stays rounded.
|
|
18649
|
+
*/
|
|
18650
|
+
renderScrolledThroughLayer(node, rec, context, ctx) {
|
|
18651
|
+
const layers = this.layers;
|
|
18652
|
+
if (!layers.active || this.layerDepth > 0) return false;
|
|
18653
|
+
const dpr = this.surface.dpr;
|
|
18654
|
+
const width = this.surface.physicalWidth;
|
|
18655
|
+
const height = this.surface.physicalHeight;
|
|
18656
|
+
const placement = rec.clips && node.type !== "editable-text" && node.firstChild !== null ? placeLayer(ctx, rec, dpr, width, height) : null;
|
|
18657
|
+
let layer = layers.get(node);
|
|
18658
|
+
if (placement === null) {
|
|
18659
|
+
layers.drop(node);
|
|
18660
|
+
return false;
|
|
18661
|
+
}
|
|
18662
|
+
if (layer !== void 0 && !layers.fits(layer, placement)) {
|
|
18663
|
+
layers.drop(node);
|
|
18664
|
+
layer = void 0;
|
|
18665
|
+
}
|
|
18666
|
+
const volatileBefore = this.volatileDraws;
|
|
18667
|
+
const liftedBefore = this.liftedPass.length;
|
|
18668
|
+
if (layer === void 0) {
|
|
18669
|
+
if (!layers.wants(node)) return false;
|
|
18670
|
+
const created = layers.create(node, placement, rec);
|
|
18671
|
+
if (created === null) return false;
|
|
18672
|
+
layer = created;
|
|
18673
|
+
this.paintLayer(layer, null, node, rec, context);
|
|
18674
|
+
} else {
|
|
18675
|
+
const strips = layers.shift(layer, rec);
|
|
18676
|
+
if (strips === null) {
|
|
18677
|
+
layers.drop(node);
|
|
18678
|
+
return false;
|
|
18679
|
+
}
|
|
18680
|
+
for (const strip of strips) this.paintLayer(layer, strip, node, rec, context);
|
|
18681
|
+
}
|
|
18682
|
+
layer.used = true;
|
|
18683
|
+
layers.stats.composited++;
|
|
18684
|
+
ctx.save();
|
|
18685
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
18686
|
+
ctx.drawImage(layer.front, placement.x, placement.y, layer.width, layer.height);
|
|
18687
|
+
ctx.restore();
|
|
18688
|
+
if (this.volatileDraws !== volatileBefore || this.liftedPass.length !== liftedBefore) layers.refuse(node);
|
|
18689
|
+
return true;
|
|
18690
|
+
}
|
|
18691
|
+
/**
|
|
18692
|
+
* Draws the container's children into its layer: the whole of it
|
|
18693
|
+
* when `strip` is null, otherwise only the strip a scroll exposed.
|
|
18694
|
+
*
|
|
18695
|
+
* A strip is clipped, not culled. The walk visits exactly the nodes a
|
|
18696
|
+
* direct draw of the viewport would, and the clip keeps their pixels
|
|
18697
|
+
* inside the strip; culling to the strip instead would miss a shadow,
|
|
18698
|
+
* a focus ring or overflowing text reaching into it from a node whose
|
|
18699
|
+
* box does not, and leave a seam where a direct draw has none. What a
|
|
18700
|
+
* strip saves is raster, which is the cost it exists for; the walk
|
|
18701
|
+
* costs what a direct draw's walk would.
|
|
18702
|
+
*/
|
|
18703
|
+
paintLayer(layer, strip, node, rec, context) {
|
|
18704
|
+
const lctx = layer.frontContext;
|
|
18705
|
+
lctx.save();
|
|
18706
|
+
if (strip !== null) {
|
|
18707
|
+
lctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
18708
|
+
lctx.beginPath();
|
|
18709
|
+
lctx.rect(strip.x, strip.y, strip.width, strip.height);
|
|
18710
|
+
lctx.clip();
|
|
18711
|
+
}
|
|
18712
|
+
const dpr = layer.dpr;
|
|
18713
|
+
lctx.setTransform(dpr, 0, 0, dpr, layer.offsetX, layer.offsetY);
|
|
18714
|
+
lctx.translate(-layer.scrollX, -layer.scrollY);
|
|
18715
|
+
const savedCullX = this.cullX;
|
|
18716
|
+
const savedCullY = this.cullY;
|
|
18717
|
+
const savedCullWidth = this.cullWidth;
|
|
18718
|
+
const savedCullHeight = this.cullHeight;
|
|
18719
|
+
this.cullX = rec.x + layer.scrollX;
|
|
18720
|
+
this.cullY = rec.y + layer.scrollY;
|
|
18721
|
+
this.cullWidth = rec.width;
|
|
18722
|
+
this.cullHeight = rec.height;
|
|
18723
|
+
this.layerDepth++;
|
|
18724
|
+
try {
|
|
18725
|
+
this.renderChildren(node, context, lctx, true);
|
|
18726
|
+
} finally {
|
|
18727
|
+
this.layerDepth--;
|
|
18728
|
+
this.cullX = savedCullX;
|
|
18729
|
+
this.cullY = savedCullY;
|
|
18730
|
+
this.cullWidth = savedCullWidth;
|
|
18731
|
+
this.cullHeight = savedCullHeight;
|
|
18732
|
+
lctx.restore();
|
|
18733
|
+
}
|
|
18734
|
+
}
|
|
18044
18735
|
applyTransform(ctx, rec, paint) {
|
|
18045
18736
|
const transform = paint.transform;
|
|
18046
18737
|
const originX = rec.x + transform.x;
|
|
@@ -18069,6 +18760,7 @@ var Canvas2DRenderer = class {
|
|
|
18069
18760
|
paintScrollbars(ctx, rec, now) {
|
|
18070
18761
|
const remaining = rec.scrollbarVisibleUntil - now;
|
|
18071
18762
|
if (remaining <= 0) return;
|
|
18763
|
+
this.volatileDraws++;
|
|
18072
18764
|
ctx.fillStyle = `rgba(128,128,128,${(Math.min(1, remaining / 350) * .55).toFixed(3)})`;
|
|
18073
18765
|
const { vertical, horizontal } = scrollbarThumbs(rec);
|
|
18074
18766
|
for (const bar of [vertical, horizontal]) {
|
|
@@ -18212,4 +18904,4 @@ function createCanvasSurface(host) {
|
|
|
18212
18904
|
//#endregion
|
|
18213
18905
|
export { prepareInputSurface as $, UiBinding as $a, defaultTextStyle as $i, EditableTextModel as $n, minmax as $r, DEFAULT_FONT_WEIGHT as $t, accumulatedOffsetTo as A, UiVisualState as Aa, themeExtension as Ai, setLinkHover as An, validateStates as Ar, proportionalFontMetrics as At, parseFlexDirection as B, UiInputEvent as Ba, spacingEqual as Bi, nextCaretToggle as Bn, transformsEqual as Br, layoutTextLines as Bt, describeLength as C, UiNode as Ca, lightTheme as Ci, isNodeTabStop as Cn, UI_ROLES as Cr, selectionRectsIn as Ct, formatNumber as D, resetOverrideWarnings as Da, defineThemeExtension as Di, linkHoverOf as Dn, normalizeStates as Dr, spannedRunsFor as Dt, formatExplanation as E, overrideSources as Ea, withDensity as Ei, hasTextLinks as En, isUiSemanticState as Er, ParagraphTextMeasurer as Et, constraintsEqual as F, MOUSE_POINTER as Fa, raiseContrast as Fi, editorFor as Fn, pathValuesEqual as Fr, applyCanvasTextStyle as Ft, MainAxisAlignment as G, UiSelectionChangeEvent as Ga, boxShadowArraysEqual as Gi, caretRectFor as Gn, radialGradient as Gr, textRunBackgrounds as Gt, sizeGridTracks as H, UiPasteEvent as Ha, defaultShadows as Hi, resolveColorValue as Hn, gradientPaint as Hr, runFontString as Ht, tightenConstraints as I, UiBeforeInputEvent as Ia, relativeLuminance as Ii, editorOf as In, UiTransforms as Ir, buildFontString as It, parseMainAxisAlignment as J, UiWheelEvent as Ja, isShapeName as Ji, lineLimit as Jn, fr as Jr, clearFontStacks as Jt, parseAlignContent as K, UiTextChangeEvent as Ka, boxShadowsEqual as Ki, lineIndexAtY as Kn, validateGradient as Kr, textRunDecorations as Kt, MAX_MEASURES_PER_CHILD as L, UiEventType as La, densityFactors as Li, isEditableNode as Ln, parseTransform as Lr, createCanvasTextStyle as Lt, LayoutRecord as M, hasVisualState as Ma, withThemeExtension as Mi, CARET_WIDTH as Mn, validateSubgrid as Mr, detectEditingPlatform as Mt, Constraints as N, visualState as Na, contrastRatio as Ni, EDITOR_PROP as Nn, inputsEqual as Nr, isPrintable as Nt, labelNode as O, writeDeclaredProperty as Oa, hasThemeExtension as Oi, linkOf as On, statesEqual as Or, ELLIPSIS as Ot, clampSize as P, visualStatesEqual as Pa, highContrastColors as Pi, caretVisibleAt as Pn, paintValuesEqual as Pr, EditableLayout as Pt, pointerDeviceOf as Q, DirtyFlags as Qa, typographyEqual as Qi, selectionRects as Qn, isPercentLength as Qr, DEFAULT_FONT_SIZE as Qt, isLayoutProtocol as R, UiFocusEvent as Ra, defaultSpacing as Ri, isMultiline as Rn, transform as Rr, drawText as Rt, SCROLLBAR_LINGER_MS as S, UiEnvironment as Sa, darkTheme as Si, isNodeSelectable as Sn, UiProperties as Sr, paragraphGeometryFrom as St, formatConstraints as T, clearOverrideProperty as Ta, withContrast as Ti, clearLinkHover as Tn, isUiRole as Tr, CharacterCountTextMeasurer as Tt, AlignContent as U, UiPinchEvent as Ua, shadowsEqual as Ui, isVideoSurface as Un, gradientsEqual as Ur, spanOf as Ut, placeGridItems as V, UiKeyboardEvent as Va, spacingSteps as Vi, syncEditorValue as Vn, MAX_GRADIENT_STOPS as Vr, runCanvasStyleInto as Vt, CrossAxisAlignment as W, UiPointerEvent as Wa, boxShadow as Wi, videoFrameSize as Wn, linearGradient as Wr, textMeasureRequest as Wt, UiPlatformAdapter as X, wheelDeltaYOf as Xa, defaultTypography as Xi, offsetAtX as Xn, isFrLength as Xr, registerFontStack as Xt, CanvasPlatformSurface as Y, noKeyModifiers as Ya, shapesEqual as Yi, offsetAtPoint as Yn, isAutoLength as Yr, fontStackFor as Yt, capturePointer as Z, UiNodeType as Za, isTypographyRole as Zi, offsetForVerticalMove as Zn, isMinMaxTrack as Zr, DEFAULT_FONT_FAMILY as Zt, LABEL_PADDING_X as _, parseColor as _a, observeViewportInsets as _i, selectionRangeOf as _n, inheritedPropertyFlags as _r, scrollbarZoneAt as _t, PaintPictureCache as a, textContentOf as aa, borderRadiusCorners as ai, normalizeTextAlign as an, previousGraphemeStart as ar, UiWheelController as at, LayoutEngine as b, createEnvironmentKey as ba, containerBands as bi, isNodeHitTestable as bn, propertyIsInherited as br, offsetAtPointIn as bt, EMPTY_RECORDING as c, colorsEqualPalette as ca, borderRadiusValuesEqual as ci, DEFAULT_LINE_HEIGHT_FACTOR as cn, resolveBoolean as cr, UiPointerController as ct, tracePathData as d, UiBasicColors as da, defineProperty as di, matchRangesOf as dn, resolvePropertyByName as dr, SCROLLBAR_HOVER_ZONE as dt, editableSpansOf as ea, percent as ei, DEFAULT_TEXT_COLOR as en, graphemeBoundaries as er, touchActionFor as et, ScaledImageCache as f, colorToHex as fa, propertyValuesEqual as fi, setMatchRanges as fn, resolveString as fr, SCROLLBAR_INSET as ft, paintsAfterChildren as g, normalizeColor as ga, noInsets as gi, selectableTextOf as gn, getPropertyNames as gr, scrollbarThumbs as gt, hasDecorationPhase as h, colorsEqual as ha, insetsEqual as hi, selectableTextNodes as hn, findPropertyDefinition as hr, scrollbarThumb as ht, traceRoundedRect as i, spannedTextOf as ia, borderRadius as ii, isPaintVisible as in, nextWordEnd as ir, UiPinchRecognizer as it, contentOffset as j, defaultVisualState as ja, themeExtensionsEqual as ji, CARET_BLINK_MS as jn, UI_SUBGRID_AXES as jr, commandForKey as jt, withOverrideSource as k, writeOverrideProperty as ka, noThemeExtensions as ki, linkRunAtPointIn as kn, validateRole as kr, layoutParagraph as kt, PaintRecorder as l, darkColors as la, normalizeBorderRadius as li, TEXT_MATCHES_PROP as ln, resolveNumber as lr, UiFocusManager as lt, decorationRect as m, colorValuesEqual as ma, UiInsetRegistry as mi, clearSelectionRange as mn, closestPropertyName as mr, SCROLLBAR_THICKNESS as mt, createCanvasSurface as n, resolvedSpansOf as na, resolveLength as ni, computeObjectFitRect as nn, lineStartAt as nr, UiTouchScroller as nt, paintPictures as o, textSpansEqual as oa, borderRadiusEqual as oi, normalizeVerticalAlign as on, previousWordStart as or, isScrollContainer as ot, decorationColor as p, colorToRgba as pa, UiEnvironmentKeys as pi, TEXT_SELECTION_PROP as pn, UiGraph as pr, SCROLLBAR_MIN_THUMB as pt, parseCrossAxisAlignment as q, UiWheelDeltaMode as qa, defaultShapes as qi, lineIndexForOffset as qn, auto as qr, bumpFontStack as qt, Canvas2DRenderer as r, spanAtOffset as ra, UiBorderRadiuses as ri, createPaintState as rn, nextGraphemeEnd as rr, UiGestureRecognizer as rt, PaintTarget as s, textStylesEqual as sa, borderRadiusIsZero as si, resolvePaintState as sn, wordRangeAt as sr, UiKeyboardController as st, CanvasSurface as t, flattenTextSpans as ta, repeat as ti, colorToCss as tn, lineEndAt as tr, UiEditingController as tt, replayPaint as u, lightColors as ua, uniformBorderRadius as ui, clearMatchRanges as un, resolveProperty as ur, UiHitTester as ut, drawOverlayShapes as v, rgb8 as va, UiContainerSizeSource as vi, setSelectionRange as vn, inheritedPropertyNames as vr, UiInputDispatcher as vt, describeOverrides as w, DirtyNodeSet as wa, themesEqual as wi, resolveCursor as wn, UI_SEMANTIC_STATES as wr, wordRangeIn as wt, SCROLLBAR_FADE_MS as x, findEnvironmentKey as xa, unknownContainerSize as xi, isNodeInert as xn, propertyValuesEqualByName as xr, paragraphGeometry as xt, labelOrigin as y, rgba as ya, bandOf as yi, isNodeFocusable as yn, propertyEffects as yr, hasDrawnText as yt, FlexDirection as z, UiGestureEvent as za, scaleSpacing as zi, isReadOnly as zn, transformIsIdentity as zr, fillTextRects as zt };
|
|
18214
18906
|
|
|
18215
|
-
//# sourceMappingURL=CanvasSurface-
|
|
18907
|
+
//# sourceMappingURL=CanvasSurface-CaNWsAlm.js.map
|