gesso-core 0.6.19 → 0.6.20

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