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.
- package/CHANGELOG.md +121 -0
- package/dist/{CanvasSurface-5dU-U3qQ.js → CanvasSurface-BR7hECHm.js} +1075 -72
- package/dist/CanvasSurface-BR7hECHm.js.map +1 -0
- package/dist/{index-DAdauEn9.d.ts → index-BzXn0Bci.d.ts} +854 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +756 -577
- 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,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 ?
|
|
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
|
|
8735
|
-
if (
|
|
8736
|
-
|
|
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
|
-
/**
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
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.
|
|
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
|
|
9773
|
-
const
|
|
9774
|
-
|
|
9775
|
-
|
|
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
|
-
|
|
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,
|
|
10353
|
-
|
|
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,
|
|
10368
|
-
const
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
14108
|
-
const
|
|
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
|
|
14359
|
-
const
|
|
14360
|
-
|
|
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
|
-
|
|
14399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15487
|
-
|
|
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
|
|
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 $,
|
|
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-
|
|
18305
|
+
//# sourceMappingURL=CanvasSurface-BR7hECHm.js.map
|