gesso-core 0.4.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.
@@ -4536,6 +4536,12 @@ const UiProperties = {
4536
4536
  * wants the default, or the article stops scrolling whenever the
4537
4537
  * cursor crosses the canvas.
4538
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
+ *
4539
4545
  * Like `scrollBehavior`, this is read by the input layer and
4540
4546
  * affects nothing laid out or painted.
4541
4547
  */
@@ -9967,6 +9973,15 @@ function hasScrollRoom(offset, max, delta) {
9967
9973
  return delta > 0 ? offset < max - SCROLL_EPSILON : offset > SCROLL_EPSILON;
9968
9974
  }
9969
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
+ */
9970
9985
  function containsOverscroll(node) {
9971
9986
  return node !== null && node.getProperty("overscrollBehavior") === "contain";
9972
9987
  }
@@ -10099,16 +10114,16 @@ var UiWheelController = class {
10099
10114
  let left = false;
10100
10115
  let right = false;
10101
10116
  for (let node = target; node !== null; node = node.parent) {
10102
- if (!isScrollContainer(node)) continue;
10103
- const state = this.scrollSink.containerState(node);
10104
- if (state === void 0) continue;
10105
- left ||= hasScrollRoom(state.scrollX, state.maxScrollX, -1);
10106
- right ||= hasScrollRoom(state.scrollX, state.maxScrollX, 1);
10107
- up ||= hasScrollRoom(state.scrollY, state.maxScrollY, -1);
10108
- down ||= hasScrollRoom(state.scrollY, state.maxScrollY, 1);
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
+ }
10109
10124
  if (containsOverscroll(node)) return EVERYTHING_SCROLLABLE;
10110
10125
  }
10111
- if (containsOverscroll(rootOf(target))) return EVERYTHING_SCROLLABLE;
10126
+ if (this.appContainsOverscroll(target)) return EVERYTHING_SCROLLABLE;
10112
10127
  return {
10113
10128
  up,
10114
10129
  down,
@@ -10147,9 +10162,14 @@ var UiWheelController = class {
10147
10162
  */
10148
10163
  scrollChain(target, event, deltaX, deltaY, deltaMode, wheelDeltaY) {
10149
10164
  for (let node = target; node !== null; node = node.parent) {
10150
- if (!isScrollContainer(node)) continue;
10151
- const state = this.scrollSink.containerState(node);
10152
- if (state === void 0) continue;
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
+ }
10153
10173
  const takesX = deltaX !== 0 && hasScrollRoom(state.scrollX, state.maxScrollX, deltaX);
10154
10174
  const takesY = deltaY !== 0 && hasScrollRoom(state.scrollY, state.maxScrollY, deltaY);
10155
10175
  if (takesX || takesY) {
@@ -10162,7 +10182,22 @@ var UiWheelController = class {
10162
10182
  return;
10163
10183
  }
10164
10184
  }
10165
- if (containsOverscroll(rootOf(target))) event.markConsumed();
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));
10166
10201
  }
10167
10202
  applyDelta(container, state, deltaX, deltaY, deltaMode, behavior) {
10168
10203
  this.scrollSink.scrollBy(container, deltaX === 0 ? 0 : this.toPixels(deltaX, deltaMode, state.viewportWidth), deltaY === 0 ? 0 : this.toPixels(deltaY, deltaMode, state.viewportHeight), behavior);
@@ -10426,6 +10461,11 @@ const VELOCITY_SAMPLES = 8;
10426
10461
  * long press raises nothing: it has a secondary button, and the
10427
10462
  * pointer controller dispatches the event from that instead.
10428
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
+ *
10429
10469
  * ## Two contacts
10430
10470
  *
10431
10471
  * Every contact the controller sees is fed to a `UiPinchRecognizer`,
@@ -10505,6 +10545,7 @@ var UiGestureRecognizer = class {
10505
10545
  return;
10506
10546
  }
10507
10547
  if (this.state === "longPressed") {
10548
+ if (!this.claimedPress && Math.hypot(event.x - this.startX, event.y - this.startY) <= this.pressSlop()) return;
10508
10549
  this.state = "dragging";
10509
10550
  this.claimedPress = true;
10510
10551
  this.dispatch("dragstart", target, this.startX, this.startY, event);
@@ -10559,8 +10600,10 @@ var UiGestureRecognizer = class {
10559
10600
  this.timer = null;
10560
10601
  if (this.state !== "pressing") return;
10561
10602
  this.state = "longPressed";
10562
- this.claimedPress = true;
10603
+ const mouse = down.pointer.kind === "mouse";
10604
+ this.claimedPress = !mouse;
10563
10605
  const held = this.dispatch("longpress", target, this.startX, this.startY, down);
10606
+ if (mouse && held.defaultPrevented) this.claimedPress = true;
10564
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);
10565
10608
  }, this.longPressDelay);
10566
10609
  }
@@ -12786,7 +12829,7 @@ function accumulatedOffsetTo(node, records, out) {
12786
12829
  }
12787
12830
  //#endregion
12788
12831
  //#region src/layout/LayoutExplanation.ts
12789
- const EPSILON = 1e-6;
12832
+ const EPSILON$1 = 1e-6;
12790
12833
  /**
12791
12834
  * Turns the facts about one axis into the rule that decided it and the
12792
12835
  * sentences that say so.
@@ -12801,7 +12844,7 @@ function buildAxisExplanation(f) {
12801
12844
  if (f.customParent !== void 0 && !f.isRoot) {
12802
12845
  decidedBy = "custom";
12803
12846
  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)}`);
12847
+ if (Math.abs(f.final - f.measured) > EPSILON$1) reasons.push(`and placed it at ${px(f.final)}`);
12805
12848
  return {
12806
12849
  axis: f.axis,
12807
12850
  content: f.content,
@@ -12840,7 +12883,7 @@ function buildAxisExplanation(f) {
12840
12883
  const to = f.effectiveMin > f.parentMin ? f.ownMin : f.ownMax;
12841
12884
  reasons.push(`clamped by its own ${clamp} ${px(to ?? f.effectiveMin)} → ${px(f.effectiveMin)}`);
12842
12885
  }
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"}`);
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"}`);
12844
12887
  } else if (f.explicit !== void 0) {
12845
12888
  decidedBy = "explicit";
12846
12889
  reasons.push(`${f.axis}: ${describeLength(f.explicitRaw)} (explicit) → ${px(f.explicit)}`);
@@ -12850,29 +12893,29 @@ function buildAxisExplanation(f) {
12850
12893
  const to = f.effectiveMin > f.explicit ? f.ownMin : f.ownMax;
12851
12894
  reasons.push(`clamped by ${clamp} ${px(to ?? f.effectiveMin)} → ${px(f.effectiveMin)}`);
12852
12895
  }
12853
- 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}`);
12854
12897
  } else {
12855
12898
  reasons.push(contentReason(f));
12856
12899
  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) {
12900
+ if (f.aspectRatio !== void 0 && Math.abs(f.content - f.measured) > EPSILON$1) {
12858
12901
  decidedBy = "aspect-ratio";
12859
12902
  reasons.push(`aspectRatio ${formatNumber(f.aspectRatio)} derives it from the other axis → ${px(f.measured)}`);
12860
12903
  }
12861
- if (f.content < f.effectiveMin - EPSILON) {
12862
- 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) {
12863
12906
  decidedBy = "min";
12864
12907
  reasons.push(`raised to ${minProp} ${px(f.ownMin)}`);
12865
12908
  } else {
12866
12909
  decidedBy = "min";
12867
12910
  reasons.push(`raised to the minimum ${f.parentLabel} asked for: ${px(f.parentMin)}`);
12868
12911
  }
12869
- } 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) {
12870
12913
  decidedBy = "max";
12871
12914
  reasons.push(`capped by ${maxProp} ${px(f.ownMax)}`);
12872
12915
  }
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}`);
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}`);
12874
12917
  }
12875
- 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)})`);
12876
12919
  return {
12877
12920
  axis: f.axis,
12878
12921
  content: f.content,
@@ -12890,8 +12933,8 @@ function buildAxisExplanation(f) {
12890
12933
  function flexReasons(f, flex, reasons) {
12891
12934
  const px = formatNumber;
12892
12935
  const final = f.parentMin;
12893
- const grew = final > flex.base + EPSILON;
12894
- const shrank = final < flex.base - EPSILON;
12936
+ const grew = final > flex.base + EPSILON$1;
12937
+ const shrank = final < flex.base - EPSILON$1;
12895
12938
  if (!grew && !shrank) {
12896
12939
  if (f.explicit !== void 0) {
12897
12940
  reasons.push(`${f.axis}: ${describeLength(f.explicitRaw)} (explicit) → ${px(f.explicit)}`);
@@ -12911,10 +12954,10 @@ function flexReasons(f, flex, reasons) {
12911
12954
  reasons.push(`flex item of ${flex.containerLabel}: base ${px(flex.base)} from ${baseSource}`);
12912
12955
  if (grew) {
12913
12956
  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)}`);
12957
+ if (Math.abs(final - flex.max) < EPSILON$1 && isFinite(flex.max)) reasons.push(`stopped at ${f.axis === "width" ? "maxWidth" : "maxHeight"} ${px(flex.max)}`);
12915
12958
  } else {
12916
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)`);
12917
- 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));
12918
12961
  }
12919
12962
  return "flex";
12920
12963
  }
@@ -12932,7 +12975,7 @@ function minimumReason(f, flex) {
12932
12975
  if (!flex.minAuto) return `stopped at ${minProp} ${px(flex.min)}`;
12933
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`;
12934
12977
  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)`;
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)`;
12936
12979
  return `stopped at its automatic minimum ${px(flex.min)} (the content's min-content ${f.axis}; set ${minProp}: 0 to allow smaller)`;
12937
12980
  }
12938
12981
  function contentReason(f) {
@@ -13344,6 +13387,23 @@ var LayoutEngine = class {
13344
13387
  }
13345
13388
  return this.records.get(node);
13346
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
+ }
13347
13407
  /** The node the last `layout` / `layoutForFrame` started from, if any. */
13348
13408
  get root() {
13349
13409
  return this.layoutRoot;
@@ -17485,6 +17545,14 @@ const MAX_PICTURE_SIDE = 8192;
17485
17545
  * is what remains: that both place the same picture in the same box,
17486
17546
  * at the same point in paint order, under the same clip and opacity.
17487
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
+ *
17488
17556
  * **The cost follows the change.** A frame that draws a painted node
17489
17557
  * whose inputs, box, scale and theme are unchanged makes no recording
17490
17558
  * and no bitmap; it draws the one already here. The key holds the
@@ -17504,7 +17572,8 @@ var PaintPictureCache = class {
17504
17572
  stats = {
17505
17573
  recorded: 0,
17506
17574
  rasterized: 0,
17507
- resolved: 0
17575
+ resolved: 0,
17576
+ direct: 0
17508
17577
  };
17509
17578
  constructor(createCanvas = offscreenPaintCanvas) {
17510
17579
  this.createCanvas = createCanvas;
@@ -17517,6 +17586,7 @@ var PaintPictureCache = class {
17517
17586
  this.stats.recorded = 0;
17518
17587
  this.stats.rasterized = 0;
17519
17588
  this.stats.resolved = 0;
17589
+ this.stats.direct = 0;
17520
17590
  }
17521
17591
  /**
17522
17592
  * The node's picture at this box and device scale, made if the
@@ -17528,39 +17598,107 @@ var PaintPictureCache = class {
17528
17598
  */
17529
17599
  pictureFor(node, rec, scale) {
17530
17600
  this.stats.resolved++;
17531
- const paint = node.properties.get("paint");
17532
- const path = node.properties.get("path");
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");
17601
+ const key = keyFor(node, rec, scale);
17602
+ if (key === null) return;
17537
17603
  const slot = this.slots.get(node);
17538
- if (slot !== void 0 && slot.picture !== null && slot.width === rec.width && slot.height === rec.height && slot.scale === scale && slot.environment === node.environment && slot.clipPath === clipPath && slot.blur === blur && paintValuesEqual(slot.paint, paint) && pathValuesEqual(slot.path, path)) return slot.picture;
17539
- const box = {
17540
- width: rec.width,
17541
- height: rec.height,
17542
- paddingTop: rec.paddingTop,
17543
- paddingRight: rec.paddingRight,
17544
- paddingBottom: rec.paddingBottom,
17545
- paddingLeft: rec.paddingLeft,
17546
- scale
17547
- };
17548
- const recording = this.record(paint, path, clipPath, blur, box);
17549
- const picture = this.rasterize(node, recording, box);
17550
- slot?.picture?.close?.();
17551
- this.slots.set(node, {
17552
- picture,
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,
17553
17690
  recording,
17554
- paint,
17555
- path,
17556
- clipPath,
17557
- blur,
17691
+ paint: key.paint,
17692
+ path: key.path,
17693
+ clipPath: key.clipPath,
17694
+ blur: key.blur,
17558
17695
  environment: node.environment,
17559
- width: rec.width,
17560
- height: rec.height,
17561
- scale
17562
- });
17563
- return picture ?? void 0;
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;
17564
17702
  }
17565
17703
  /**
17566
17704
  * The recording a node's picture was made from.
@@ -17607,6 +17745,36 @@ var PaintPictureCache = class {
17607
17745
  * make it possible for them not to.
17608
17746
  */
17609
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
+ }
17610
17778
  /** A painter's colour values, against the node's own theme. */
17611
17779
  function resolverFor(node) {
17612
17780
  return {
@@ -17669,6 +17837,310 @@ function offscreenPaintCanvas(width, height) {
17669
17837
  };
17670
17838
  }
17671
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
17672
18144
  //#region src/rendering/canvas2d/Canvas2DRenderer.ts
17673
18145
  /**
17674
18146
  * Canvas2D rendering backend.
@@ -17701,6 +18173,9 @@ function offscreenPaintCanvas(width, height) {
17701
18173
  * The frame strategy is a full clear + full traversal. Off-screen
17702
18174
  * subtrees are culled (while no ancestor transform is active) so
17703
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`).
17704
18179
  */
17705
18180
  var Canvas2DRenderer = class {
17706
18181
  options;
@@ -17754,8 +18229,32 @@ var Canvas2DRenderer = class {
17754
18229
  * Drained after the walk; see `render`.
17755
18230
  */
17756
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;
17757
18248
  constructor(options) {
17758
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();
17759
18258
  }
17760
18259
  get surface() {
17761
18260
  return this.options.surface;
@@ -17769,10 +18268,12 @@ var Canvas2DRenderer = class {
17769
18268
  }
17770
18269
  resize(width, height, dpr) {
17771
18270
  this.surface.setLogicalSize(width, height, dpr);
18271
+ this.layers.dropAll();
17772
18272
  }
17773
18273
  dispose() {
17774
18274
  this.disposed = true;
17775
18275
  this.scaledImages.dispose();
18276
+ this.layers.dropAll();
17776
18277
  }
17777
18278
  render(root, context) {
17778
18279
  const ctx = this.surface.getContext2D();
@@ -17784,9 +18285,12 @@ var Canvas2DRenderer = class {
17784
18285
  this.cullHeight = this.surface.logicalHeight;
17785
18286
  this.liftedPass.length = 0;
17786
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);
17787
18290
  this.renderNode(root, context, ctx, true, false);
17788
18291
  this.renderLifted(context, ctx);
17789
18292
  if (context.overlay !== void 0 && context.overlay.length > 0) drawOverlayShapes(ctx, context.overlay);
18293
+ this.layers.endFrame();
17790
18294
  }
17791
18295
  /**
17792
18296
  * Folds a node's style into the shared scratch and records that it
@@ -17837,13 +18341,15 @@ var Canvas2DRenderer = class {
17837
18341
  ctx.clip();
17838
18342
  this.pushCull(rec);
17839
18343
  }
17840
- if (rec.scrollable) {
17841
- ctx.save();
17842
- ctx.translate(-rec.scrollX, -rec.scrollY);
17843
- }
17844
18344
  const liftedBefore = this.liftedPass.length;
17845
- this.renderChildren(node, context, ctx, cull && !paint.hasTransform && !sticky);
17846
- if (rec.scrollable) ctx.restore();
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
+ }
17847
18353
  if (rec.clips) this.popCull();
17848
18354
  if (hasText) {
17849
18355
  if (this.paintOwner !== node) this.resolvePaint(node);
@@ -17942,6 +18448,7 @@ var Canvas2DRenderer = class {
17942
18448
  const video = paint.video;
17943
18449
  const source = video?.frame ?? paint.image;
17944
18450
  if (source === void 0 || source === null) return;
18451
+ if (video !== void 0) this.volatileDraws++;
17945
18452
  const size = video === void 0 ? {
17946
18453
  width: paint.image.width,
17947
18454
  height: paint.image.height
@@ -17979,9 +18486,7 @@ var Canvas2DRenderer = class {
17979
18486
  * story for a painted node.
17980
18487
  */
17981
18488
  paintPicture(ctx, node, rec) {
17982
- const picture = paintPictures.pictureFor(node, rec, this.surface.dpr);
17983
- if (picture === void 0) return;
17984
- ctx.drawImage(picture, rec.x, rec.y, rec.width, rec.height);
18489
+ paintPictures.draw(node, rec, this.surface.dpr, ctx);
17985
18490
  }
17986
18491
  /**
17987
18492
  * The image to draw for a destination `width` x `height` in the
@@ -18106,6 +18611,7 @@ var Canvas2DRenderer = class {
18106
18611
  */
18107
18612
  paintEditable(ctx, paint, context) {
18108
18613
  const model = paint.editor;
18614
+ if (model.focused) this.volatileDraws++;
18109
18615
  const layout = new EditableLayout(model, this.contentBox, paint, context.text);
18110
18616
  fillTextRects(ctx, textRunBackgrounds(layout.lines, paint));
18111
18617
  if (model.focused && !model.collapsed) {
@@ -18131,6 +18637,101 @@ var Canvas2DRenderer = class {
18131
18637
  ctx.fillRect(Math.round(caret.x), caret.y, 1, caret.height);
18132
18638
  }
18133
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
+ }
18134
18735
  applyTransform(ctx, rec, paint) {
18135
18736
  const transform = paint.transform;
18136
18737
  const originX = rec.x + transform.x;
@@ -18159,6 +18760,7 @@ var Canvas2DRenderer = class {
18159
18760
  paintScrollbars(ctx, rec, now) {
18160
18761
  const remaining = rec.scrollbarVisibleUntil - now;
18161
18762
  if (remaining <= 0) return;
18763
+ this.volatileDraws++;
18162
18764
  ctx.fillStyle = `rgba(128,128,128,${(Math.min(1, remaining / 350) * .55).toFixed(3)})`;
18163
18765
  const { vertical, horizontal } = scrollbarThumbs(rec);
18164
18766
  for (const bar of [vertical, horizontal]) {
@@ -18302,4 +18904,4 @@ function createCanvasSurface(host) {
18302
18904
  //#endregion
18303
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 };
18304
18906
 
18305
- //# sourceMappingURL=CanvasSurface-BR7hECHm.js.map
18907
+ //# sourceMappingURL=CanvasSurface-CaNWsAlm.js.map