gesso-core 0.6.19 → 0.6.21

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.
@@ -877,9 +881,24 @@ var UiNode = class {
877
881
  * opacity → 0.5
878
882
  * visible → true
879
883
  * width → 200
884
+ *
885
+ * Read it directly; write it through `setProperty` and
886
+ * `removeProperty`, which count the change in `propertyVersion`.
880
887
  */
881
888
  properties = /* @__PURE__ */ new Map();
882
889
  /**
890
+ * Counts writes to `properties`.
891
+ *
892
+ * Lets something derived from a node's own properties tell that it
893
+ * is still right without reading them again. The layout engine
894
+ * resolves forty of them into each record, and on a full pass over a
895
+ * tree where nothing changed that reading was most of a sixth of the
896
+ * pass. A counter is exact where a dirty flag is not: flags are
897
+ * cleared by whichever pass runs next, and a record is resolved by
898
+ * whichever pass reaches it.
899
+ */
900
+ propertyVersion = 0;
901
+ /**
883
902
  * Values a modifier has written over the element's own.
884
903
  *
885
904
  * Null until the first modifier writes, so a tree without modifiers
@@ -925,6 +944,13 @@ var UiNode = class {
925
944
  }
926
945
  setProperty(property, value) {
927
946
  this.properties.set(property, value);
947
+ this.propertyVersion++;
948
+ }
949
+ /** Removes a property; whether it was there. */
950
+ removeProperty(property) {
951
+ if (!this.properties.delete(property)) return false;
952
+ this.propertyVersion++;
953
+ return true;
928
954
  }
929
955
  };
930
956
  //#endregion
@@ -1609,6 +1635,7 @@ function resolvedSpansOf(node) {
1609
1635
  * as soon as the application catches up.
1610
1636
  */
1611
1637
  function editableSpansOf(node, text, composition) {
1638
+ if (node.properties.get("secret") === true) return EMPTY_SPANS;
1612
1639
  const spanned = spannedTextOf(node);
1613
1640
  if (spanned === void 0) return EMPTY_SPANS;
1614
1641
  if (spanned.text === text) return spanned.spans;
@@ -3919,6 +3946,19 @@ const UiProperties = {
3919
3946
  inherited: false,
3920
3947
  affects: 6
3921
3948
  }),
3949
+ /**
3950
+ * Whether a scroll container shows its overlay scrollbars: \`'auto'\`
3951
+ * (the default) while scrolling, \`'none'\` never, and then they cannot
3952
+ * be grabbed either. For a container scrolled by something else, as a
3953
+ * picture is moved inside a window onto it, where a bar would claim
3954
+ * the person could scroll it.
3955
+ */
3956
+ scrollbars: defineProperty({
3957
+ name: "scrollbars",
3958
+ defaultValue: void 0,
3959
+ inherited: false,
3960
+ affects: 6
3961
+ }),
3922
3962
  inset: defineProperty({
3923
3963
  name: "inset",
3924
3964
  defaultValue: void 0,
@@ -4202,6 +4242,17 @@ const UiProperties = {
4202
4242
  inherited: false,
4203
4243
  affects: 64
4204
4244
  }),
4245
+ /**
4246
+ * A password field: every character is drawn as a bullet, copy and
4247
+ * cut take nothing, and the shell edits it through a password input.
4248
+ * The value itself is unchanged. Single-line only.
4249
+ */
4250
+ secret: defineProperty({
4251
+ name: "secret",
4252
+ defaultValue: void 0,
4253
+ inherited: false,
4254
+ affects: 262
4255
+ }),
4205
4256
  /** Colour of the caret; defaults to the text colour. */
4206
4257
  caretColor: defineProperty({
4207
4258
  name: "caretColor",
@@ -4949,39 +5000,6 @@ function createFallbackKey(definition) {
4949
5000
  * no line height is set. Shared by ParagraphLayout and PaintState.
4950
5001
  */
4951
5002
  const DEFAULT_LINE_HEIGHT_FACTOR = 1.2;
4952
- /**
4953
- * Resolves a node's font the one way layout and paint both use.
4954
- *
4955
- * Font size, family, weight and letter spacing inherit through the
4956
- * environment's text style. Line height is the subtle one: a text
4957
- * style carries a line height that belongs with its font size, so a
4958
- * node that inherits the style takes it whole. A node that sets its
4959
- * own `fontSize` without a `lineHeight` gets a normal line height for
4960
- * that size, `fontSize × 1.2`, rather than the inherited pixel value
4961
- * sized for a different font — the inherited 16.8px of the default
4962
- * style is right for 14px text and wrong for 22px. An explicit
4963
- * `lineHeight` always wins.
4964
- *
4965
- * Before this, layout read the raw props (unset line height → ×1.2)
4966
- * and paint read the inherited ones (→ 16.8px), so any text whose size
4967
- * differed from the style's was measured in one line box and drawn in
4968
- * another, and sat high or low inside it.
4969
- */
4970
- function resolveFont(node) {
4971
- const out = {
4972
- fontSize: 0,
4973
- fontFamily: "",
4974
- fontWeight: "normal",
4975
- lineHeight: 0,
4976
- letterSpacing: 0,
4977
- fontStyle: "normal",
4978
- fontStretch: "normal",
4979
- fontVariant: "normal",
4980
- fontKerning: "auto"
4981
- };
4982
- resolveFontInto(node, out);
4983
- return out;
4984
- }
4985
5003
  /** `resolveFont`, writing into a target the caller owns. */
4986
5004
  function resolveFontInto(node, out) {
4987
5005
  const fontSize = positive$1(resolveProperty(node, UiProperties.fontSize)) ?? UiProperties.fontSize.defaultValue;
@@ -5161,6 +5179,36 @@ var UiGraph = class {
5161
5179
  this.nodeRemovedListener?.(node);
5162
5180
  for (let i = removed.length - 1; i > 0; i--) this.detachNode(removed[i]);
5163
5181
  }
5182
+ /**
5183
+ * Moves a child before \`reference\` among its own siblings, or to the
5184
+ * end when that is null. Its parent is unchanged, and so is everything
5185
+ * it inherits, so its subtree is not visited: a lazy list recycling its
5186
+ * rows moves one from end to end on every frame of a scroll, and
5187
+ * re-inheriting walked the whole row each time.
5188
+ */
5189
+ moveWithin(child, reference) {
5190
+ const parent = child.parent;
5191
+ if (parent === null) throw new Error(`Node '${child.id}' has no parent to move within.`);
5192
+ if (reference !== null && reference.parent !== parent) throw new Error(`Reference node '${reference.id}' is not a sibling of '${child.id}'.`);
5193
+ if (child === reference || child.nextSibling === reference) return;
5194
+ this.detachNode(child);
5195
+ parent.childOrderVersion++;
5196
+ child.parent = parent;
5197
+ if (reference === null) {
5198
+ const last = parent.lastChild;
5199
+ child.previousSibling = last;
5200
+ if (last !== null) last.nextSibling = child;
5201
+ else parent.firstChild = child;
5202
+ parent.lastChild = child;
5203
+ return;
5204
+ }
5205
+ const previous = reference.previousSibling;
5206
+ if (previous !== null) previous.nextSibling = child;
5207
+ else parent.firstChild = child;
5208
+ child.previousSibling = previous;
5209
+ child.nextSibling = reference;
5210
+ reference.previousSibling = child;
5211
+ }
5164
5212
  detachNode(node) {
5165
5213
  const parent = node.parent;
5166
5214
  if (!parent) return;
@@ -5426,7 +5474,7 @@ var UiGraph = class {
5426
5474
  applyResolvedProperty(node, property, present, value, dirtyFlags) {
5427
5475
  if (!present) {
5428
5476
  if (!node.properties.has(property)) return false;
5429
- node.properties.delete(property);
5477
+ node.removeProperty(property);
5430
5478
  this.markDirty(node, this.withProvidedFlags(property, dirtyFlags));
5431
5479
  return true;
5432
5480
  }
@@ -6801,7 +6849,10 @@ const FIT_EPSILON = 1 / 64;
6801
6849
  * narrower than the widest segment. When a segment is wider than the
6802
6850
  * available width the paragraph grows to it, and every line wraps
6803
6851
  * against that grown width rather than the one asked for, because that
6804
- * is the box the lines will be painted in.
6852
+ * is the box the lines will be painted in. When the width is the box's
6853
+ * own (`exactWidth`: a width set from outside, or the box being
6854
+ * painted), it does not grow: the lines wrap against it and the wide
6855
+ * segment overflows alone, as CSS wraps text in a box of a fixed width.
6805
6856
  *
6806
6857
  * The first baseline sits half the leading below the line top, plus the
6807
6858
  * ascent, with the half-leading floored to a whole pixel. That floor is
@@ -6867,7 +6918,7 @@ function layoutParagraph(request, runs) {
6867
6918
  if (paragraphEnd >= text.length) break;
6868
6919
  paragraphStart = paragraphEnd + 1;
6869
6920
  }
6870
- const available = Math.max(maxWidth, minContentWidth);
6921
+ const available = request.exactWidth === true ? maxWidth : Math.max(maxWidth, minContentWidth);
6871
6922
  const unwrapped = wrap === "none" || !isFinite(available);
6872
6923
  const lines = [];
6873
6924
  for (const paragraph of paragraphs) {
@@ -6876,7 +6927,7 @@ function layoutParagraph(request, runs) {
6876
6927
  const key = unwrapped ? -1 : available;
6877
6928
  let relative = paragraph.cached.lines.get(key);
6878
6929
  if (relative === void 0) {
6879
- relative = breakHardLine(paragraph.text, paragraph.segments, width, unwrapped, measure);
6930
+ relative = breakHardLine(paragraph.cached.text, paragraph.segments, width, unwrapped, measure);
6880
6931
  if (paragraph.cached.lines.size >= WIDTHS_PER_LINE) paragraph.cached.lines.clear();
6881
6932
  paragraph.cached.lines.set(key, relative);
6882
6933
  }
@@ -6950,12 +7001,14 @@ function hardLine(memo, style, text, wrap, measure) {
6950
7001
  memo.set(key, cached);
6951
7002
  return cached;
6952
7003
  }
6953
- const segments = segmentParagraph(text, wrap);
7004
+ const own = ownCopy(text);
7005
+ const segments = segmentParagraph(own, wrap);
6954
7006
  let minContentWidth = 0;
6955
- for (const segment of segments) minContentWidth = Math.max(minContentWidth, measure(text.slice(segment.start, segment.end), segment.start, segment.end));
7007
+ for (const segment of segments) minContentWidth = Math.max(minContentWidth, measure(own.slice(segment.start, segment.end), segment.start, segment.end));
6956
7008
  const entry = {
7009
+ text: own,
6957
7010
  segments,
6958
- maxContentWidth: measure(text, 0, text.length),
7011
+ maxContentWidth: measure(own, 0, own.length),
6959
7012
  minContentWidth,
6960
7013
  lines: /* @__PURE__ */ new Map()
6961
7014
  };
@@ -6966,6 +7019,23 @@ function hardLine(memo, style, text, wrap, measure) {
6966
7019
  memo.set(key, entry);
6967
7020
  return entry;
6968
7021
  }
7022
+ /**
7023
+ * `text` in a string of its own.
7024
+ *
7025
+ * A hard line arrives as a slice of the whole paragraph, and a slice
7026
+ * in V8 is a view that keeps the string it was cut from alive. The
7027
+ * memo outlives the paragraph — it is what a later edit finds the
7028
+ * unchanged lines in — so each line it remembered, and every line
7029
+ * broken from it, held on to the whole text it came from. Typing into
7030
+ * a 115 KB field kept every version of it: 100 MB over 4,096 edits.
7031
+ * Prefixing a character makes a new string that has to be flattened
7032
+ * before it can be sliced, and the slice is then of that copy. A short
7033
+ * line is copied by `slice` anyway, and an engine that copies every
7034
+ * slice loses nothing.
7035
+ */
7036
+ function ownCopy(text) {
7037
+ return text.length < 16 ? text : (" " + text).slice(1);
7038
+ }
6969
7039
  /** Breaks one hard line on its own, as if it were the whole text. */
6970
7040
  function breakHardLine(text, segments, available, unwrapped, measure) {
6971
7041
  const lines = [];
@@ -7266,8 +7336,58 @@ function spannedRunsFor(request, runs) {
7266
7336
  }
7267
7337
  };
7268
7338
  }
7269
- /** Paragraphs remembered before the least recently asked for is forgotten. */
7270
- const PARAGRAPH_CACHE_ENTRIES = 4096;
7339
+ /**
7340
+ * How much the paragraph cache holds before the least recently asked
7341
+ * for is forgotten: a number of paragraphs, and their text counted in
7342
+ * UTF-16 code units, whichever runs out first.
7343
+ *
7344
+ * A count alone treated a two-word label and a whole document as the
7345
+ * same size. Every keystroke in a long field is a new request, its key
7346
+ * the whole text, so 4,096 edits to a 115 KB field held 4,096 copies of
7347
+ * it, keys and lines: 1.1 GB of heap, for paragraphs nobody would ask
7348
+ * for again. The text budget keeps a handful of those; the count is
7349
+ * what bounds a screen of labels, and is larger than it was, since a
7350
+ * label costs a few hundred bytes and a list that remounts its rows
7351
+ * should find them still here.
7352
+ */
7353
+ const PARAGRAPH_CACHE_ENTRIES = 16384;
7354
+ const PARAGRAPH_CACHE_TEXT = 1 << 20;
7355
+ /** A least-recently-used map of paragraphs, bounded by count and by text; see `PARAGRAPH_CACHE_ENTRIES`. */
7356
+ var ParagraphCache = class {
7357
+ entries = /* @__PURE__ */ new Map();
7358
+ text = 0;
7359
+ get size() {
7360
+ return this.entries.size;
7361
+ }
7362
+ get(key) {
7363
+ const entry = this.entries.get(key);
7364
+ if (entry === void 0) return;
7365
+ this.entries.delete(key);
7366
+ this.entries.set(key, entry);
7367
+ return entry.paragraph;
7368
+ }
7369
+ set(key, paragraph, text) {
7370
+ const previous = this.entries.get(key);
7371
+ if (previous !== void 0) {
7372
+ this.entries.delete(key);
7373
+ this.text -= previous.text;
7374
+ }
7375
+ while (this.entries.size > 0 && (this.entries.size >= PARAGRAPH_CACHE_ENTRIES || this.text + text > PARAGRAPH_CACHE_TEXT)) {
7376
+ const [oldest, entry] = this.entries.entries().next().value;
7377
+ this.entries.delete(oldest);
7378
+ this.text -= entry.text;
7379
+ }
7380
+ this.entries.set(key, {
7381
+ paragraph,
7382
+ text
7383
+ });
7384
+ this.text += text;
7385
+ }
7386
+ clear() {
7387
+ this.entries.clear();
7388
+ this.text = 0;
7389
+ }
7390
+ };
7271
7391
  /**
7272
7392
  * Base class for measurers: implement the two primitives and the
7273
7393
  * paragraph algorithm does the rest.
@@ -7289,21 +7409,31 @@ const PARAGRAPH_CACHE_ENTRIES = 4096;
7289
7409
  * The result is shared between callers and must not be mutated.
7290
7410
  */
7291
7411
  var ParagraphTextMeasurer = class {
7292
- paragraphs = /* @__PURE__ */ new Map();
7412
+ paragraphs = new ParagraphCache();
7413
+ /**
7414
+ * Paragraphs that came out the same at every width they fit in, by
7415
+ * their key without the width; see `layoutFitsAnyWidth`.
7416
+ *
7417
+ * A resize asks every paragraph again at a width a few pixels from
7418
+ * the last, and a title, a caption or a label that fitted before
7419
+ * fits still: its lines, its box and everything else are what they
7420
+ * were. Keyed by its width as well, each of those missed on every
7421
+ * step of a dragged window edge and was laid out again, and the
7422
+ * box it was then painted in (its own width, exact) missed once
7423
+ * more.
7424
+ */
7425
+ fitting = new ParagraphCache();
7426
+ generations = 0;
7293
7427
  layout(request) {
7294
- const key = paragraphKey(request);
7428
+ const style = paragraphStyleKey(request);
7429
+ const key = `${request.maxWidth ?? ""}\0${request.exactWidth === true ? 1 : ""}\0${style}`;
7295
7430
  const cached = this.paragraphs.get(key);
7296
- if (cached !== void 0) {
7297
- this.paragraphs.delete(key);
7298
- this.paragraphs.set(key, cached);
7299
- return cached;
7300
- }
7431
+ if (cached !== void 0) return cached;
7432
+ const fitted = this.fitting.get(style);
7433
+ if (fitted !== void 0 && fitsWidth(fitted, request.maxWidth)) return fitted;
7301
7434
  const paragraph = layoutParagraph(request, this);
7302
- if (this.paragraphs.size >= PARAGRAPH_CACHE_ENTRIES) {
7303
- const oldest = this.paragraphs.keys().next().value;
7304
- if (oldest !== void 0) this.paragraphs.delete(oldest);
7305
- }
7306
- this.paragraphs.set(key, paragraph);
7435
+ this.paragraphs.set(key, paragraph, request.text.length);
7436
+ if (layoutFitsAnyWidth(request, paragraph)) this.fitting.set(style, paragraph, request.text.length);
7307
7437
  return paragraph;
7308
7438
  }
7309
7439
  measure(request) {
@@ -7317,6 +7447,9 @@ var ParagraphTextMeasurer = class {
7317
7447
  get cachedParagraphs() {
7318
7448
  return this.paragraphs.size;
7319
7449
  }
7450
+ get generation() {
7451
+ return this.generations;
7452
+ }
7320
7453
  /**
7321
7454
  * Forgets every laid-out paragraph. A subclass that caches its own
7322
7455
  * primitives overrides this and calls it, so a font that finished
@@ -7324,13 +7457,47 @@ var ParagraphTextMeasurer = class {
7324
7457
  * built from.
7325
7458
  */
7326
7459
  invalidate() {
7460
+ this.generations++;
7327
7461
  this.paragraphs.clear();
7462
+ this.fitting.clear();
7328
7463
  forgetHardLines(this);
7329
7464
  }
7330
7465
  };
7331
- /** Every field of a request, in a fixed order; two requests that lay out alike share a key. */
7332
- function paragraphKey(request) {
7333
- return `${request.fontSize}\0${request.maxWidth ?? ""}\0${request.fontFamily ?? ""}\0${request.fontWeight ?? ""}\0${request.lineHeight ?? ""}\0${request.letterSpacing ?? ""}\0${request.wrap ?? ""}\0${request.maxLines ?? ""}\0${request.overflow ?? ""}\0${request.fontStyle ?? ""}\0${request.fontStretch ?? ""}\0${request.fontVariant ?? ""}\0${request.fontKerning ?? ""}\0${spansKey(request.spans)}\0${request.text}`;
7466
+ /**
7467
+ * Whether `paragraph`, laid out for `request`, is what a request
7468
+ * differing only in its width would get at any width it fits in.
7469
+ *
7470
+ * A paragraph's width enters its layout in four places, and none of
7471
+ * them can tell two widths apart once every hard line fits whole:
7472
+ * nothing breaks (a line that fits is not broken, `exactWidth` or
7473
+ * not); the box is fit-content, which is the natural width; no line is
7474
+ * wider than the box, so none is ellipsised in place; and the one
7475
+ * ellipsis that is fitted to the width — the last line kept by
7476
+ * `maxLines` — is excluded here, because it is only drawn when there
7477
+ * were more hard lines than that. An unbounded request is left out:
7478
+ * it is laid out unwrapped, which is a different path, and it has a
7479
+ * key of its own that never changes anyway.
7480
+ */
7481
+ function layoutFitsAnyWidth(request, paragraph) {
7482
+ if (!fitsWidth(paragraph, request.maxWidth)) return false;
7483
+ const maxLines = request.maxLines;
7484
+ return request.overflow !== "ellipsis" || maxLines === void 0 || maxLines < 1 || !hasMoreHardLines(request.text, maxLines);
7485
+ }
7486
+ /** Whether a paragraph with nothing in it that depends on the width fits at `maxWidth`. */
7487
+ function fitsWidth(paragraph, maxWidth) {
7488
+ return maxWidth !== void 0 && maxWidth >= 0 && paragraph.maxContentWidth <= maxWidth;
7489
+ }
7490
+ function hasMoreHardLines(text, count) {
7491
+ let at = -1;
7492
+ for (let i = 0; i < count; i++) {
7493
+ at = text.indexOf("\n", at + 1);
7494
+ if (at < 0) return false;
7495
+ }
7496
+ return true;
7497
+ }
7498
+ /** Every field of a request but its width, in a fixed order; two requests that lay out alike at one width share a key. */
7499
+ function paragraphStyleKey(request) {
7500
+ return `${request.fontSize}\0${request.fontFamily ?? ""}\0${request.fontWeight ?? ""}\0${request.lineHeight ?? ""}\0${request.letterSpacing ?? ""}\0${request.wrap ?? ""}\0${request.maxLines ?? ""}\0${request.overflow ?? ""}\0${request.fontStyle ?? ""}\0${request.fontStretch ?? ""}\0${request.fontVariant ?? ""}\0${request.fontKerning ?? ""}\0${spansKey(request.spans)}\0${request.text}`;
7334
7501
  }
7335
7502
  /**
7336
7503
  * The runs' part of a paragraph key, remembered by the identity of the
@@ -7582,7 +7749,7 @@ function editorFor(node) {
7582
7749
  let model = node.properties.get(EDITOR_PROP);
7583
7750
  if (!(model instanceof EditableTextModel)) {
7584
7751
  model = new EditableTextModel();
7585
- node.properties.set(EDITOR_PROP, model);
7752
+ node.setProperty(EDITOR_PROP, model);
7586
7753
  }
7587
7754
  syncEditorValue(node, model);
7588
7755
  syncEditorHidden(node, model);
@@ -7612,12 +7779,33 @@ function syncEditorHidden(node, model) {
7612
7779
  function isEditableNode(node) {
7613
7780
  return node.type === "editable-text";
7614
7781
  }
7782
+ /** Whether Enter inserts a newline. Never in a secret field, which is one line, as a password input is. */
7615
7783
  function isMultiline(node) {
7616
- return node.properties.get("multiline") === true;
7784
+ return node.properties.get("multiline") === true && !isSecret(node);
7617
7785
  }
7618
7786
  function isReadOnly(node) {
7619
7787
  return node.properties.get("readOnly") === true;
7620
7788
  }
7789
+ function isSecret(node) {
7790
+ return node.properties.get("secret") === true;
7791
+ }
7792
+ /**
7793
+ * A secret field's text as it is drawn: one bullet per UTF-16 code
7794
+ * unit, not per character. Every offset the editor works in (caret,
7795
+ * selection, hit-testing, the shell's mirror) is a code unit offset
7796
+ * into the model's text, and a mask of the same length keeps each of
7797
+ * them meaning the same position in both strings. A character outside
7798
+ * the BMP draws as two bullets, which says a little about it; counting
7799
+ * graphemes instead would need a map between the two strings' offsets
7800
+ * in every one of those places.
7801
+ */
7802
+ function maskText(text) {
7803
+ return "•".repeat(text.length);
7804
+ }
7805
+ /** The text an editable draws and measures: its model's, masked when it is secret. */
7806
+ function shownTextOf(node, model) {
7807
+ return isSecret(node) ? maskText(model.text) : model.text;
7808
+ }
7621
7809
  /**
7622
7810
  * Whether the caret is in the visible half of its blink at `now`. It
7623
7811
  * is always visible while a composition is open — the IME caret must
@@ -7655,8 +7843,8 @@ function linkHoverOf(node) {
7655
7843
  /** Records the hovered run. Returns whether it changed. */
7656
7844
  function setLinkHover(node, index) {
7657
7845
  if (linkHoverOf(node) === index) return false;
7658
- if (index < 0) node.properties.delete(TEXT_LINK_HOVER_PROP);
7659
- else node.properties.set(TEXT_LINK_HOVER_PROP, index);
7846
+ if (index < 0) node.removeProperty(TEXT_LINK_HOVER_PROP);
7847
+ else node.setProperty(TEXT_LINK_HOVER_PROP, index);
7660
7848
  return true;
7661
7849
  }
7662
7850
  /** Forgets the hovered run. Returns whether there was one. */
@@ -7857,7 +8045,7 @@ function selectionRangeOf(node) {
7857
8045
  function setSelectionRange(node, start, end) {
7858
8046
  const current = selectionRangeOf(node);
7859
8047
  if (current !== void 0 && current.start === start && current.end === end) return false;
7860
- node.properties.set(TEXT_SELECTION_PROP, {
8048
+ node.setProperty(TEXT_SELECTION_PROP, {
7861
8049
  start,
7862
8050
  end
7863
8051
  });
@@ -7865,7 +8053,7 @@ function setSelectionRange(node, start, end) {
7865
8053
  }
7866
8054
  /** Clears a node's selected range. Returns whether it had one. */
7867
8055
  function clearSelectionRange(node) {
7868
- return node.properties.delete(TEXT_SELECTION_PROP);
8056
+ return node.removeProperty(TEXT_SELECTION_PROP);
7869
8057
  }
7870
8058
  /**
7871
8059
  * The text a `Text` node draws, when the user may select it.
@@ -7935,12 +8123,12 @@ function setMatchRanges(node, ranges) {
7935
8123
  const current = matchRangesOf(node);
7936
8124
  if (current !== void 0 && rangesEqual(current, ranges)) return false;
7937
8125
  if (ranges.length === 0) return clearMatchRanges(node);
7938
- node.properties.set(TEXT_MATCHES_PROP, ranges);
8126
+ node.setProperty(TEXT_MATCHES_PROP, ranges);
7939
8127
  return true;
7940
8128
  }
7941
8129
  /** Clears a node's matched ranges. Returns whether it had any. */
7942
8130
  function clearMatchRanges(node) {
7943
- return node.properties.delete(TEXT_MATCHES_PROP);
8131
+ return node.removeProperty(TEXT_MATCHES_PROP);
7944
8132
  }
7945
8133
  function rangesEqual(a, b) {
7946
8134
  if (a.length !== b.length) return false;
@@ -8045,7 +8233,7 @@ function resolvePaintState(node, out) {
8045
8233
  if (isEditableNode(node)) {
8046
8234
  const model = editorFor(node);
8047
8235
  out.editor = model;
8048
- out.text = model.text;
8236
+ out.text = shownTextOf(node, model);
8049
8237
  out.placeholder = resolveString(node, "placeholder");
8050
8238
  out.placeholderColor = resolveColor(node, UiProperties.placeholderColor) ?? themeColor(node, "textMuted") ?? DEFAULT_PLACEHOLDER_COLOR;
8051
8239
  out.selectionColor = resolveColor(node, UiProperties.selectionColor) ?? defaultSelectionColor(node);
@@ -8507,6 +8695,7 @@ function textMeasureRequest(text, state, box) {
8507
8695
  fontVariant: state.fontVariant,
8508
8696
  fontKerning: state.fontKerning,
8509
8697
  maxWidth: box.width > 0 ? box.width : void 0,
8698
+ exactWidth: box.width > 0,
8510
8699
  wrap: state.textWrap,
8511
8700
  maxLines: state.maxLines,
8512
8701
  overflow: state.textOverflow,
@@ -8834,23 +9023,32 @@ var EditableLayout = class {
8834
9023
  placeholderLines;
8835
9024
  measure;
8836
9025
  rtl;
9026
+ /**
9027
+ * The text as drawn: the model's, or a secret field's mask of it
9028
+ * (see `maskText`). The geometry is the drawing's, so it is measured
9029
+ * from this; the two are the same length, so its offsets are the
9030
+ * model's.
9031
+ */
9032
+ text;
8837
9033
  constructor(model, box, state, measurer) {
8838
9034
  this.model = model;
8839
9035
  this.box = box;
8840
- const request = textMeasureRequest(model.text, state, box);
9036
+ const text = state.text ?? model.text;
9037
+ this.text = text;
9038
+ const request = textMeasureRequest(text, state, box);
8841
9039
  request.maxLines = void 0;
8842
9040
  request.overflow = "clip";
8843
9041
  this.lines = placeLines(box, state, measurer.layout(request));
8844
9042
  const spanned = spannedRunsFor(request, measurer);
8845
- this.measure = (text, start) => text.length === 0 ? 0 : spanned === void 0 || start === void 0 ? measurer.measureRunWidth(text, request) : spanned.width(model.text, start, start + text.length);
9043
+ this.measure = (text, start) => text.length === 0 ? 0 : spanned === void 0 || start === void 0 ? measurer.measureRunWidth(text, request) : spanned.width(this.text, start, start + text.length);
8846
9044
  this.rtl = state.rtl;
8847
- this.placeholderLines = model.text.length === 0 && state.placeholder !== void 0 && state.placeholder.length > 0 ? placeLines(box, state, measurer.layout({
9045
+ this.placeholderLines = text.length === 0 && state.placeholder !== void 0 && state.placeholder.length > 0 ? placeLines(box, state, measurer.layout({
8848
9046
  ...request,
8849
9047
  text: state.placeholder
8850
9048
  })) : [];
8851
9049
  }
8852
9050
  caretRect(offset = this.model.focus) {
8853
- return caretRectFor(this.lines, this.model.text, offset, this.measure, this.rtl);
9051
+ return caretRectFor(this.lines, this.text, offset, this.measure, this.rtl);
8854
9052
  }
8855
9053
  /**
8856
9054
  * The caret position nearest a point. Where hidden text sits at the
@@ -8861,10 +9059,10 @@ var EditableLayout = class {
8861
9059
  * apart. Never inside hidden text.
8862
9060
  */
8863
9061
  offsetAt(x, y) {
8864
- return this.model.caretOffsetNear(offsetAtPoint(this.lines, this.model.text, x, y, this.measure, this.rtl));
9062
+ return this.model.caretOffsetNear(offsetAtPoint(this.lines, this.text, x, y, this.measure, this.rtl));
8865
9063
  }
8866
9064
  verticalMove(offset, direction, goalX) {
8867
- const move = offsetForVerticalMove(this.lines, this.model.text, offset, direction, this.measure, this.rtl, goalX);
9065
+ const move = offsetForVerticalMove(this.lines, this.text, offset, direction, this.measure, this.rtl, goalX);
8868
9066
  return move === null ? null : {
8869
9067
  offset: this.model.caretOffsetNear(move.offset),
8870
9068
  x: move.x
@@ -8877,13 +9075,13 @@ var EditableLayout = class {
8877
9075
  selectionBoxes(range) {
8878
9076
  const start = range?.start ?? this.model.start;
8879
9077
  const end = range?.end ?? this.model.end;
8880
- return selectionRects(this.lines, this.model.text, start, end, this.measure, this.rtl);
9078
+ return selectionRects(this.lines, this.text, start, end, this.measure, this.rtl);
8881
9079
  }
8882
9080
  /** Boxes under the text the IME is composing, for its underline. */
8883
9081
  compositionBoxes() {
8884
9082
  const range = this.model.composition;
8885
9083
  if (range === null) return [];
8886
- return selectionRects(this.lines, this.model.text, range.start, range.end, this.measure, this.rtl);
9084
+ return selectionRects(this.lines, this.text, range.start, range.end, this.measure, this.rtl);
8887
9085
  }
8888
9086
  };
8889
9087
  //#endregion
@@ -9379,6 +9577,7 @@ function scrollbarThumb(rec, axis) {
9379
9577
  return thumbFor(rec, axis);
9380
9578
  }
9381
9579
  function thumbFor(rec, axis) {
9580
+ if (rec.hidesScrollbars) return null;
9382
9581
  const viewport = axis === "y" ? rec.height : rec.width;
9383
9582
  const content = axis === "y" ? rec.contentHeight : rec.contentWidth;
9384
9583
  if (!(content > viewport) || viewport <= 0) return null;
@@ -10109,8 +10308,21 @@ var UiFocusManager = class {
10109
10308
  * when that scope was pushed, when that node is still in the tree
10110
10309
  * and still focusable. Otherwise focus is dropped rather than left
10111
10310
  * on a node the scope was hiding.
10112
- */
10113
- popScope() {
10311
+ *
10312
+ * Given its root, only that scope ends: none, when it has gone already
10313
+ * (its subtree removed, which pops it), so a trap released after its
10314
+ * content was taken away cannot end the one under it, a submenu's
10315
+ * menu's; and one under another is taken out where it is.
10316
+ */
10317
+ popScope(root) {
10318
+ if (root !== void 0 && this.scopes.at(-1)?.root !== root) {
10319
+ const at = this.scopes.findIndex((each) => each.root === root);
10320
+ if (at >= 0) {
10321
+ this.scopes.splice(at, 1);
10322
+ this.notifyScope();
10323
+ }
10324
+ return;
10325
+ }
10114
10326
  const scope = this.scopes.pop();
10115
10327
  if (scope === void 0) return;
10116
10328
  const restore = scope.restore;
@@ -10930,6 +11142,11 @@ var UiWheelController = class {
10930
11142
  * wheel of a burst land correctly even though the first could not.
10931
11143
  */
10932
11144
  lastWheelTarget = null;
11145
+ /**
11146
+ * The container the wheel last scrolled, for a frame's report of where
11147
+ * scrolling put it. Null until the wheel has scrolled something.
11148
+ */
11149
+ lastScrolled = null;
10933
11150
  wheel(x, y, deltaX, deltaY, modifiers = noKeyModifiers(), deltaMode = 0, wheelDeltaY, at) {
10934
11151
  const target = this.hitTester.hitTest(x, y)?.node ?? null;
10935
11152
  this.lastWheelTarget = target;
@@ -11082,7 +11299,8 @@ var UiWheelController = class {
11082
11299
  dirX: 0,
11083
11300
  dirY: 0,
11084
11301
  quiet: 0,
11085
- fresh: false
11302
+ fresh: false,
11303
+ fitted: false
11086
11304
  };
11087
11305
  this.pacing.set(container, paced);
11088
11306
  }
@@ -11098,9 +11316,11 @@ var UiWheelController = class {
11098
11316
  });
11099
11317
  while (paced.samples.length > 2 && paced.samples[0].t < this.eventTime - VELOCITY_WINDOW_MS) paced.samples.shift();
11100
11318
  this.scrollSink.scrollBy(container, 0, 0, "instant");
11319
+ this.lastScrolled = container;
11101
11320
  return;
11102
11321
  }
11103
11322
  this.scrollSink.scrollBy(container, dx, dy, behavior);
11323
+ this.lastScrolled = container;
11104
11324
  }
11105
11325
  /**
11106
11326
  * A container's state as a step should judge it: during a paced gesture,
@@ -11128,16 +11348,15 @@ var UiWheelController = class {
11128
11348
  * comes a varying time after the last step: applied as they arrive, a
11129
11349
  * steady flick moves the page by uneven amounts, which reads as
11130
11350
  * 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.
11351
+ * the hand. So this does what a browser does for its own scrolling: it
11352
+ * resamples the steps at the frame's time, reading a line fitted through
11353
+ * the last `VELOCITY_WINDOW_MS` of them. Frames come out even, and
11354
+ * nothing trails.
11136
11355
  *
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.
11356
+ * A gesture's first step, with no line yet, goes as it is. Once the
11357
+ * steps stop, the page eases over a few frames to exactly where they add
11358
+ * up to; a trackpad's momentum slows to nothing first, so there is next
11359
+ * to nothing to take up.
11141
11360
  *
11142
11361
  * True while a gesture is still being paced, so the host keeps frames
11143
11362
  * coming until it has settled.
@@ -11157,14 +11376,38 @@ var UiWheelController = class {
11157
11376
  let y = paced.totalY;
11158
11377
  const samples = paced.samples;
11159
11378
  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];
11379
+ const first = samples[0];
11162
11380
  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;
11381
+ const at = time === void 0 ? void 0 : time - RESAMPLE_MS;
11382
+ 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) {
11383
+ let meanT = 0;
11384
+ let meanX = 0;
11385
+ let meanY = 0;
11386
+ for (const sample of samples) {
11387
+ meanT += sample.t;
11388
+ meanX += sample.x;
11389
+ meanY += sample.y;
11390
+ }
11391
+ meanT /= samples.length;
11392
+ meanX /= samples.length;
11393
+ meanY /= samples.length;
11394
+ let varT = 0;
11395
+ let covX = 0;
11396
+ let covY = 0;
11397
+ for (const sample of samples) {
11398
+ const dt = sample.t - meanT;
11399
+ varT += dt * dt;
11400
+ covX += dt * (sample.x - meanX);
11401
+ covY += dt * (sample.y - meanY);
11402
+ }
11403
+ x = meanX + covX / varT * (at - meanT);
11404
+ y = meanY + covY / varT * (at - meanT);
11405
+ paced.fitted = true;
11406
+ } else if (paced.fitted) {
11407
+ const drawnX = paced.lastX - paced.baseX;
11408
+ const drawnY = paced.lastY - paced.baseY;
11409
+ x = Math.abs(x - drawnX) < SETTLED_PX ? x : drawnX + (x - drawnX) * SETTLE_RATE;
11410
+ y = Math.abs(y - drawnY) < SETTLED_PX ? y : drawnY + (y - drawnY) * SETTLE_RATE;
11168
11411
  }
11169
11412
  const state = this.scrollSink.containerState(container);
11170
11413
  if (state === void 0) {
@@ -11190,7 +11433,7 @@ var UiWheelController = class {
11190
11433
  if (dx !== 0 || dy !== 0) this.scrollSink.scrollBy(container, dx, dy, "instant");
11191
11434
  paced.lastX = toX;
11192
11435
  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);
11436
+ 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
11437
  }
11195
11438
  return this.pacing.size > 0;
11196
11439
  }
@@ -11244,18 +11487,29 @@ function isScrollContainer(node) {
11244
11487
  * hand from a trackpad to a mouse.
11245
11488
  */
11246
11489
  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
11490
  /**
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.
11491
+ * How far back the steps are fitted: five or six of a trackpad's. Less, and
11492
+ * the line follows each step's rounding; more, and it lags a flick that is
11493
+ * speeding up or slowing down. Measured on a real flick on a Mac, 48 ms left
11494
+ * a sixth of the frame-to-frame variation of the last-few-steps velocity,
11495
+ * with no frame standing still and the page no further behind the hand.
11255
11496
  */
11256
- const MAX_AHEAD_FRAMES = 1.5;
11257
- /** How far back the velocity is read for a prediction. */
11258
- const RECENT_MS = 20;
11497
+ const VELOCITY_WINDOW_MS = 48;
11498
+ /** How long after the last step its line is still trusted. */
11499
+ const STALE_MS = 40;
11500
+ /**
11501
+ * How long before the frame the line is read. Each millisecond is one more
11502
+ * of lag and less of a guess: measured on recorded flicks, 12 left the
11503
+ * worst frames a third as uneven as reading at the frame, and the page
11504
+ * about 11 ms behind the hand, against 15 for steps applied as they come.
11505
+ */
11506
+ const RESAMPLE_MS = 12;
11507
+ /** Steps a line is fitted through: two are a line through their rounding. */
11508
+ const MIN_FIT_STEPS = 3;
11509
+ /** How much of the way to where the steps add up to a frame goes once they stop. */
11510
+ const SETTLE_RATE = .35;
11511
+ /** Close enough to where the steps add up to, to go the rest at once. */
11512
+ const SETTLED_PX = .25;
11259
11513
  /** The display's interval before one has been measured. */
11260
11514
  const DEFAULT_FRAME_MS = 1e3 / 60;
11261
11515
  /** Quiet frames, once settled, after which a gesture is over. */
@@ -12009,9 +12263,13 @@ var UiEditingController = class {
12009
12263
  if (command === null) return false;
12010
12264
  return this.execute(node, command);
12011
12265
  }
12012
- execute(node, command) {
12266
+ execute(node, keyed) {
12013
12267
  const model = editorFor(node);
12014
12268
  const readOnly = isReadOnly(node);
12269
+ const command = (keyed.kind === "move" || keyed.kind === "delete") && keyed.unit === "word" ? {
12270
+ ...keyed,
12271
+ unit: secretWordUnit(node)
12272
+ } : keyed;
12015
12273
  switch (command.kind) {
12016
12274
  case "move":
12017
12275
  if (this.span !== null && !command.extend) {
@@ -12182,7 +12440,8 @@ var UiEditingController = class {
12182
12440
  case "deleteByCut":
12183
12441
  case "deleteByDrag": {
12184
12442
  if (readOnly) return true;
12185
- const unit = /Word/.test(inputType) ? "word" : /Line/.test(inputType) ? "line" : "grapheme";
12443
+ if (inputType === "deleteByCut" && isSecret(node)) return true;
12444
+ const unit = /Word/.test(inputType) ? secretWordUnit(node) : /Line/.test(inputType) ? "line" : "grapheme";
12186
12445
  const forward = /Forward/.test(inputType);
12187
12446
  return this.applyEdit(node, model, inputType, null, () => {
12188
12447
  if (inputType === "deleteContent" || inputType === "deleteByCut" || inputType === "deleteByDrag") model.replaceRange(model.start, model.end, "");
@@ -12387,6 +12646,7 @@ var UiEditingController = class {
12387
12646
  };
12388
12647
  this.dragging = node;
12389
12648
  if (modifiers.shift) model.moveTo(offset, true);
12649
+ else if (count === 2 && isSecret(node)) model.selectAll();
12390
12650
  else if (count === 2) {
12391
12651
  const word = visibleWordRange(model.text, model.hidden, offset);
12392
12652
  model.select(word.start, word.end);
@@ -12452,6 +12712,16 @@ var UiEditingController = class {
12452
12712
  ...html === void 0 ? {} : { html }
12453
12713
  };
12454
12714
  }
12715
+ if (isSecret(node)) return {
12716
+ text: maskText(model.text),
12717
+ selectionStart: model.start,
12718
+ selectionEnd: model.end,
12719
+ caret,
12720
+ multiline: false,
12721
+ composing: model.composing,
12722
+ clipboard: "",
12723
+ secret: true
12724
+ };
12455
12725
  const html = model.collapsed || model.composing ? void 0 : this.fieldHtml(node, model.start, model.end);
12456
12726
  return {
12457
12727
  text: model.text,
@@ -12722,10 +12992,11 @@ var UiEditingController = class {
12722
12992
  const end = forward ? span.focus : span.anchor;
12723
12993
  const key = [span];
12724
12994
  for (const node of span.lit) key.push(editorFor(node).text);
12995
+ const textOf = (node) => isSecret(node) ? maskText(editorFor(node).text) : editorFor(node).text;
12725
12996
  return this.cachedCopy(key, () => {
12726
12997
  return {
12727
12998
  text: span.group.copyText !== void 0 ? span.group.copyText(start, end) : span.lit.map((node) => {
12728
- const text = editorFor(node).text;
12999
+ const text = textOf(node);
12729
13000
  return text.slice(node === start.node ? start.offset : 0, node === end.node ? end.offset : text.length);
12730
13001
  }).join("\n"),
12731
13002
  html: span.group.copyHtml?.(start, end) ?? void 0
@@ -12882,6 +13153,13 @@ var UiEditingController = class {
12882
13153
  return new EditableLayout(state.editor, box, state, this.host.measurer);
12883
13154
  }
12884
13155
  };
13156
+ /**
13157
+ * The unit a word move or delete takes in a field: the whole text in a
13158
+ * secret one, whose word boundaries the caret stopping at would show.
13159
+ */
13160
+ function secretWordUnit(node) {
13161
+ return isSecret(node) ? "document" : "word";
13162
+ }
12885
13163
  function deleteInputType(unit, direction) {
12886
13164
  const side = direction < 0 ? "Backward" : "Forward";
12887
13165
  switch (unit) {
@@ -13985,6 +14263,8 @@ var LayoutRecord = class {
13985
14263
  * multiline, the overlay scrollbars.
13986
14264
  */
13987
14265
  scrollsText = false;
14266
+ /** A scroll container set to show no scrollbars (\`scrollbars: 'none'\`). */
14267
+ hidesScrollbars = false;
13988
14268
  /**
13989
14269
  * A multiline editable, which shows the overlay scrollbars a scroll
13990
14270
  * container does over its own text. A single-line field scrolls
@@ -14050,6 +14330,23 @@ var LayoutRecord = class {
14050
14330
  /** Constraints this record was last measured under. */
14051
14331
  lastConstraints = NEVER_MEASURED;
14052
14332
  /**
14333
+ * The paragraphs a text leaf was last measured as, one asked with a
14334
+ * width and one without; see `LayoutEngine.measureLeaf`. Flex asks
14335
+ * an item both questions every pass (its max-content basis, then its
14336
+ * final width), so one slot would be overwritten by the other every
14337
+ * time. Null until the node is measured as text that way.
14338
+ *
14339
+ * **Not reset.** These are the one thing a record carries from one
14340
+ * full pass into the next, on purpose: a full pass re-asks every
14341
+ * text leaf, and a memo that every pass cleared would answer
14342
+ * nothing a resize asks. They are safe to keep because they check
14343
+ * themselves — a memo is used only when every field of the request
14344
+ * it was laid out for matches and the measurer has not been
14345
+ * invalidated since — rather than trusting that nothing changed.
14346
+ */
14347
+ textBounded = null;
14348
+ textUnbounded = null;
14349
+ /**
14053
14350
  * The percentage base the current measurement resolved against, and
14054
14351
  * whether the node has a length that resolves against one.
14055
14352
  *
@@ -14165,6 +14462,20 @@ var LayoutRecord = class {
14165
14462
  propsBaseWidth = void 0;
14166
14463
  propsBaseHeight = void 0;
14167
14464
  /**
14465
+ * The node's `propertyVersion` when its layout properties were last
14466
+ * resolved, and whether that resolution read the node and nothing
14467
+ * else (no inherited direction, no scroll leader). Together with the
14468
+ * percentage base they let a later pass, a full one included, keep
14469
+ * what the last one resolved instead of reading forty properties
14470
+ * again; see `LayoutEngine.resolveLayoutProps`.
14471
+ *
14472
+ * **Not reset,** with the fields they vouch for: `reset()` leaves
14473
+ * every field `resolveLayoutProps` writes as it was, because these
14474
+ * say whether it is still right.
14475
+ */
14476
+ propsVersion = -1;
14477
+ propsFromNodeAlone = false;
14478
+ /**
14168
14479
  * Returns the record to the state a freshly constructed one is in,
14169
14480
  * so a full layout can reuse the object instead of replacing it.
14170
14481
  *
@@ -14178,7 +14489,9 @@ var LayoutRecord = class {
14178
14489
  * never reused across.
14179
14490
  *
14180
14491
  * **Every field above has to appear here, with the value it is
14181
- * declared with.** A field that is added and not reset would carry
14492
+ * declared with,** but the two text memos and the resolved layout
14493
+ * properties, which check themselves and are kept across passes on
14494
+ * purpose. A field that is added and not reset would carry
14182
14495
  * one pass's answer into the next, which is the kind of fault that
14183
14496
  * shows as a stale box on a screen and as nothing at all in a test
14184
14497
  * that lays out once. `LayoutRecord.spec.ts` compares a reset record
@@ -14193,28 +14506,6 @@ var LayoutRecord = class {
14193
14506
  this.measuredHeight = 0;
14194
14507
  this.outerWidth = 0;
14195
14508
  this.outerHeight = 0;
14196
- this.paddingLeft = 0;
14197
- this.paddingRight = 0;
14198
- this.paddingTop = 0;
14199
- this.paddingBottom = 0;
14200
- this.marginLeft = 0;
14201
- this.marginRight = 0;
14202
- this.marginTop = 0;
14203
- this.marginBottom = 0;
14204
- this.minWidth = 0;
14205
- this.maxWidth = Infinity;
14206
- this.minHeight = 0;
14207
- this.maxHeight = Infinity;
14208
- this.flexGrow = 0;
14209
- this.flexShrink = 1;
14210
- this.flexBasisZero = false;
14211
- this.minWidthAuto = true;
14212
- this.minHeightAuto = true;
14213
- this.marginLeftAuto = false;
14214
- this.marginRightAuto = false;
14215
- this.marginTopAuto = false;
14216
- this.marginBottomAuto = false;
14217
- this.aspectRatio = void 0;
14218
14509
  this.hasBaseline = false;
14219
14510
  this.baseline = 0;
14220
14511
  this.minContentWidth = 0;
@@ -14227,16 +14518,6 @@ var LayoutRecord = class {
14227
14518
  this.flexMin = 0;
14228
14519
  this.flexMax = Infinity;
14229
14520
  this.flexMinAuto = false;
14230
- this.positioned = false;
14231
- this.absolute = false;
14232
- this.top = void 0;
14233
- this.right = void 0;
14234
- this.bottom = void 0;
14235
- this.left = void 0;
14236
- this.zIndex = 0;
14237
- this.lifted = false;
14238
- this.liftBoundary = false;
14239
- this.clips = false;
14240
14521
  this.extentMinX = 0;
14241
14522
  this.extentMinY = 0;
14242
14523
  this.extentMaxX = 0;
@@ -14245,11 +14526,6 @@ var LayoutRecord = class {
14245
14526
  this.paintReachTop = 0;
14246
14527
  this.paintReachRight = 0;
14247
14528
  this.paintReachBottom = 0;
14248
- this.scrollable = false;
14249
- this.scrollsText = false;
14250
- this.textScrollbars = false;
14251
- this.mirrored = false;
14252
- this.sticky = false;
14253
14529
  this.stickyOffsetX = 0;
14254
14530
  this.stickyOffsetY = 0;
14255
14531
  this.scrollbarVisibleUntil = 0;
@@ -14268,8 +14544,6 @@ var LayoutRecord = class {
14268
14544
  this.lastConstraints = NEVER_MEASURED;
14269
14545
  this.lastBaseWidth = void 0;
14270
14546
  this.lastBaseHeight = void 0;
14271
- this.percentWidth = false;
14272
- this.percentHeight = false;
14273
14547
  this.alternates = null;
14274
14548
  this.contentMatters = true;
14275
14549
  this.relayoutBoundary = false;
@@ -14278,8 +14552,6 @@ var LayoutRecord = class {
14278
14552
  this.placeDirty = true;
14279
14553
  this.transformDirty = false;
14280
14554
  this.propsPass = 0;
14281
- this.propsBaseWidth = void 0;
14282
- this.propsBaseHeight = void 0;
14283
14555
  }
14284
14556
  };
14285
14557
  /** How many earlier measurements a record remembers besides the current one. */
@@ -15174,6 +15446,18 @@ var LayoutEngine = class {
15174
15446
  */
15175
15447
  stickyShifted = /* @__PURE__ */ new Set();
15176
15448
  textMeasurer;
15449
+ /** Where `measureLeaf` resolves a text node's font; read before anything else can resolve one. */
15450
+ fontScratch = {
15451
+ fontSize: 0,
15452
+ fontFamily: "",
15453
+ fontWeight: "normal",
15454
+ lineHeight: 0,
15455
+ letterSpacing: 0,
15456
+ fontStyle: "normal",
15457
+ fontStretch: "normal",
15458
+ fontVariant: "normal",
15459
+ fontKerning: "auto"
15460
+ };
15177
15461
  /**
15178
15462
  * The definite size percentages resolve against while a container's
15179
15463
  * children are measured: its content box. Undefined on an axis whose
@@ -17012,37 +17296,79 @@ var LayoutEngine = class {
17012
17296
  const paddingV = rec.paddingTop + rec.paddingBottom;
17013
17297
  const editable = node.type === "editable-text";
17014
17298
  if (node.type === "text" || node.type === "button" || editable) {
17015
- const typed = editable ? editorFor(node).text : "";
17299
+ const typed = editable ? shownTextOf(node, editorFor(node)) : "";
17016
17300
  const spans = editable ? editableSpansOf(node, typed, editorFor(node).composition) : resolvedSpansOf(node);
17017
17301
  const text = editable ? typed : spans.length > 0 ? textContentOf(node) : String(node.properties.get("text") ?? "");
17018
17302
  const placeholderProp = node.properties.get("placeholder");
17019
17303
  const placeholder = editable && text.length === 0 && typeof placeholderProp === "string" ? placeholderProp : void 0;
17020
- const font = resolveFont(node);
17304
+ const font = this.fontScratch;
17305
+ resolveFontInto(node, font);
17021
17306
  const maxLines = editable ? void 0 : this.numberProp(node, "maxLines");
17022
- const request = {
17023
- text,
17024
- fontSize: font.fontSize,
17025
- fontFamily: font.fontFamily,
17026
- fontWeight: font.fontWeight,
17027
- lineHeight: font.lineHeight,
17028
- letterSpacing: font.letterSpacing,
17029
- fontStyle: font.fontStyle,
17030
- fontStretch: font.fontStretch,
17031
- fontVariant: font.fontVariant,
17032
- fontKerning: font.fontKerning,
17033
- maxWidth: isFinite(effective.maxWidth) ? Math.max(0, effective.maxWidth - paddingH) : void 0,
17034
- wrap: this.textWrapProp(node),
17035
- maxLines: maxLines !== void 0 && maxLines >= 1 ? Math.floor(maxLines) : void 0,
17036
- overflow: editable ? "clip" : this.textOverflowProp(node),
17037
- spans: spans.length > 0 ? spans : void 0
17038
- };
17039
- const paragraph = this.textMeasurer.layout(request);
17307
+ const maxWidth = isFinite(effective.maxWidth) ? Math.max(0, effective.maxWidth - paddingH) : void 0;
17308
+ const exactWidth = isFinite(effective.maxWidth) && effective.minWidth === effective.maxWidth;
17309
+ const wrap = this.textWrapProp(node);
17310
+ const lines = maxLines !== void 0 && maxLines >= 1 ? Math.floor(maxLines) : void 0;
17311
+ const overflow = editable ? "clip" : this.textOverflowProp(node);
17312
+ const runs = spans.length > 0 ? spans : void 0;
17313
+ /**
17314
+ * The node's own last paragraph, when nothing it was laid out
17315
+ * from has changed.
17316
+ *
17317
+ * Flex asks a text leaf three or four questions a pass (its
17318
+ * basis, its min- and max-content sizes, its final size), and
17319
+ * each one went to the measurer as a new request, whose cache
17320
+ * key spells out every field and the whole text before it can
17321
+ * look anything up. That was a fifth of a full layout pass over
17322
+ * the benchmark list with every answer already cached. Compared
17323
+ * field by field here it is a handful of identity checks, and a
17324
+ * paragraph that fits at the width it was laid out at also
17325
+ * answers any wider question, which is most of the questions a
17326
+ * row of labels is asked.
17327
+ */
17328
+ const generation = this.textMeasurer.generation;
17329
+ const memo = maxWidth === void 0 ? rec.textUnbounded : rec.textBounded;
17330
+ const last = memo?.request;
17331
+ let paragraph;
17332
+ let asked;
17333
+ if (memo !== null && last !== void 0 && generation !== void 0 && memo.generation === generation && last.text === text && last.fontSize === font.fontSize && last.fontFamily === font.fontFamily && last.fontWeight === font.fontWeight && last.lineHeight === font.lineHeight && last.letterSpacing === font.letterSpacing && last.fontStyle === font.fontStyle && last.fontStretch === font.fontStretch && last.fontVariant === font.fontVariant && last.fontKerning === font.fontKerning && last.wrap === wrap && last.maxLines === lines && last.overflow === overflow && last.spans === runs && (last.maxWidth === maxWidth && last.exactWidth === exactWidth || memo.fitsAnyWidth && fitsWidth(memo.paragraph, maxWidth))) {
17334
+ paragraph = memo.paragraph;
17335
+ asked = last;
17336
+ } else {
17337
+ const request = {
17338
+ text,
17339
+ fontSize: font.fontSize,
17340
+ fontFamily: font.fontFamily,
17341
+ fontWeight: font.fontWeight,
17342
+ lineHeight: font.lineHeight,
17343
+ letterSpacing: font.letterSpacing,
17344
+ fontStyle: font.fontStyle,
17345
+ fontStretch: font.fontStretch,
17346
+ fontVariant: font.fontVariant,
17347
+ fontKerning: font.fontKerning,
17348
+ maxWidth,
17349
+ exactWidth,
17350
+ wrap,
17351
+ maxLines: lines,
17352
+ overflow,
17353
+ spans: runs
17354
+ };
17355
+ paragraph = this.textMeasurer.layout(request);
17356
+ asked = request;
17357
+ const fresh = {
17358
+ request,
17359
+ paragraph,
17360
+ generation: generation ?? -1,
17361
+ fitsAnyWidth: layoutFitsAnyWidth(request, paragraph)
17362
+ };
17363
+ if (maxWidth === void 0) rec.textUnbounded = fresh;
17364
+ else rec.textBounded = fresh;
17365
+ }
17040
17366
  let width = paragraph.width;
17041
17367
  let minContentWidth = paragraph.minContentWidth;
17042
17368
  let maxContentWidth = paragraph.maxContentWidth;
17043
17369
  if (placeholder !== void 0 && placeholder.length > 0) {
17044
17370
  const hint = this.textMeasurer.layout({
17045
- ...request,
17371
+ ...asked,
17046
17372
  text: placeholder
17047
17373
  });
17048
17374
  width = Math.max(width, hint.width);
@@ -18225,7 +18551,28 @@ var LayoutEngine = class {
18225
18551
  const props = node.properties;
18226
18552
  const base = this.percentBase;
18227
18553
  if (rec.propsPass === this.layoutPass && rec.propsBaseWidth === base.width && rec.propsBaseHeight === base.height) return;
18554
+ /**
18555
+ * What an earlier pass resolved, when none of what it read has
18556
+ * changed: the node's own properties (`propertyVersion`), and the
18557
+ * percentage base only on an axis that has a percentage.
18558
+ *
18559
+ * Each pass used to read every property below again for every node
18560
+ * it reached, and a full pass reaches all of them: a sixth of a full
18561
+ * pass over the benchmark list, where nothing had changed since the
18562
+ * last. A node whose resolution read anything inherited (a logical
18563
+ * edge or a scroll container's direction) or another node (a scroll
18564
+ * leader) is resolved again every pass, as it always was; they are
18565
+ * few. The record keeps these fields across `reset()` for this.
18566
+ */
18567
+ if (rec.propsFromNodeAlone && rec.propsVersion === node.propertyVersion && (!rec.percentWidth || rec.propsBaseWidth === base.width) && (!rec.percentHeight || rec.propsBaseHeight === base.height)) {
18568
+ rec.propsPass = this.layoutPass;
18569
+ rec.propsBaseWidth = base.width;
18570
+ rec.propsBaseHeight = base.height;
18571
+ if (rec.lifted) this.liftedNodes.add(node);
18572
+ return;
18573
+ }
18228
18574
  rec.propsPass = this.layoutPass;
18575
+ rec.propsVersion = node.propertyVersion;
18229
18576
  rec.propsBaseWidth = base.width;
18230
18577
  rec.propsBaseHeight = base.height;
18231
18578
  rec.percentWidth = hasPercentLength(props, WIDTH_RELATIVE);
@@ -18266,6 +18613,7 @@ var LayoutEngine = class {
18266
18613
  const overflow = props.get("overflow");
18267
18614
  rec.scrollable = node.type === "scroll-view" || overflow === "scroll" || overflow === "auto" || overflow === "hidden" && scrollLeaderOf(node) !== null;
18268
18615
  rec.scrollsText = node.type === "editable-text";
18616
+ rec.hidesScrollbars = props.get("scrollbars") === "none";
18269
18617
  rec.textScrollbars = rec.scrollsText && props.get("multiline") === true;
18270
18618
  rec.mirrored = (rec.scrollable || rec.textScrollbars) && this.startEdge(node) === "right";
18271
18619
  rec.clips = rec.scrollable || overflow === "hidden" || node.type === "editable-text";
@@ -18277,9 +18625,11 @@ var LayoutEngine = class {
18277
18625
  rec.left = this.lengthProp(node, "left", base.width) ?? insetH;
18278
18626
  rec.zIndex = this.numberProp(node, "zIndex") ?? 0;
18279
18627
  this.setLifted(node, rec, node.properties.get("lift") === true);
18628
+ if (rec.lifted) this.liftedNodes.add(node);
18280
18629
  rec.liftBoundary = node.properties.get("liftBoundary") === true;
18281
18630
  const ratio = this.numberProp(node, "aspectRatio");
18282
18631
  rec.aspectRatio = ratio !== void 0 && ratio > 0 ? ratio : void 0;
18632
+ rec.propsFromNodeAlone = !hasLogicalSpacing(props) && !rec.scrollable && !rec.textScrollbars && overflow !== "hidden";
18283
18633
  }
18284
18634
  /**
18285
18635
  * Sizes with a fixed width:height ratio (CSS `aspect-ratio`). The
@@ -20441,15 +20791,10 @@ var ScrollLayerCache = class {
20441
20791
  * is how every one of those ends.
20442
20792
  */
20443
20793
  refused = /* @__PURE__ */ new Set();
20444
- geometryVersion = NaN;
20445
20794
  dpr = 0;
20446
20795
  surfaceWidth = 0;
20447
20796
  surfaceHeight = 0;
20448
20797
  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
20798
  /** Consecutive frames each container without a layer has scrolled quietly. See `SCROLL_LAYER_SETTLE_FRAMES`. */
20454
20799
  streaks = /* @__PURE__ */ new WeakMap();
20455
20800
  /**
@@ -20481,46 +20826,26 @@ var ScrollLayerCache = class {
20481
20826
  /**
20482
20827
  * Settles which layers this frame's changes leave standing, before
20483
20828
  * anything is drawn. Every rule here drops; none keeps. A frame the
20484
- * caller could not vouch for drops them all.
20829
+ * caller could not vouch for drops them all. What changed inside a
20830
+ * container is not settled here but drawn over: see \`damage\`.
20485
20831
  */
20486
20832
  beginFrame(changes, dpr, surfaceWidth, surfaceHeight) {
20487
20833
  const interrupted = this.open;
20488
20834
  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) {
20835
+ if (interrupted || changes === void 0 || dpr !== this.dpr || surfaceWidth !== this.surfaceWidth || surfaceHeight !== this.surfaceHeight) {
20499
20836
  this.dropAll();
20500
20837
  this.dpr = dpr;
20501
20838
  this.surfaceWidth = surfaceWidth;
20502
20839
  this.surfaceHeight = surfaceHeight;
20503
- this.geometryVersion = changes?.geometryVersion ?? NaN;
20504
20840
  this.frame = changes?.frame ?? null;
20505
20841
  return;
20506
20842
  }
20507
20843
  const frame = changes.frame;
20508
20844
  this.frame = frame;
20509
- if (this.layers.size === 0 && this.refused.size === 0) return;
20845
+ for (const node of this.layers.keys()) if (dirtyAbove(frame, node)) this.forget(node);
20510
20846
  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
- }
20847
+ if (flags === 32) continue;
20848
+ for (const refused of this.refused) if (node === refused || contains(node, refused) || contains(refused, node)) this.refused.delete(refused);
20524
20849
  }
20525
20850
  }
20526
20851
  /** Drops the layers the frame did not composite: their container was culled, removed, or drawn directly. */
@@ -20534,14 +20859,16 @@ var ScrollLayerCache = class {
20534
20859
  }
20535
20860
  /**
20536
20861
  * 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.
20862
+ * scrolled on the last few frames running with nothing above it
20863
+ * changing, which is the evidence a layer would be kept, and it has
20864
+ * not just shown it holds something a layer cannot. What changes
20865
+ * inside it is absorbed item by item (see \`damage\`), so a lazy list
20866
+ * mounting rows as it scrolls keeps its layer.
20540
20867
  */
20541
20868
  wants(node) {
20542
20869
  const frame = this.frame;
20543
20870
  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)) {
20871
+ if (dirtyAbove(frame, node)) {
20545
20872
  this.streaks.delete(node);
20546
20873
  return false;
20547
20874
  }
@@ -20571,9 +20898,10 @@ var ScrollLayerCache = class {
20571
20898
  offsetX: placement.offsetX,
20572
20899
  offsetY: placement.offsetY,
20573
20900
  dpr: this.dpr,
20574
- scrollX: rec.scrollX,
20575
- scrollY: rec.scrollY,
20576
- used: true
20901
+ scrollX: snapScroll(rec.scrollX, this.dpr),
20902
+ scrollY: snapScroll(rec.scrollY, this.dpr),
20903
+ used: true,
20904
+ items: /* @__PURE__ */ new Map()
20577
20905
  };
20578
20906
  this.layers.set(node, layer);
20579
20907
  this.stats.built++;
@@ -20594,8 +20922,8 @@ var ScrollLayerCache = class {
20594
20922
  * device pixels, which a copy cannot follow without resampling.
20595
20923
  */
20596
20924
  shift(layer, rec) {
20597
- const exactX = (layer.scrollX - rec.scrollX) * layer.dpr;
20598
- const exactY = (layer.scrollY - rec.scrollY) * layer.dpr;
20925
+ const exactX = (layer.scrollX - snapScroll(rec.scrollX, layer.dpr)) * layer.dpr;
20926
+ const exactY = (layer.scrollY - snapScroll(rec.scrollY, layer.dpr)) * layer.dpr;
20599
20927
  const dx = Math.round(exactX);
20600
20928
  const dy = Math.round(exactY);
20601
20929
  if (Math.abs(exactX - dx) > EPSILON || Math.abs(exactY - dy) > EPSILON) return null;
@@ -20633,6 +20961,79 @@ var ScrollLayerCache = class {
20633
20961
  });
20634
20962
  return strips;
20635
20963
  }
20964
+ /**
20965
+ * The parts of a layer this frame's changes inside the container left
20966
+ * wrong, in the layer's pixels, or \`'all'\` when that is most of it.
20967
+ * Called after \`shift\`, so the layer's offset is the frame's.
20968
+ *
20969
+ * Judged item by item, an item being one of the container's children
20970
+ * (through any fragments): a lazy list's row, say. An item added,
20971
+ * removed or moved is wrong where it was and where it is; an item with
20972
+ * anything changed inside it is wrong where it is. Whatever none of
20973
+ * that touches is right already, which in a lazy list scrolling is
20974
+ * everything on screen: the rows it mounts and unmounts are in its
20975
+ * overscan, and the spacers that stand for the rest only grow and
20976
+ * shrink off the top and bottom.
20977
+ */
20978
+ damage(layer, container, layout) {
20979
+ const frame = this.frame;
20980
+ if (frame === null) return "all";
20981
+ const boxes = [];
20982
+ for (const [node, flags] of frame.entries()) {
20983
+ if (node === container) {
20984
+ if ((flags & 128) !== 0) return "all";
20985
+ continue;
20986
+ }
20987
+ const item = itemHolding(container, node);
20988
+ if (item === null) continue;
20989
+ const was = layer.items.get(item);
20990
+ const now = boxOf(layout, item);
20991
+ if (was !== void 0) boxes.push(was);
20992
+ if (now !== void 0 && (was === void 0 || !sameBox(was, now))) boxes.push(now);
20993
+ }
20994
+ const seen = /* @__PURE__ */ new Set();
20995
+ for (const item of itemsOf(container)) {
20996
+ seen.add(item);
20997
+ const now = boxOf(layout, item);
20998
+ const was = layer.items.get(item);
20999
+ if (now === void 0) continue;
21000
+ if (was === void 0) boxes.push(now);
21001
+ else if (!sameBox(was, now)) boxes.push(was, now);
21002
+ }
21003
+ for (const [item, was] of layer.items) if (!seen.has(item)) boxes.push(was);
21004
+ const rects = [];
21005
+ let area = 0;
21006
+ for (const box of boxes) {
21007
+ const rect = this.toLayer(layer, box);
21008
+ if (rect !== null && !rects.some((other) => sameBox(other, rect))) {
21009
+ rects.push(rect);
21010
+ area += rect.width * rect.height;
21011
+ }
21012
+ }
21013
+ return area > layer.width * layer.height * MOSTLY ? "all" : rects;
21014
+ }
21015
+ /** Notes where the container's items are, for the next frame's \`damage\`. */
21016
+ remember(layer, container, layout) {
21017
+ layer.items.clear();
21018
+ for (const item of itemsOf(container)) {
21019
+ const box = boxOf(layout, item);
21020
+ if (box !== void 0) layer.items.set(item, box);
21021
+ }
21022
+ }
21023
+ /** An item's box in the layer's pixels, widened for what paints past it, or null off the layer. */
21024
+ toLayer(layer, box) {
21025
+ const dpr = layer.dpr;
21026
+ const left = Math.max(0, Math.floor(layer.offsetX + dpr * (box.x - layer.scrollX - 8)));
21027
+ const top = Math.max(0, Math.floor(layer.offsetY + dpr * (box.y - layer.scrollY - 8)));
21028
+ const right = Math.min(layer.width, Math.ceil(layer.offsetX + dpr * (box.x + box.width - layer.scrollX + 8)));
21029
+ const bottom = Math.min(layer.height, Math.ceil(layer.offsetY + dpr * (box.y + box.height - layer.scrollY + 8)));
21030
+ return right > left && bottom > top ? {
21031
+ x: left,
21032
+ y: top,
21033
+ width: right - left,
21034
+ height: bottom - top
21035
+ } : null;
21036
+ }
20636
21037
  /** Stops building layers for a container until its subtree is next dirtied. */
20637
21038
  refuse(node) {
20638
21039
  this.drop(node);
@@ -20655,14 +21056,63 @@ var ScrollLayerCache = class {
20655
21056
  this.refused.delete(node);
20656
21057
  }
20657
21058
  };
20658
- /** Whether an ancestor was dirtied by more than its own scroll: inherited paint reaches the subtree without saying so. */
21059
+ /**
21060
+ * Whether an ancestor's properties or environment changed. An inherited
21061
+ * value set on an ancestor marks every descendant that reads it as well
21062
+ * (the graph re-provides it), so what it changes inside the container
21063
+ * is damage like any other; this keeps the old caution for the rest.
21064
+ * An ancestor marked for paint alone is the runtime asking for a frame
21065
+ * (a scrollbar fading, an inspector outline), which nothing inside the
21066
+ * container hears; one laid out again either leaves the container where
21067
+ * it was or moves it, which \`fits\` hears about; one whose children
21068
+ * changed has changed nothing inside this one. The last two happen on
21069
+ * every frame a lazy list mounts a row, since a layout need is marked
21070
+ * all the way up.
21071
+ */
20659
21072
  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
- }
21073
+ for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) if ((frame.dirtyFlagsFor(ancestor) & INHERITED) !== 0) return true;
21074
+ return false;
21075
+ }
21076
+ /** What an ancestor's dirt can say about how its descendants look. */
21077
+ const INHERITED = 192;
21078
+ /** Whether \`ancestor\` is above \`node\` in the tree. */
21079
+ function contains(ancestor, node) {
21080
+ for (let current = node.parent; current !== null; current = current.parent) if (current === ancestor) return true;
20664
21081
  return false;
20665
21082
  }
21083
+ /** A container's items: its children, and the children of any fragment among them, in order. */
21084
+ function itemsOf(container) {
21085
+ const items = [];
21086
+ const visit = (parent) => {
21087
+ for (let child = parent.firstChild; child !== null; child = child.nextSibling) if (child.type === "fragment") visit(child);
21088
+ else items.push(child);
21089
+ };
21090
+ visit(container);
21091
+ return items;
21092
+ }
21093
+ /** 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. */
21094
+ function itemHolding(container, node) {
21095
+ let item = null;
21096
+ for (let current = node; current !== null; current = current.parent) {
21097
+ if (current === container) return item;
21098
+ if (current.type !== "fragment") item = current;
21099
+ }
21100
+ return null;
21101
+ }
21102
+ function sameBox(a, b) {
21103
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
21104
+ }
21105
+ function boxOf(layout, node) {
21106
+ const rec = layout.recordFor(node);
21107
+ return rec === void 0 ? void 0 : {
21108
+ x: rec.x,
21109
+ y: rec.y,
21110
+ width: rec.width,
21111
+ height: rec.height
21112
+ };
21113
+ }
21114
+ /** Damage over this share of a layer repaints all of it in one walk. */
21115
+ const MOSTLY = .5;
20666
21116
  /**
20667
21117
  * Where a scroll container's viewport lands in device pixels, or null
20668
21118
  * when a layer could not reproduce the direct draw exactly: no
@@ -20708,6 +21158,273 @@ function clearLayer(ctx, width, height) {
20708
21158
  ctx.clearRect(0, 0, width, height);
20709
21159
  }
20710
21160
  //#endregion
21161
+ //#region src/rendering/canvas2d/TextRasterCache.ts
21162
+ /**
21163
+ * Text kept as pixels, so a frame that only moved it copies it rather
21164
+ * than drawing its glyphs again.
21165
+ *
21166
+ * **Why.** Drawing glyphs is most of what a frame of text costs:
21167
+ * profiled while flicking through a file in Easel, `fillText` was 1,064
21168
+ * of the renderer's 1,985 ms. A scroll moves every line on screen and
21169
+ * changes none of them, so every one of those glyphs was drawn again
21170
+ * only to land a few pixels from where it was. In WKWebView a frame of
21171
+ * that cost 4 to 7 ms, which is more than a 165 Hz refresh holds.
21172
+ *
21173
+ * **What is kept.** What `drawText` draws for one node, drawn once into
21174
+ * a canvas of its own at the device scale and fraction of a pixel it
21175
+ * lands on, and copied onto the frame from then on. The copy lands on
21176
+ * whole device pixels, so the pixels are the ones a direct draw at that
21177
+ * fraction would have made, and a scroll, which moves by whole device
21178
+ * pixels, keeps the fraction.
21179
+ *
21180
+ * **When it is drawn again.** When anything `drawText` reads changes:
21181
+ * the text, its runs, its font, its colour, its box, the scale, or the
21182
+ * fraction of a pixel it sits at. Each is compared every frame, which is
21183
+ * a few comparisons; a run list or a colour that has not changed is the
21184
+ * same object it was.
21185
+ *
21186
+ * **What it is not for.** Text that changes on most frames (a clock, a
21187
+ * counter, an animated colour) would be drawn twice a frame, into the
21188
+ * cache and out of it. A node redrawn on most of its recent frames is
21189
+ * drawn directly instead, until it has been still for a while.
21190
+ */
21191
+ var TextRasterCache = class {
21192
+ createCanvas;
21193
+ entries = /* @__PURE__ */ new Map();
21194
+ frame = 0;
21195
+ pixels = 0;
21196
+ /** Draws served from the cache and drawn into it, since the last reset. For budget specs. */
21197
+ stats = {
21198
+ copied: 0,
21199
+ drawn: 0,
21200
+ direct: 0
21201
+ };
21202
+ constructor(createCanvas) {
21203
+ this.createCanvas = createCanvas;
21204
+ }
21205
+ beginFrame() {
21206
+ this.frame++;
21207
+ }
21208
+ /** Lets go of text not drawn lately: removed, scrolled far away, or hidden. */
21209
+ endFrame() {
21210
+ for (const [node, entry] of this.entries) if (this.frame - entry.used > IDLE_FRAMES) this.drop(node, entry);
21211
+ }
21212
+ dropAll() {
21213
+ for (const [node, entry] of this.entries) this.drop(node, entry);
21214
+ }
21215
+ resetStats() {
21216
+ this.stats.copied = 0;
21217
+ this.stats.drawn = 0;
21218
+ this.stats.direct = 0;
21219
+ }
21220
+ /**
21221
+ * Draws a node's text from the cache, drawing it into the cache first
21222
+ * when what it holds is out of date. False when the text should be
21223
+ * drawn directly: a context that cannot say where it draws, one that
21224
+ * rotates or skews, text too large to keep, or text that changes too
21225
+ * often to be worth keeping.
21226
+ */
21227
+ draw(ctx, node, box, paint, measurer) {
21228
+ const m = ctx.getTransform?.();
21229
+ if (m === void 0 || m.b !== 0 || m.c !== 0 || !(m.a > 0) || !(m.d > 0)) return false;
21230
+ let entry = this.entries.get(node);
21231
+ if (entry !== void 0 && entry.directUntil > this.frame) {
21232
+ entry.used = this.frame;
21233
+ this.stats.direct++;
21234
+ return false;
21235
+ }
21236
+ let extent = entry !== void 0 && entry.drawnFrom(paint, box, m.a, m.d) ? entry.extent : null;
21237
+ if (extent === null) {
21238
+ extent = inkExtent(box, paint, measurer);
21239
+ if (extent === null) return false;
21240
+ }
21241
+ const left = m.a * (box.x + extent.left) + m.e;
21242
+ const top = m.d * (box.y + extent.top) + m.f;
21243
+ const x = Math.floor(left);
21244
+ const y = Math.floor(top);
21245
+ const width = Math.ceil(m.a * (extent.right - extent.left) + (left - x)) + 1;
21246
+ const height = Math.ceil(m.d * (extent.bottom - extent.top) + (top - y)) + 1;
21247
+ if (width * height > MAX_ENTRY_PIXELS) return false;
21248
+ const offsetX = m.e - x;
21249
+ const offsetY = m.f - y;
21250
+ let raster = entry ?? null;
21251
+ if (raster === null || !raster.holds(paint, box, m.a, m.d, offsetX, offsetY, width, height)) {
21252
+ raster = this.redraw(node, entry, paint, box, measurer, m.a, m.d, offsetX, offsetY, width, height, extent);
21253
+ if (raster === null) return false;
21254
+ }
21255
+ raster.used = this.frame;
21256
+ ctx.save();
21257
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
21258
+ ctx.drawImage(raster.canvas, x, y, raster.canvasWidth, raster.canvasHeight);
21259
+ ctx.restore();
21260
+ this.stats.copied++;
21261
+ return true;
21262
+ }
21263
+ redraw(node, previous, paint, box, measurer, scaleX, scaleY, offsetX, offsetY, width, height, extent) {
21264
+ let entry = previous;
21265
+ if (entry !== void 0) {
21266
+ entry.churn = this.frame - entry.drawnAt <= CHURN_FRAMES ? entry.churn + 1 : 0;
21267
+ if (entry.churn >= CHURN_LIMIT) {
21268
+ entry.churn = 0;
21269
+ entry.directUntil = this.frame + DIRECT_FRAMES;
21270
+ entry.used = this.frame;
21271
+ this.stats.direct++;
21272
+ return null;
21273
+ }
21274
+ }
21275
+ if (entry === void 0 || entry.canvasWidth < width || entry.canvasHeight < height) {
21276
+ const canvasWidth = Math.max(width, entry?.canvasWidth ?? 0);
21277
+ const canvasHeight = Math.max(height, entry?.canvasHeight ?? 0);
21278
+ const canvas = this.createCanvas(canvasWidth, canvasHeight);
21279
+ const context = canvas?.getContext("2d");
21280
+ if (canvas === null || !context) return null;
21281
+ this.make(canvasWidth * canvasHeight);
21282
+ if (entry !== void 0) {
21283
+ this.pixels -= entry.canvasWidth * entry.canvasHeight;
21284
+ entry.canvas = canvas;
21285
+ entry.context = context;
21286
+ entry.canvasWidth = canvasWidth;
21287
+ entry.canvasHeight = canvasHeight;
21288
+ } else {
21289
+ entry = new TextRaster(canvas, context, canvasWidth, canvasHeight);
21290
+ this.entries.set(node, entry);
21291
+ }
21292
+ this.pixels += canvasWidth * canvasHeight;
21293
+ }
21294
+ const context = entry.context;
21295
+ context.setTransform(1, 0, 0, 1, 0, 0);
21296
+ context.clearRect(0, 0, entry.canvasWidth, entry.canvasHeight);
21297
+ context.setTransform(scaleX, 0, 0, scaleY, offsetX, offsetY);
21298
+ context.globalAlpha = 1;
21299
+ drawText(context, box, paint, measurer);
21300
+ entry.remember(paint, box, scaleX, scaleY, offsetX, offsetY, width, height);
21301
+ entry.extent = extent;
21302
+ entry.drawnAt = this.frame;
21303
+ this.stats.drawn++;
21304
+ return entry;
21305
+ }
21306
+ /** Makes room for `needed` more pixels, letting go of what was drawn least recently. */
21307
+ make(needed) {
21308
+ if (this.pixels + needed <= MAX_PIXELS) return;
21309
+ const byAge = [...this.entries].sort((a, b) => a[1].used - b[1].used);
21310
+ for (const [node, entry] of byAge) {
21311
+ if (this.pixels + needed <= MAX_PIXELS || entry.used === this.frame) break;
21312
+ this.drop(node, entry);
21313
+ }
21314
+ }
21315
+ drop(node, entry) {
21316
+ this.entries.delete(node);
21317
+ this.pixels -= entry.canvasWidth * entry.canvasHeight;
21318
+ entry.canvas.width = 0;
21319
+ entry.canvas.height = 0;
21320
+ }
21321
+ };
21322
+ /** One node's text as pixels, and what it was drawn from. */
21323
+ var TextRaster = class {
21324
+ canvas;
21325
+ context;
21326
+ canvasWidth;
21327
+ canvasHeight;
21328
+ used = 0;
21329
+ drawnAt = 0;
21330
+ churn = 0;
21331
+ directUntil = 0;
21332
+ /** Where the ink reaches, from the box's corner, in layout pixels. */
21333
+ extent = null;
21334
+ text;
21335
+ spans;
21336
+ font = "";
21337
+ color = "";
21338
+ style = "";
21339
+ linkHover = 0;
21340
+ boxWidth = 0;
21341
+ boxHeight = 0;
21342
+ scaleX = 0;
21343
+ scaleY = 0;
21344
+ offsetX = 0;
21345
+ offsetY = 0;
21346
+ width = 0;
21347
+ height = 0;
21348
+ constructor(canvas, context, canvasWidth, canvasHeight) {
21349
+ this.canvas = canvas;
21350
+ this.context = context;
21351
+ this.canvasWidth = canvasWidth;
21352
+ this.canvasHeight = canvasHeight;
21353
+ }
21354
+ holds(paint, box, scaleX, scaleY, offsetX, offsetY, width, height) {
21355
+ 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;
21356
+ }
21357
+ /** Whether the pixels are of this text, in this style, at this size and scale, wherever they are. */
21358
+ drawnFrom(paint, box, scaleX, scaleY) {
21359
+ 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);
21360
+ }
21361
+ remember(paint, box, scaleX, scaleY, offsetX, offsetY, width, height) {
21362
+ this.text = paint.text;
21363
+ this.spans = paint.spans;
21364
+ this.linkHover = paint.linkHover;
21365
+ this.boxWidth = box.width;
21366
+ this.boxHeight = box.height;
21367
+ this.scaleX = scaleX;
21368
+ this.scaleY = scaleY;
21369
+ this.offsetX = offsetX;
21370
+ this.offsetY = offsetY;
21371
+ this.width = width;
21372
+ this.height = height;
21373
+ this.font = fontOf(paint);
21374
+ this.color = colorToCss(paint.textColor);
21375
+ this.style = styleOf(paint);
21376
+ }
21377
+ };
21378
+ /**
21379
+ * The lines' extent, and a margin for what glyphs reach past their line
21380
+ * box (an italic's lean, a descender below a tight line height, an
21381
+ * underline). Null for text that draws nothing.
21382
+ */
21383
+ function inkExtent(box, paint, measurer) {
21384
+ const placements = layoutTextLines(box, paint, measurer);
21385
+ if (placements.length === 0) return null;
21386
+ let left = Infinity;
21387
+ let top = Infinity;
21388
+ let right = -Infinity;
21389
+ let bottom = -Infinity;
21390
+ for (const line of placements) {
21391
+ left = Math.min(left, line.x);
21392
+ top = Math.min(top, line.y);
21393
+ right = Math.max(right, line.x + line.width);
21394
+ bottom = Math.max(bottom, line.y + line.height);
21395
+ }
21396
+ const margin = Math.ceil(paint.fontSize * INK_MARGIN) + 2;
21397
+ return {
21398
+ left: left - box.x - margin,
21399
+ top: top - box.y - margin,
21400
+ right: right - box.x + margin,
21401
+ bottom: bottom - box.y + margin
21402
+ };
21403
+ }
21404
+ function fontOf(paint) {
21405
+ return `${paint.fontStyle} ${paint.fontVariant} ${paint.fontWeight} ${paint.fontStretch} ${paint.fontSize}/${paint.lineHeight} ${paint.fontFamily}`;
21406
+ }
21407
+ /** Everything else `drawText` reads that places or decorates the glyphs. */
21408
+ function styleOf(paint) {
21409
+ return `${paint.letterSpacing}|${paint.fontKerning}|${paint.textDecoration}|${paint.textAlign}|${paint.verticalAlign}|${paint.textWrap}|${paint.maxLines}|${paint.textOverflow}|${paint.rtl}`;
21410
+ }
21411
+ /** How far past its line box, as a share of the font size, a glyph's ink may reach. */
21412
+ const INK_MARGIN = .5;
21413
+ /** Within this much of a device pixel, text is at the same place. */
21414
+ const SAME_PLACE = 1 / 256;
21415
+ /** The largest single piece of text kept: a few screens of a long paragraph. */
21416
+ const MAX_ENTRY_PIXELS = 4e6;
21417
+ /** Everything kept, at most: about 128 MB. */
21418
+ const MAX_PIXELS = 32e6;
21419
+ /** Frames text is kept for without being drawn: about two seconds at 60 Hz. */
21420
+ const IDLE_FRAMES = 120;
21421
+ /** Drawn again within this many frames of the last time counts toward churn. */
21422
+ const CHURN_FRAMES = 2;
21423
+ /** Times in a row text may churn before it is drawn directly. */
21424
+ const CHURN_LIMIT = 3;
21425
+ /** Frames churning text is drawn directly for before the cache tries it again. */
21426
+ const DIRECT_FRAMES = 60;
21427
+ //#endregion
20711
21428
  //#region src/rendering/canvas2d/Canvas2DRenderer.ts
20712
21429
  /**
20713
21430
  * Canvas2D rendering backend.
@@ -20819,6 +21536,13 @@ var Canvas2DRenderer = class {
20819
21536
  constructor(options) {
20820
21537
  this.options = options;
20821
21538
  this.layers = new ScrollLayerCache(options.createLayerCanvas ?? offscreenLayerCanvas, options.scrollLayerSettleFrames);
21539
+ this.textCache = options.textCache === false ? null : new TextRasterCache(options.createLayerCanvas ?? offscreenLayerCanvas);
21540
+ }
21541
+ /** Text kept as pixels; see `TextRasterCache.ts`. */
21542
+ textCache;
21543
+ /** What the text cache did since the last reset, or null with it off. For budget specs. */
21544
+ get textCacheStats() {
21545
+ return this.textCache?.stats ?? null;
20822
21546
  }
20823
21547
  /** What the scroll layers did since the last reset. For budget specs. */
20824
21548
  get scrollLayerStats() {
@@ -20844,11 +21568,13 @@ var Canvas2DRenderer = class {
20844
21568
  /** A restored scroll layer reads as healthy and holds nothing. */
20845
21569
  surfaceRestored() {
20846
21570
  this.layers.dropAll();
21571
+ this.textCache?.dropAll();
20847
21572
  }
20848
21573
  dispose() {
20849
21574
  this.disposed = true;
20850
21575
  this.scaledImages.dispose();
20851
21576
  this.layers.dropAll();
21577
+ this.textCache?.dropAll();
20852
21578
  }
20853
21579
  render(root, context) {
20854
21580
  const raw = this.surface.getContext2D();
@@ -20862,11 +21588,13 @@ var Canvas2DRenderer = class {
20862
21588
  this.liftedPass.length = 0;
20863
21589
  this.paintOwner = null;
20864
21590
  this.layerDepth = 0;
21591
+ this.textCache?.beginFrame();
20865
21592
  this.layers.beginFrame(this.options.scrollLayers === false ? void 0 : context.changes, this.surface.dpr, this.surface.physicalWidth, this.surface.physicalHeight);
20866
21593
  this.renderNode(root, context, ctx, true, false);
20867
21594
  this.renderLifted(context, ctx);
20868
21595
  if (context.overlay !== void 0 && context.overlay.length > 0) drawOverlayShapes(ctx, context.overlay);
20869
21596
  this.layers.endFrame();
21597
+ this.textCache?.endFrame();
20870
21598
  }
20871
21599
  /**
20872
21600
  * Folds a node's style into the shared scratch and records that it
@@ -20931,7 +21659,7 @@ var Canvas2DRenderer = class {
20931
21659
  if (rec.clips) this.popCull();
20932
21660
  if (hasText) {
20933
21661
  if (this.paintOwner !== node) this.resolvePaint(node);
20934
- this.paintContent(ctx, rec, this.paint, context);
21662
+ this.paintContent(ctx, node, rec, this.paint, context);
20935
21663
  }
20936
21664
  if (rec.liftBoundary && this.liftedPass.length > liftedBefore) this.renderLifted(context, ctx, liftedBefore, node);
20937
21665
  if (rec.clips) ctx.restore();
@@ -21205,7 +21933,7 @@ var Canvas2DRenderer = class {
21205
21933
  } else ctx.strokeRect(x, y, width, height);
21206
21934
  }
21207
21935
  }
21208
- paintContent(ctx, rec, paint, context) {
21936
+ paintContent(ctx, node, rec, paint, context) {
21209
21937
  if (paint.text !== void 0) {
21210
21938
  this.contentBox.x = rec.x + rec.paddingLeft - rec.scrollX;
21211
21939
  this.contentBox.y = rec.y + rec.paddingTop - rec.scrollY;
@@ -21219,6 +21947,7 @@ var Canvas2DRenderer = class {
21219
21947
  this.paintHighlightedText(ctx, paint, context);
21220
21948
  return;
21221
21949
  }
21950
+ if (this.textCache?.draw(ctx, node, this.contentBox, paint, context.text) === true) return;
21222
21951
  drawText(ctx, this.contentBox, paint, context.text);
21223
21952
  }
21224
21953
  }
@@ -21320,8 +22049,21 @@ var Canvas2DRenderer = class {
21320
22049
  layers.drop(node);
21321
22050
  return false;
21322
22051
  }
21323
- for (const strip of strips) this.paintLayer(layer, strip, node, rec, context);
22052
+ const damage = layers.damage(layer, node, context.layout);
22053
+ const wrong = damage === "all" ? [{
22054
+ x: 0,
22055
+ y: 0,
22056
+ width: layer.width,
22057
+ height: layer.height
22058
+ }] : damage;
22059
+ if (wrong.length > 0) {
22060
+ const front = layer.frontContext;
22061
+ front.setTransform(1, 0, 0, 1, 0, 0);
22062
+ for (const rect of wrong) front.clearRect(rect.x, rect.y, rect.width, rect.height);
22063
+ }
22064
+ if (strips.length > 0 || wrong.length > 0) this.paintLayer(layer, [...strips, ...wrong], node, rec, context);
21324
22065
  }
22066
+ layers.remember(layer, node, context.layout);
21325
22067
  layer.used = true;
21326
22068
  layers.stats.composited++;
21327
22069
  ctx.save();
@@ -21343,13 +22085,13 @@ var Canvas2DRenderer = class {
21343
22085
  * strip saves is raster, which is the cost it exists for; the walk
21344
22086
  * costs what a direct draw's walk would.
21345
22087
  */
21346
- paintLayer(layer, strip, node, rec, context) {
22088
+ paintLayer(layer, strips, node, rec, context) {
21347
22089
  const lctx = new PreciseContext(layer.frontContext);
21348
22090
  lctx.save();
21349
- if (strip !== null) {
22091
+ if (strips !== null) {
21350
22092
  lctx.setTransform(1, 0, 0, 1, 0, 0);
21351
22093
  lctx.beginPath();
21352
- lctx.rect(strip.x, strip.y, strip.width, strip.height);
22094
+ for (const strip of strips) lctx.rect(strip.x, strip.y, strip.width, strip.height);
21353
22095
  lctx.clip();
21354
22096
  }
21355
22097
  const dpr = layer.dpr;
@@ -21363,6 +22105,26 @@ var Canvas2DRenderer = class {
21363
22105
  this.cullY = rec.y + layer.scrollY;
21364
22106
  this.cullWidth = rec.width;
21365
22107
  this.cullHeight = rec.height;
22108
+ if (strips !== null && strips.length > 0) {
22109
+ let left = Infinity;
22110
+ let top = Infinity;
22111
+ let right = -Infinity;
22112
+ let bottom = -Infinity;
22113
+ for (const strip of strips) {
22114
+ left = Math.min(left, strip.x);
22115
+ top = Math.min(top, strip.y);
22116
+ right = Math.max(right, strip.x + strip.width);
22117
+ bottom = Math.max(bottom, strip.y + strip.height);
22118
+ }
22119
+ const x0 = Math.max(this.cullX, (left - layer.offsetX) / dpr + layer.scrollX - 8);
22120
+ const y0 = Math.max(this.cullY, (top - layer.offsetY) / dpr + layer.scrollY - 8);
22121
+ const x1 = Math.min(this.cullX + this.cullWidth, (right - layer.offsetX) / dpr + layer.scrollX + 8);
22122
+ const y1 = Math.min(this.cullY + this.cullHeight, (bottom - layer.offsetY) / dpr + layer.scrollY + 8);
22123
+ this.cullX = x0;
22124
+ this.cullY = y0;
22125
+ this.cullWidth = Math.max(0, x1 - x0);
22126
+ this.cullHeight = Math.max(0, y1 - y0);
22127
+ }
21366
22128
  this.layerDepth++;
21367
22129
  try {
21368
22130
  this.renderChildren(node, context, lctx, true);
@@ -21571,6 +22333,6 @@ function createCanvasSurface(host) {
21571
22333
  return new CanvasSurface(host);
21572
22334
  }
21573
22335
  //#endregion
21574
- 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 };
22336
+ export { isImeKey as $, visualStatesEqual as $a, scaleSpacing as $i, spannedRunsFor as $n, transformIsIdentity as $r, registerFontStack as $t, withOverrideSource as A, UiBasicColors as Aa, noInsets as Ai, clearLinkHover as An, inheritedPropertyFlags as Ar, adjacentField as At, FlexDirection as B, findEnvironmentKey as Ba, withDensity as Bi, editorFor as Bn, isUiSemanticState as Br, createCanvasTextStyle as Bt, SCROLLBAR_LINGER_MS as C, spansDifferOnlyInPaint as Ca, normalizeBorderRadius as Ci, isNodeInert as Cn, resolveNumber as Cr, hasDrawnText as Ct, formatExplanation as D, colorsEqualPalette as Da, UiEnvironmentKeys as Di, isSubtreeInert as Dn, closestPropertyName as Dr, selectionRectsIn as Dt, formatConstraints as E, textStylesEqual as Ea, propertyValuesEqual as Ei, isPressable as En, resolveString as Er, paragraphGeometryFrom as Et, clampSize as F, normalizeColor as Fa, unknownContainerSize as Fi, setLinkHover as Fn, propertyValuesEqualByName as Fr, detectEditingPlatform as Ft, CrossAxisAlignment as G, overrideSources as Ga, themeExtensionsEqual as Gi, maskText as Gn, UI_SUBGRID_AXES as Gr, runFontString as Gt, placeGridItems as H, UiNode as Ha, hasThemeExtension as Hi, isEditableNode as Hn, statesEqual as Hr, fillTextRects as Ht, constraintsEqual as I, parseColor as Ia, darkTheme as Ii, CARET_BLINK_MS as In, UiProperties as Ir, isPrintable as It, parseCrossAxisAlignment as J, writeOverrideProperty as Ja, highContrastColors as Ji, resolveColorValue as Jn, paintValuesEqual as Jr, textRunBackgrounds as Jt, MainAxisAlignment as K, resetOverrideWarnings as Ka, withThemeExtension as Ki, nextCaretToggle as Kn, validateSubgrid as Kr, spanOf as Kt, tightenConstraints as L, rgb8 as La, lightTheme as Li, CARET_WIDTH as Ln, UI_ROLES as Lr, EditableLayout as Lt, contentOffset as M, colorToRgba as Ma, UiContainerSizeSource as Mi, linkHoverOf as Mn, propertyEffects as Mr, edgeField as Mt, LayoutRecord as N, colorValuesEqual as Na, bandOf as Ni, linkOf as Nn, propertyIsInherited as Nr, editingGroupOf as Nt, formatNumber as O, darkColors as Oa, UiInsetRegistry as Oi, pointerEventsOf as On, findPropertyDefinition as Or, wordRangeIn as Ot, Constraints as P, colorsEqual as Pa, containerBands as Pi, linkRunAtPointIn as Pn, propertyNarrowers as Pr, commandForKey as Pt, capturePointer as Q, visualState as Qa, defaultSpacing as Qi, ParagraphTextMeasurer as Qn, transform as Qr, fontStackFor as Qt, MAX_MEASURES_PER_CHILD as R, rgba as Ra, themesEqual as Ri, EDITOR_PROP as Rn, UI_SEMANTIC_STATES as Rr, applyCanvasTextStyle as Rt, SCROLLBAR_FADE_MS as S, spannedTextOf as Sa, borderRadiusValuesEqual as Si, isNodeHitTestable as Sn, resolveBoolean as Sr, UiInputDispatcher as St, describeOverrides as T, textSpansEqual as Ta, defineProperty as Ti, isNodeTabStop as Tn, resolvePropertyByName as Tr, paragraphGeometry as Tt, sizeGridTracks as U, DirtyNodeSet as Ua, noThemeExtensions as Ui, isMultiline as Un, validateRole as Ur, layoutTextLines as Ut, parseFlexDirection as V, UiEnvironment as Va, defineThemeExtension as Vi, editorOf as Vn, normalizeStates as Vr, drawText as Vt, AlignContent as W, clearOverrideProperty as Wa, themeExtension as Wi, isReadOnly as Wn, validateStates as Wr, runCanvasStyleInto as Wt, CanvasPlatformSurface as X, defaultVisualState as Xa, relativeLuminance as Xi, videoFrameSize as Xn, UiTransforms as Xr, bumpFontStack as Xt, parseMainAxisAlignment as Y, UiVisualState as Ya, raiseContrast as Yi, isVideoSurface as Yn, pathValuesEqual as Yr, textRunDecorations as Yt, UiPlatformAdapter as Z, hasVisualState as Za, densityFactors as Zi, CharacterCountTextMeasurer as Zn, parseTransform as Zr, clearFontStacks as Zt, LABEL_PADDING_X as _, editableSpansOf as _a, UiBorderRadiuses as _i, selectableTextNodes as _n, UiNodeType as _o, previousGraphemeStart as _r, scrollLeaderOf as _t, PaintPictureCache as a, boxShadow as aa, radialGradient as ai, computeObjectFitRect as an, UiGestureEvent as ao, lineIndexForOffset as ar, UiGestureRecognizer as at, snapScroll as b, resolvedSpansOf as ba, borderRadiusEqual as bi, setSelectionRange as bn, UiGraph as br, scrollbarThumbs as bt, EMPTY_RECORDING as c, boxShadowReach as ca, fr as ci, normalizeTextAlign as cn, UiPasteEvent as co, offsetAtX as cr, isScrollContainer as ct, tracePathData as d, isShapeName as da, isMinMaxTrack as di, TEXT_MATCHES_PROP as dn, UiSelectionChangeEvent as do, EditableTextModel as dr, UiFocusManager as dt, spacingEqual as ea, transformsEqual as ei, DEFAULT_FONT_FAMILY as en, MOUSE_POINTER as eo, ELLIPSIS as er, pointerDeviceOf as et, ScaledImageCache as f, shapesEqual as fa, isPercentLength as fi, clearMatchRanges as fn, UiTextChangeEvent as fo, graphemeBoundaries as fr, UiHitTester as ft, paintsAfterChildren as g, defaultTextStyle as ga, resolveLength as gi, clearSelectionRange as gn, wheelDeltaYOf as go, nextWordEnd as gr, SCROLLBAR_THICKNESS as gt, hasDecorationPhase as h, typographyEqual as ha, repeat as hi, TEXT_SELECTION_PROP as hn, noKeyModifiers as ho, nextGraphemeEnd as hr, SCROLLBAR_MIN_THUMB as ht, traceRoundedRect as i, BOX_SHADOW_BLUR_REACH as ia, linearGradient as ii, colorToCss as in, UiFocusEvent as io, lineIndexAtY as ir, UiTouchScroller as it, accumulatedOffsetTo as j, colorToHex as ja, observeViewportInsets as ji, hasTextLinks as jn, inheritedPropertyNames as jr, comparePositions as jt, labelNode as k, lightColors as ka, insetsEqual as ki, resolveCursor as kn, getPropertyNames as kr, textInputOf as kt, PaintRecorder as l, boxShadowsEqual as la, isAutoLength as li, normalizeVerticalAlign as ln, UiPinchEvent as lo, offsetForVerticalMove as lr, UiKeyboardController as lt, decorationRect as m, isTypographyRole as ma, percent as mi, setMatchRanges as mn, UiWheelEvent as mo, lineStartAt as mr, SCROLLBAR_INSET as mt, createCanvasSurface as n, defaultShadows as na, gradientPaint as ni, DEFAULT_FONT_WEIGHT as nn, UiCompositionEvent as no, proportionalFontMetrics as nr, touchActionFor as nt, paintPictures as o, boxShadowArraysEqual as oa, validateGradient as oi, createPaintState as on, UiInputEvent as oo, lineLimit as or, UiPinchRecognizer as ot, decorationColor as p, defaultTypography as pa, minmax as pi, matchRangesOf as pn, UiWheelDeltaMode as po, lineEndAt as pr, SCROLLBAR_HOVER_ZONE as pt, parseAlignContent as q, writeDeclaredProperty as qa, contrastRatio as qi, syncEditorValue as qn, inputsEqual as qr, textMeasureRequest as qt, Canvas2DRenderer as r, shadowsEqual as ra, gradientsEqual as ri, DEFAULT_TEXT_COLOR as rn, UiEventType as ro, caretRectFor as rr, UiEditingController as rt, PaintTarget as s, boxShadowRadius as sa, auto as si, isPaintVisible as sn, UiKeyboardEvent as so, offsetAtPoint as sr, UiWheelController as st, CanvasSurface as t, spacingSteps as ta, MAX_GRADIENT_STOPS as ti, DEFAULT_FONT_SIZE as tn, UiBeforeInputEvent as to, layoutParagraph as tr, prepareInputSurface as tt, replayPaint as u, defaultShapes as ua, isFrLength as ui, resolvePaintState as un, UiPointerEvent as uo, selectionRects as ur, UiPointerController as ut, drawOverlayShapes as v, flattenTextSpans as va, borderRadius as vi, selectableTextOf as vn, DirtyFlags as vo, previousWordStart as vr, scrollRange as vt, describeLength as w, textContentOf as wa, uniformBorderRadius as wi, isNodeSelectable as wn, resolveProperty as wr, offsetAtPointIn as wt, LayoutEngine as x, spanAtOffset as xa, borderRadiusIsZero as xi, isNodeFocusable as xn, DEFAULT_LINE_HEIGHT_FACTOR as xr, scrollbarZoneAt as xt, labelOrigin as y, hiddenRangesOf as ya, borderRadiusCorners as yi, selectionRangeOf as yn, UiBinding as yo, wordRangeAt as yr, scrollbarThumb as yt, isLayoutProtocol as z, createEnvironmentKey as za, withContrast as zi, caretVisibleAt as zn, isUiRole as zr, buildFontString as zt };
21575
22337
 
21576
- //# sourceMappingURL=CanvasSurface-tAao5BLh.js.map
22338
+ //# sourceMappingURL=CanvasSurface-BAhcLj2K.js.map