gesso-core 0.2.1 → 0.4.0

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.
@@ -161,7 +161,43 @@ let UiEventType = /* @__PURE__ */ function(UiEventType) {
161
161
  UiEventType["BeforeInput"] = "beforeinput";
162
162
  /** An editable's text changed. */
163
163
  UiEventType["Input"] = "input";
164
+ /**
165
+ * An editable's selection moved, whether or not its text changed.
166
+ *
167
+ * Separate from `Input` because the caret moves without the text on
168
+ * every arrow key, every click into the text and every
169
+ * select-all — and something has to be able to follow it. A formula
170
+ * editor that colours the bracket beside the caret has no other way
171
+ * to know it moved.
172
+ *
173
+ * Fires *after* `Input` when a change did both, so a listener that
174
+ * reads the text sees the new one. It is skipped entirely when the
175
+ * selection did not actually move, so holding a key that is already
176
+ * at the end of the text is silent.
177
+ */
178
+ UiEventType["SelectionChange"] = "selectionchange";
179
+ /**
180
+ * Text arrived from the system clipboard with nothing editable
181
+ * focused.
182
+ *
183
+ * A paste with a caret in it belongs to the text and never reaches
184
+ * here. This is the other case: a surface that owns a selection of
185
+ * its own — a grid with a rectangle of cells picked out — and is the
186
+ * only thing that knows what a block of text means to it.
187
+ * `preventDefault()` says it was taken.
188
+ */
189
+ UiEventType["Paste"] = "paste";
164
190
  UiEventType["Click"] = "click";
191
+ /**
192
+ * A second Click on the same node, soon enough after the first and
193
+ * close enough to it.
194
+ *
195
+ * Dispatched after that second Click rather than instead of it, as
196
+ * the DOM's `dblclick` is: a listener that answers a click has
197
+ * already answered both by the time this arrives, which is what lets
198
+ * a double click on a cell select it and then do more.
199
+ */
200
+ UiEventType["DoubleClick"] = "doubleclick";
165
201
  UiEventType["LongPress"] = "longpress";
166
202
  UiEventType["DragStart"] = "dragstart";
167
203
  UiEventType["DragMove"] = "dragmove";
@@ -406,6 +442,21 @@ var UiBeforeInputEvent = class extends UiInputEvent {
406
442
  * An editable node's text changed: the new value and the selection
407
443
  * after the change. Bubbles, so a form can watch all its fields.
408
444
  */
445
+ /**
446
+ * Text from the system clipboard, offered to whatever holds focus.
447
+ *
448
+ * Carries the text rather than a clipboard handle, because a render
449
+ * worker has neither a clipboard nor a window: the shell reads it and
450
+ * sends it across, which is the same path `ShellService.copyText`
451
+ * takes in the other direction.
452
+ */
453
+ var UiPasteEvent = class extends UiInputEvent {
454
+ text;
455
+ constructor(text) {
456
+ super("paste");
457
+ this.text = text;
458
+ }
459
+ };
409
460
  var UiTextChangeEvent = class extends UiInputEvent {
410
461
  value;
411
462
  selectionStart;
@@ -418,6 +469,36 @@ var UiTextChangeEvent = class extends UiInputEvent {
418
469
  }
419
470
  };
420
471
  /**
472
+ * An editable's selection moved.
473
+ *
474
+ * Carries the text as well as the offsets, because every use of this
475
+ * is "what is under the caret" and the answer needs both — and
476
+ * reading the text back off the model afterwards is a second source
477
+ * of truth that can disagree by a frame.
478
+ *
479
+ * `start` and `end` are ordered; `anchor` and `focus` are where the
480
+ * selection was begun and where the caret now is, which is the pair
481
+ * that says which way it was dragged.
482
+ */
483
+ var UiSelectionChangeEvent = class extends UiInputEvent {
484
+ value;
485
+ start;
486
+ end;
487
+ anchor;
488
+ focus;
489
+ constructor(value, start, end, anchor, focus) {
490
+ super("selectionchange");
491
+ this.value = value;
492
+ this.start = start;
493
+ this.end = end;
494
+ this.anchor = anchor;
495
+ this.focus = focus;
496
+ }
497
+ get collapsed() {
498
+ return this.start === this.end;
499
+ }
500
+ };
501
+ /**
421
502
  * Wheel scroll input event.
422
503
  *
423
504
  * `deltaX`/`deltaY` are the scroll deltas in canvas pixels produced
@@ -1615,6 +1696,27 @@ function textContentOf(node) {
1615
1696
  function resolvedSpansOf(node) {
1616
1697
  return spannedTextOf(node)?.spans ?? EMPTY_SPANS;
1617
1698
  }
1699
+ /**
1700
+ * The runs an *editable* node styles its own text with.
1701
+ *
1702
+ * An editable's paragraph is the user's, held by its model rather
1703
+ * than by a property — so unlike a `Text`, its runs do not supply the
1704
+ * text, they only describe it. `spans` on an editable is presentation
1705
+ * over a string somebody is typing.
1706
+ *
1707
+ * Which is why this checks. Every offset the editor works in — the
1708
+ * caret, the selection, the hit test that turns a click into a text
1709
+ * position — is an offset into the model's string. Runs describing a
1710
+ * *different* string would put the glyphs somewhere the caret does
1711
+ * not agree with, and the field would look subtly, unfixably wrong.
1712
+ * When they disagree the text is drawn unstyled, which is a frame of
1713
+ * plain colour rather than a frame of nonsense, and corrects itself
1714
+ * as soon as the application catches up.
1715
+ */
1716
+ function editableSpansOf(node, text) {
1717
+ const spanned = spannedTextOf(node);
1718
+ return spanned !== void 0 && spanned.text === text ? spanned.spans : EMPTY_SPANS;
1719
+ }
1618
1720
  /** The span covering an offset, or undefined where no run does. */
1619
1721
  function spanAtOffset(spans, offset) {
1620
1722
  for (const span of spans) if (offset >= span.start && offset < span.end) return span;
@@ -3845,6 +3947,20 @@ const UiProperties = {
3845
3947
  inherited: false,
3846
3948
  affects: L
3847
3949
  }),
3950
+ /**
3951
+ * A point to be placed beside, as though it were an anchor of no
3952
+ * size: where a context menu was asked for. Relative to the
3953
+ * containing block, like `top` and `left`, and with `placement` the
3954
+ * same flip and clamp an anchored node gets — so a menu opened near
3955
+ * the bottom edge opens upward instead of off the screen. Ignored
3956
+ * when `anchor` is set.
3957
+ */
3958
+ anchorPoint: defineProperty({
3959
+ name: "anchorPoint",
3960
+ defaultValue: void 0,
3961
+ inherited: false,
3962
+ affects: L
3963
+ }),
3848
3964
  /** Gap between an anchored node and its anchor. */
3849
3965
  anchorOffset: defineProperty({
3850
3966
  name: "anchorOffset",
@@ -4223,6 +4339,12 @@ const UiProperties = {
4223
4339
  inherited: false,
4224
4340
  affects: 64
4225
4341
  }),
4342
+ tabStop: defineProperty({
4343
+ name: "tabStop",
4344
+ defaultValue: void 0,
4345
+ inherited: false,
4346
+ affects: 64
4347
+ }),
4226
4348
  disabled: defineProperty({
4227
4349
  name: "disabled",
4228
4350
  defaultValue: void 0,
@@ -4451,6 +4573,13 @@ const UiProperties = {
4451
4573
  inherited: false,
4452
4574
  affects: 64
4453
4575
  }),
4576
+ /** Set by LazySheet on the scroll container: its UiVirtualSheet. */
4577
+ virtualSheet: defineProperty({
4578
+ name: "virtualSheet",
4579
+ defaultValue: void 0,
4580
+ inherited: false,
4581
+ affects: 64
4582
+ }),
4454
4583
  theme: defineProperty({
4455
4584
  name: "theme",
4456
4585
  defaultValue: void 0,
@@ -5547,6 +5676,21 @@ var EditableTextModel = class {
5547
5676
  blinkOrigin = 0;
5548
5677
  /** The last `value` the host synchronised in; see `UiEditable`. */
5549
5678
  syncedValue = void 0;
5679
+ /**
5680
+ * The selection the last `selectionchange` reported.
5681
+ *
5682
+ * Kept on the model rather than worked out at each call site,
5683
+ * because the caret is moved from a dozen places — arrows, clicks,
5684
+ * select-all, an edit that replaces a range — and every one of them
5685
+ * would otherwise have to remember to compare. Storing what was
5686
+ * last said makes the comparison the notifier's job and the answer
5687
+ * the same wherever the move came from.
5688
+ *
5689
+ * `NaN` to begin with, so the first report always fires: a fresh
5690
+ * model at offset zero has genuinely not told anyone yet.
5691
+ */
5692
+ notifiedAnchor = NaN;
5693
+ notifiedFocus = NaN;
5550
5694
  constructor(text = "") {
5551
5695
  this.textValue = text;
5552
5696
  }
@@ -6310,6 +6454,26 @@ function isNodeFocusable(node) {
6310
6454
  return node.type === "button" || node.type === "editable-text";
6311
6455
  }
6312
6456
  /**
6457
+ * Whether Tab should stop here.
6458
+ *
6459
+ * Every tab stop is focusable and not every focusable node is a tab
6460
+ * stop, which is the distinction `tabindex="-1"` draws in the DOM and
6461
+ * the one this engine was missing. `focusable` answers "may this node
6462
+ * hold focus at all", and that is the wrong question for a container:
6463
+ * a dialog's body must be able to *take* the keyboard, so that opening
6464
+ * a dialog whose content is a sentence does not hand it to nothing, and
6465
+ * must not be a place Tab *lands*, or every dialog gains a stop that
6466
+ * announces nothing and does nothing.
6467
+ *
6468
+ * So `tabStop: false` means focusable, reachable by a press and by
6469
+ * `focus()`, and skipped by the cycle. It says nothing on a node that
6470
+ * is not focusable in the first place, because there is no order to be
6471
+ * out of.
6472
+ */
6473
+ function isNodeTabStop(node) {
6474
+ return isNodeFocusable(node) && node.properties.get("tabStop") !== false;
6475
+ }
6476
+ /**
6313
6477
  * Whether the user may select this node's text with the pointer.
6314
6478
  *
6315
6479
  * `selectable` inherits down the tree the way CSS `user-select` does:
@@ -6655,7 +6819,7 @@ function resolvePaintState(node, out) {
6655
6819
  }
6656
6820
  out.rtl = resolveProperty(node, UiProperties.textDirection) === "rtl";
6657
6821
  resolveFontInto(node, out);
6658
- out.spans = out.editor === void 0 ? resolvePaintSpans(node) : void 0;
6822
+ out.spans = resolvePaintSpans(node, out.editor === void 0 ? resolvedSpansOf(node) : editableSpansOf(node, out.text ?? ""));
6659
6823
  out.linkHover = out.spans === void 0 ? -1 : linkHoverOf(node);
6660
6824
  out.textDecoration = resolveProperty(node, UiProperties.textDecoration);
6661
6825
  out.textColor = resolveColor(node, UiProperties.color) ?? UiBasicColors.black;
@@ -6718,8 +6882,7 @@ function resetTextStyle(out) {
6718
6882
  * anything having to notice.
6719
6883
  */
6720
6884
  const paintSpans = /* @__PURE__ */ new WeakMap();
6721
- function resolvePaintSpans(node) {
6722
- const spans = resolvedSpansOf(node);
6885
+ function resolvePaintSpans(node, spans) {
6723
6886
  if (spans.length === 0) return;
6724
6887
  const theme = node.environment !== null ? node.environment.get(UiEnvironmentKeys.theme) : void 0;
6725
6888
  const cached = paintSpans.get(spans);
@@ -7422,6 +7585,122 @@ var EditableLayout = class {
7422
7585
  }
7423
7586
  };
7424
7587
  //#endregion
7588
+ //#region src/editing/EditingKeymap.ts
7589
+ /**
7590
+ * The platform the runtime is on, decided once. Available in a worker
7591
+ * too, since `navigator.userAgent` is; tests set it explicitly.
7592
+ */
7593
+ function detectEditingPlatform() {
7594
+ const agent = typeof navigator !== "undefined" ? navigator.userAgent : "";
7595
+ return /Mac|iPhone|iPad|iPod/.test(agent) ? "mac" : "other";
7596
+ }
7597
+ /**
7598
+ * Maps a key press to an editing command, following the host's
7599
+ * conventions:
7600
+ *
7601
+ * Mac — Option steps by word, Command jumps to line and document
7602
+ * ends, Command+Backspace deletes to the line start.
7603
+ * Others — Control steps by word, Home/End and Control+Home/End are
7604
+ * the line and document ends.
7605
+ *
7606
+ * Shift extends every move. Enter is a newline (the controller drops
7607
+ * it in a single-line field). A printable key becomes `insert` only
7608
+ * when `textFromKeys` is set: a host with an editing proxy delivers
7609
+ * text through `beforeinput` instead, where the IME and dead keys
7610
+ * have already been resolved.
7611
+ */
7612
+ function commandForKey(key, modifiers, platform, textFromKeys) {
7613
+ const mac = platform === "mac";
7614
+ const primary = mac ? modifiers.meta : modifiers.ctrl;
7615
+ const word = mac ? modifiers.alt : modifiers.ctrl;
7616
+ const extend = modifiers.shift;
7617
+ switch (key) {
7618
+ case "ArrowLeft":
7619
+ case "ArrowRight": {
7620
+ const direction = key === "ArrowLeft" ? -1 : 1;
7621
+ if (mac && modifiers.meta) return {
7622
+ kind: "move",
7623
+ unit: "line",
7624
+ direction,
7625
+ extend
7626
+ };
7627
+ return {
7628
+ kind: "move",
7629
+ unit: word ? "word" : "grapheme",
7630
+ direction,
7631
+ extend
7632
+ };
7633
+ }
7634
+ case "ArrowUp":
7635
+ case "ArrowDown": {
7636
+ const direction = key === "ArrowUp" ? -1 : 1;
7637
+ if (mac && modifiers.meta) return {
7638
+ kind: "move",
7639
+ unit: "document",
7640
+ direction,
7641
+ extend
7642
+ };
7643
+ return {
7644
+ kind: "move",
7645
+ unit: "vertical",
7646
+ direction,
7647
+ extend
7648
+ };
7649
+ }
7650
+ case "Home": return {
7651
+ kind: "move",
7652
+ unit: modifiers.ctrl ? "document" : "line",
7653
+ direction: -1,
7654
+ extend
7655
+ };
7656
+ case "End": return {
7657
+ kind: "move",
7658
+ unit: modifiers.ctrl ? "document" : "line",
7659
+ direction: 1,
7660
+ extend
7661
+ };
7662
+ case "Backspace":
7663
+ if (mac && modifiers.meta) return {
7664
+ kind: "delete",
7665
+ unit: "line",
7666
+ direction: -1
7667
+ };
7668
+ return {
7669
+ kind: "delete",
7670
+ unit: word ? "word" : "grapheme",
7671
+ direction: -1
7672
+ };
7673
+ case "Delete": return {
7674
+ kind: "delete",
7675
+ unit: word ? "word" : "grapheme",
7676
+ direction: 1
7677
+ };
7678
+ case "Enter": return primary ? null : { kind: "newline" };
7679
+ }
7680
+ if (primary && !modifiers.alt) {
7681
+ switch (key.toLowerCase()) {
7682
+ case "a": return { kind: "selectAll" };
7683
+ case "z": return modifiers.shift ? { kind: "redo" } : { kind: "undo" };
7684
+ case "y": return mac ? null : { kind: "redo" };
7685
+ }
7686
+ return null;
7687
+ }
7688
+ if (textFromKeys && isPrintable(key) && !modifiers.ctrl && !modifiers.meta) return {
7689
+ kind: "insert",
7690
+ text: key
7691
+ };
7692
+ return null;
7693
+ }
7694
+ /** A key value that is the character it types: one grapheme, not a name like 'Shift'. */
7695
+ function isPrintable(key) {
7696
+ if (key.length === 1) return true;
7697
+ if (key.length === 2) {
7698
+ const code = key.charCodeAt(0);
7699
+ return code >= 55296 && code <= 56319;
7700
+ }
7701
+ return false;
7702
+ }
7703
+ //#endregion
7425
7704
  //#region src/layout/LineBreaks.ts
7426
7705
  function segmentParagraph(paragraph, wrap) {
7427
7706
  const length = paragraph.length;
@@ -8410,6 +8689,31 @@ function scrollbarZoneAt(rec, x, y) {
8410
8689
  if (rec.contentWidth > rec.width && y >= rec.y + rec.height - 16) return "x";
8411
8690
  return null;
8412
8691
  }
8692
+ /**
8693
+ * The rectangle a press grabs a thumb by.
8694
+ *
8695
+ * The painted thumb is six pixels thick. Six pixels at the very edge
8696
+ * of a window is a hard thing to hit, and missing it is not a near
8697
+ * miss — the press lands on the track instead and pages a whole
8698
+ * screenful, which reads as the bar refusing to be dragged. So the
8699
+ * grab target is the thumb's length by the whole band that reveals it:
8700
+ * as easy to catch as it is to summon, which is what an overlay
8701
+ * scrollbar is expected to be. What is drawn is unchanged.
8702
+ */
8703
+ function scrollbarGrabBox(rec, bar) {
8704
+ if (bar.axis === "y") return {
8705
+ x: rec.mirrored ? rec.x : rec.x + rec.width - 16,
8706
+ y: bar.thumb.y,
8707
+ width: 16,
8708
+ height: bar.thumb.height
8709
+ };
8710
+ return {
8711
+ x: bar.thumb.x,
8712
+ y: rec.y + rec.height - 16,
8713
+ width: bar.thumb.width,
8714
+ height: 16
8715
+ };
8716
+ }
8413
8717
  function pointInBox(box, x, y) {
8414
8718
  return x >= box.x && x < box.x + box.width && y >= box.y && y < box.y + box.height;
8415
8719
  }
@@ -8731,11 +9035,21 @@ var UiHitTester = class {
8731
9035
  * only while the bar is showing (or when asked for the band alone).
8732
9036
  */
8733
9037
  hitScrollbar(node, rec, px, py) {
8734
- const axis = scrollbarZoneAt(rec, px, py);
8735
- if (axis === null) return false;
8736
- const bar = scrollbarThumb(rec, axis);
9038
+ const zone = scrollbarZoneAt(rec, px, py);
9039
+ if (zone === null) return false;
9040
+ let axis = zone;
9041
+ let bar = scrollbarThumb(rec, axis);
9042
+ let onThumb = bar !== null && pointInBox(scrollbarGrabBox(rec, bar), px, py);
9043
+ if (!onThumb) {
9044
+ const other = axis === "y" ? "x" : "y";
9045
+ const otherBar = scrollbarThumb(rec, other);
9046
+ if (otherBar !== null && pointInBox(scrollbarGrabBox(rec, otherBar), px, py)) {
9047
+ axis = other;
9048
+ bar = otherBar;
9049
+ onThumb = true;
9050
+ }
9051
+ }
8737
9052
  if (bar === null) return false;
8738
- const onThumb = pointInBox(bar.thumb, px, py);
8739
9053
  const visible = rec.scrollbarVisibleUntil > (typeof performance !== "undefined" ? performance.now() : Date.now());
8740
9054
  if (!onThumb && !visible && !this.zoneOnly) return false;
8741
9055
  this.recordHit(node, rec, px, py);
@@ -8992,10 +9306,24 @@ var UiFocusManager = class {
8992
9306
  * this again once the frame's tree is built, which is when a dialog
8993
9307
  * gets its caret. Idempotent: with focus already inside the scope,
8994
9308
  * or no scope at all, it does nothing.
9309
+ *
9310
+ * **The scope root is the fallback, and it is the whole reason
9311
+ * `tabStop` exists.** A scope with no tab stop inside it used to
9312
+ * blur, which hands the keyboard to nothing: a `Dialog` whose body
9313
+ * is a sentence and a button could not be dismissed with Escape,
9314
+ * because Escape had nowhere to be delivered. Falling back to the
9315
+ * root fixes that only if the root can hold focus without becoming a
9316
+ * stop of its own, or every dialog grows a tab stop that announces
9317
+ * nothing. `focusable: true, tabStop: false` on the body is exactly
9318
+ * that pair, and the fallback is what makes it worth setting.
9319
+ *
9320
+ * Blurring is still what happens when the root cannot hold focus
9321
+ * either, because a scope that has genuinely nothing to focus is
9322
+ * better admitted than faked.
8995
9323
  */
8996
9324
  settleScope() {
8997
9325
  if (this.scopes.length === 0 || this.focused !== null && this.withinScope(this.focused)) return;
8998
- if (!this.focusNext()) this.blur();
9326
+ if (!this.focusNext() && !this.focus(this.scopeRoot)) this.blur();
8999
9327
  }
9000
9328
  /**
9001
9329
  * Ends the innermost scope and hands focus back to whatever held it
@@ -9043,11 +9371,18 @@ var UiFocusManager = class {
9043
9371
  const next = focusables[(index + delta + focusables.length) % focusables.length];
9044
9372
  return this.focus(next, "keyboard");
9045
9373
  }
9046
- /** Focusable nodes in document order (parent before children). */
9374
+ /**
9375
+ * The tab stops in document order (parent before children).
9376
+ *
9377
+ * Tab stops and not every focusable node: a container with
9378
+ * `tabStop: false` can hold focus and is not a place the cycle
9379
+ * stops, which is what lets a dialog body be the keyboard's home of
9380
+ * last resort without becoming a stop of its own.
9381
+ */
9047
9382
  collectFocusable() {
9048
9383
  const result = [];
9049
9384
  const visit = (node) => {
9050
- if (isNodeFocusable(node)) result.push(node);
9385
+ if (isNodeTabStop(node)) result.push(node);
9051
9386
  for (let child = node.firstChild; child !== null; child = child.nextSibling) visit(child);
9052
9387
  };
9053
9388
  visit(this.scopeRoot);
@@ -9124,6 +9459,15 @@ var UiPointerController = class {
9124
9459
  onHoverChange;
9125
9460
  editing;
9126
9461
  selection;
9462
+ now;
9463
+ /**
9464
+ * The last Click, for telling whether the next one makes a pair.
9465
+ *
9466
+ * Only a Click counts, not a press: a press that became a drag, or
9467
+ * was cancelled, is not half of a double click, and a release that
9468
+ * followed one would otherwise be read as the second half of it.
9469
+ */
9470
+ lastClick = null;
9127
9471
  hoverNode = null;
9128
9472
  downTarget = null;
9129
9473
  downX = 0;
@@ -9150,6 +9494,7 @@ var UiPointerController = class {
9150
9494
  this.onHoverChange = options.onHoverChange ?? null;
9151
9495
  this.editing = options.editing;
9152
9496
  this.selection = options.selection;
9497
+ this.now = options.now ?? defaultClock$2;
9153
9498
  }
9154
9499
  /** The node currently under the pointer, or null over empty space. */
9155
9500
  /** Where the pointer was last seen, in canvas space; null before it has been. */
@@ -9286,12 +9631,39 @@ var UiPointerController = class {
9286
9631
  if (dx <= slop && dy <= slop) {
9287
9632
  const click = new UiPointerEvent("click", x, y, buttons, modifiers, pointer);
9288
9633
  this.dispatcher.dispatch(click, target);
9289
- }
9290
- }
9634
+ this.countClick(target, x, y, buttons, modifiers, pointer);
9635
+ } else this.lastClick = null;
9636
+ } else this.lastClick = null;
9291
9637
  this.releaseHover(x, y, modifiers, pointer);
9292
9638
  return event;
9293
9639
  }
9294
9640
  /**
9641
+ * Pairs this Click with the one before it, and dispatches a
9642
+ * DoubleClick when they make a pair.
9643
+ *
9644
+ * A pair is two Clicks on the same node within `MULTI_CLICK_MS` and
9645
+ * `MULTI_CLICK_SLOP` of each other — the window the editing and
9646
+ * selection controllers already use for a word, so a double click
9647
+ * means the same thing on a label as it does in a field. The pair is
9648
+ * spent once it has fired: a third Click starts the next pair rather
9649
+ * than making a second DoubleClick out of the second and third.
9650
+ */
9651
+ countClick(target, x, y, buttons, modifiers, pointer) {
9652
+ const now = this.now();
9653
+ const last = this.lastClick;
9654
+ if (!(last !== null && last.node === target && now - last.at <= MULTI_CLICK_MS$1 && Math.abs(last.x - x) <= MULTI_CLICK_SLOP$1 && Math.abs(last.y - y) <= MULTI_CLICK_SLOP$1)) {
9655
+ this.lastClick = {
9656
+ node: target,
9657
+ x,
9658
+ y,
9659
+ at: now
9660
+ };
9661
+ return;
9662
+ }
9663
+ this.lastClick = null;
9664
+ this.dispatcher.dispatch(new UiPointerEvent("doubleclick", x, y, buttons, modifiers, pointer), target);
9665
+ }
9666
+ /**
9295
9667
  * Aborts the active press: dispatches PointerCancel to the pressed
9296
9668
  * node and clears press state so no Click is synthesized.
9297
9669
  */
@@ -9443,6 +9815,12 @@ var UiPointerController = class {
9443
9815
  function isSecondaryButton(buttons) {
9444
9816
  return buttons === 2;
9445
9817
  }
9818
+ /** How long, and how far, a second Click may be from the first and still pair with it. */
9819
+ const MULTI_CLICK_MS$1 = 500;
9820
+ const MULTI_CLICK_SLOP$1 = 4;
9821
+ function defaultClock$2() {
9822
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
9823
+ }
9446
9824
  //#endregion
9447
9825
  //#region src/input/UiKeyboardController.ts
9448
9826
  /**
@@ -9497,13 +9875,19 @@ var UiKeyboardController = class {
9497
9875
  this.find = options.find;
9498
9876
  this.activation = options.activation;
9499
9877
  }
9500
- keyDown(key, modifiers = noKeyModifiers()) {
9878
+ /**
9879
+ * `textFollows` says whether the shell will send the key's text on its
9880
+ * own, as a `beforeinput`. False — a key that reached the canvas while
9881
+ * an editable held the focus — has the key inserted here, whatever the
9882
+ * text source; absent leaves it to the editing controller's setting.
9883
+ */
9884
+ keyDown(key, modifiers = noKeyModifiers(), textFollows) {
9501
9885
  if (!MODIFIER_KEYS.has(key)) this.focusManager.noteInput("keyboard");
9502
9886
  const event = new UiKeyboardEvent("keydown", key, modifiers);
9503
9887
  const focused = this.focusManager.focusedNode;
9504
9888
  const target = focused ?? this.root();
9505
9889
  this.dispatcher.dispatch(event, target);
9506
- if (!event.defaultPrevented && focused !== null && this.editing !== void 0 && this.editing.handleKey(focused, key, modifiers)) {
9890
+ if (!event.defaultPrevented && focused !== null && this.editing !== void 0 && this.editing.handleKey(focused, key, modifiers, textFollows === false ? true : void 0)) {
9507
9891
  event.preventDefault();
9508
9892
  return event;
9509
9893
  }
@@ -9579,7 +9963,7 @@ const EVERYTHING_SCROLLABLE = Object.freeze({
9579
9963
  */
9580
9964
  const SCROLL_EPSILON = .5;
9581
9965
  /** Whether an offset can still move in the direction `delta` points. */
9582
- function hasRoom(offset, max, delta) {
9966
+ function hasScrollRoom(offset, max, delta) {
9583
9967
  return delta > 0 ? offset < max - SCROLL_EPSILON : offset > SCROLL_EPSILON;
9584
9968
  }
9585
9969
  /** Whether this node keeps overscroll rather than chaining it outwards. */
@@ -9718,13 +10102,10 @@ var UiWheelController = class {
9718
10102
  if (!isScrollContainer(node)) continue;
9719
10103
  const state = this.scrollSink.containerState(node);
9720
10104
  if (state === void 0) continue;
9721
- if (state.horizontal) {
9722
- left ||= hasRoom(state.scrollX, state.maxScrollX, -1);
9723
- right ||= hasRoom(state.scrollX, state.maxScrollX, 1);
9724
- } else {
9725
- up ||= hasRoom(state.scrollY, state.maxScrollY, -1);
9726
- down ||= hasRoom(state.scrollY, state.maxScrollY, 1);
9727
- }
10105
+ left ||= hasScrollRoom(state.scrollX, state.maxScrollX, -1);
10106
+ right ||= hasScrollRoom(state.scrollX, state.maxScrollX, 1);
10107
+ up ||= hasScrollRoom(state.scrollY, state.maxScrollY, -1);
10108
+ down ||= hasScrollRoom(state.scrollY, state.maxScrollY, 1);
9728
10109
  if (containsOverscroll(node)) return EVERYTHING_SCROLLABLE;
9729
10110
  }
9730
10111
  if (containsOverscroll(rootOf(target))) return EVERYTHING_SCROLLABLE;
@@ -9751,7 +10132,7 @@ var UiWheelController = class {
9751
10132
  for (const node of this.scrollSink.scrollContainers?.() ?? []) {
9752
10133
  const state = this.scrollSink.containerState(node);
9753
10134
  if (state === void 0) continue;
9754
- if (state.horizontal ? state.maxScrollX > SCROLL_EPSILON : state.maxScrollY > SCROLL_EPSILON) return true;
10135
+ if (state.maxScrollX > SCROLL_EPSILON || state.maxScrollY > SCROLL_EPSILON) return true;
9755
10136
  }
9756
10137
  return false;
9757
10138
  }
@@ -9769,11 +10150,10 @@ var UiWheelController = class {
9769
10150
  if (!isScrollContainer(node)) continue;
9770
10151
  const state = this.scrollSink.containerState(node);
9771
10152
  if (state === void 0) continue;
9772
- const delta = state.horizontal ? deltaX : deltaY;
9773
- const offset = state.horizontal ? state.scrollX : state.scrollY;
9774
- const max = state.horizontal ? state.maxScrollX : state.maxScrollY;
9775
- if (delta !== 0 && hasRoom(offset, max, delta)) {
9776
- this.applyDelta(node, state, deltaX, deltaY, deltaMode, behaviorFor(node, deltaMode, wheelDeltaY));
10153
+ const takesX = deltaX !== 0 && hasScrollRoom(state.scrollX, state.maxScrollX, deltaX);
10154
+ const takesY = deltaY !== 0 && hasScrollRoom(state.scrollY, state.maxScrollY, deltaY);
10155
+ if (takesX || takesY) {
10156
+ this.applyDelta(node, state, takesX ? deltaX : 0, takesY ? deltaY : 0, deltaMode, behaviorFor(node, deltaMode, wheelDeltaY));
9777
10157
  event.markConsumed();
9778
10158
  return;
9779
10159
  }
@@ -9785,8 +10165,7 @@ var UiWheelController = class {
9785
10165
  if (containsOverscroll(rootOf(target))) event.markConsumed();
9786
10166
  }
9787
10167
  applyDelta(container, state, deltaX, deltaY, deltaMode, behavior) {
9788
- if (state.horizontal) this.scrollSink.scrollBy(container, this.toPixels(deltaX, deltaMode, state.viewportWidth), 0, behavior);
9789
- else this.scrollSink.scrollBy(container, 0, this.toPixels(deltaY, deltaMode, state.viewportHeight), behavior);
10168
+ this.scrollSink.scrollBy(container, deltaX === 0 ? 0 : this.toPixels(deltaX, deltaMode, state.viewportWidth), deltaY === 0 ? 0 : this.toPixels(deltaY, deltaMode, state.viewportHeight), behavior);
9790
10169
  }
9791
10170
  /**
9792
10171
  * A wheel delta in whatever unit it arrived in, as pixels.
@@ -10349,9 +10728,8 @@ var UiTouchScroller = class {
10349
10728
  this.sample(event.x, event.y);
10350
10729
  const resolved = this.resolve(event.target, dx, dy);
10351
10730
  if (resolved === null) return;
10352
- const { node, state } = resolved;
10353
- if (state.horizontal) this.scrollSink.scrollBy(node, dx, 0);
10354
- else this.scrollSink.scrollBy(node, 0, dy);
10731
+ const { node, takesX, takesY } = resolved;
10732
+ this.scrollSink.scrollBy(node, takesX ? dx : 0, takesY ? dy : 0);
10355
10733
  this.scrollSink.revealScrollbars?.(node);
10356
10734
  }
10357
10735
  panEnd(event) {
@@ -10364,12 +10742,11 @@ var UiTouchScroller = class {
10364
10742
  if (velocity === null) return;
10365
10743
  const resolved = this.resolve(event.target, velocity.x, velocity.y);
10366
10744
  if (resolved === null) return;
10367
- const { node, state } = resolved;
10368
- const speed = state.horizontal ? velocity.x : velocity.y;
10369
- if (Math.abs(speed) < this.flingVelocity) return;
10370
- const distance = speed * this.momentum;
10371
- if (state.horizontal) this.scrollSink.scrollBy(node, distance, 0, "smooth");
10372
- else this.scrollSink.scrollBy(node, 0, distance, "smooth");
10745
+ const { node, takesX, takesY } = resolved;
10746
+ const flingX = takesX && Math.abs(velocity.x) >= this.flingVelocity ? velocity.x * this.momentum : 0;
10747
+ const flingY = takesY && Math.abs(velocity.y) >= this.flingVelocity ? velocity.y * this.momentum : 0;
10748
+ if (flingX === 0 && flingY === 0) return;
10749
+ this.scrollSink.scrollBy(node, flingX, flingY, "smooth");
10373
10750
  }
10374
10751
  sample(x, y) {
10375
10752
  const t = this.now();
@@ -10396,7 +10773,7 @@ var UiTouchScroller = class {
10396
10773
  };
10397
10774
  }
10398
10775
  /**
10399
- * The container that should take this movement.
10776
+ * The container that should take this movement, and which of its axes.
10400
10777
  *
10401
10778
  * The nearest scroll container that can still move in the direction
10402
10779
  * asked for, walking outward. A container already at its end is
@@ -10404,6 +10781,18 @@ var UiTouchScroller = class {
10404
10781
  * that has hit its bottom keep scrolling the page it sits in. When
10405
10782
  * nothing can move, the innermost container is returned so the
10406
10783
  * gesture still belongs somewhere.
10784
+ *
10785
+ * **Each axis is asked for separately**, because a container may
10786
+ * scroll on both, and this used to pick one from the container's
10787
+ * flex direction and drop the other on the floor. A spreadsheet is
10788
+ * the case that found it on the wheel in 38e70a3: one `ScrollView`
10789
+ * whose content overflows in both directions, classified as vertical,
10790
+ * so a sideways drag moved nothing at all. This is the same fix on
10791
+ * the same shared state, so the two input paths cannot disagree about
10792
+ * which container takes a gesture.
10793
+ *
10794
+ * A container that overflows on one axis only is unaffected: the axis
10795
+ * it does not scroll has no room, so it is not taken.
10407
10796
  */
10408
10797
  resolve(target, dx, dy) {
10409
10798
  let innermost = null;
@@ -10411,16 +10800,19 @@ var UiTouchScroller = class {
10411
10800
  if (!isScrollContainer(node)) continue;
10412
10801
  const state = this.scrollSink.containerState(node);
10413
10802
  if (state === void 0) continue;
10803
+ const takesX = dx !== 0 && hasScrollRoom(state.scrollX, state.maxScrollX, dx);
10804
+ const takesY = dy !== 0 && hasScrollRoom(state.scrollY, state.maxScrollY, dy);
10414
10805
  innermost ??= {
10415
10806
  node,
10416
- state
10807
+ state,
10808
+ takesX: false,
10809
+ takesY: false
10417
10810
  };
10418
- const delta = state.horizontal ? dx : dy;
10419
- const offset = state.horizontal ? state.scrollX : state.scrollY;
10420
- const max = state.horizontal ? state.maxScrollX : state.maxScrollY;
10421
- if (delta < 0 && offset > 0 || delta > 0 && offset < max) return {
10811
+ if (takesX || takesY) return {
10422
10812
  node,
10423
- state
10813
+ state,
10814
+ takesX,
10815
+ takesY
10424
10816
  };
10425
10817
  }
10426
10818
  return innermost;
@@ -10430,6 +10822,505 @@ function defaultClock() {
10430
10822
  return typeof performance !== "undefined" ? performance.now() : Date.now();
10431
10823
  }
10432
10824
  //#endregion
10825
+ //#region src/input/UiEditingController.ts
10826
+ /** How long two presses may be apart and still count as a double click. */
10827
+ const MULTI_CLICK_MS = 500;
10828
+ const MULTI_CLICK_SLOP = 4;
10829
+ /** Room kept around the caret when scrolling it into view. */
10830
+ const CARET_REVEAL_PADDING = 4;
10831
+ /**
10832
+ * Editing behaviour for EditableText nodes.
10833
+ *
10834
+ * The keyboard and pointer controllers call in here as their default
10835
+ * behaviour for an editable target — after the app's listeners have
10836
+ * run and only when none of them called preventDefault() — and the
10837
+ * host feeds it the shell's text input: `beforeinput` edits,
10838
+ * composition, paste. The model on the node is the single source of
10839
+ * truth; this class turns intents into model changes, marks the node
10840
+ * dirty so layout and paint follow, reports the change through an
10841
+ * `input` event, and keeps the caret in view.
10842
+ *
10843
+ * Text arrives one of two ways. A shell with an editing proxy sends
10844
+ * `beforeinput`, where the browser has already resolved the IME, dead
10845
+ * keys and the keyboard layout; then printable key presses are not
10846
+ * text and `textFromKeys` is off. Without a proxy — tests, a bare
10847
+ * runtime — printable keys become insertions.
10848
+ */
10849
+ var UiEditingController = class {
10850
+ host;
10851
+ dispatcher;
10852
+ focus;
10853
+ /** Insert printable key presses as text; off when a shell delivers text through beforeinput. */
10854
+ textFromKeys = true;
10855
+ platform;
10856
+ paint = createPaintState();
10857
+ focusedEditable = null;
10858
+ visible = true;
10859
+ /** x the current run of vertical moves keeps returning to. */
10860
+ verticalGoalX = void 0;
10861
+ dragging = null;
10862
+ lastPress = null;
10863
+ compositionOpen = false;
10864
+ constructor(host, dispatcher, focus, options = {}) {
10865
+ this.host = host;
10866
+ this.dispatcher = dispatcher;
10867
+ this.focus = focus;
10868
+ this.platform = options.platform ?? detectEditingPlatform();
10869
+ this.focus.onFocusChange((node) => this.handleFocusChange(node));
10870
+ }
10871
+ /** The editable that holds focus, or null. */
10872
+ get focused() {
10873
+ return this.focusedEditable;
10874
+ }
10875
+ /** Whether the pointer controller should hand presses on the node to this controller. */
10876
+ isEditable(node) {
10877
+ return isEditableNode(node);
10878
+ }
10879
+ /**
10880
+ * Default keyboard behaviour for the focused editable. Returns true
10881
+ * when the key was an editing command, so the caller can mark the
10882
+ * event handled.
10883
+ */
10884
+ /** `textFromKeys` overrides the controller's own setting for this one key; see `UiKeyboardController.keyDown`. */
10885
+ handleKey(node, key, modifiers, textFromKeys = this.textFromKeys) {
10886
+ if (!isEditableNode(node)) return false;
10887
+ const command = commandForKey(key, modifiers, this.platform, textFromKeys);
10888
+ if (command === null) return false;
10889
+ return this.execute(node, command);
10890
+ }
10891
+ execute(node, command) {
10892
+ const model = editorFor(node);
10893
+ const readOnly = isReadOnly(node);
10894
+ switch (command.kind) {
10895
+ case "move":
10896
+ this.move(node, model, command.unit, command.direction, command.extend);
10897
+ return true;
10898
+ case "delete":
10899
+ if (readOnly) return true;
10900
+ return this.applyEdit(node, model, deleteInputType(command.unit, command.direction), null, () => {
10901
+ if (command.direction < 0) model.deleteBackward(command.unit);
10902
+ else model.deleteForward(command.unit);
10903
+ });
10904
+ case "newline":
10905
+ if (!isMultiline(node)) return false;
10906
+ if (readOnly) return true;
10907
+ return this.applyEdit(node, model, "insertLineBreak", "\n", () => model.insertText("\n"));
10908
+ case "insert":
10909
+ if (readOnly) return true;
10910
+ return this.applyEdit(node, model, "insertText", command.text, () => model.insertText(command.text));
10911
+ case "selectAll":
10912
+ model.selectAll();
10913
+ this.afterSelectionChange(node, model);
10914
+ return true;
10915
+ case "undo":
10916
+ if (readOnly) return true;
10917
+ this.history(node, model, "historyUndo", () => model.undo());
10918
+ return true;
10919
+ case "redo":
10920
+ if (readOnly) return true;
10921
+ this.history(node, model, "historyRedo", () => model.redo());
10922
+ return true;
10923
+ }
10924
+ }
10925
+ move(node, model, unit, direction, extend) {
10926
+ if (unit === "vertical") {
10927
+ const target = this.layoutOf(node).verticalMove(model.focus, direction, this.verticalGoalX);
10928
+ if (target === null) {
10929
+ model.moveTo(direction < 0 ? 0 : model.text.length, extend);
10930
+ this.verticalGoalX = void 0;
10931
+ } else {
10932
+ model.moveTo(target.offset, extend);
10933
+ this.verticalGoalX = target.x;
10934
+ }
10935
+ this.afterSelectionChange(node, model, true);
10936
+ return;
10937
+ }
10938
+ if (unit === "line") {
10939
+ const layout = this.layoutOf(node);
10940
+ const caret = layout.caretRect(model.focus);
10941
+ const line = layout.lines[caret.line];
10942
+ const hard = direction < 0 ? lineStartAt(model.text, model.focus) : lineEndAt(model.text, model.focus);
10943
+ const visual = direction < 0 ? line.start : line.end;
10944
+ model.moveTo(direction < 0 ? Math.max(hard, visual) : Math.min(hard, visual), extend);
10945
+ this.afterSelectionChange(node, model);
10946
+ return;
10947
+ }
10948
+ model.move(unit, direction, extend);
10949
+ this.afterSelectionChange(node, model);
10950
+ }
10951
+ /**
10952
+ * A `beforeinput` from the shell's editing proxy, in the DOM's
10953
+ * vocabulary. Returns true when it was applied to the focused
10954
+ * editable. Composition types are not handled here: the composition
10955
+ * methods carry them, and an engine that fires both must not insert
10956
+ * twice.
10957
+ */
10958
+ beforeInput(inputType, data) {
10959
+ const node = this.focusedEditable;
10960
+ if (node === null) return false;
10961
+ const model = editorFor(node);
10962
+ const readOnly = isReadOnly(node);
10963
+ switch (inputType) {
10964
+ case "insertText":
10965
+ case "insertReplacementText":
10966
+ case "insertFromDrop":
10967
+ case "insertFromYank":
10968
+ if (data === null || data.length === 0) return false;
10969
+ return readOnly || this.insert(node, model, inputType, data);
10970
+ case "insertFromPaste": return readOnly || data !== null && this.insert(node, model, inputType, data);
10971
+ case "insertLineBreak":
10972
+ case "insertParagraph":
10973
+ if (!isMultiline(node)) return false;
10974
+ return readOnly || this.applyEdit(node, model, inputType, "\n", () => model.insertText("\n"));
10975
+ case "deleteContentBackward":
10976
+ case "deleteWordBackward":
10977
+ case "deleteSoftLineBackward":
10978
+ case "deleteHardLineBackward":
10979
+ case "deleteContentForward":
10980
+ case "deleteWordForward":
10981
+ case "deleteSoftLineForward":
10982
+ case "deleteHardLineForward":
10983
+ case "deleteContent":
10984
+ case "deleteByCut":
10985
+ case "deleteByDrag": {
10986
+ if (readOnly) return true;
10987
+ const unit = /Word/.test(inputType) ? "word" : /Line/.test(inputType) ? "line" : "grapheme";
10988
+ const forward = /Forward/.test(inputType);
10989
+ return this.applyEdit(node, model, inputType, null, () => {
10990
+ if (inputType === "deleteContent" || inputType === "deleteByCut" || inputType === "deleteByDrag") model.replaceRange(model.start, model.end, "");
10991
+ else if (forward) model.deleteForward(unit);
10992
+ else model.deleteBackward(unit);
10993
+ });
10994
+ }
10995
+ case "historyUndo": return readOnly || this.history(node, model, inputType, () => model.undo());
10996
+ case "historyRedo": return readOnly || this.history(node, model, inputType, () => model.redo());
10997
+ default: return false;
10998
+ }
10999
+ }
11000
+ /** Inserts text at the selection of the focused editable, as typing would. */
11001
+ insertText(text) {
11002
+ const node = this.focusedEditable;
11003
+ if (node === null || isReadOnly(node)) return false;
11004
+ return this.insert(node, editorFor(node), "insertText", text);
11005
+ }
11006
+ /**
11007
+ * Replaces the focused editable's whole text, as an assistive
11008
+ * technology's "set value" does.
11009
+ *
11010
+ * `insertReplacementText` is the DOM's own input type for this, so an
11011
+ * `onBeforeInput` that vets edits sees the same vocabulary it sees
11012
+ * for typing, and an app that rejects the edit rejects this one too.
11013
+ * Selecting everything first is what makes it one undo step and one
11014
+ * `onInput` rather than a delete and an insert.
11015
+ */
11016
+ replaceText(text) {
11017
+ const node = this.focusedEditable;
11018
+ if (node === null || isReadOnly(node)) return false;
11019
+ const model = editorFor(node);
11020
+ model.selectAll();
11021
+ return this.insert(node, model, "insertReplacementText", text);
11022
+ }
11023
+ /**
11024
+ * Pastes text: newlines are kept in a multiline field and become
11025
+ * spaces in a single-line one.
11026
+ *
11027
+ * With nothing editable focused the text is offered to whatever is,
11028
+ * as a `Paste` event, instead of being dropped. That is the case a
11029
+ * grid needs: it owns a rectangle of cells, a block of tab-separated
11030
+ * text means something to it that it means to nothing else, and
11031
+ * before this there was no way for it to hear about one at all.
11032
+ */
11033
+ paste(text) {
11034
+ const node = this.focusedEditable;
11035
+ if (node === null || isReadOnly(node)) return this.offerPaste(text);
11036
+ return this.insert(node, editorFor(node), "insertFromPaste", text);
11037
+ }
11038
+ offerPaste(text) {
11039
+ const target = this.focus.focusedNode;
11040
+ if (target === null) return false;
11041
+ const event = new UiPasteEvent(text);
11042
+ this.dispatcher.dispatch(event, target);
11043
+ return event.defaultPrevented;
11044
+ }
11045
+ insert(node, model, inputType, text) {
11046
+ const data = isMultiline(node) ? text : text.replace(/\r\n|\r|\n/g, " ");
11047
+ return this.applyEdit(node, model, inputType, data, () => model.insertText(data));
11048
+ }
11049
+ compositionStart() {
11050
+ const node = this.focusedEditable;
11051
+ if (node === null || isReadOnly(node)) return;
11052
+ const model = editorFor(node);
11053
+ model.beginComposition();
11054
+ this.compositionOpen = true;
11055
+ this.afterTextChange(node, model, false);
11056
+ }
11057
+ /** The composition text so far and the caret within it. */
11058
+ compositionUpdate(text, caret = text.length) {
11059
+ const node = this.focusedEditable;
11060
+ if (node === null || isReadOnly(node)) return;
11061
+ const model = editorFor(node);
11062
+ model.updateComposition(text, caret);
11063
+ this.compositionOpen = true;
11064
+ this.afterTextChange(node, model, false);
11065
+ }
11066
+ /** The IME committed `text` (possibly empty: the composition was cancelled). */
11067
+ compositionEnd(text) {
11068
+ const node = this.focusedEditable;
11069
+ this.compositionOpen = false;
11070
+ if (node === null || isReadOnly(node)) return;
11071
+ const model = editorFor(node);
11072
+ const data = isMultiline(node) ? text : text.replace(/\r\n|\r|\n/g, " ");
11073
+ const before = model.text;
11074
+ model.commitComposition(data);
11075
+ this.afterTextChange(node, model, before !== model.text);
11076
+ }
11077
+ /**
11078
+ * A press on an editable places the caret (Shift extends the
11079
+ * selection to it); a second press within the double-click window
11080
+ * selects the word, a third the line. Dragging afterwards extends the
11081
+ * selection from the anchor.
11082
+ */
11083
+ pointerDown(node, x, y, modifiers) {
11084
+ if (!isEditableNode(node)) return;
11085
+ const model = editorFor(node);
11086
+ const layout = this.layoutOf(node);
11087
+ const local = this.host.toLocal(node, x, y);
11088
+ const offset = layout.offsetAt(local.x, local.y);
11089
+ const now = this.host.now();
11090
+ const last = this.lastPress;
11091
+ const count = !modifiers.shift && last !== null && last.node === node && now - last.at <= MULTI_CLICK_MS && Math.abs(last.x - x) <= MULTI_CLICK_SLOP && Math.abs(last.y - y) <= MULTI_CLICK_SLOP ? last.count + 1 : 1;
11092
+ this.lastPress = {
11093
+ node,
11094
+ x,
11095
+ y,
11096
+ at: now,
11097
+ count
11098
+ };
11099
+ this.dragging = node;
11100
+ if (modifiers.shift) model.moveTo(offset, true);
11101
+ else if (count === 2) {
11102
+ const word = wordRangeAt(model.text, offset);
11103
+ model.select(word.start, word.end);
11104
+ } else if (count >= 3) model.select(lineStartAt(model.text, offset), lineEndAt(model.text, offset));
11105
+ else model.select(offset);
11106
+ model.breakUndoGroup();
11107
+ this.afterSelectionChange(node, model);
11108
+ }
11109
+ pointerMove(node, x, y) {
11110
+ if (this.dragging !== node) return;
11111
+ const model = editorFor(node);
11112
+ const layout = this.layoutOf(node);
11113
+ const local = this.host.toLocal(node, x, y);
11114
+ const offset = layout.offsetAt(local.x, local.y);
11115
+ if (offset !== model.focus) {
11116
+ model.moveTo(offset, true);
11117
+ this.afterSelectionChange(node, model);
11118
+ }
11119
+ }
11120
+ pointerUp() {
11121
+ this.dragging = null;
11122
+ }
11123
+ /** The focused editable's state for the shell to mirror, or null. */
11124
+ state() {
11125
+ const node = this.focusedEditable;
11126
+ if (node === null || this.host.recordFor(node) === void 0) return null;
11127
+ const model = editorFor(node);
11128
+ const caret = this.layoutOf(node).caretRect();
11129
+ const visible = this.host.visibleBox(node);
11130
+ return {
11131
+ text: model.text,
11132
+ selectionStart: model.start,
11133
+ selectionEnd: model.end,
11134
+ caret: {
11135
+ x: visible.x + caret.x,
11136
+ y: visible.y + caret.y,
11137
+ width: 1,
11138
+ height: caret.height
11139
+ },
11140
+ multiline: isMultiline(node),
11141
+ composing: model.composing
11142
+ };
11143
+ }
11144
+ /**
11145
+ * When the focused caret next toggles, or undefined when nothing
11146
+ * blinks: no focused editable, a composition in progress (the caret
11147
+ * holds), or a hidden page.
11148
+ */
11149
+ nextCaretChange(now) {
11150
+ const node = this.focusedEditable;
11151
+ if (node === null || !this.visible) return;
11152
+ const model = editorFor(node);
11153
+ if (model.composing || !model.collapsed) return;
11154
+ return nextCaretToggle(model, now);
11155
+ }
11156
+ /**
11157
+ * The page was hidden or shown. A hidden page stops the blink so a
11158
+ * background tab schedules no frames; showing it restarts the caret.
11159
+ */
11160
+ setVisible(visible) {
11161
+ if (this.visible === visible) return;
11162
+ this.visible = visible;
11163
+ const node = this.focusedEditable;
11164
+ if (node !== null) {
11165
+ editorFor(node).blinkOrigin = this.host.now();
11166
+ this.host.markDirty(node, 2);
11167
+ }
11168
+ }
11169
+ /** Whether the caret may be drawn at all (the page is visible). */
11170
+ get caretEnabled() {
11171
+ return this.visible;
11172
+ }
11173
+ handleFocusChange(node) {
11174
+ const previous = this.focusedEditable;
11175
+ const next = node !== null && isEditableNode(node) ? node : null;
11176
+ if (previous === next) return;
11177
+ if (previous !== null) {
11178
+ const model = editorFor(previous);
11179
+ if (model.composing) model.commitComposition(model.text.slice(model.composition.start, model.composition.end));
11180
+ model.focused = false;
11181
+ this.host.markDirty(previous, 2);
11182
+ }
11183
+ this.focusedEditable = next;
11184
+ this.compositionOpen = false;
11185
+ this.verticalGoalX = void 0;
11186
+ if (next !== null) {
11187
+ const model = editorFor(next);
11188
+ model.focused = true;
11189
+ model.blinkOrigin = this.host.now();
11190
+ this.host.markDirty(next, 2);
11191
+ }
11192
+ }
11193
+ /**
11194
+ * Runs an edit unless a `beforeinput` listener cancels it, then
11195
+ * reports it. Returns true when the input was consumed either way.
11196
+ */
11197
+ applyEdit(node, model, inputType, data, run) {
11198
+ const before = new UiBeforeInputEvent(inputType, data);
11199
+ this.dispatcher.dispatch(before, node);
11200
+ if (before.defaultPrevented) return true;
11201
+ const text = model.text;
11202
+ run();
11203
+ this.afterTextChange(node, model, text !== model.text);
11204
+ return true;
11205
+ }
11206
+ history(node, model, inputType, run) {
11207
+ const before = new UiBeforeInputEvent(inputType, null);
11208
+ this.dispatcher.dispatch(before, node);
11209
+ if (before.defaultPrevented) return true;
11210
+ const text = model.text;
11211
+ run();
11212
+ this.afterTextChange(node, model, text !== model.text);
11213
+ return true;
11214
+ }
11215
+ afterTextChange(node, model, changed) {
11216
+ this.verticalGoalX = void 0;
11217
+ model.blinkOrigin = this.host.now();
11218
+ this.host.markDirty(node, changed || this.compositionOpen ? 5 : 2);
11219
+ if (changed) this.dispatcher.dispatch(new UiTextChangeEvent(model.text, model.start, model.end), node);
11220
+ this.notifySelection(node, model);
11221
+ this.revealCaret(node);
11222
+ }
11223
+ /**
11224
+ * Reports the selection, if it has actually moved since last time.
11225
+ *
11226
+ * The guard is what keeps this quiet: holding a right arrow against
11227
+ * the end of the text moves nothing and must say nothing, and a
11228
+ * listener that recomputed decoration on every key would otherwise
11229
+ * do it forever for no reason.
11230
+ *
11231
+ * `hasListeners` already makes an unheard dispatch free, so the
11232
+ * cost when nobody is listening is the two comparisons here.
11233
+ */
11234
+ notifySelection(node, model) {
11235
+ if (model.anchor === model.notifiedAnchor && model.focus === model.notifiedFocus) return;
11236
+ model.notifiedAnchor = model.anchor;
11237
+ model.notifiedFocus = model.focus;
11238
+ this.dispatcher.dispatch(new UiSelectionChangeEvent(model.text, model.start, model.end, model.anchor, model.focus), node);
11239
+ }
11240
+ afterSelectionChange(node, model, keepGoal = false) {
11241
+ if (!keepGoal) this.verticalGoalX = void 0;
11242
+ model.blinkOrigin = this.host.now();
11243
+ this.host.markDirty(node, 2);
11244
+ this.notifySelection(node, model);
11245
+ this.revealCaret(node);
11246
+ }
11247
+ /**
11248
+ * Asks the host to scroll the caret into view, when the node has been
11249
+ * laid out: the field's own text first, then any scroll container
11250
+ * around it.
11251
+ *
11252
+ * `layoutOf` places the text where it is seen, so the caret comes
11253
+ * back with the field's own scroll already taken off; the reveal is
11254
+ * asked for in the node's unscrolled coordinates, which is where the
11255
+ * offset it wants is measured from.
11256
+ */
11257
+ revealCaret(node) {
11258
+ const rec = this.host.recordFor(node);
11259
+ if (rec === void 0) return;
11260
+ const caret = this.layoutOf(node).caretRect();
11261
+ this.host.reveal(node, {
11262
+ x: caret.x + rec.scrollX - CARET_REVEAL_PADDING,
11263
+ y: caret.y + rec.scrollY - CARET_REVEAL_PADDING,
11264
+ width: 9,
11265
+ height: caret.height + 8
11266
+ });
11267
+ }
11268
+ /**
11269
+ * The node's text laid out in node-local coordinates: the content box
11270
+ * starts at the padding and is shifted by the field's own scroll, as
11271
+ * it is for the renderers in the parent's space. Points from the
11272
+ * pointer and the caret box the shell is given are both in that same
11273
+ * seen space, so neither has to know the offset. Before the first
11274
+ * layout the record is empty and the lines fall at the origin, which
11275
+ * is still a valid place for a caret.
11276
+ */
11277
+ /**
11278
+ * Where the caret is inside a field, in the field's own coordinates.
11279
+ *
11280
+ * Relative to the node's border-box origin, with the field's own
11281
+ * scroll already applied — so a popup placed under the caret adds
11282
+ * the node's position on screen and nothing else.
11283
+ *
11284
+ * Public because an application cannot work this out. The geometry
11285
+ * needs the paragraph as it was *laid out*, which means the
11286
+ * measurer, the resolved paint and the layout record, and an
11287
+ * application that re-measured the text to find the caret would be
11288
+ * a second measurer that must never disagree with this one. Asking
11289
+ * is the only version that stays right.
11290
+ *
11291
+ * Null when the node is not an editable, or has not been laid out.
11292
+ */
11293
+ caretRectOf(node) {
11294
+ if (!isEditableNode(node) || this.host.recordFor(node) === void 0) return null;
11295
+ return this.layoutOf(node).caretRect();
11296
+ }
11297
+ layoutOf(node) {
11298
+ const rec = this.host.recordFor(node);
11299
+ const state = resolvePaintState(node, this.paint);
11300
+ const box = rec === void 0 ? {
11301
+ x: 0,
11302
+ y: 0,
11303
+ width: 0,
11304
+ height: 0
11305
+ } : {
11306
+ x: rec.paddingLeft - rec.scrollX,
11307
+ y: rec.paddingTop - rec.scrollY,
11308
+ width: Math.max(0, rec.width - rec.paddingLeft - rec.paddingRight),
11309
+ height: Math.max(0, rec.height - rec.paddingTop - rec.paddingBottom)
11310
+ };
11311
+ return new EditableLayout(state.editor, box, state, this.host.measurer);
11312
+ }
11313
+ };
11314
+ function deleteInputType(unit, direction) {
11315
+ const side = direction < 0 ? "Backward" : "Forward";
11316
+ switch (unit) {
11317
+ case "word": return `deleteWord${side}`;
11318
+ case "line":
11319
+ case "document": return `deleteSoftLine${side}`;
11320
+ default: return `deleteContent${side}`;
11321
+ }
11322
+ }
11323
+ //#endregion
10433
11324
  //#region src/input/UiPlatformAdapter.ts
10434
11325
  /**
10435
11326
  * Bridges platform/browser input events to the framework input
@@ -12157,8 +13048,6 @@ const SCROLLBAR_FADE_MS = 350;
12157
13048
  * The limit is what stops a cycle of anchors from spinning.
12158
13049
  */
12159
13050
  const ANCHOR_CHAIN_LIMIT = 4;
12160
- /** An editable's text is the user's; runs are a property of a `Text` node. */
12161
- const EMPTY_TEXT_SPANS = [];
12162
13051
  /**
12163
13052
  * Whether a node names any of the four logical horizontal insets, and
12164
13053
  * so needs its writing direction resolved before its box is worked
@@ -13561,7 +14450,7 @@ var LayoutEngine = class {
13561
14450
  contentMain = Math.max(contentMain, this.lineOuterMain(line, config.gapMain, "final"));
13562
14451
  } else {
13563
14452
  for (const item of line.items) item.finalMain = item.hypotheticalMain;
13564
- contentMain = Math.max(contentMain, lineTotal);
14453
+ contentMain = Math.max(contentMain, this.lineMaxContentMain(line, config.gapMain));
13565
14454
  }
13566
14455
  }
13567
14456
  if (mainDefinite) contentMain = Math.max(mainMax, contentMain);
@@ -13668,6 +14557,7 @@ var LayoutEngine = class {
13668
14557
  child,
13669
14558
  rec: cRec,
13670
14559
  baseMain,
14560
+ maxContentMain: measuredMain,
13671
14561
  hypotheticalMain: this.clamp(baseMain, minMain, maxMain),
13672
14562
  finalMain: baseMain,
13673
14563
  cross: row ? cRec.measuredHeight : cRec.measuredWidth,
@@ -13776,6 +14666,33 @@ var LayoutEngine = class {
13776
14666
  });
13777
14667
  return lines;
13778
14668
  }
14669
+ /**
14670
+ * A line's main size when the container shrink-wraps it.
14671
+ *
14672
+ * The sum of the hypothetical sizes would be the answer if an item
14673
+ * never grew — but `flex: 1` is `flex-basis: 0`, and an item that
14674
+ * starts flexing from zero asks for nothing while its content still
14675
+ * wants the width of its text. A container wrapped around that sum
14676
+ * comes out too narrow; the item's text then wraps inside a box that
14677
+ * was measured as though it had not, and the container is left
14678
+ * shorter than the children it draws — a menu whose last rows fall
14679
+ * out of its own panel.
14680
+ *
14681
+ * So an item that can grow contributes whichever is larger, the size
14682
+ * its basis gives it or the size its content asks for. Chrome ignores
14683
+ * the basis entirely here and browsers disagree with each other about
14684
+ * it; the larger of the two is the reading that keeps `flex-basis`
14685
+ * counting for something, which is what `basis/sizes-container`
14686
+ * already records this engine as doing.
14687
+ */
14688
+ lineMaxContentMain(line, gap) {
14689
+ let total = gap * (line.items.length - 1);
14690
+ for (const item of line.items) {
14691
+ const wanted = item.grow > 0 ? this.clamp(item.maxContentMain, item.minMain, item.maxMain) : 0;
14692
+ total += Math.max(item.hypotheticalMain, wanted) + item.marginMainStart + item.marginMainEnd;
14693
+ }
14694
+ return total;
14695
+ }
13779
14696
  lineOuterMain(line, gap, size) {
13780
14697
  let total = gap * (line.items.length - 1);
13781
14698
  for (const item of line.items) total += (size === "final" ? item.finalMain : item.hypotheticalMain) + item.marginMainStart + item.marginMainEnd;
@@ -14104,8 +15021,9 @@ var LayoutEngine = class {
14104
15021
  const paddingV = rec.paddingTop + rec.paddingBottom;
14105
15022
  const editable = node.type === "editable-text";
14106
15023
  if (node.type === "text" || node.type === "button" || editable) {
14107
- const spans = editable ? EMPTY_TEXT_SPANS : resolvedSpansOf(node);
14108
- const text = editable ? editorFor(node).text : spans.length > 0 ? textContentOf(node) : String(node.properties.get("text") ?? "");
15024
+ const typed = editable ? editorFor(node).text : "";
15025
+ const spans = editable ? editableSpansOf(node, typed) : resolvedSpansOf(node);
15026
+ const text = editable ? typed : spans.length > 0 ? textContentOf(node) : String(node.properties.get("text") ?? "");
14109
15027
  const placeholderProp = node.properties.get("placeholder");
14110
15028
  const placeholder = editable && text.length === 0 && typeof placeholderProp === "string" ? placeholderProp : void 0;
14111
15029
  const font = resolveFont(node);
@@ -14339,6 +15257,7 @@ var LayoutEngine = class {
14339
15257
  };
14340
15258
  this.resolveLayoutProps(child, cRec);
14341
15259
  const anchor = child.properties.get("anchor");
15260
+ const point = child.properties.get("anchorPoint");
14342
15261
  if (anchor !== void 0 && anchor !== null) {
14343
15262
  this.anchoredNodes.add(child);
14344
15263
  this.trackAnchor(child, anchor);
@@ -14347,6 +15266,12 @@ var LayoutEngine = class {
14347
15266
  this.placeAnchored(child, cRec, anchorRec, anchor, block);
14348
15267
  return;
14349
15268
  }
15269
+ } else if (point !== void 0 && point !== null) {
15270
+ this.anchoredNodes.delete(child);
15271
+ this.trackAnchor(child, null);
15272
+ this.measure(child, new Constraints(0, Math.max(0, block.width), 0, Math.max(0, block.height)));
15273
+ this.placeBeside(child, cRec, block.x + point.x, block.y + point.y, 0, 0, block);
15274
+ return;
14350
15275
  } else {
14351
15276
  this.anchoredNodes.delete(child);
14352
15277
  this.trackAnchor(child, null);
@@ -14355,9 +15280,11 @@ var LayoutEngine = class {
14355
15280
  const marginV = cRec.marginTop + cRec.marginBottom;
14356
15281
  const availableWidth = Math.max(0, block.width - (cRec.left ?? 0) - (cRec.right ?? 0) - marginH);
14357
15282
  const availableHeight = Math.max(0, block.height - (cRec.top ?? 0) - (cRec.bottom ?? 0) - marginV);
14358
- const tightWidth = cRec.left !== void 0 && cRec.right !== void 0 && this.lengthProp(child, "width", block.width) === void 0;
14359
- const tightHeight = cRec.top !== void 0 && cRec.bottom !== void 0 && this.lengthProp(child, "height", block.height) === void 0;
14360
- this.measure(child, new Constraints(tightWidth ? availableWidth : 0, availableWidth, tightHeight ? availableHeight : 0, availableHeight));
15283
+ const explicitWidth = this.lengthProp(child, "width", block.width) !== void 0;
15284
+ const explicitHeight = this.lengthProp(child, "height", block.height) !== void 0;
15285
+ const tightWidth = cRec.left !== void 0 && cRec.right !== void 0 && !explicitWidth;
15286
+ const tightHeight = cRec.top !== void 0 && cRec.bottom !== void 0 && !explicitHeight;
15287
+ this.measure(child, new Constraints(tightWidth ? availableWidth : 0, explicitWidth ? Infinity : availableWidth, tightHeight ? availableHeight : 0, explicitHeight ? Infinity : availableHeight));
14361
15288
  const width = cRec.measuredWidth;
14362
15289
  const height = cRec.measuredHeight;
14363
15290
  let x;
@@ -14385,18 +15312,25 @@ var LayoutEngine = class {
14385
15312
  */
14386
15313
  placeAnchored(child, cRec, anchorRec, anchor, block) {
14387
15314
  this.measure(child, new Constraints(0, Math.max(0, block.width), 0, Math.max(0, block.height)));
14388
- const width = cRec.measuredWidth;
14389
- const height = cRec.measuredHeight;
14390
- const gap = this.numberProp(child, "anchorOffset") ?? 0;
14391
- const { side, align } = this.parsePlacement(child.properties.get("placement"));
14392
15315
  const scrollAnchor = this.scrollOffsetOf(anchor);
14393
15316
  const scrollChild = this.scrollOffsetOf(child);
14394
15317
  const stickyAnchor = this.stickyOffsetOf(anchor);
14395
15318
  const stickyChild = this.stickyOffsetOf(child);
14396
15319
  const ax = anchorRec.x + stickyAnchor.x - scrollAnchor.x + scrollChild.x - stickyChild.x;
14397
15320
  const ay = anchorRec.y + stickyAnchor.y - scrollAnchor.y + scrollChild.y - stickyChild.y;
14398
- const aw = anchorRec.width;
14399
- const ah = anchorRec.height;
15321
+ this.placeBeside(child, cRec, ax, ay, anchorRec.width, anchorRec.height, block);
15322
+ }
15323
+ /**
15324
+ * Places a measured node beside a box — an anchor's, or a point's of
15325
+ * no size — flipping to the opposite side when the named one would
15326
+ * overflow the block and the other has more room, and clamping along
15327
+ * the side to stay inside it.
15328
+ */
15329
+ placeBeside(child, cRec, ax, ay, aw, ah, block) {
15330
+ const width = cRec.measuredWidth;
15331
+ const height = cRec.measuredHeight;
15332
+ const gap = this.numberProp(child, "anchorOffset") ?? 0;
15333
+ const { side, align } = this.parsePlacement(child.properties.get("placement"));
14400
15334
  const vertical = side === "top" || side === "bottom";
14401
15335
  let resolvedSide = side;
14402
15336
  if (vertical) {
@@ -14847,6 +15781,7 @@ var LayoutEngine = class {
14847
15781
  */
14848
15782
  markLayoutDirty(node, selfCanBound = false) {
14849
15783
  let current = node;
15784
+ let bounded = false;
14850
15785
  for (;;) {
14851
15786
  const rec = this.records.get(current);
14852
15787
  if (rec !== void 0) {
@@ -14854,12 +15789,35 @@ var LayoutEngine = class {
14854
15789
  rec.placeDirty = true;
14855
15790
  }
14856
15791
  if (current === this.layoutRoot || current.parent === null) {
14857
- this.relayoutRoots.add(this.layoutRoot ?? current);
15792
+ if (!bounded) this.relayoutRoots.add(this.layoutRoot ?? current);
14858
15793
  return;
14859
15794
  }
14860
- if (rec?.relayoutBoundary && !rec.positioned && (current !== node || selfCanBound)) {
15795
+ if (!bounded && rec?.relayoutBoundary && !rec.positioned && (current !== node || selfCanBound)) {
14861
15796
  this.relayoutRoots.add(current);
14862
- return;
15797
+ /**
15798
+ * The boundary is where the *relayout* starts, and the walk
15799
+ * carries on past it marking ancestors.
15800
+ *
15801
+ * Stopping here as well is sound only while the pass really
15802
+ * does start at this boundary. It does not always: another
15803
+ * node dirtied in the same frame can drag the layout root
15804
+ * into `relayoutRoots`, and then the pass is a `fullLayout`
15805
+ * that measures from the root — down through ancestors this
15806
+ * walk left clean, every one of which short-circuits on its
15807
+ * own measure memo and never reaches the node that changed.
15808
+ *
15809
+ * Found with a bar switched into the chrome by choosing a
15810
+ * menu item: the menu's own teardown put the root in the set,
15811
+ * the frame did a full layout, and `measure` was never called
15812
+ * on the column that had gained a child. It drew at zero
15813
+ * height, with every spec passing — a spec asks what a node's
15814
+ * properties are, and they were all correct.
15815
+ *
15816
+ * Marking to the root costs a walk of the depth and two
15817
+ * booleans a node, against a memo that is otherwise believed
15818
+ * when it should not be.
15819
+ */
15820
+ bounded = true;
14863
15821
  }
14864
15822
  current = current.parent;
14865
15823
  }
@@ -15239,10 +16197,26 @@ var LayoutEngine = class {
15239
16197
  */
15240
16198
  assignBox(node, x, y, width, height) {
15241
16199
  const rec = this.record(node);
15242
- if (rec.positioned && !rec.absolute && node !== this.layoutRoot) {
16200
+ if (rec.positioned && !rec.absolute && !rec.sticky && node !== this.layoutRoot) {
15243
16201
  x += rec.left ?? (rec.right !== void 0 ? -rec.right : 0);
15244
16202
  y += rec.top ?? (rec.bottom !== void 0 ? -rec.bottom : 0);
15245
16203
  }
16204
+ /**
16205
+ * A **sticky** node is not offset by its insets, and that is the
16206
+ * difference between the two.
16207
+ *
16208
+ * `top: 0` on a sticky header is a threshold — "do not let me go
16209
+ * above the top of the scrollport" — and not a displacement, which
16210
+ * is what CSS means by it and what `resolveStickyOffset` below
16211
+ * assumes when it computes `viewLeft + rec.left - rec.x`. Offset
16212
+ * here *and* used as a threshold there, an inset is applied twice
16213
+ * and the two errors cancel only when it is zero.
16214
+ *
16215
+ * Which is why nothing noticed: every sticky node in the framework
16216
+ * and its examples sticks at zero. The first one that did not — a
16217
+ * spreadsheet's second frozen column, which sticks at the width of
16218
+ * the gutter — was drawn one gutter too far to the right.
16219
+ */
15246
16220
  if (rec.x !== x || rec.y !== y || rec.width !== width || rec.height !== height) {
15247
16221
  rec.x = x;
15248
16222
  rec.y = y;
@@ -15482,12 +16456,37 @@ function drawOverlayShapes(ctx, shapes) {
15482
16456
  */
15483
16457
  function decorationRect(shape, rec, nodeRadius) {
15484
16458
  const outset = shape.outset ?? 0;
16459
+ const horizontal = axis(rec.x, rec.width, shape.x, shape.width, shape.right, outset);
16460
+ const vertical = axis(rec.y, rec.height, shape.y, shape.height, shape.bottom, outset);
16461
+ const radius = Math.max(0, shape.radius ?? nodeRadius + outset);
16462
+ return {
16463
+ x: horizontal.start,
16464
+ y: vertical.start,
16465
+ width: horizontal.size,
16466
+ height: vertical.size,
16467
+ radius
16468
+ };
16469
+ }
16470
+ /**
16471
+ * One axis of the placement, from any two of near, size and far.
16472
+ *
16473
+ * The outset applies after the three are reconciled, growing the
16474
+ * rectangle by it on both sides, so it means the same thing however
16475
+ * the rectangle was described.
16476
+ */
16477
+ function axis(origin, extent, near, size, far, outset) {
16478
+ let start;
16479
+ let length;
16480
+ if (near === void 0 && size !== void 0 && far !== void 0) {
16481
+ length = size;
16482
+ start = extent - far - size;
16483
+ } else {
16484
+ start = near ?? 0;
16485
+ length = size ?? extent - start - (far ?? 0);
16486
+ }
15485
16487
  return {
15486
- x: rec.x + (shape.x ?? 0) - outset,
15487
- y: rec.y + (shape.y ?? 0) - outset,
15488
- width: Math.max(0, (shape.width ?? rec.width) + 2 * outset),
15489
- height: Math.max(0, (shape.height ?? rec.height) + 2 * outset),
15490
- radius: Math.max(0, shape.radius ?? nodeRadius + outset)
16488
+ start: origin + start - outset,
16489
+ size: Math.max(0, length + 2 * outset)
15491
16490
  };
15492
16491
  }
15493
16492
  /**
@@ -17108,12 +18107,16 @@ var Canvas2DRenderer = class {
17108
18107
  paintEditable(ctx, paint, context) {
17109
18108
  const model = paint.editor;
17110
18109
  const layout = new EditableLayout(model, this.contentBox, paint, context.text);
18110
+ fillTextRects(ctx, textRunBackgrounds(layout.lines, paint));
17111
18111
  if (model.focused && !model.collapsed) {
17112
18112
  ctx.fillStyle = colorToCss(paint.selectionColor);
17113
18113
  for (const box of layout.selectionBoxes()) ctx.fillRect(box.x, box.y, box.width, box.height);
17114
18114
  }
17115
18115
  if (layout.placeholderLines.length > 0) drawTextLines(ctx, layout.placeholderLines, buildFontString(paint), colorToCss(paint.placeholderColor), paint.letterSpacing, paint.rtl);
17116
- else drawTextLines(ctx, layout.lines, buildFontString(paint), colorToCss(paint.textColor), paint.letterSpacing, paint.rtl);
18116
+ else {
18117
+ drawTextLines(ctx, layout.lines, buildFontString(paint), colorToCss(paint.textColor), paint.letterSpacing, paint.rtl, paint);
18118
+ fillTextRects(ctx, textRunDecorations(layout.lines, paint));
18119
+ }
17117
18120
  if (model.composing) {
17118
18121
  ctx.fillStyle = colorToCss(paint.textColor);
17119
18122
  for (const box of layout.compositionBoxes()) {
@@ -17297,6 +18300,6 @@ function createCanvasSurface(host) {
17297
18300
  return new CanvasSurface(host);
17298
18301
  }
17299
18302
  //#endregion
17300
- export { prepareInputSurface as $, textContentOf as $i, nextWordEnd as $n, borderRadius as $r, createPaintState as $t, accumulatedOffsetTo as A, UiBeforeInputEvent as Aa, raiseContrast as Ai, editorFor as An, pathValuesEqual as Ar, EditableLayout as At, parseFlexDirection as B, UiWheelEvent as Ba, boxShadowArraysEqual as Bi, caretRectFor as Bn, radialGradient as Br, textMeasureRequest as Bt, describeLength as C, writeOverrideProperty as Ca, hasThemeExtension as Ci, linkOf as Cn, statesEqual as Cr, wordRangeIn as Ct, formatNumber as D, visualState as Da, withThemeExtension as Di, CARET_WIDTH as Dn, validateSubgrid as Dr, ELLIPSIS as Dt, formatExplanation as E, hasVisualState as Ea, themeExtensionsEqual as Ei, CARET_BLINK_MS as En, UI_SUBGRID_AXES as Er, spannedRunsFor as Et, constraintsEqual as F, UiKeyboardEvent as Fa, spacingEqual as Fi, nextCaretToggle as Fn, transformsEqual as Fr, fillTextRects as Ft, MainAxisAlignment as G, UiBinding as Ga, defaultTypography as Gi, offsetAtX as Gn, isFrLength as Gr, fontStackFor as Gt, sizeGridTracks as H, wheelDeltaYOf as Ha, defaultShapes as Hi, lineIndexForOffset as Hn, auto as Hr, textRunDecorations as Ht, tightenConstraints as I, UiPinchEvent as Ia, spacingSteps as Ii, syncEditorValue as In, MAX_GRADIENT_STOPS as Ir, layoutTextLines as It, parseMainAxisAlignment as J, defaultTextStyle as Ji, EditableTextModel as Jn, minmax as Jr, DEFAULT_FONT_SIZE as Jt, parseAlignContent as K, isTypographyRole as Ki, offsetForVerticalMove as Kn, isMinMaxTrack as Kr, registerFontStack as Kt, MAX_MEASURES_PER_CHILD as L, UiPointerEvent as La, defaultShadows as Li, resolveColorValue as Ln, gradientPaint as Lr, runCanvasStyleInto as Lt, LayoutRecord as M, UiFocusEvent as Ma, densityFactors as Mi, isEditableNode as Mn, parseTransform as Mr, buildFontString as Mt, Constraints as N, UiGestureEvent as Na, defaultSpacing as Ni, isMultiline as Nn, transform as Nr, createCanvasTextStyle as Nt, labelNode as O, visualStatesEqual as Oa, contrastRatio as Oi, EDITOR_PROP as On, inputsEqual as Or, layoutParagraph as Ot, clampSize as P, UiInputEvent as Pa, scaleSpacing as Pi, isReadOnly as Pn, transformIsIdentity as Pr, drawText as Pt, pointerDeviceOf as Q, spannedTextOf as Qi, nextGraphemeEnd as Qn, UiBorderRadiuses as Qr, computeObjectFitRect as Qt, isLayoutProtocol as R, UiTextChangeEvent as Ra, shadowsEqual as Ri, isVideoSurface as Rn, gradientsEqual as Rr, runFontString as Rt, SCROLLBAR_LINGER_MS as S, writeDeclaredProperty as Sa, defineThemeExtension as Si, linkHoverOf as Sn, normalizeStates as Sr, selectionRectsIn as St, formatConstraints as T, defaultVisualState as Ta, themeExtension as Ti, setLinkHover as Tn, validateStates as Tr, ParagraphTextMeasurer as Tt, AlignContent as U, UiNodeType as Ua, isShapeName as Ui, lineLimit as Un, fr as Ur, bumpFontStack as Ut, placeGridItems as V, noKeyModifiers as Va, boxShadowsEqual as Vi, lineIndexAtY as Vn, validateGradient as Vr, textRunBackgrounds as Vt, CrossAxisAlignment as W, DirtyFlags as Wa, shapesEqual as Wi, offsetAtPoint as Wn, isAutoLength as Wr, clearFontStacks as Wt, UiPlatformAdapter as X, resolvedSpansOf as Xi, lineEndAt as Xn, repeat as Xr, DEFAULT_TEXT_COLOR as Xt, CanvasPlatformSurface as Y, flattenTextSpans as Yi, graphemeBoundaries as Yn, percent as Yr, DEFAULT_FONT_WEIGHT as Yt, capturePointer as Z, spanAtOffset as Zi, lineStartAt as Zn, resolveLength as Zr, colorToCss as Zt, LABEL_PADDING_X as _, UiNode as _a, darkTheme as _i, isNodeInert as _n, UiProperties as _r, UiInputDispatcher as _t, PaintPictureCache as a, UiBasicColors as aa, uniformBorderRadius as ai, TEXT_MATCHES_PROP as an, resolveProperty as ar, isScrollContainer as at, LayoutEngine as b, overrideSources as ba, withContrast as bi, clearLinkHover as bn, isUiRole as br, paragraphGeometry as bt, EMPTY_RECORDING as c, colorValuesEqual as ca, UiEnvironmentKeys as ci, setMatchRanges as cn, UiGraph as cr, UiFocusManager as ct, tracePathData as d, parseColor as da, noInsets as di, selectableTextNodes as dn, getPropertyNames as dr, SCROLLBAR_INSET as dt, textSpansEqual as ea, borderRadiusCorners as ei, isPaintVisible as en, previousGraphemeStart as er, touchActionFor as et, ScaledImageCache as f, rgb8 as fa, observeViewportInsets as fi, selectableTextOf as fn, inheritedPropertyFlags as fr, SCROLLBAR_MIN_THUMB as ft, paintsAfterChildren as g, UiEnvironment as ga, unknownContainerSize as gi, isNodeHitTestable as gn, propertyValuesEqualByName as gr, scrollbarZoneAt as gt, hasDecorationPhase as h, findEnvironmentKey as ha, containerBands as hi, isNodeFocusable as hn, propertyIsInherited as hr, scrollbarThumbs as ht, traceRoundedRect as i, lightColors as ia, normalizeBorderRadius as ii, DEFAULT_LINE_HEIGHT_FACTOR as in, resolveNumber as ir, UiWheelController as it, contentOffset as j, UiEventType as ja, relativeLuminance as ji, editorOf as jn, UiTransforms as jr, applyCanvasTextStyle as jt, withOverrideSource as k, MOUSE_POINTER as ka, highContrastColors as ki, caretVisibleAt as kn, paintValuesEqual as kr, proportionalFontMetrics as kt, PaintRecorder as l, colorsEqual as la, UiInsetRegistry as li, TEXT_SELECTION_PROP as ln, closestPropertyName as lr, UiHitTester as lt, decorationRect as m, createEnvironmentKey as ma, bandOf as mi, setSelectionRange as mn, propertyEffects as mr, scrollbarThumb as mt, createCanvasSurface as n, colorsEqualPalette as na, borderRadiusIsZero as ni, normalizeVerticalAlign as nn, wordRangeAt as nr, UiGestureRecognizer as nt, paintPictures as o, colorToHex as oa, defineProperty as oi, clearMatchRanges as on, resolvePropertyByName as or, UiKeyboardController as ot, decorationColor as p, rgba as pa, UiContainerSizeSource as pi, selectionRangeOf as pn, inheritedPropertyNames as pr, SCROLLBAR_THICKNESS as pt, parseCrossAxisAlignment as q, typographyEqual as qi, selectionRects as qn, isPercentLength as qr, DEFAULT_FONT_FAMILY as qt, Canvas2DRenderer as r, darkColors as ra, borderRadiusValuesEqual as ri, resolvePaintState as rn, resolveBoolean as rr, UiPinchRecognizer as rt, PaintTarget as s, colorToRgba as sa, propertyValuesEqual as si, matchRangesOf as sn, resolveString as sr, UiPointerController as st, CanvasSurface as t, textStylesEqual as ta, borderRadiusEqual as ti, normalizeTextAlign as tn, previousWordStart as tr, UiTouchScroller as tt, replayPaint as u, normalizeColor as ua, insetsEqual as ui, clearSelectionRange as un, findPropertyDefinition as ur, SCROLLBAR_HOVER_ZONE as ut, drawOverlayShapes as v, DirtyNodeSet as va, lightTheme as vi, isNodeSelectable as vn, UI_ROLES as vr, hasDrawnText as vt, describeOverrides as w, UiVisualState as wa, noThemeExtensions as wi, linkRunAtPointIn as wn, validateRole as wr, CharacterCountTextMeasurer as wt, SCROLLBAR_FADE_MS as x, resetOverrideWarnings as xa, withDensity as xi, hasTextLinks as xn, isUiSemanticState as xr, paragraphGeometryFrom as xt, labelOrigin as y, clearOverrideProperty as ya, themesEqual as yi, resolveCursor as yn, UI_SEMANTIC_STATES as yr, offsetAtPointIn as yt, FlexDirection as z, UiWheelDeltaMode as za, boxShadow as zi, videoFrameSize as zn, linearGradient as zr, spanOf as zt };
18303
+ export { prepareInputSurface as $, UiBinding as $a, defaultTextStyle as $i, EditableTextModel as $n, minmax as $r, DEFAULT_FONT_WEIGHT as $t, accumulatedOffsetTo as A, UiVisualState as Aa, themeExtension as Ai, setLinkHover as An, validateStates as Ar, proportionalFontMetrics as At, parseFlexDirection as B, UiInputEvent as Ba, spacingEqual as Bi, nextCaretToggle as Bn, transformsEqual as Br, layoutTextLines as Bt, describeLength as C, UiNode as Ca, lightTheme as Ci, isNodeTabStop as Cn, UI_ROLES as Cr, selectionRectsIn as Ct, formatNumber as D, resetOverrideWarnings as Da, defineThemeExtension as Di, linkHoverOf as Dn, normalizeStates as Dr, spannedRunsFor as Dt, formatExplanation as E, overrideSources as Ea, withDensity as Ei, hasTextLinks as En, isUiSemanticState as Er, ParagraphTextMeasurer as Et, constraintsEqual as F, MOUSE_POINTER as Fa, raiseContrast as Fi, editorFor as Fn, pathValuesEqual as Fr, applyCanvasTextStyle as Ft, MainAxisAlignment as G, UiSelectionChangeEvent as Ga, boxShadowArraysEqual as Gi, caretRectFor as Gn, radialGradient as Gr, textRunBackgrounds as Gt, sizeGridTracks as H, UiPasteEvent as Ha, defaultShadows as Hi, resolveColorValue as Hn, gradientPaint as Hr, runFontString as Ht, tightenConstraints as I, UiBeforeInputEvent as Ia, relativeLuminance as Ii, editorOf as In, UiTransforms as Ir, buildFontString as It, parseMainAxisAlignment as J, UiWheelEvent as Ja, isShapeName as Ji, lineLimit as Jn, fr as Jr, clearFontStacks as Jt, parseAlignContent as K, UiTextChangeEvent as Ka, boxShadowsEqual as Ki, lineIndexAtY as Kn, validateGradient as Kr, textRunDecorations as Kt, MAX_MEASURES_PER_CHILD as L, UiEventType as La, densityFactors as Li, isEditableNode as Ln, parseTransform as Lr, createCanvasTextStyle as Lt, LayoutRecord as M, hasVisualState as Ma, withThemeExtension as Mi, CARET_WIDTH as Mn, validateSubgrid as Mr, detectEditingPlatform as Mt, Constraints as N, visualState as Na, contrastRatio as Ni, EDITOR_PROP as Nn, inputsEqual as Nr, isPrintable as Nt, labelNode as O, writeDeclaredProperty as Oa, hasThemeExtension as Oi, linkOf as On, statesEqual as Or, ELLIPSIS as Ot, clampSize as P, visualStatesEqual as Pa, highContrastColors as Pi, caretVisibleAt as Pn, paintValuesEqual as Pr, EditableLayout as Pt, pointerDeviceOf as Q, DirtyFlags as Qa, typographyEqual as Qi, selectionRects as Qn, isPercentLength as Qr, DEFAULT_FONT_SIZE as Qt, isLayoutProtocol as R, UiFocusEvent as Ra, defaultSpacing as Ri, isMultiline as Rn, transform as Rr, drawText as Rt, SCROLLBAR_LINGER_MS as S, UiEnvironment as Sa, darkTheme as Si, isNodeSelectable as Sn, UiProperties as Sr, paragraphGeometryFrom as St, formatConstraints as T, clearOverrideProperty as Ta, withContrast as Ti, clearLinkHover as Tn, isUiRole as Tr, CharacterCountTextMeasurer as Tt, AlignContent as U, UiPinchEvent as Ua, shadowsEqual as Ui, isVideoSurface as Un, gradientsEqual as Ur, spanOf as Ut, placeGridItems as V, UiKeyboardEvent as Va, spacingSteps as Vi, syncEditorValue as Vn, MAX_GRADIENT_STOPS as Vr, runCanvasStyleInto as Vt, CrossAxisAlignment as W, UiPointerEvent as Wa, boxShadow as Wi, videoFrameSize as Wn, linearGradient as Wr, textMeasureRequest as Wt, UiPlatformAdapter as X, wheelDeltaYOf as Xa, defaultTypography as Xi, offsetAtX as Xn, isFrLength as Xr, registerFontStack as Xt, CanvasPlatformSurface as Y, noKeyModifiers as Ya, shapesEqual as Yi, offsetAtPoint as Yn, isAutoLength as Yr, fontStackFor as Yt, capturePointer as Z, UiNodeType as Za, isTypographyRole as Zi, offsetForVerticalMove as Zn, isMinMaxTrack as Zr, DEFAULT_FONT_FAMILY as Zt, LABEL_PADDING_X as _, parseColor as _a, observeViewportInsets as _i, selectionRangeOf as _n, inheritedPropertyFlags as _r, scrollbarZoneAt as _t, PaintPictureCache as a, textContentOf as aa, borderRadiusCorners as ai, normalizeTextAlign as an, previousGraphemeStart as ar, UiWheelController as at, LayoutEngine as b, createEnvironmentKey as ba, containerBands as bi, isNodeHitTestable as bn, propertyIsInherited as br, offsetAtPointIn as bt, EMPTY_RECORDING as c, colorsEqualPalette as ca, borderRadiusValuesEqual as ci, DEFAULT_LINE_HEIGHT_FACTOR as cn, resolveBoolean as cr, UiPointerController as ct, tracePathData as d, UiBasicColors as da, defineProperty as di, matchRangesOf as dn, resolvePropertyByName as dr, SCROLLBAR_HOVER_ZONE as dt, editableSpansOf as ea, percent as ei, DEFAULT_TEXT_COLOR as en, graphemeBoundaries as er, touchActionFor as et, ScaledImageCache as f, colorToHex as fa, propertyValuesEqual as fi, setMatchRanges as fn, resolveString as fr, SCROLLBAR_INSET as ft, paintsAfterChildren as g, normalizeColor as ga, noInsets as gi, selectableTextOf as gn, getPropertyNames as gr, scrollbarThumbs as gt, hasDecorationPhase as h, colorsEqual as ha, insetsEqual as hi, selectableTextNodes as hn, findPropertyDefinition as hr, scrollbarThumb as ht, traceRoundedRect as i, spannedTextOf as ia, borderRadius as ii, isPaintVisible as in, nextWordEnd as ir, UiPinchRecognizer as it, contentOffset as j, defaultVisualState as ja, themeExtensionsEqual as ji, CARET_BLINK_MS as jn, UI_SUBGRID_AXES as jr, commandForKey as jt, withOverrideSource as k, writeOverrideProperty as ka, noThemeExtensions as ki, linkRunAtPointIn as kn, validateRole as kr, layoutParagraph as kt, PaintRecorder as l, darkColors as la, normalizeBorderRadius as li, TEXT_MATCHES_PROP as ln, resolveNumber as lr, UiFocusManager as lt, decorationRect as m, colorValuesEqual as ma, UiInsetRegistry as mi, clearSelectionRange as mn, closestPropertyName as mr, SCROLLBAR_THICKNESS as mt, createCanvasSurface as n, resolvedSpansOf as na, resolveLength as ni, computeObjectFitRect as nn, lineStartAt as nr, UiTouchScroller as nt, paintPictures as o, textSpansEqual as oa, borderRadiusEqual as oi, normalizeVerticalAlign as on, previousWordStart as or, isScrollContainer as ot, decorationColor as p, colorToRgba as pa, UiEnvironmentKeys as pi, TEXT_SELECTION_PROP as pn, UiGraph as pr, SCROLLBAR_MIN_THUMB as pt, parseCrossAxisAlignment as q, UiWheelDeltaMode as qa, defaultShapes as qi, lineIndexForOffset as qn, auto as qr, bumpFontStack as qt, Canvas2DRenderer as r, spanAtOffset as ra, UiBorderRadiuses as ri, createPaintState as rn, nextGraphemeEnd as rr, UiGestureRecognizer as rt, PaintTarget as s, textStylesEqual as sa, borderRadiusIsZero as si, resolvePaintState as sn, wordRangeAt as sr, UiKeyboardController as st, CanvasSurface as t, flattenTextSpans as ta, repeat as ti, colorToCss as tn, lineEndAt as tr, UiEditingController as tt, replayPaint as u, lightColors as ua, uniformBorderRadius as ui, clearMatchRanges as un, resolveProperty as ur, UiHitTester as ut, drawOverlayShapes as v, rgb8 as va, UiContainerSizeSource as vi, setSelectionRange as vn, inheritedPropertyNames as vr, UiInputDispatcher as vt, describeOverrides as w, DirtyNodeSet as wa, themesEqual as wi, resolveCursor as wn, UI_SEMANTIC_STATES as wr, wordRangeIn as wt, SCROLLBAR_FADE_MS as x, findEnvironmentKey as xa, unknownContainerSize as xi, isNodeInert as xn, propertyValuesEqualByName as xr, paragraphGeometry as xt, labelOrigin as y, rgba as ya, bandOf as yi, isNodeFocusable as yn, propertyEffects as yr, hasDrawnText as yt, FlexDirection as z, UiGestureEvent as za, scaleSpacing as zi, isReadOnly as zn, transformIsIdentity as zr, fillTextRects as zt };
17301
18304
 
17302
- //# sourceMappingURL=CanvasSurface-5dU-U3qQ.js.map
18305
+ //# sourceMappingURL=CanvasSurface-BR7hECHm.js.map