gesso-core 0.2.0 → 0.3.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.
- package/CHANGELOG.md +66 -0
- package/dist/{CanvasSurface-5dU-U3qQ.js → CanvasSurface-ifHkg0-g.js} +972 -59
- package/dist/CanvasSurface-ifHkg0-g.js.map +1 -0
- package/dist/{index-DAdauEn9.d.ts → index-B4o3nKt2.d.ts} +786 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +748 -576
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +10 -1
- package/dist/testing.js +24 -1
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/CanvasSurface-5dU-U3qQ.js.map +0 -1
|
@@ -161,6 +161,32 @@ 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";
|
|
165
191
|
UiEventType["LongPress"] = "longpress";
|
|
166
192
|
UiEventType["DragStart"] = "dragstart";
|
|
@@ -406,6 +432,21 @@ var UiBeforeInputEvent = class extends UiInputEvent {
|
|
|
406
432
|
* An editable node's text changed: the new value and the selection
|
|
407
433
|
* after the change. Bubbles, so a form can watch all its fields.
|
|
408
434
|
*/
|
|
435
|
+
/**
|
|
436
|
+
* Text from the system clipboard, offered to whatever holds focus.
|
|
437
|
+
*
|
|
438
|
+
* Carries the text rather than a clipboard handle, because a render
|
|
439
|
+
* worker has neither a clipboard nor a window: the shell reads it and
|
|
440
|
+
* sends it across, which is the same path `ShellService.copyText`
|
|
441
|
+
* takes in the other direction.
|
|
442
|
+
*/
|
|
443
|
+
var UiPasteEvent = class extends UiInputEvent {
|
|
444
|
+
text;
|
|
445
|
+
constructor(text) {
|
|
446
|
+
super("paste");
|
|
447
|
+
this.text = text;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
409
450
|
var UiTextChangeEvent = class extends UiInputEvent {
|
|
410
451
|
value;
|
|
411
452
|
selectionStart;
|
|
@@ -418,6 +459,36 @@ var UiTextChangeEvent = class extends UiInputEvent {
|
|
|
418
459
|
}
|
|
419
460
|
};
|
|
420
461
|
/**
|
|
462
|
+
* An editable's selection moved.
|
|
463
|
+
*
|
|
464
|
+
* Carries the text as well as the offsets, because every use of this
|
|
465
|
+
* is "what is under the caret" and the answer needs both — and
|
|
466
|
+
* reading the text back off the model afterwards is a second source
|
|
467
|
+
* of truth that can disagree by a frame.
|
|
468
|
+
*
|
|
469
|
+
* `start` and `end` are ordered; `anchor` and `focus` are where the
|
|
470
|
+
* selection was begun and where the caret now is, which is the pair
|
|
471
|
+
* that says which way it was dragged.
|
|
472
|
+
*/
|
|
473
|
+
var UiSelectionChangeEvent = class extends UiInputEvent {
|
|
474
|
+
value;
|
|
475
|
+
start;
|
|
476
|
+
end;
|
|
477
|
+
anchor;
|
|
478
|
+
focus;
|
|
479
|
+
constructor(value, start, end, anchor, focus) {
|
|
480
|
+
super("selectionchange");
|
|
481
|
+
this.value = value;
|
|
482
|
+
this.start = start;
|
|
483
|
+
this.end = end;
|
|
484
|
+
this.anchor = anchor;
|
|
485
|
+
this.focus = focus;
|
|
486
|
+
}
|
|
487
|
+
get collapsed() {
|
|
488
|
+
return this.start === this.end;
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
421
492
|
* Wheel scroll input event.
|
|
422
493
|
*
|
|
423
494
|
* `deltaX`/`deltaY` are the scroll deltas in canvas pixels produced
|
|
@@ -1615,6 +1686,27 @@ function textContentOf(node) {
|
|
|
1615
1686
|
function resolvedSpansOf(node) {
|
|
1616
1687
|
return spannedTextOf(node)?.spans ?? EMPTY_SPANS;
|
|
1617
1688
|
}
|
|
1689
|
+
/**
|
|
1690
|
+
* The runs an *editable* node styles its own text with.
|
|
1691
|
+
*
|
|
1692
|
+
* An editable's paragraph is the user's, held by its model rather
|
|
1693
|
+
* than by a property — so unlike a `Text`, its runs do not supply the
|
|
1694
|
+
* text, they only describe it. `spans` on an editable is presentation
|
|
1695
|
+
* over a string somebody is typing.
|
|
1696
|
+
*
|
|
1697
|
+
* Which is why this checks. Every offset the editor works in — the
|
|
1698
|
+
* caret, the selection, the hit test that turns a click into a text
|
|
1699
|
+
* position — is an offset into the model's string. Runs describing a
|
|
1700
|
+
* *different* string would put the glyphs somewhere the caret does
|
|
1701
|
+
* not agree with, and the field would look subtly, unfixably wrong.
|
|
1702
|
+
* When they disagree the text is drawn unstyled, which is a frame of
|
|
1703
|
+
* plain colour rather than a frame of nonsense, and corrects itself
|
|
1704
|
+
* as soon as the application catches up.
|
|
1705
|
+
*/
|
|
1706
|
+
function editableSpansOf(node, text) {
|
|
1707
|
+
const spanned = spannedTextOf(node);
|
|
1708
|
+
return spanned !== void 0 && spanned.text === text ? spanned.spans : EMPTY_SPANS;
|
|
1709
|
+
}
|
|
1618
1710
|
/** The span covering an offset, or undefined where no run does. */
|
|
1619
1711
|
function spanAtOffset(spans, offset) {
|
|
1620
1712
|
for (const span of spans) if (offset >= span.start && offset < span.end) return span;
|
|
@@ -4223,6 +4315,12 @@ const UiProperties = {
|
|
|
4223
4315
|
inherited: false,
|
|
4224
4316
|
affects: 64
|
|
4225
4317
|
}),
|
|
4318
|
+
tabStop: defineProperty({
|
|
4319
|
+
name: "tabStop",
|
|
4320
|
+
defaultValue: void 0,
|
|
4321
|
+
inherited: false,
|
|
4322
|
+
affects: 64
|
|
4323
|
+
}),
|
|
4226
4324
|
disabled: defineProperty({
|
|
4227
4325
|
name: "disabled",
|
|
4228
4326
|
defaultValue: void 0,
|
|
@@ -4451,6 +4549,13 @@ const UiProperties = {
|
|
|
4451
4549
|
inherited: false,
|
|
4452
4550
|
affects: 64
|
|
4453
4551
|
}),
|
|
4552
|
+
/** Set by LazySheet on the scroll container: its UiVirtualSheet. */
|
|
4553
|
+
virtualSheet: defineProperty({
|
|
4554
|
+
name: "virtualSheet",
|
|
4555
|
+
defaultValue: void 0,
|
|
4556
|
+
inherited: false,
|
|
4557
|
+
affects: 64
|
|
4558
|
+
}),
|
|
4454
4559
|
theme: defineProperty({
|
|
4455
4560
|
name: "theme",
|
|
4456
4561
|
defaultValue: void 0,
|
|
@@ -5547,6 +5652,21 @@ var EditableTextModel = class {
|
|
|
5547
5652
|
blinkOrigin = 0;
|
|
5548
5653
|
/** The last `value` the host synchronised in; see `UiEditable`. */
|
|
5549
5654
|
syncedValue = void 0;
|
|
5655
|
+
/**
|
|
5656
|
+
* The selection the last `selectionchange` reported.
|
|
5657
|
+
*
|
|
5658
|
+
* Kept on the model rather than worked out at each call site,
|
|
5659
|
+
* because the caret is moved from a dozen places — arrows, clicks,
|
|
5660
|
+
* select-all, an edit that replaces a range — and every one of them
|
|
5661
|
+
* would otherwise have to remember to compare. Storing what was
|
|
5662
|
+
* last said makes the comparison the notifier's job and the answer
|
|
5663
|
+
* the same wherever the move came from.
|
|
5664
|
+
*
|
|
5665
|
+
* `NaN` to begin with, so the first report always fires: a fresh
|
|
5666
|
+
* model at offset zero has genuinely not told anyone yet.
|
|
5667
|
+
*/
|
|
5668
|
+
notifiedAnchor = NaN;
|
|
5669
|
+
notifiedFocus = NaN;
|
|
5550
5670
|
constructor(text = "") {
|
|
5551
5671
|
this.textValue = text;
|
|
5552
5672
|
}
|
|
@@ -6310,6 +6430,26 @@ function isNodeFocusable(node) {
|
|
|
6310
6430
|
return node.type === "button" || node.type === "editable-text";
|
|
6311
6431
|
}
|
|
6312
6432
|
/**
|
|
6433
|
+
* Whether Tab should stop here.
|
|
6434
|
+
*
|
|
6435
|
+
* Every tab stop is focusable and not every focusable node is a tab
|
|
6436
|
+
* stop, which is the distinction `tabindex="-1"` draws in the DOM and
|
|
6437
|
+
* the one this engine was missing. `focusable` answers "may this node
|
|
6438
|
+
* hold focus at all", and that is the wrong question for a container:
|
|
6439
|
+
* a dialog's body must be able to *take* the keyboard, so that opening
|
|
6440
|
+
* a dialog whose content is a sentence does not hand it to nothing, and
|
|
6441
|
+
* must not be a place Tab *lands*, or every dialog gains a stop that
|
|
6442
|
+
* announces nothing and does nothing.
|
|
6443
|
+
*
|
|
6444
|
+
* So `tabStop: false` means focusable, reachable by a press and by
|
|
6445
|
+
* `focus()`, and skipped by the cycle. It says nothing on a node that
|
|
6446
|
+
* is not focusable in the first place, because there is no order to be
|
|
6447
|
+
* out of.
|
|
6448
|
+
*/
|
|
6449
|
+
function isNodeTabStop(node) {
|
|
6450
|
+
return isNodeFocusable(node) && node.properties.get("tabStop") !== false;
|
|
6451
|
+
}
|
|
6452
|
+
/**
|
|
6313
6453
|
* Whether the user may select this node's text with the pointer.
|
|
6314
6454
|
*
|
|
6315
6455
|
* `selectable` inherits down the tree the way CSS `user-select` does:
|
|
@@ -6655,7 +6795,7 @@ function resolvePaintState(node, out) {
|
|
|
6655
6795
|
}
|
|
6656
6796
|
out.rtl = resolveProperty(node, UiProperties.textDirection) === "rtl";
|
|
6657
6797
|
resolveFontInto(node, out);
|
|
6658
|
-
out.spans = out.editor === void 0 ?
|
|
6798
|
+
out.spans = resolvePaintSpans(node, out.editor === void 0 ? resolvedSpansOf(node) : editableSpansOf(node, out.text ?? ""));
|
|
6659
6799
|
out.linkHover = out.spans === void 0 ? -1 : linkHoverOf(node);
|
|
6660
6800
|
out.textDecoration = resolveProperty(node, UiProperties.textDecoration);
|
|
6661
6801
|
out.textColor = resolveColor(node, UiProperties.color) ?? UiBasicColors.black;
|
|
@@ -6718,8 +6858,7 @@ function resetTextStyle(out) {
|
|
|
6718
6858
|
* anything having to notice.
|
|
6719
6859
|
*/
|
|
6720
6860
|
const paintSpans = /* @__PURE__ */ new WeakMap();
|
|
6721
|
-
function resolvePaintSpans(node) {
|
|
6722
|
-
const spans = resolvedSpansOf(node);
|
|
6861
|
+
function resolvePaintSpans(node, spans) {
|
|
6723
6862
|
if (spans.length === 0) return;
|
|
6724
6863
|
const theme = node.environment !== null ? node.environment.get(UiEnvironmentKeys.theme) : void 0;
|
|
6725
6864
|
const cached = paintSpans.get(spans);
|
|
@@ -7422,6 +7561,122 @@ var EditableLayout = class {
|
|
|
7422
7561
|
}
|
|
7423
7562
|
};
|
|
7424
7563
|
//#endregion
|
|
7564
|
+
//#region src/editing/EditingKeymap.ts
|
|
7565
|
+
/**
|
|
7566
|
+
* The platform the runtime is on, decided once. Available in a worker
|
|
7567
|
+
* too, since `navigator.userAgent` is; tests set it explicitly.
|
|
7568
|
+
*/
|
|
7569
|
+
function detectEditingPlatform() {
|
|
7570
|
+
const agent = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
|
7571
|
+
return /Mac|iPhone|iPad|iPod/.test(agent) ? "mac" : "other";
|
|
7572
|
+
}
|
|
7573
|
+
/**
|
|
7574
|
+
* Maps a key press to an editing command, following the host's
|
|
7575
|
+
* conventions:
|
|
7576
|
+
*
|
|
7577
|
+
* Mac — Option steps by word, Command jumps to line and document
|
|
7578
|
+
* ends, Command+Backspace deletes to the line start.
|
|
7579
|
+
* Others — Control steps by word, Home/End and Control+Home/End are
|
|
7580
|
+
* the line and document ends.
|
|
7581
|
+
*
|
|
7582
|
+
* Shift extends every move. Enter is a newline (the controller drops
|
|
7583
|
+
* it in a single-line field). A printable key becomes `insert` only
|
|
7584
|
+
* when `textFromKeys` is set: a host with an editing proxy delivers
|
|
7585
|
+
* text through `beforeinput` instead, where the IME and dead keys
|
|
7586
|
+
* have already been resolved.
|
|
7587
|
+
*/
|
|
7588
|
+
function commandForKey(key, modifiers, platform, textFromKeys) {
|
|
7589
|
+
const mac = platform === "mac";
|
|
7590
|
+
const primary = mac ? modifiers.meta : modifiers.ctrl;
|
|
7591
|
+
const word = mac ? modifiers.alt : modifiers.ctrl;
|
|
7592
|
+
const extend = modifiers.shift;
|
|
7593
|
+
switch (key) {
|
|
7594
|
+
case "ArrowLeft":
|
|
7595
|
+
case "ArrowRight": {
|
|
7596
|
+
const direction = key === "ArrowLeft" ? -1 : 1;
|
|
7597
|
+
if (mac && modifiers.meta) return {
|
|
7598
|
+
kind: "move",
|
|
7599
|
+
unit: "line",
|
|
7600
|
+
direction,
|
|
7601
|
+
extend
|
|
7602
|
+
};
|
|
7603
|
+
return {
|
|
7604
|
+
kind: "move",
|
|
7605
|
+
unit: word ? "word" : "grapheme",
|
|
7606
|
+
direction,
|
|
7607
|
+
extend
|
|
7608
|
+
};
|
|
7609
|
+
}
|
|
7610
|
+
case "ArrowUp":
|
|
7611
|
+
case "ArrowDown": {
|
|
7612
|
+
const direction = key === "ArrowUp" ? -1 : 1;
|
|
7613
|
+
if (mac && modifiers.meta) return {
|
|
7614
|
+
kind: "move",
|
|
7615
|
+
unit: "document",
|
|
7616
|
+
direction,
|
|
7617
|
+
extend
|
|
7618
|
+
};
|
|
7619
|
+
return {
|
|
7620
|
+
kind: "move",
|
|
7621
|
+
unit: "vertical",
|
|
7622
|
+
direction,
|
|
7623
|
+
extend
|
|
7624
|
+
};
|
|
7625
|
+
}
|
|
7626
|
+
case "Home": return {
|
|
7627
|
+
kind: "move",
|
|
7628
|
+
unit: modifiers.ctrl ? "document" : "line",
|
|
7629
|
+
direction: -1,
|
|
7630
|
+
extend
|
|
7631
|
+
};
|
|
7632
|
+
case "End": return {
|
|
7633
|
+
kind: "move",
|
|
7634
|
+
unit: modifiers.ctrl ? "document" : "line",
|
|
7635
|
+
direction: 1,
|
|
7636
|
+
extend
|
|
7637
|
+
};
|
|
7638
|
+
case "Backspace":
|
|
7639
|
+
if (mac && modifiers.meta) return {
|
|
7640
|
+
kind: "delete",
|
|
7641
|
+
unit: "line",
|
|
7642
|
+
direction: -1
|
|
7643
|
+
};
|
|
7644
|
+
return {
|
|
7645
|
+
kind: "delete",
|
|
7646
|
+
unit: word ? "word" : "grapheme",
|
|
7647
|
+
direction: -1
|
|
7648
|
+
};
|
|
7649
|
+
case "Delete": return {
|
|
7650
|
+
kind: "delete",
|
|
7651
|
+
unit: word ? "word" : "grapheme",
|
|
7652
|
+
direction: 1
|
|
7653
|
+
};
|
|
7654
|
+
case "Enter": return primary ? null : { kind: "newline" };
|
|
7655
|
+
}
|
|
7656
|
+
if (primary && !modifiers.alt) {
|
|
7657
|
+
switch (key.toLowerCase()) {
|
|
7658
|
+
case "a": return { kind: "selectAll" };
|
|
7659
|
+
case "z": return modifiers.shift ? { kind: "redo" } : { kind: "undo" };
|
|
7660
|
+
case "y": return mac ? null : { kind: "redo" };
|
|
7661
|
+
}
|
|
7662
|
+
return null;
|
|
7663
|
+
}
|
|
7664
|
+
if (textFromKeys && isPrintable(key) && !modifiers.ctrl && !modifiers.meta) return {
|
|
7665
|
+
kind: "insert",
|
|
7666
|
+
text: key
|
|
7667
|
+
};
|
|
7668
|
+
return null;
|
|
7669
|
+
}
|
|
7670
|
+
/** A key value that is the character it types: one grapheme, not a name like 'Shift'. */
|
|
7671
|
+
function isPrintable(key) {
|
|
7672
|
+
if (key.length === 1) return true;
|
|
7673
|
+
if (key.length === 2) {
|
|
7674
|
+
const code = key.charCodeAt(0);
|
|
7675
|
+
return code >= 55296 && code <= 56319;
|
|
7676
|
+
}
|
|
7677
|
+
return false;
|
|
7678
|
+
}
|
|
7679
|
+
//#endregion
|
|
7425
7680
|
//#region src/layout/LineBreaks.ts
|
|
7426
7681
|
function segmentParagraph(paragraph, wrap) {
|
|
7427
7682
|
const length = paragraph.length;
|
|
@@ -8410,6 +8665,31 @@ function scrollbarZoneAt(rec, x, y) {
|
|
|
8410
8665
|
if (rec.contentWidth > rec.width && y >= rec.y + rec.height - 16) return "x";
|
|
8411
8666
|
return null;
|
|
8412
8667
|
}
|
|
8668
|
+
/**
|
|
8669
|
+
* The rectangle a press grabs a thumb by.
|
|
8670
|
+
*
|
|
8671
|
+
* The painted thumb is six pixels thick. Six pixels at the very edge
|
|
8672
|
+
* of a window is a hard thing to hit, and missing it is not a near
|
|
8673
|
+
* miss — the press lands on the track instead and pages a whole
|
|
8674
|
+
* screenful, which reads as the bar refusing to be dragged. So the
|
|
8675
|
+
* grab target is the thumb's length by the whole band that reveals it:
|
|
8676
|
+
* as easy to catch as it is to summon, which is what an overlay
|
|
8677
|
+
* scrollbar is expected to be. What is drawn is unchanged.
|
|
8678
|
+
*/
|
|
8679
|
+
function scrollbarGrabBox(rec, bar) {
|
|
8680
|
+
if (bar.axis === "y") return {
|
|
8681
|
+
x: rec.mirrored ? rec.x : rec.x + rec.width - 16,
|
|
8682
|
+
y: bar.thumb.y,
|
|
8683
|
+
width: 16,
|
|
8684
|
+
height: bar.thumb.height
|
|
8685
|
+
};
|
|
8686
|
+
return {
|
|
8687
|
+
x: bar.thumb.x,
|
|
8688
|
+
y: rec.y + rec.height - 16,
|
|
8689
|
+
width: bar.thumb.width,
|
|
8690
|
+
height: 16
|
|
8691
|
+
};
|
|
8692
|
+
}
|
|
8413
8693
|
function pointInBox(box, x, y) {
|
|
8414
8694
|
return x >= box.x && x < box.x + box.width && y >= box.y && y < box.y + box.height;
|
|
8415
8695
|
}
|
|
@@ -8731,11 +9011,21 @@ var UiHitTester = class {
|
|
|
8731
9011
|
* only while the bar is showing (or when asked for the band alone).
|
|
8732
9012
|
*/
|
|
8733
9013
|
hitScrollbar(node, rec, px, py) {
|
|
8734
|
-
const
|
|
8735
|
-
if (
|
|
8736
|
-
|
|
9014
|
+
const zone = scrollbarZoneAt(rec, px, py);
|
|
9015
|
+
if (zone === null) return false;
|
|
9016
|
+
let axis = zone;
|
|
9017
|
+
let bar = scrollbarThumb(rec, axis);
|
|
9018
|
+
let onThumb = bar !== null && pointInBox(scrollbarGrabBox(rec, bar), px, py);
|
|
9019
|
+
if (!onThumb) {
|
|
9020
|
+
const other = axis === "y" ? "x" : "y";
|
|
9021
|
+
const otherBar = scrollbarThumb(rec, other);
|
|
9022
|
+
if (otherBar !== null && pointInBox(scrollbarGrabBox(rec, otherBar), px, py)) {
|
|
9023
|
+
axis = other;
|
|
9024
|
+
bar = otherBar;
|
|
9025
|
+
onThumb = true;
|
|
9026
|
+
}
|
|
9027
|
+
}
|
|
8737
9028
|
if (bar === null) return false;
|
|
8738
|
-
const onThumb = pointInBox(bar.thumb, px, py);
|
|
8739
9029
|
const visible = rec.scrollbarVisibleUntil > (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
8740
9030
|
if (!onThumb && !visible && !this.zoneOnly) return false;
|
|
8741
9031
|
this.recordHit(node, rec, px, py);
|
|
@@ -8992,10 +9282,24 @@ var UiFocusManager = class {
|
|
|
8992
9282
|
* this again once the frame's tree is built, which is when a dialog
|
|
8993
9283
|
* gets its caret. Idempotent: with focus already inside the scope,
|
|
8994
9284
|
* or no scope at all, it does nothing.
|
|
9285
|
+
*
|
|
9286
|
+
* **The scope root is the fallback, and it is the whole reason
|
|
9287
|
+
* `tabStop` exists.** A scope with no tab stop inside it used to
|
|
9288
|
+
* blur, which hands the keyboard to nothing: a `Dialog` whose body
|
|
9289
|
+
* is a sentence and a button could not be dismissed with Escape,
|
|
9290
|
+
* because Escape had nowhere to be delivered. Falling back to the
|
|
9291
|
+
* root fixes that only if the root can hold focus without becoming a
|
|
9292
|
+
* stop of its own, or every dialog grows a tab stop that announces
|
|
9293
|
+
* nothing. `focusable: true, tabStop: false` on the body is exactly
|
|
9294
|
+
* that pair, and the fallback is what makes it worth setting.
|
|
9295
|
+
*
|
|
9296
|
+
* Blurring is still what happens when the root cannot hold focus
|
|
9297
|
+
* either, because a scope that has genuinely nothing to focus is
|
|
9298
|
+
* better admitted than faked.
|
|
8995
9299
|
*/
|
|
8996
9300
|
settleScope() {
|
|
8997
9301
|
if (this.scopes.length === 0 || this.focused !== null && this.withinScope(this.focused)) return;
|
|
8998
|
-
if (!this.focusNext()) this.blur();
|
|
9302
|
+
if (!this.focusNext() && !this.focus(this.scopeRoot)) this.blur();
|
|
8999
9303
|
}
|
|
9000
9304
|
/**
|
|
9001
9305
|
* Ends the innermost scope and hands focus back to whatever held it
|
|
@@ -9043,11 +9347,18 @@ var UiFocusManager = class {
|
|
|
9043
9347
|
const next = focusables[(index + delta + focusables.length) % focusables.length];
|
|
9044
9348
|
return this.focus(next, "keyboard");
|
|
9045
9349
|
}
|
|
9046
|
-
/**
|
|
9350
|
+
/**
|
|
9351
|
+
* The tab stops in document order (parent before children).
|
|
9352
|
+
*
|
|
9353
|
+
* Tab stops and not every focusable node: a container with
|
|
9354
|
+
* `tabStop: false` can hold focus and is not a place the cycle
|
|
9355
|
+
* stops, which is what lets a dialog body be the keyboard's home of
|
|
9356
|
+
* last resort without becoming a stop of its own.
|
|
9357
|
+
*/
|
|
9047
9358
|
collectFocusable() {
|
|
9048
9359
|
const result = [];
|
|
9049
9360
|
const visit = (node) => {
|
|
9050
|
-
if (
|
|
9361
|
+
if (isNodeTabStop(node)) result.push(node);
|
|
9051
9362
|
for (let child = node.firstChild; child !== null; child = child.nextSibling) visit(child);
|
|
9052
9363
|
};
|
|
9053
9364
|
visit(this.scopeRoot);
|
|
@@ -9579,7 +9890,7 @@ const EVERYTHING_SCROLLABLE = Object.freeze({
|
|
|
9579
9890
|
*/
|
|
9580
9891
|
const SCROLL_EPSILON = .5;
|
|
9581
9892
|
/** Whether an offset can still move in the direction `delta` points. */
|
|
9582
|
-
function
|
|
9893
|
+
function hasScrollRoom(offset, max, delta) {
|
|
9583
9894
|
return delta > 0 ? offset < max - SCROLL_EPSILON : offset > SCROLL_EPSILON;
|
|
9584
9895
|
}
|
|
9585
9896
|
/** Whether this node keeps overscroll rather than chaining it outwards. */
|
|
@@ -9718,13 +10029,10 @@ var UiWheelController = class {
|
|
|
9718
10029
|
if (!isScrollContainer(node)) continue;
|
|
9719
10030
|
const state = this.scrollSink.containerState(node);
|
|
9720
10031
|
if (state === void 0) continue;
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
up ||= hasRoom(state.scrollY, state.maxScrollY, -1);
|
|
9726
|
-
down ||= hasRoom(state.scrollY, state.maxScrollY, 1);
|
|
9727
|
-
}
|
|
10032
|
+
left ||= hasScrollRoom(state.scrollX, state.maxScrollX, -1);
|
|
10033
|
+
right ||= hasScrollRoom(state.scrollX, state.maxScrollX, 1);
|
|
10034
|
+
up ||= hasScrollRoom(state.scrollY, state.maxScrollY, -1);
|
|
10035
|
+
down ||= hasScrollRoom(state.scrollY, state.maxScrollY, 1);
|
|
9728
10036
|
if (containsOverscroll(node)) return EVERYTHING_SCROLLABLE;
|
|
9729
10037
|
}
|
|
9730
10038
|
if (containsOverscroll(rootOf(target))) return EVERYTHING_SCROLLABLE;
|
|
@@ -9751,7 +10059,7 @@ var UiWheelController = class {
|
|
|
9751
10059
|
for (const node of this.scrollSink.scrollContainers?.() ?? []) {
|
|
9752
10060
|
const state = this.scrollSink.containerState(node);
|
|
9753
10061
|
if (state === void 0) continue;
|
|
9754
|
-
if (state.
|
|
10062
|
+
if (state.maxScrollX > SCROLL_EPSILON || state.maxScrollY > SCROLL_EPSILON) return true;
|
|
9755
10063
|
}
|
|
9756
10064
|
return false;
|
|
9757
10065
|
}
|
|
@@ -9769,11 +10077,10 @@ var UiWheelController = class {
|
|
|
9769
10077
|
if (!isScrollContainer(node)) continue;
|
|
9770
10078
|
const state = this.scrollSink.containerState(node);
|
|
9771
10079
|
if (state === void 0) continue;
|
|
9772
|
-
const
|
|
9773
|
-
const
|
|
9774
|
-
|
|
9775
|
-
|
|
9776
|
-
this.applyDelta(node, state, deltaX, deltaY, deltaMode, behaviorFor(node, deltaMode, wheelDeltaY));
|
|
10080
|
+
const takesX = deltaX !== 0 && hasScrollRoom(state.scrollX, state.maxScrollX, deltaX);
|
|
10081
|
+
const takesY = deltaY !== 0 && hasScrollRoom(state.scrollY, state.maxScrollY, deltaY);
|
|
10082
|
+
if (takesX || takesY) {
|
|
10083
|
+
this.applyDelta(node, state, takesX ? deltaX : 0, takesY ? deltaY : 0, deltaMode, behaviorFor(node, deltaMode, wheelDeltaY));
|
|
9777
10084
|
event.markConsumed();
|
|
9778
10085
|
return;
|
|
9779
10086
|
}
|
|
@@ -9785,8 +10092,7 @@ var UiWheelController = class {
|
|
|
9785
10092
|
if (containsOverscroll(rootOf(target))) event.markConsumed();
|
|
9786
10093
|
}
|
|
9787
10094
|
applyDelta(container, state, deltaX, deltaY, deltaMode, behavior) {
|
|
9788
|
-
|
|
9789
|
-
else this.scrollSink.scrollBy(container, 0, this.toPixels(deltaY, deltaMode, state.viewportHeight), behavior);
|
|
10095
|
+
this.scrollSink.scrollBy(container, deltaX === 0 ? 0 : this.toPixels(deltaX, deltaMode, state.viewportWidth), deltaY === 0 ? 0 : this.toPixels(deltaY, deltaMode, state.viewportHeight), behavior);
|
|
9790
10096
|
}
|
|
9791
10097
|
/**
|
|
9792
10098
|
* A wheel delta in whatever unit it arrived in, as pixels.
|
|
@@ -10349,9 +10655,8 @@ var UiTouchScroller = class {
|
|
|
10349
10655
|
this.sample(event.x, event.y);
|
|
10350
10656
|
const resolved = this.resolve(event.target, dx, dy);
|
|
10351
10657
|
if (resolved === null) return;
|
|
10352
|
-
const { node,
|
|
10353
|
-
|
|
10354
|
-
else this.scrollSink.scrollBy(node, 0, dy);
|
|
10658
|
+
const { node, takesX, takesY } = resolved;
|
|
10659
|
+
this.scrollSink.scrollBy(node, takesX ? dx : 0, takesY ? dy : 0);
|
|
10355
10660
|
this.scrollSink.revealScrollbars?.(node);
|
|
10356
10661
|
}
|
|
10357
10662
|
panEnd(event) {
|
|
@@ -10364,12 +10669,11 @@ var UiTouchScroller = class {
|
|
|
10364
10669
|
if (velocity === null) return;
|
|
10365
10670
|
const resolved = this.resolve(event.target, velocity.x, velocity.y);
|
|
10366
10671
|
if (resolved === null) return;
|
|
10367
|
-
const { node,
|
|
10368
|
-
const
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
else this.scrollSink.scrollBy(node, 0, distance, "smooth");
|
|
10672
|
+
const { node, takesX, takesY } = resolved;
|
|
10673
|
+
const flingX = takesX && Math.abs(velocity.x) >= this.flingVelocity ? velocity.x * this.momentum : 0;
|
|
10674
|
+
const flingY = takesY && Math.abs(velocity.y) >= this.flingVelocity ? velocity.y * this.momentum : 0;
|
|
10675
|
+
if (flingX === 0 && flingY === 0) return;
|
|
10676
|
+
this.scrollSink.scrollBy(node, flingX, flingY, "smooth");
|
|
10373
10677
|
}
|
|
10374
10678
|
sample(x, y) {
|
|
10375
10679
|
const t = this.now();
|
|
@@ -10396,7 +10700,7 @@ var UiTouchScroller = class {
|
|
|
10396
10700
|
};
|
|
10397
10701
|
}
|
|
10398
10702
|
/**
|
|
10399
|
-
* The container that should take this movement.
|
|
10703
|
+
* The container that should take this movement, and which of its axes.
|
|
10400
10704
|
*
|
|
10401
10705
|
* The nearest scroll container that can still move in the direction
|
|
10402
10706
|
* asked for, walking outward. A container already at its end is
|
|
@@ -10404,6 +10708,18 @@ var UiTouchScroller = class {
|
|
|
10404
10708
|
* that has hit its bottom keep scrolling the page it sits in. When
|
|
10405
10709
|
* nothing can move, the innermost container is returned so the
|
|
10406
10710
|
* gesture still belongs somewhere.
|
|
10711
|
+
*
|
|
10712
|
+
* **Each axis is asked for separately**, because a container may
|
|
10713
|
+
* scroll on both, and this used to pick one from the container's
|
|
10714
|
+
* flex direction and drop the other on the floor. A spreadsheet is
|
|
10715
|
+
* the case that found it on the wheel in 38e70a3: one `ScrollView`
|
|
10716
|
+
* whose content overflows in both directions, classified as vertical,
|
|
10717
|
+
* so a sideways drag moved nothing at all. This is the same fix on
|
|
10718
|
+
* the same shared state, so the two input paths cannot disagree about
|
|
10719
|
+
* which container takes a gesture.
|
|
10720
|
+
*
|
|
10721
|
+
* A container that overflows on one axis only is unaffected: the axis
|
|
10722
|
+
* it does not scroll has no room, so it is not taken.
|
|
10407
10723
|
*/
|
|
10408
10724
|
resolve(target, dx, dy) {
|
|
10409
10725
|
let innermost = null;
|
|
@@ -10411,16 +10727,19 @@ var UiTouchScroller = class {
|
|
|
10411
10727
|
if (!isScrollContainer(node)) continue;
|
|
10412
10728
|
const state = this.scrollSink.containerState(node);
|
|
10413
10729
|
if (state === void 0) continue;
|
|
10730
|
+
const takesX = dx !== 0 && hasScrollRoom(state.scrollX, state.maxScrollX, dx);
|
|
10731
|
+
const takesY = dy !== 0 && hasScrollRoom(state.scrollY, state.maxScrollY, dy);
|
|
10414
10732
|
innermost ??= {
|
|
10415
10733
|
node,
|
|
10416
|
-
state
|
|
10734
|
+
state,
|
|
10735
|
+
takesX: false,
|
|
10736
|
+
takesY: false
|
|
10417
10737
|
};
|
|
10418
|
-
|
|
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 {
|
|
10738
|
+
if (takesX || takesY) return {
|
|
10422
10739
|
node,
|
|
10423
|
-
state
|
|
10740
|
+
state,
|
|
10741
|
+
takesX,
|
|
10742
|
+
takesY
|
|
10424
10743
|
};
|
|
10425
10744
|
}
|
|
10426
10745
|
return innermost;
|
|
@@ -10430,6 +10749,504 @@ function defaultClock() {
|
|
|
10430
10749
|
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
10431
10750
|
}
|
|
10432
10751
|
//#endregion
|
|
10752
|
+
//#region src/input/UiEditingController.ts
|
|
10753
|
+
/** How long two presses may be apart and still count as a double click. */
|
|
10754
|
+
const MULTI_CLICK_MS = 500;
|
|
10755
|
+
const MULTI_CLICK_SLOP = 4;
|
|
10756
|
+
/** Room kept around the caret when scrolling it into view. */
|
|
10757
|
+
const CARET_REVEAL_PADDING = 4;
|
|
10758
|
+
/**
|
|
10759
|
+
* Editing behaviour for EditableText nodes.
|
|
10760
|
+
*
|
|
10761
|
+
* The keyboard and pointer controllers call in here as their default
|
|
10762
|
+
* behaviour for an editable target — after the app's listeners have
|
|
10763
|
+
* run and only when none of them called preventDefault() — and the
|
|
10764
|
+
* host feeds it the shell's text input: `beforeinput` edits,
|
|
10765
|
+
* composition, paste. The model on the node is the single source of
|
|
10766
|
+
* truth; this class turns intents into model changes, marks the node
|
|
10767
|
+
* dirty so layout and paint follow, reports the change through an
|
|
10768
|
+
* `input` event, and keeps the caret in view.
|
|
10769
|
+
*
|
|
10770
|
+
* Text arrives one of two ways. A shell with an editing proxy sends
|
|
10771
|
+
* `beforeinput`, where the browser has already resolved the IME, dead
|
|
10772
|
+
* keys and the keyboard layout; then printable key presses are not
|
|
10773
|
+
* text and `textFromKeys` is off. Without a proxy — tests, a bare
|
|
10774
|
+
* runtime — printable keys become insertions.
|
|
10775
|
+
*/
|
|
10776
|
+
var UiEditingController = class {
|
|
10777
|
+
host;
|
|
10778
|
+
dispatcher;
|
|
10779
|
+
focus;
|
|
10780
|
+
/** Insert printable key presses as text; off when a shell delivers text through beforeinput. */
|
|
10781
|
+
textFromKeys = true;
|
|
10782
|
+
platform;
|
|
10783
|
+
paint = createPaintState();
|
|
10784
|
+
focusedEditable = null;
|
|
10785
|
+
visible = true;
|
|
10786
|
+
/** x the current run of vertical moves keeps returning to. */
|
|
10787
|
+
verticalGoalX = void 0;
|
|
10788
|
+
dragging = null;
|
|
10789
|
+
lastPress = null;
|
|
10790
|
+
compositionOpen = false;
|
|
10791
|
+
constructor(host, dispatcher, focus, options = {}) {
|
|
10792
|
+
this.host = host;
|
|
10793
|
+
this.dispatcher = dispatcher;
|
|
10794
|
+
this.focus = focus;
|
|
10795
|
+
this.platform = options.platform ?? detectEditingPlatform();
|
|
10796
|
+
this.focus.onFocusChange((node) => this.handleFocusChange(node));
|
|
10797
|
+
}
|
|
10798
|
+
/** The editable that holds focus, or null. */
|
|
10799
|
+
get focused() {
|
|
10800
|
+
return this.focusedEditable;
|
|
10801
|
+
}
|
|
10802
|
+
/** Whether the pointer controller should hand presses on the node to this controller. */
|
|
10803
|
+
isEditable(node) {
|
|
10804
|
+
return isEditableNode(node);
|
|
10805
|
+
}
|
|
10806
|
+
/**
|
|
10807
|
+
* Default keyboard behaviour for the focused editable. Returns true
|
|
10808
|
+
* when the key was an editing command, so the caller can mark the
|
|
10809
|
+
* event handled.
|
|
10810
|
+
*/
|
|
10811
|
+
handleKey(node, key, modifiers) {
|
|
10812
|
+
if (!isEditableNode(node)) return false;
|
|
10813
|
+
const command = commandForKey(key, modifiers, this.platform, this.textFromKeys);
|
|
10814
|
+
if (command === null) return false;
|
|
10815
|
+
return this.execute(node, command);
|
|
10816
|
+
}
|
|
10817
|
+
execute(node, command) {
|
|
10818
|
+
const model = editorFor(node);
|
|
10819
|
+
const readOnly = isReadOnly(node);
|
|
10820
|
+
switch (command.kind) {
|
|
10821
|
+
case "move":
|
|
10822
|
+
this.move(node, model, command.unit, command.direction, command.extend);
|
|
10823
|
+
return true;
|
|
10824
|
+
case "delete":
|
|
10825
|
+
if (readOnly) return true;
|
|
10826
|
+
return this.applyEdit(node, model, deleteInputType(command.unit, command.direction), null, () => {
|
|
10827
|
+
if (command.direction < 0) model.deleteBackward(command.unit);
|
|
10828
|
+
else model.deleteForward(command.unit);
|
|
10829
|
+
});
|
|
10830
|
+
case "newline":
|
|
10831
|
+
if (!isMultiline(node)) return false;
|
|
10832
|
+
if (readOnly) return true;
|
|
10833
|
+
return this.applyEdit(node, model, "insertLineBreak", "\n", () => model.insertText("\n"));
|
|
10834
|
+
case "insert":
|
|
10835
|
+
if (readOnly) return true;
|
|
10836
|
+
return this.applyEdit(node, model, "insertText", command.text, () => model.insertText(command.text));
|
|
10837
|
+
case "selectAll":
|
|
10838
|
+
model.selectAll();
|
|
10839
|
+
this.afterSelectionChange(node, model);
|
|
10840
|
+
return true;
|
|
10841
|
+
case "undo":
|
|
10842
|
+
if (readOnly) return true;
|
|
10843
|
+
this.history(node, model, "historyUndo", () => model.undo());
|
|
10844
|
+
return true;
|
|
10845
|
+
case "redo":
|
|
10846
|
+
if (readOnly) return true;
|
|
10847
|
+
this.history(node, model, "historyRedo", () => model.redo());
|
|
10848
|
+
return true;
|
|
10849
|
+
}
|
|
10850
|
+
}
|
|
10851
|
+
move(node, model, unit, direction, extend) {
|
|
10852
|
+
if (unit === "vertical") {
|
|
10853
|
+
const target = this.layoutOf(node).verticalMove(model.focus, direction, this.verticalGoalX);
|
|
10854
|
+
if (target === null) {
|
|
10855
|
+
model.moveTo(direction < 0 ? 0 : model.text.length, extend);
|
|
10856
|
+
this.verticalGoalX = void 0;
|
|
10857
|
+
} else {
|
|
10858
|
+
model.moveTo(target.offset, extend);
|
|
10859
|
+
this.verticalGoalX = target.x;
|
|
10860
|
+
}
|
|
10861
|
+
this.afterSelectionChange(node, model, true);
|
|
10862
|
+
return;
|
|
10863
|
+
}
|
|
10864
|
+
if (unit === "line") {
|
|
10865
|
+
const layout = this.layoutOf(node);
|
|
10866
|
+
const caret = layout.caretRect(model.focus);
|
|
10867
|
+
const line = layout.lines[caret.line];
|
|
10868
|
+
const hard = direction < 0 ? lineStartAt(model.text, model.focus) : lineEndAt(model.text, model.focus);
|
|
10869
|
+
const visual = direction < 0 ? line.start : line.end;
|
|
10870
|
+
model.moveTo(direction < 0 ? Math.max(hard, visual) : Math.min(hard, visual), extend);
|
|
10871
|
+
this.afterSelectionChange(node, model);
|
|
10872
|
+
return;
|
|
10873
|
+
}
|
|
10874
|
+
model.move(unit, direction, extend);
|
|
10875
|
+
this.afterSelectionChange(node, model);
|
|
10876
|
+
}
|
|
10877
|
+
/**
|
|
10878
|
+
* A `beforeinput` from the shell's editing proxy, in the DOM's
|
|
10879
|
+
* vocabulary. Returns true when it was applied to the focused
|
|
10880
|
+
* editable. Composition types are not handled here: the composition
|
|
10881
|
+
* methods carry them, and an engine that fires both must not insert
|
|
10882
|
+
* twice.
|
|
10883
|
+
*/
|
|
10884
|
+
beforeInput(inputType, data) {
|
|
10885
|
+
const node = this.focusedEditable;
|
|
10886
|
+
if (node === null) return false;
|
|
10887
|
+
const model = editorFor(node);
|
|
10888
|
+
const readOnly = isReadOnly(node);
|
|
10889
|
+
switch (inputType) {
|
|
10890
|
+
case "insertText":
|
|
10891
|
+
case "insertReplacementText":
|
|
10892
|
+
case "insertFromDrop":
|
|
10893
|
+
case "insertFromYank":
|
|
10894
|
+
if (data === null || data.length === 0) return false;
|
|
10895
|
+
return readOnly || this.insert(node, model, inputType, data);
|
|
10896
|
+
case "insertFromPaste": return readOnly || data !== null && this.insert(node, model, inputType, data);
|
|
10897
|
+
case "insertLineBreak":
|
|
10898
|
+
case "insertParagraph":
|
|
10899
|
+
if (!isMultiline(node)) return false;
|
|
10900
|
+
return readOnly || this.applyEdit(node, model, inputType, "\n", () => model.insertText("\n"));
|
|
10901
|
+
case "deleteContentBackward":
|
|
10902
|
+
case "deleteWordBackward":
|
|
10903
|
+
case "deleteSoftLineBackward":
|
|
10904
|
+
case "deleteHardLineBackward":
|
|
10905
|
+
case "deleteContentForward":
|
|
10906
|
+
case "deleteWordForward":
|
|
10907
|
+
case "deleteSoftLineForward":
|
|
10908
|
+
case "deleteHardLineForward":
|
|
10909
|
+
case "deleteContent":
|
|
10910
|
+
case "deleteByCut":
|
|
10911
|
+
case "deleteByDrag": {
|
|
10912
|
+
if (readOnly) return true;
|
|
10913
|
+
const unit = /Word/.test(inputType) ? "word" : /Line/.test(inputType) ? "line" : "grapheme";
|
|
10914
|
+
const forward = /Forward/.test(inputType);
|
|
10915
|
+
return this.applyEdit(node, model, inputType, null, () => {
|
|
10916
|
+
if (inputType === "deleteContent" || inputType === "deleteByCut" || inputType === "deleteByDrag") model.replaceRange(model.start, model.end, "");
|
|
10917
|
+
else if (forward) model.deleteForward(unit);
|
|
10918
|
+
else model.deleteBackward(unit);
|
|
10919
|
+
});
|
|
10920
|
+
}
|
|
10921
|
+
case "historyUndo": return readOnly || this.history(node, model, inputType, () => model.undo());
|
|
10922
|
+
case "historyRedo": return readOnly || this.history(node, model, inputType, () => model.redo());
|
|
10923
|
+
default: return false;
|
|
10924
|
+
}
|
|
10925
|
+
}
|
|
10926
|
+
/** Inserts text at the selection of the focused editable, as typing would. */
|
|
10927
|
+
insertText(text) {
|
|
10928
|
+
const node = this.focusedEditable;
|
|
10929
|
+
if (node === null || isReadOnly(node)) return false;
|
|
10930
|
+
return this.insert(node, editorFor(node), "insertText", text);
|
|
10931
|
+
}
|
|
10932
|
+
/**
|
|
10933
|
+
* Replaces the focused editable's whole text, as an assistive
|
|
10934
|
+
* technology's "set value" does.
|
|
10935
|
+
*
|
|
10936
|
+
* `insertReplacementText` is the DOM's own input type for this, so an
|
|
10937
|
+
* `onBeforeInput` that vets edits sees the same vocabulary it sees
|
|
10938
|
+
* for typing, and an app that rejects the edit rejects this one too.
|
|
10939
|
+
* Selecting everything first is what makes it one undo step and one
|
|
10940
|
+
* `onInput` rather than a delete and an insert.
|
|
10941
|
+
*/
|
|
10942
|
+
replaceText(text) {
|
|
10943
|
+
const node = this.focusedEditable;
|
|
10944
|
+
if (node === null || isReadOnly(node)) return false;
|
|
10945
|
+
const model = editorFor(node);
|
|
10946
|
+
model.selectAll();
|
|
10947
|
+
return this.insert(node, model, "insertReplacementText", text);
|
|
10948
|
+
}
|
|
10949
|
+
/**
|
|
10950
|
+
* Pastes text: newlines are kept in a multiline field and become
|
|
10951
|
+
* spaces in a single-line one.
|
|
10952
|
+
*
|
|
10953
|
+
* With nothing editable focused the text is offered to whatever is,
|
|
10954
|
+
* as a `Paste` event, instead of being dropped. That is the case a
|
|
10955
|
+
* grid needs: it owns a rectangle of cells, a block of tab-separated
|
|
10956
|
+
* text means something to it that it means to nothing else, and
|
|
10957
|
+
* before this there was no way for it to hear about one at all.
|
|
10958
|
+
*/
|
|
10959
|
+
paste(text) {
|
|
10960
|
+
const node = this.focusedEditable;
|
|
10961
|
+
if (node === null || isReadOnly(node)) return this.offerPaste(text);
|
|
10962
|
+
return this.insert(node, editorFor(node), "insertFromPaste", text);
|
|
10963
|
+
}
|
|
10964
|
+
offerPaste(text) {
|
|
10965
|
+
const target = this.focus.focusedNode;
|
|
10966
|
+
if (target === null) return false;
|
|
10967
|
+
const event = new UiPasteEvent(text);
|
|
10968
|
+
this.dispatcher.dispatch(event, target);
|
|
10969
|
+
return event.defaultPrevented;
|
|
10970
|
+
}
|
|
10971
|
+
insert(node, model, inputType, text) {
|
|
10972
|
+
const data = isMultiline(node) ? text : text.replace(/\r\n|\r|\n/g, " ");
|
|
10973
|
+
return this.applyEdit(node, model, inputType, data, () => model.insertText(data));
|
|
10974
|
+
}
|
|
10975
|
+
compositionStart() {
|
|
10976
|
+
const node = this.focusedEditable;
|
|
10977
|
+
if (node === null || isReadOnly(node)) return;
|
|
10978
|
+
const model = editorFor(node);
|
|
10979
|
+
model.beginComposition();
|
|
10980
|
+
this.compositionOpen = true;
|
|
10981
|
+
this.afterTextChange(node, model, false);
|
|
10982
|
+
}
|
|
10983
|
+
/** The composition text so far and the caret within it. */
|
|
10984
|
+
compositionUpdate(text, caret = text.length) {
|
|
10985
|
+
const node = this.focusedEditable;
|
|
10986
|
+
if (node === null || isReadOnly(node)) return;
|
|
10987
|
+
const model = editorFor(node);
|
|
10988
|
+
model.updateComposition(text, caret);
|
|
10989
|
+
this.compositionOpen = true;
|
|
10990
|
+
this.afterTextChange(node, model, false);
|
|
10991
|
+
}
|
|
10992
|
+
/** The IME committed `text` (possibly empty: the composition was cancelled). */
|
|
10993
|
+
compositionEnd(text) {
|
|
10994
|
+
const node = this.focusedEditable;
|
|
10995
|
+
this.compositionOpen = false;
|
|
10996
|
+
if (node === null || isReadOnly(node)) return;
|
|
10997
|
+
const model = editorFor(node);
|
|
10998
|
+
const data = isMultiline(node) ? text : text.replace(/\r\n|\r|\n/g, " ");
|
|
10999
|
+
const before = model.text;
|
|
11000
|
+
model.commitComposition(data);
|
|
11001
|
+
this.afterTextChange(node, model, before !== model.text);
|
|
11002
|
+
}
|
|
11003
|
+
/**
|
|
11004
|
+
* A press on an editable places the caret (Shift extends the
|
|
11005
|
+
* selection to it); a second press within the double-click window
|
|
11006
|
+
* selects the word, a third the line. Dragging afterwards extends the
|
|
11007
|
+
* selection from the anchor.
|
|
11008
|
+
*/
|
|
11009
|
+
pointerDown(node, x, y, modifiers) {
|
|
11010
|
+
if (!isEditableNode(node)) return;
|
|
11011
|
+
const model = editorFor(node);
|
|
11012
|
+
const layout = this.layoutOf(node);
|
|
11013
|
+
const local = this.host.toLocal(node, x, y);
|
|
11014
|
+
const offset = layout.offsetAt(local.x, local.y);
|
|
11015
|
+
const now = this.host.now();
|
|
11016
|
+
const last = this.lastPress;
|
|
11017
|
+
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;
|
|
11018
|
+
this.lastPress = {
|
|
11019
|
+
node,
|
|
11020
|
+
x,
|
|
11021
|
+
y,
|
|
11022
|
+
at: now,
|
|
11023
|
+
count
|
|
11024
|
+
};
|
|
11025
|
+
this.dragging = node;
|
|
11026
|
+
if (modifiers.shift) model.moveTo(offset, true);
|
|
11027
|
+
else if (count === 2) {
|
|
11028
|
+
const word = wordRangeAt(model.text, offset);
|
|
11029
|
+
model.select(word.start, word.end);
|
|
11030
|
+
} else if (count >= 3) model.select(lineStartAt(model.text, offset), lineEndAt(model.text, offset));
|
|
11031
|
+
else model.select(offset);
|
|
11032
|
+
model.breakUndoGroup();
|
|
11033
|
+
this.afterSelectionChange(node, model);
|
|
11034
|
+
}
|
|
11035
|
+
pointerMove(node, x, y) {
|
|
11036
|
+
if (this.dragging !== node) return;
|
|
11037
|
+
const model = editorFor(node);
|
|
11038
|
+
const layout = this.layoutOf(node);
|
|
11039
|
+
const local = this.host.toLocal(node, x, y);
|
|
11040
|
+
const offset = layout.offsetAt(local.x, local.y);
|
|
11041
|
+
if (offset !== model.focus) {
|
|
11042
|
+
model.moveTo(offset, true);
|
|
11043
|
+
this.afterSelectionChange(node, model);
|
|
11044
|
+
}
|
|
11045
|
+
}
|
|
11046
|
+
pointerUp() {
|
|
11047
|
+
this.dragging = null;
|
|
11048
|
+
}
|
|
11049
|
+
/** The focused editable's state for the shell to mirror, or null. */
|
|
11050
|
+
state() {
|
|
11051
|
+
const node = this.focusedEditable;
|
|
11052
|
+
if (node === null || this.host.recordFor(node) === void 0) return null;
|
|
11053
|
+
const model = editorFor(node);
|
|
11054
|
+
const caret = this.layoutOf(node).caretRect();
|
|
11055
|
+
const visible = this.host.visibleBox(node);
|
|
11056
|
+
return {
|
|
11057
|
+
text: model.text,
|
|
11058
|
+
selectionStart: model.start,
|
|
11059
|
+
selectionEnd: model.end,
|
|
11060
|
+
caret: {
|
|
11061
|
+
x: visible.x + caret.x,
|
|
11062
|
+
y: visible.y + caret.y,
|
|
11063
|
+
width: 1,
|
|
11064
|
+
height: caret.height
|
|
11065
|
+
},
|
|
11066
|
+
multiline: isMultiline(node),
|
|
11067
|
+
composing: model.composing
|
|
11068
|
+
};
|
|
11069
|
+
}
|
|
11070
|
+
/**
|
|
11071
|
+
* When the focused caret next toggles, or undefined when nothing
|
|
11072
|
+
* blinks: no focused editable, a composition in progress (the caret
|
|
11073
|
+
* holds), or a hidden page.
|
|
11074
|
+
*/
|
|
11075
|
+
nextCaretChange(now) {
|
|
11076
|
+
const node = this.focusedEditable;
|
|
11077
|
+
if (node === null || !this.visible) return;
|
|
11078
|
+
const model = editorFor(node);
|
|
11079
|
+
if (model.composing || !model.collapsed) return;
|
|
11080
|
+
return nextCaretToggle(model, now);
|
|
11081
|
+
}
|
|
11082
|
+
/**
|
|
11083
|
+
* The page was hidden or shown. A hidden page stops the blink so a
|
|
11084
|
+
* background tab schedules no frames; showing it restarts the caret.
|
|
11085
|
+
*/
|
|
11086
|
+
setVisible(visible) {
|
|
11087
|
+
if (this.visible === visible) return;
|
|
11088
|
+
this.visible = visible;
|
|
11089
|
+
const node = this.focusedEditable;
|
|
11090
|
+
if (node !== null) {
|
|
11091
|
+
editorFor(node).blinkOrigin = this.host.now();
|
|
11092
|
+
this.host.markDirty(node, 2);
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
11095
|
+
/** Whether the caret may be drawn at all (the page is visible). */
|
|
11096
|
+
get caretEnabled() {
|
|
11097
|
+
return this.visible;
|
|
11098
|
+
}
|
|
11099
|
+
handleFocusChange(node) {
|
|
11100
|
+
const previous = this.focusedEditable;
|
|
11101
|
+
const next = node !== null && isEditableNode(node) ? node : null;
|
|
11102
|
+
if (previous === next) return;
|
|
11103
|
+
if (previous !== null) {
|
|
11104
|
+
const model = editorFor(previous);
|
|
11105
|
+
if (model.composing) model.commitComposition(model.text.slice(model.composition.start, model.composition.end));
|
|
11106
|
+
model.focused = false;
|
|
11107
|
+
this.host.markDirty(previous, 2);
|
|
11108
|
+
}
|
|
11109
|
+
this.focusedEditable = next;
|
|
11110
|
+
this.compositionOpen = false;
|
|
11111
|
+
this.verticalGoalX = void 0;
|
|
11112
|
+
if (next !== null) {
|
|
11113
|
+
const model = editorFor(next);
|
|
11114
|
+
model.focused = true;
|
|
11115
|
+
model.blinkOrigin = this.host.now();
|
|
11116
|
+
this.host.markDirty(next, 2);
|
|
11117
|
+
}
|
|
11118
|
+
}
|
|
11119
|
+
/**
|
|
11120
|
+
* Runs an edit unless a `beforeinput` listener cancels it, then
|
|
11121
|
+
* reports it. Returns true when the input was consumed either way.
|
|
11122
|
+
*/
|
|
11123
|
+
applyEdit(node, model, inputType, data, run) {
|
|
11124
|
+
const before = new UiBeforeInputEvent(inputType, data);
|
|
11125
|
+
this.dispatcher.dispatch(before, node);
|
|
11126
|
+
if (before.defaultPrevented) return true;
|
|
11127
|
+
const text = model.text;
|
|
11128
|
+
run();
|
|
11129
|
+
this.afterTextChange(node, model, text !== model.text);
|
|
11130
|
+
return true;
|
|
11131
|
+
}
|
|
11132
|
+
history(node, model, inputType, run) {
|
|
11133
|
+
const before = new UiBeforeInputEvent(inputType, null);
|
|
11134
|
+
this.dispatcher.dispatch(before, node);
|
|
11135
|
+
if (before.defaultPrevented) return true;
|
|
11136
|
+
const text = model.text;
|
|
11137
|
+
run();
|
|
11138
|
+
this.afterTextChange(node, model, text !== model.text);
|
|
11139
|
+
return true;
|
|
11140
|
+
}
|
|
11141
|
+
afterTextChange(node, model, changed) {
|
|
11142
|
+
this.verticalGoalX = void 0;
|
|
11143
|
+
model.blinkOrigin = this.host.now();
|
|
11144
|
+
this.host.markDirty(node, changed || this.compositionOpen ? 5 : 2);
|
|
11145
|
+
if (changed) this.dispatcher.dispatch(new UiTextChangeEvent(model.text, model.start, model.end), node);
|
|
11146
|
+
this.notifySelection(node, model);
|
|
11147
|
+
this.revealCaret(node);
|
|
11148
|
+
}
|
|
11149
|
+
/**
|
|
11150
|
+
* Reports the selection, if it has actually moved since last time.
|
|
11151
|
+
*
|
|
11152
|
+
* The guard is what keeps this quiet: holding a right arrow against
|
|
11153
|
+
* the end of the text moves nothing and must say nothing, and a
|
|
11154
|
+
* listener that recomputed decoration on every key would otherwise
|
|
11155
|
+
* do it forever for no reason.
|
|
11156
|
+
*
|
|
11157
|
+
* `hasListeners` already makes an unheard dispatch free, so the
|
|
11158
|
+
* cost when nobody is listening is the two comparisons here.
|
|
11159
|
+
*/
|
|
11160
|
+
notifySelection(node, model) {
|
|
11161
|
+
if (model.anchor === model.notifiedAnchor && model.focus === model.notifiedFocus) return;
|
|
11162
|
+
model.notifiedAnchor = model.anchor;
|
|
11163
|
+
model.notifiedFocus = model.focus;
|
|
11164
|
+
this.dispatcher.dispatch(new UiSelectionChangeEvent(model.text, model.start, model.end, model.anchor, model.focus), node);
|
|
11165
|
+
}
|
|
11166
|
+
afterSelectionChange(node, model, keepGoal = false) {
|
|
11167
|
+
if (!keepGoal) this.verticalGoalX = void 0;
|
|
11168
|
+
model.blinkOrigin = this.host.now();
|
|
11169
|
+
this.host.markDirty(node, 2);
|
|
11170
|
+
this.notifySelection(node, model);
|
|
11171
|
+
this.revealCaret(node);
|
|
11172
|
+
}
|
|
11173
|
+
/**
|
|
11174
|
+
* Asks the host to scroll the caret into view, when the node has been
|
|
11175
|
+
* laid out: the field's own text first, then any scroll container
|
|
11176
|
+
* around it.
|
|
11177
|
+
*
|
|
11178
|
+
* `layoutOf` places the text where it is seen, so the caret comes
|
|
11179
|
+
* back with the field's own scroll already taken off; the reveal is
|
|
11180
|
+
* asked for in the node's unscrolled coordinates, which is where the
|
|
11181
|
+
* offset it wants is measured from.
|
|
11182
|
+
*/
|
|
11183
|
+
revealCaret(node) {
|
|
11184
|
+
const rec = this.host.recordFor(node);
|
|
11185
|
+
if (rec === void 0) return;
|
|
11186
|
+
const caret = this.layoutOf(node).caretRect();
|
|
11187
|
+
this.host.reveal(node, {
|
|
11188
|
+
x: caret.x + rec.scrollX - CARET_REVEAL_PADDING,
|
|
11189
|
+
y: caret.y + rec.scrollY - CARET_REVEAL_PADDING,
|
|
11190
|
+
width: 9,
|
|
11191
|
+
height: caret.height + 8
|
|
11192
|
+
});
|
|
11193
|
+
}
|
|
11194
|
+
/**
|
|
11195
|
+
* The node's text laid out in node-local coordinates: the content box
|
|
11196
|
+
* starts at the padding and is shifted by the field's own scroll, as
|
|
11197
|
+
* it is for the renderers in the parent's space. Points from the
|
|
11198
|
+
* pointer and the caret box the shell is given are both in that same
|
|
11199
|
+
* seen space, so neither has to know the offset. Before the first
|
|
11200
|
+
* layout the record is empty and the lines fall at the origin, which
|
|
11201
|
+
* is still a valid place for a caret.
|
|
11202
|
+
*/
|
|
11203
|
+
/**
|
|
11204
|
+
* Where the caret is inside a field, in the field's own coordinates.
|
|
11205
|
+
*
|
|
11206
|
+
* Relative to the node's border-box origin, with the field's own
|
|
11207
|
+
* scroll already applied — so a popup placed under the caret adds
|
|
11208
|
+
* the node's position on screen and nothing else.
|
|
11209
|
+
*
|
|
11210
|
+
* Public because an application cannot work this out. The geometry
|
|
11211
|
+
* needs the paragraph as it was *laid out*, which means the
|
|
11212
|
+
* measurer, the resolved paint and the layout record, and an
|
|
11213
|
+
* application that re-measured the text to find the caret would be
|
|
11214
|
+
* a second measurer that must never disagree with this one. Asking
|
|
11215
|
+
* is the only version that stays right.
|
|
11216
|
+
*
|
|
11217
|
+
* Null when the node is not an editable, or has not been laid out.
|
|
11218
|
+
*/
|
|
11219
|
+
caretRectOf(node) {
|
|
11220
|
+
if (!isEditableNode(node) || this.host.recordFor(node) === void 0) return null;
|
|
11221
|
+
return this.layoutOf(node).caretRect();
|
|
11222
|
+
}
|
|
11223
|
+
layoutOf(node) {
|
|
11224
|
+
const rec = this.host.recordFor(node);
|
|
11225
|
+
const state = resolvePaintState(node, this.paint);
|
|
11226
|
+
const box = rec === void 0 ? {
|
|
11227
|
+
x: 0,
|
|
11228
|
+
y: 0,
|
|
11229
|
+
width: 0,
|
|
11230
|
+
height: 0
|
|
11231
|
+
} : {
|
|
11232
|
+
x: rec.paddingLeft - rec.scrollX,
|
|
11233
|
+
y: rec.paddingTop - rec.scrollY,
|
|
11234
|
+
width: Math.max(0, rec.width - rec.paddingLeft - rec.paddingRight),
|
|
11235
|
+
height: Math.max(0, rec.height - rec.paddingTop - rec.paddingBottom)
|
|
11236
|
+
};
|
|
11237
|
+
return new EditableLayout(state.editor, box, state, this.host.measurer);
|
|
11238
|
+
}
|
|
11239
|
+
};
|
|
11240
|
+
function deleteInputType(unit, direction) {
|
|
11241
|
+
const side = direction < 0 ? "Backward" : "Forward";
|
|
11242
|
+
switch (unit) {
|
|
11243
|
+
case "word": return `deleteWord${side}`;
|
|
11244
|
+
case "line":
|
|
11245
|
+
case "document": return `deleteSoftLine${side}`;
|
|
11246
|
+
default: return `deleteContent${side}`;
|
|
11247
|
+
}
|
|
11248
|
+
}
|
|
11249
|
+
//#endregion
|
|
10433
11250
|
//#region src/input/UiPlatformAdapter.ts
|
|
10434
11251
|
/**
|
|
10435
11252
|
* Bridges platform/browser input events to the framework input
|
|
@@ -12157,8 +12974,6 @@ const SCROLLBAR_FADE_MS = 350;
|
|
|
12157
12974
|
* The limit is what stops a cycle of anchors from spinning.
|
|
12158
12975
|
*/
|
|
12159
12976
|
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
12977
|
/**
|
|
12163
12978
|
* Whether a node names any of the four logical horizontal insets, and
|
|
12164
12979
|
* so needs its writing direction resolved before its box is worked
|
|
@@ -13561,7 +14376,7 @@ var LayoutEngine = class {
|
|
|
13561
14376
|
contentMain = Math.max(contentMain, this.lineOuterMain(line, config.gapMain, "final"));
|
|
13562
14377
|
} else {
|
|
13563
14378
|
for (const item of line.items) item.finalMain = item.hypotheticalMain;
|
|
13564
|
-
contentMain = Math.max(contentMain,
|
|
14379
|
+
contentMain = Math.max(contentMain, this.lineMaxContentMain(line, config.gapMain));
|
|
13565
14380
|
}
|
|
13566
14381
|
}
|
|
13567
14382
|
if (mainDefinite) contentMain = Math.max(mainMax, contentMain);
|
|
@@ -13668,6 +14483,7 @@ var LayoutEngine = class {
|
|
|
13668
14483
|
child,
|
|
13669
14484
|
rec: cRec,
|
|
13670
14485
|
baseMain,
|
|
14486
|
+
maxContentMain: measuredMain,
|
|
13671
14487
|
hypotheticalMain: this.clamp(baseMain, minMain, maxMain),
|
|
13672
14488
|
finalMain: baseMain,
|
|
13673
14489
|
cross: row ? cRec.measuredHeight : cRec.measuredWidth,
|
|
@@ -13776,6 +14592,33 @@ var LayoutEngine = class {
|
|
|
13776
14592
|
});
|
|
13777
14593
|
return lines;
|
|
13778
14594
|
}
|
|
14595
|
+
/**
|
|
14596
|
+
* A line's main size when the container shrink-wraps it.
|
|
14597
|
+
*
|
|
14598
|
+
* The sum of the hypothetical sizes would be the answer if an item
|
|
14599
|
+
* never grew — but `flex: 1` is `flex-basis: 0`, and an item that
|
|
14600
|
+
* starts flexing from zero asks for nothing while its content still
|
|
14601
|
+
* wants the width of its text. A container wrapped around that sum
|
|
14602
|
+
* comes out too narrow; the item's text then wraps inside a box that
|
|
14603
|
+
* was measured as though it had not, and the container is left
|
|
14604
|
+
* shorter than the children it draws — a menu whose last rows fall
|
|
14605
|
+
* out of its own panel.
|
|
14606
|
+
*
|
|
14607
|
+
* So an item that can grow contributes whichever is larger, the size
|
|
14608
|
+
* its basis gives it or the size its content asks for. Chrome ignores
|
|
14609
|
+
* the basis entirely here and browsers disagree with each other about
|
|
14610
|
+
* it; the larger of the two is the reading that keeps `flex-basis`
|
|
14611
|
+
* counting for something, which is what `basis/sizes-container`
|
|
14612
|
+
* already records this engine as doing.
|
|
14613
|
+
*/
|
|
14614
|
+
lineMaxContentMain(line, gap) {
|
|
14615
|
+
let total = gap * (line.items.length - 1);
|
|
14616
|
+
for (const item of line.items) {
|
|
14617
|
+
const wanted = item.grow > 0 ? this.clamp(item.maxContentMain, item.minMain, item.maxMain) : 0;
|
|
14618
|
+
total += Math.max(item.hypotheticalMain, wanted) + item.marginMainStart + item.marginMainEnd;
|
|
14619
|
+
}
|
|
14620
|
+
return total;
|
|
14621
|
+
}
|
|
13779
14622
|
lineOuterMain(line, gap, size) {
|
|
13780
14623
|
let total = gap * (line.items.length - 1);
|
|
13781
14624
|
for (const item of line.items) total += (size === "final" ? item.finalMain : item.hypotheticalMain) + item.marginMainStart + item.marginMainEnd;
|
|
@@ -14104,8 +14947,9 @@ var LayoutEngine = class {
|
|
|
14104
14947
|
const paddingV = rec.paddingTop + rec.paddingBottom;
|
|
14105
14948
|
const editable = node.type === "editable-text";
|
|
14106
14949
|
if (node.type === "text" || node.type === "button" || editable) {
|
|
14107
|
-
const
|
|
14108
|
-
const
|
|
14950
|
+
const typed = editable ? editorFor(node).text : "";
|
|
14951
|
+
const spans = editable ? editableSpansOf(node, typed) : resolvedSpansOf(node);
|
|
14952
|
+
const text = editable ? typed : spans.length > 0 ? textContentOf(node) : String(node.properties.get("text") ?? "");
|
|
14109
14953
|
const placeholderProp = node.properties.get("placeholder");
|
|
14110
14954
|
const placeholder = editable && text.length === 0 && typeof placeholderProp === "string" ? placeholderProp : void 0;
|
|
14111
14955
|
const font = resolveFont(node);
|
|
@@ -14847,6 +15691,7 @@ var LayoutEngine = class {
|
|
|
14847
15691
|
*/
|
|
14848
15692
|
markLayoutDirty(node, selfCanBound = false) {
|
|
14849
15693
|
let current = node;
|
|
15694
|
+
let bounded = false;
|
|
14850
15695
|
for (;;) {
|
|
14851
15696
|
const rec = this.records.get(current);
|
|
14852
15697
|
if (rec !== void 0) {
|
|
@@ -14854,12 +15699,35 @@ var LayoutEngine = class {
|
|
|
14854
15699
|
rec.placeDirty = true;
|
|
14855
15700
|
}
|
|
14856
15701
|
if (current === this.layoutRoot || current.parent === null) {
|
|
14857
|
-
this.relayoutRoots.add(this.layoutRoot ?? current);
|
|
15702
|
+
if (!bounded) this.relayoutRoots.add(this.layoutRoot ?? current);
|
|
14858
15703
|
return;
|
|
14859
15704
|
}
|
|
14860
|
-
if (rec?.relayoutBoundary && !rec.positioned && (current !== node || selfCanBound)) {
|
|
15705
|
+
if (!bounded && rec?.relayoutBoundary && !rec.positioned && (current !== node || selfCanBound)) {
|
|
14861
15706
|
this.relayoutRoots.add(current);
|
|
14862
|
-
|
|
15707
|
+
/**
|
|
15708
|
+
* The boundary is where the *relayout* starts, and the walk
|
|
15709
|
+
* carries on past it marking ancestors.
|
|
15710
|
+
*
|
|
15711
|
+
* Stopping here as well is sound only while the pass really
|
|
15712
|
+
* does start at this boundary. It does not always: another
|
|
15713
|
+
* node dirtied in the same frame can drag the layout root
|
|
15714
|
+
* into `relayoutRoots`, and then the pass is a `fullLayout`
|
|
15715
|
+
* that measures from the root — down through ancestors this
|
|
15716
|
+
* walk left clean, every one of which short-circuits on its
|
|
15717
|
+
* own measure memo and never reaches the node that changed.
|
|
15718
|
+
*
|
|
15719
|
+
* Found with a bar switched into the chrome by choosing a
|
|
15720
|
+
* menu item: the menu's own teardown put the root in the set,
|
|
15721
|
+
* the frame did a full layout, and `measure` was never called
|
|
15722
|
+
* on the column that had gained a child. It drew at zero
|
|
15723
|
+
* height, with every spec passing — a spec asks what a node's
|
|
15724
|
+
* properties are, and they were all correct.
|
|
15725
|
+
*
|
|
15726
|
+
* Marking to the root costs a walk of the depth and two
|
|
15727
|
+
* booleans a node, against a memo that is otherwise believed
|
|
15728
|
+
* when it should not be.
|
|
15729
|
+
*/
|
|
15730
|
+
bounded = true;
|
|
14863
15731
|
}
|
|
14864
15732
|
current = current.parent;
|
|
14865
15733
|
}
|
|
@@ -15239,10 +16107,26 @@ var LayoutEngine = class {
|
|
|
15239
16107
|
*/
|
|
15240
16108
|
assignBox(node, x, y, width, height) {
|
|
15241
16109
|
const rec = this.record(node);
|
|
15242
|
-
if (rec.positioned && !rec.absolute && node !== this.layoutRoot) {
|
|
16110
|
+
if (rec.positioned && !rec.absolute && !rec.sticky && node !== this.layoutRoot) {
|
|
15243
16111
|
x += rec.left ?? (rec.right !== void 0 ? -rec.right : 0);
|
|
15244
16112
|
y += rec.top ?? (rec.bottom !== void 0 ? -rec.bottom : 0);
|
|
15245
16113
|
}
|
|
16114
|
+
/**
|
|
16115
|
+
* A **sticky** node is not offset by its insets, and that is the
|
|
16116
|
+
* difference between the two.
|
|
16117
|
+
*
|
|
16118
|
+
* `top: 0` on a sticky header is a threshold — "do not let me go
|
|
16119
|
+
* above the top of the scrollport" — and not a displacement, which
|
|
16120
|
+
* is what CSS means by it and what `resolveStickyOffset` below
|
|
16121
|
+
* assumes when it computes `viewLeft + rec.left - rec.x`. Offset
|
|
16122
|
+
* here *and* used as a threshold there, an inset is applied twice
|
|
16123
|
+
* and the two errors cancel only when it is zero.
|
|
16124
|
+
*
|
|
16125
|
+
* Which is why nothing noticed: every sticky node in the framework
|
|
16126
|
+
* and its examples sticks at zero. The first one that did not — a
|
|
16127
|
+
* spreadsheet's second frozen column, which sticks at the width of
|
|
16128
|
+
* the gutter — was drawn one gutter too far to the right.
|
|
16129
|
+
*/
|
|
15246
16130
|
if (rec.x !== x || rec.y !== y || rec.width !== width || rec.height !== height) {
|
|
15247
16131
|
rec.x = x;
|
|
15248
16132
|
rec.y = y;
|
|
@@ -15482,12 +16366,37 @@ function drawOverlayShapes(ctx, shapes) {
|
|
|
15482
16366
|
*/
|
|
15483
16367
|
function decorationRect(shape, rec, nodeRadius) {
|
|
15484
16368
|
const outset = shape.outset ?? 0;
|
|
16369
|
+
const horizontal = axis(rec.x, rec.width, shape.x, shape.width, shape.right, outset);
|
|
16370
|
+
const vertical = axis(rec.y, rec.height, shape.y, shape.height, shape.bottom, outset);
|
|
16371
|
+
const radius = Math.max(0, shape.radius ?? nodeRadius + outset);
|
|
15485
16372
|
return {
|
|
15486
|
-
x:
|
|
15487
|
-
y:
|
|
15488
|
-
width:
|
|
15489
|
-
height:
|
|
15490
|
-
radius
|
|
16373
|
+
x: horizontal.start,
|
|
16374
|
+
y: vertical.start,
|
|
16375
|
+
width: horizontal.size,
|
|
16376
|
+
height: vertical.size,
|
|
16377
|
+
radius
|
|
16378
|
+
};
|
|
16379
|
+
}
|
|
16380
|
+
/**
|
|
16381
|
+
* One axis of the placement, from any two of near, size and far.
|
|
16382
|
+
*
|
|
16383
|
+
* The outset applies after the three are reconciled, growing the
|
|
16384
|
+
* rectangle by it on both sides, so it means the same thing however
|
|
16385
|
+
* the rectangle was described.
|
|
16386
|
+
*/
|
|
16387
|
+
function axis(origin, extent, near, size, far, outset) {
|
|
16388
|
+
let start;
|
|
16389
|
+
let length;
|
|
16390
|
+
if (near === void 0 && size !== void 0 && far !== void 0) {
|
|
16391
|
+
length = size;
|
|
16392
|
+
start = extent - far - size;
|
|
16393
|
+
} else {
|
|
16394
|
+
start = near ?? 0;
|
|
16395
|
+
length = size ?? extent - start - (far ?? 0);
|
|
16396
|
+
}
|
|
16397
|
+
return {
|
|
16398
|
+
start: origin + start - outset,
|
|
16399
|
+
size: Math.max(0, length + 2 * outset)
|
|
15491
16400
|
};
|
|
15492
16401
|
}
|
|
15493
16402
|
/**
|
|
@@ -17108,12 +18017,16 @@ var Canvas2DRenderer = class {
|
|
|
17108
18017
|
paintEditable(ctx, paint, context) {
|
|
17109
18018
|
const model = paint.editor;
|
|
17110
18019
|
const layout = new EditableLayout(model, this.contentBox, paint, context.text);
|
|
18020
|
+
fillTextRects(ctx, textRunBackgrounds(layout.lines, paint));
|
|
17111
18021
|
if (model.focused && !model.collapsed) {
|
|
17112
18022
|
ctx.fillStyle = colorToCss(paint.selectionColor);
|
|
17113
18023
|
for (const box of layout.selectionBoxes()) ctx.fillRect(box.x, box.y, box.width, box.height);
|
|
17114
18024
|
}
|
|
17115
18025
|
if (layout.placeholderLines.length > 0) drawTextLines(ctx, layout.placeholderLines, buildFontString(paint), colorToCss(paint.placeholderColor), paint.letterSpacing, paint.rtl);
|
|
17116
|
-
else
|
|
18026
|
+
else {
|
|
18027
|
+
drawTextLines(ctx, layout.lines, buildFontString(paint), colorToCss(paint.textColor), paint.letterSpacing, paint.rtl, paint);
|
|
18028
|
+
fillTextRects(ctx, textRunDecorations(layout.lines, paint));
|
|
18029
|
+
}
|
|
17117
18030
|
if (model.composing) {
|
|
17118
18031
|
ctx.fillStyle = colorToCss(paint.textColor);
|
|
17119
18032
|
for (const box of layout.compositionBoxes()) {
|
|
@@ -17297,6 +18210,6 @@ function createCanvasSurface(host) {
|
|
|
17297
18210
|
return new CanvasSurface(host);
|
|
17298
18211
|
}
|
|
17299
18212
|
//#endregion
|
|
17300
|
-
export { prepareInputSurface as $,
|
|
18213
|
+
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
18214
|
|
|
17302
|
-
//# sourceMappingURL=CanvasSurface-
|
|
18215
|
+
//# sourceMappingURL=CanvasSurface-ifHkg0-g.js.map
|