gesso-framework 0.6.19 → 0.6.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/{index-CGLXEx9R.d.ts → index-C2nyeg5Y.d.ts} +171 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +382 -76
- package/dist/index.js.map +1 -1
- package/dist/remote/index.d.ts +1 -1
- package/dist/worker/index.d.ts +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { a as Each, i as show, n as getComponentMetadata, o as each, r as Show,
|
|
|
4
4
|
import { c as findUnplainPath, d as describeChannel, l as requirePlainData, t as AGENT_PORT, u as channelSchema } from "./remote-BoB8zh7z.js";
|
|
5
5
|
import { n as serveApplicationAgent } from "./app-Yig59Ygf.js";
|
|
6
6
|
import { BehaviorSubject, EMPTY, Observable, Subject, Subscription, distinctUntilChanged, map } from "rxjs";
|
|
7
|
-
import { AnimationDriver, Box, Canvas2DRenderer, CanvasPlatformSurface, CanvasTextMeasurer, Constraints, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, MARK_PREFIX, Stack, TEXT_RUN_ID_SEPARATOR, UI_ROLES, UI_SEMANTIC_STATES, UiAnimationFrameClock, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiNodeType, UiPlatformAdapter, UiPointerController, UiPointerEvent, UiScheduler, UiSelectionController, UiSharedElements, UiSpring, UiTimerFrameClock, UiTouchScroller, UiVirtualSheet, UiVirtualWindow, UiWheelController, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_SHEET_PROP, VIRTUAL_WINDOW_PROP, WebGPURenderer, buildSemanticsSubtree, buildSemanticsTree, bumpFontStack, capturePointer, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, easings, fade, formatExplanation, insetsEqual, isComponentLikeElement, isImeKey, isObservable, isWebGPUAvailable, linear, linkOf, markInstant, markNow, measure, measureSpan, motion, noInsets, observeViewportInsets, percent, performanceMarksEnabled, pointerDeviceOf, prepareInputSurface, propertyEffects, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollRange, scrollbarThumb, semanticsInertAbove, semanticsMemory, setPerformanceMarks, setPerformanceMarks as setPerformanceMarks$1, textRunOfRecordId, themeTokens, touchActionFor, wheelDeltaYOf } from "gesso-core";
|
|
7
|
+
import { AnimationDriver, Box, Canvas2DRenderer, CanvasPlatformSurface, CanvasTextMeasurer, Constraints, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, MARK_PREFIX, Stack, TEXT_RUN_ID_SEPARATOR, UI_ROLES, UI_SEMANTIC_STATES, UiAnimationFrameClock, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiFrame, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiNodeType, UiPlatformAdapter, UiPointerController, UiPointerEvent, UiScheduler, UiSelectionController, UiSharedElements, UiSpring, UiTimerFrameClock, UiTouchScroller, UiVirtualSheet, UiVirtualWindow, UiWheelController, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_SHEET_PROP, VIRTUAL_WINDOW_PROP, WebGPURenderer, buildSemanticsSubtree, buildSemanticsTree, bumpFontStack, capturePointer, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, easings, fade, formatExplanation, insetsEqual, isComponentLikeElement, isImeKey, isObservable, isWebGPUAvailable, linear, linkOf, markInstant, markNow, measure, measureSpan, motion, noInsets, observeViewportInsets, percent, performanceMarksEnabled, pointerDeviceOf, prepareInputSurface, propertyEffects, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollRange, scrollbarThumb, semanticsInertAbove, semanticsMemory, setPerformanceMarks, setPerformanceMarks as setPerformanceMarks$1, textRunOfRecordId, themeTokens, touchActionFor, wheelDeltaYOf } from "gesso-core";
|
|
8
8
|
//#region src/Component.ts
|
|
9
9
|
/**
|
|
10
10
|
* Base class for all Gesso framework components.
|
|
@@ -1370,6 +1370,23 @@ function isInputMessage(message) {
|
|
|
1370
1370
|
return INPUT_MESSAGE_TYPES.has(message.type);
|
|
1371
1371
|
}
|
|
1372
1372
|
/**
|
|
1373
|
+
* The inputs a person makes one at a time, whose frame is drawn as soon
|
|
1374
|
+
* as they are routed (`GessoRuntime.answerInput`). Pointer moves and the
|
|
1375
|
+
* wheel come in streams, and wait for the refresh that coalesces them.
|
|
1376
|
+
*/
|
|
1377
|
+
const DISCRETE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
1378
|
+
"pointerDown",
|
|
1379
|
+
"pointerUp",
|
|
1380
|
+
"keyDown",
|
|
1381
|
+
"beforeInput",
|
|
1382
|
+
"compositionUpdate",
|
|
1383
|
+
"compositionEnd",
|
|
1384
|
+
"paste"
|
|
1385
|
+
]);
|
|
1386
|
+
function isDiscreteInputMessage(message) {
|
|
1387
|
+
return DISCRETE_INPUT_TYPES.has(message.type);
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1373
1390
|
* Milliseconds since the Unix epoch, at `performance.now()`'s
|
|
1374
1391
|
* resolution.
|
|
1375
1392
|
*
|
|
@@ -1809,6 +1826,9 @@ var ShellService = class {
|
|
|
1809
1826
|
/** Clipboard writes asked for and not yet answered, by the id sent with each. */
|
|
1810
1827
|
copies = /* @__PURE__ */ new Map();
|
|
1811
1828
|
nextCopyId = 1;
|
|
1829
|
+
/** Clipboard reads asked for and not yet answered, by the id sent with each. */
|
|
1830
|
+
reads = /* @__PURE__ */ new Map();
|
|
1831
|
+
nextReadId = 1;
|
|
1812
1832
|
/** File requests asked for and not yet answered, by the id sent with each. */
|
|
1813
1833
|
files = /* @__PURE__ */ new Map();
|
|
1814
1834
|
nextFileId = 1;
|
|
@@ -1968,6 +1988,37 @@ var ShellService = class {
|
|
|
1968
1988
|
this.copies.delete(id);
|
|
1969
1989
|
resolve(copied);
|
|
1970
1990
|
}
|
|
1991
|
+
/**
|
|
1992
|
+
* The text on the system clipboard, for a Paste chosen from a menu
|
|
1993
|
+
* rather than typed: a key's paste comes with its text, a menu's has
|
|
1994
|
+
* to ask. Null when there is none to give: the browser refused (it
|
|
1995
|
+
* wants a secure context, a focused document, and in some browsers
|
|
1996
|
+
* the person's say-so, which it asks for itself), or no shell is
|
|
1997
|
+
* installed. It never rejects.
|
|
1998
|
+
*/
|
|
1999
|
+
readText() {
|
|
2000
|
+
const handler = this.handler;
|
|
2001
|
+
if (handler === void 0 || handler === null) return Promise.resolve(null);
|
|
2002
|
+
const id = this.nextReadId++;
|
|
2003
|
+
const settled = new Promise((resolve) => {
|
|
2004
|
+
this.reads.set(id, resolve);
|
|
2005
|
+
});
|
|
2006
|
+
handler({
|
|
2007
|
+
type: "clipboardRead",
|
|
2008
|
+
id
|
|
2009
|
+
});
|
|
2010
|
+
return settled;
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Called by the runtime with what a clipboard read found. Not for
|
|
2014
|
+
* applications. An unknown id is ignored, on `settlePopup`'s terms.
|
|
2015
|
+
*/
|
|
2016
|
+
settleClipboardRead(id, text) {
|
|
2017
|
+
const resolve = this.reads.get(id);
|
|
2018
|
+
if (resolve === void 0) return;
|
|
2019
|
+
this.reads.delete(id);
|
|
2020
|
+
resolve(text);
|
|
2021
|
+
}
|
|
1971
2022
|
/** Opens a URL in the user's browser, in a new tab or window. */
|
|
1972
2023
|
openUrl(url) {
|
|
1973
2024
|
this.handler?.({
|
|
@@ -2884,11 +2935,27 @@ const KEY_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
|
2884
2935
|
*
|
|
2885
2936
|
* Nothing here reads the textarea's content as truth except the
|
|
2886
2937
|
* composition string, which is the one thing only the browser knows.
|
|
2938
|
+
*
|
|
2939
|
+
* A secret field (`EditingState.secret`) is edited through an
|
|
2940
|
+
* `<input type="password">` in the textarea's place, made the first
|
|
2941
|
+
* time one takes focus. Only a password input makes the browser and the
|
|
2942
|
+
* OS treat what is typed as a password: macOS turns on secure input, so
|
|
2943
|
+
* no other application can read the keys; nothing autocorrects, learns
|
|
2944
|
+
* or spell-checks it; a screen reader calls it a secure text field and
|
|
2945
|
+
* does not echo the characters. Every listener the textarea has, the
|
|
2946
|
+
* input has too. Its value is the state's text, which for a secret
|
|
2947
|
+
* field is already the mask, so the value reaches no element.
|
|
2887
2948
|
*/
|
|
2888
2949
|
var EditingProxy = class {
|
|
2889
2950
|
canvas;
|
|
2890
2951
|
sink;
|
|
2891
2952
|
textarea;
|
|
2953
|
+
/** The password input, once a secret field has had focus. */
|
|
2954
|
+
password = null;
|
|
2955
|
+
/** Whichever of the two the focused editable is edited through. */
|
|
2956
|
+
field;
|
|
2957
|
+
/** What `describe` was last given, so a swap of element carries it across. */
|
|
2958
|
+
described = [null];
|
|
2892
2959
|
doc;
|
|
2893
2960
|
view;
|
|
2894
2961
|
state = null;
|
|
@@ -2904,14 +2971,24 @@ var EditingProxy = class {
|
|
|
2904
2971
|
this.view = this.doc.defaultView;
|
|
2905
2972
|
const textarea = this.doc.createElement("textarea");
|
|
2906
2973
|
this.textarea = textarea;
|
|
2907
|
-
|
|
2908
|
-
textarea.setAttribute("autocomplete", "off");
|
|
2909
|
-
textarea.setAttribute("autocorrect", "off");
|
|
2910
|
-
textarea.setAttribute("autocapitalize", "off");
|
|
2911
|
-
textarea.setAttribute("spellcheck", "false");
|
|
2974
|
+
this.field = textarea;
|
|
2912
2975
|
textarea.setAttribute("wrap", "off");
|
|
2913
|
-
|
|
2914
|
-
|
|
2976
|
+
const detachTextarea = this.prepare(textarea);
|
|
2977
|
+
const detachWindow = this.listenWindow();
|
|
2978
|
+
this.detach = () => {
|
|
2979
|
+
detachTextarea();
|
|
2980
|
+
detachWindow();
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
/** Hides an element, puts it over the canvas and listens to it. */
|
|
2984
|
+
prepare(field) {
|
|
2985
|
+
field.setAttribute("aria-hidden", "true");
|
|
2986
|
+
field.setAttribute("autocomplete", "off");
|
|
2987
|
+
field.setAttribute("autocorrect", "off");
|
|
2988
|
+
field.setAttribute("autocapitalize", "off");
|
|
2989
|
+
field.setAttribute("spellcheck", "false");
|
|
2990
|
+
field.tabIndex = -1;
|
|
2991
|
+
Object.assign(field.style, {
|
|
2915
2992
|
position: "fixed",
|
|
2916
2993
|
left: "0px",
|
|
2917
2994
|
top: "0px",
|
|
@@ -2928,16 +3005,62 @@ var EditingProxy = class {
|
|
|
2928
3005
|
pointerEvents: "none",
|
|
2929
3006
|
zIndex: "2147483647"
|
|
2930
3007
|
});
|
|
2931
|
-
this.doc.body.appendChild(
|
|
2932
|
-
|
|
3008
|
+
this.doc.body.appendChild(field);
|
|
3009
|
+
return this.listen(field);
|
|
3010
|
+
}
|
|
3011
|
+
/** The password input, made the first time a secret field needs it. */
|
|
3012
|
+
passwordField() {
|
|
3013
|
+
if (this.password === null) {
|
|
3014
|
+
const input = this.doc.createElement("input");
|
|
3015
|
+
input.setAttribute("type", "password");
|
|
3016
|
+
const detach = this.prepare(input);
|
|
3017
|
+
input.setAttribute("data-1p-ignore", "");
|
|
3018
|
+
input.setAttribute("data-lpignore", "true");
|
|
3019
|
+
this.password = input;
|
|
3020
|
+
const previous = this.detach;
|
|
3021
|
+
this.detach = () => {
|
|
3022
|
+
previous();
|
|
3023
|
+
detach();
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
return this.password;
|
|
3027
|
+
}
|
|
3028
|
+
/** Whether an element is one this proxy edits through. */
|
|
3029
|
+
owns(element) {
|
|
3030
|
+
return element !== null && (element === this.textarea || element === this.password);
|
|
3031
|
+
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Moves editing to the other element: the focused editable became, or
|
|
3034
|
+
* stopped being, a secret one. Focus follows if the old element had
|
|
3035
|
+
* it; the blur that causes is ours, not the person leaving the field.
|
|
3036
|
+
*/
|
|
3037
|
+
swapTo(next) {
|
|
3038
|
+
const previous = this.field;
|
|
3039
|
+
const hadFocus = this.doc.activeElement === previous;
|
|
3040
|
+
this.field = next;
|
|
3041
|
+
this.composing = false;
|
|
3042
|
+
this.write(previous, null);
|
|
3043
|
+
this.write(next, ...this.described);
|
|
3044
|
+
if (previous === this.password) previous.value = "";
|
|
3045
|
+
if (hadFocus) {
|
|
3046
|
+
this.refocusing = true;
|
|
3047
|
+
try {
|
|
3048
|
+
next.focus({ preventScroll: true });
|
|
3049
|
+
} finally {
|
|
3050
|
+
this.refocusing = false;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
2933
3053
|
}
|
|
2934
3054
|
/** True while an editable in the runtime has focus. */
|
|
2935
3055
|
get active() {
|
|
2936
3056
|
return this.state !== null;
|
|
2937
3057
|
}
|
|
2938
|
-
/**
|
|
3058
|
+
/**
|
|
3059
|
+
* The hidden element editing goes through: the textarea, or the
|
|
3060
|
+
* password input while a secret field has focus. For tests.
|
|
3061
|
+
*/
|
|
2939
3062
|
get element() {
|
|
2940
|
-
return this.
|
|
3063
|
+
return this.field;
|
|
2941
3064
|
}
|
|
2942
3065
|
/**
|
|
2943
3066
|
* The runtime's editing state changed. Null means no editable has
|
|
@@ -2950,19 +3073,22 @@ var EditingProxy = class {
|
|
|
2950
3073
|
this.state = state;
|
|
2951
3074
|
if (state === null) {
|
|
2952
3075
|
if (wasActive) {
|
|
2953
|
-
if (this.doc.activeElement === this.
|
|
2954
|
-
this.
|
|
3076
|
+
if (this.doc.activeElement === this.field) {
|
|
3077
|
+
this.field.blur();
|
|
2955
3078
|
this.canvas.focus({ preventScroll: true });
|
|
2956
3079
|
}
|
|
3080
|
+
if (this.password !== null) this.password.value = "";
|
|
2957
3081
|
}
|
|
2958
3082
|
return;
|
|
2959
3083
|
}
|
|
3084
|
+
const wanted = state.secret === true ? this.passwordField() : this.textarea;
|
|
3085
|
+
if (wanted !== this.field) this.swapTo(wanted);
|
|
2960
3086
|
this.position(state);
|
|
2961
3087
|
this.mirror(state);
|
|
2962
|
-
this.
|
|
2963
|
-
if (!wasActive || this.doc.activeElement !== this.
|
|
3088
|
+
this.field.setAttribute("enterkeyhint", state.multiline ? "enter" : "done");
|
|
3089
|
+
if (!wasActive || this.doc.activeElement !== this.field) {
|
|
2964
3090
|
const active = this.doc.activeElement;
|
|
2965
|
-
if (active === null || active === this.doc.body || active === this.canvas ||
|
|
3091
|
+
if (active === null || active === this.doc.body || active === this.canvas || this.owns(active)) this.field.focus({ preventScroll: true });
|
|
2966
3092
|
}
|
|
2967
3093
|
}
|
|
2968
3094
|
/**
|
|
@@ -2977,8 +3103,8 @@ var EditingProxy = class {
|
|
|
2977
3103
|
* IME with nothing to compose into.
|
|
2978
3104
|
*/
|
|
2979
3105
|
focus() {
|
|
2980
|
-
if (this.disposed || this.state === null || this.doc.activeElement === this.
|
|
2981
|
-
this.
|
|
3106
|
+
if (this.disposed || this.state === null || this.doc.activeElement === this.field) return;
|
|
3107
|
+
this.field.focus({ preventScroll: true });
|
|
2982
3108
|
}
|
|
2983
3109
|
/**
|
|
2984
3110
|
* Re-takes DOM focus from inside a user gesture, so a phone opens its
|
|
@@ -3010,15 +3136,15 @@ var EditingProxy = class {
|
|
|
3010
3136
|
*/
|
|
3011
3137
|
raiseKeyboard() {
|
|
3012
3138
|
if (this.disposed || this.state === null) return;
|
|
3013
|
-
const
|
|
3014
|
-
if (this.doc.activeElement !==
|
|
3015
|
-
|
|
3139
|
+
const field = this.field;
|
|
3140
|
+
if (this.doc.activeElement !== field) {
|
|
3141
|
+
field.focus({ preventScroll: true });
|
|
3016
3142
|
return;
|
|
3017
3143
|
}
|
|
3018
3144
|
this.refocusing = true;
|
|
3019
3145
|
try {
|
|
3020
|
-
|
|
3021
|
-
|
|
3146
|
+
field.blur();
|
|
3147
|
+
field.focus({ preventScroll: true });
|
|
3022
3148
|
} finally {
|
|
3023
3149
|
this.refocusing = false;
|
|
3024
3150
|
}
|
|
@@ -3034,10 +3160,21 @@ var EditingProxy = class {
|
|
|
3034
3160
|
* the person is really typing into. That the two cannot disagree is
|
|
3035
3161
|
* the reason the field's semantics live here rather than on a second
|
|
3036
3162
|
* element beside it.
|
|
3163
|
+
*
|
|
3164
|
+
* Nothing of the record's value is written: the element's own value
|
|
3165
|
+
* is the field's text (a secret field's mask), and that is what a
|
|
3166
|
+
* screen reader reads.
|
|
3037
3167
|
*/
|
|
3038
3168
|
describe(record, activeDescendant, controls) {
|
|
3039
3169
|
if (this.disposed) return;
|
|
3040
|
-
|
|
3170
|
+
this.described = [
|
|
3171
|
+
record,
|
|
3172
|
+
activeDescendant,
|
|
3173
|
+
controls
|
|
3174
|
+
];
|
|
3175
|
+
this.write(this.field, record, activeDescendant, controls);
|
|
3176
|
+
}
|
|
3177
|
+
write(textarea, record, activeDescendant, controls) {
|
|
3041
3178
|
if (record === null) {
|
|
3042
3179
|
textarea.setAttribute("aria-hidden", "true");
|
|
3043
3180
|
for (const attribute of [
|
|
@@ -3075,6 +3212,7 @@ var EditingProxy = class {
|
|
|
3075
3212
|
this.detach();
|
|
3076
3213
|
this.state = null;
|
|
3077
3214
|
this.textarea.remove();
|
|
3215
|
+
this.password?.remove();
|
|
3078
3216
|
}
|
|
3079
3217
|
/** Puts the element where the caret is, so the IME window opens there. */
|
|
3080
3218
|
position(state) {
|
|
@@ -3082,7 +3220,7 @@ var EditingProxy = class {
|
|
|
3082
3220
|
const height = Math.max(1, state.caret.height);
|
|
3083
3221
|
const left = rect.left + Math.min(Math.max(0, state.caret.x), Math.max(0, rect.width - 1));
|
|
3084
3222
|
const top = rect.top + Math.min(Math.max(0, state.caret.y), Math.max(0, rect.height - height));
|
|
3085
|
-
const style = this.
|
|
3223
|
+
const style = this.field.style;
|
|
3086
3224
|
style.left = `${left}px`;
|
|
3087
3225
|
style.top = `${top}px`;
|
|
3088
3226
|
style.height = `${height}px`;
|
|
@@ -3092,15 +3230,15 @@ var EditingProxy = class {
|
|
|
3092
3230
|
/** Mirrors the runtime's text and selection, except while the IME owns the element. */
|
|
3093
3231
|
mirror(state) {
|
|
3094
3232
|
if (this.composing) return;
|
|
3095
|
-
const textarea = this.
|
|
3233
|
+
const textarea = this.field;
|
|
3096
3234
|
if (textarea.value !== state.text) textarea.value = state.text;
|
|
3097
3235
|
const direction = state.selectionDirection ?? "forward";
|
|
3098
3236
|
if (textarea.selectionStart !== state.selectionStart || textarea.selectionEnd !== state.selectionEnd || state.selectionEnd > state.selectionStart && textarea.selectionDirection !== direction) try {
|
|
3099
3237
|
textarea.setSelectionRange(state.selectionStart, state.selectionEnd, direction);
|
|
3100
3238
|
} catch {}
|
|
3101
3239
|
}
|
|
3102
|
-
|
|
3103
|
-
|
|
3240
|
+
/** Listens to one of the two elements; each has the same listeners. */
|
|
3241
|
+
listen(textarea) {
|
|
3104
3242
|
const onBeforeInput = (event) => {
|
|
3105
3243
|
const input = event;
|
|
3106
3244
|
const type = input.inputType;
|
|
@@ -3127,42 +3265,19 @@ var EditingProxy = class {
|
|
|
3127
3265
|
this.sink.compositionEnd(text);
|
|
3128
3266
|
if (this.state !== null) this.mirror(this.state);
|
|
3129
3267
|
};
|
|
3130
|
-
const onPaste = (event) => {
|
|
3131
|
-
const clipboard = event.clipboardData;
|
|
3132
|
-
event.preventDefault();
|
|
3133
|
-
const text = clipboard?.getData("text/plain") ?? "";
|
|
3134
|
-
const html = clipboard?.getData("text/html") ?? "";
|
|
3135
|
-
if (text.length > 0 || html.length > 0) this.sink.paste(text, html.length > 0 ? html : null);
|
|
3136
|
-
};
|
|
3137
|
-
/**
|
|
3138
|
-
* A paste aimed at the canvas rather than at the field.
|
|
3139
|
-
*
|
|
3140
|
-
* When no editable has focus this proxy blurs and hands focus back
|
|
3141
|
-
* to the canvas, so the browser fires `paste` there — and the
|
|
3142
|
-
* listener above, which is on the textarea, never hears it. The
|
|
3143
|
-
* text was dropped before it reached the application at all, which
|
|
3144
|
-
* is half of why a grid could not be pasted into; the other half
|
|
3145
|
-
* was `UiEditingController.paste` having nowhere to send it.
|
|
3146
|
-
*
|
|
3147
|
-
* The two listeners cannot both fire for one paste: the event goes
|
|
3148
|
-
* to whichever element has focus, and only one of them ever does.
|
|
3149
|
-
*/
|
|
3150
|
-
const onCanvasPaste = (event) => {
|
|
3151
|
-
if (this.doc.activeElement === textarea) return;
|
|
3152
|
-
onPaste(event);
|
|
3153
|
-
};
|
|
3154
3268
|
const onCopy = (event) => {
|
|
3155
3269
|
const clipboard = event.clipboardData;
|
|
3156
3270
|
const state = this.state;
|
|
3157
3271
|
if (clipboard === null || state === null) return;
|
|
3158
3272
|
event.preventDefault();
|
|
3273
|
+
if (state.secret === true) return;
|
|
3159
3274
|
clipboard.setData("text/plain", state.clipboard ?? state.text.slice(state.selectionStart, state.selectionEnd));
|
|
3160
3275
|
if (state.html !== void 0) clipboard.setData("text/html", state.html);
|
|
3161
3276
|
};
|
|
3162
3277
|
const onCut = (event) => {
|
|
3163
3278
|
const state = this.state;
|
|
3164
3279
|
onCopy(event);
|
|
3165
|
-
if (state !== null && (state.clipboard !== void 0 ? state.clipboard.length > 0 : state.selectionEnd > state.selectionStart)) this.sink.beforeInput("deleteByCut", null);
|
|
3280
|
+
if (state !== null && state.secret !== true && (state.clipboard !== void 0 ? state.clipboard.length > 0 : state.selectionEnd > state.selectionStart)) this.sink.beforeInput("deleteByCut", null);
|
|
3166
3281
|
};
|
|
3167
3282
|
const onKeyDown = (event) => {
|
|
3168
3283
|
this.sink.keyDown?.(event);
|
|
@@ -3173,45 +3288,83 @@ var EditingProxy = class {
|
|
|
3173
3288
|
const onBlur = (event) => {
|
|
3174
3289
|
if (this.refocusing) return;
|
|
3175
3290
|
const related = event.relatedTarget;
|
|
3176
|
-
if (this.state !== null && this.doc.hasFocus() && related !== this.canvas) this.sink.blur();
|
|
3177
|
-
};
|
|
3178
|
-
const onReposition = () => {
|
|
3179
|
-
if (this.state !== null) this.position(this.state);
|
|
3291
|
+
if (this.state !== null && this.doc.hasFocus() && related !== this.canvas && !this.owns(related)) this.sink.blur();
|
|
3180
3292
|
};
|
|
3181
3293
|
textarea.addEventListener("beforeinput", onBeforeInput);
|
|
3182
3294
|
textarea.addEventListener("compositionstart", onCompositionStart);
|
|
3183
3295
|
textarea.addEventListener("input", onInput);
|
|
3184
3296
|
textarea.addEventListener("compositionend", onCompositionEnd);
|
|
3185
|
-
textarea.addEventListener("paste", onPaste);
|
|
3297
|
+
textarea.addEventListener("paste", this.onPaste);
|
|
3186
3298
|
textarea.addEventListener("copy", onCopy);
|
|
3187
3299
|
textarea.addEventListener("cut", onCut);
|
|
3188
3300
|
textarea.addEventListener("keydown", onKeyDown);
|
|
3189
|
-
this.canvas.addEventListener("paste", onCanvasPaste);
|
|
3190
3301
|
textarea.addEventListener("keyup", onKeyUp);
|
|
3191
3302
|
textarea.addEventListener("blur", onBlur);
|
|
3192
|
-
this.view?.addEventListener("scroll", onReposition, {
|
|
3193
|
-
capture: true,
|
|
3194
|
-
passive: true
|
|
3195
|
-
});
|
|
3196
|
-
this.view?.addEventListener("resize", onReposition);
|
|
3197
3303
|
return () => {
|
|
3198
3304
|
textarea.removeEventListener("beforeinput", onBeforeInput);
|
|
3199
3305
|
textarea.removeEventListener("compositionstart", onCompositionStart);
|
|
3200
3306
|
textarea.removeEventListener("input", onInput);
|
|
3201
3307
|
textarea.removeEventListener("compositionend", onCompositionEnd);
|
|
3202
|
-
textarea.removeEventListener("paste", onPaste);
|
|
3308
|
+
textarea.removeEventListener("paste", this.onPaste);
|
|
3203
3309
|
textarea.removeEventListener("copy", onCopy);
|
|
3204
3310
|
textarea.removeEventListener("cut", onCut);
|
|
3205
3311
|
textarea.removeEventListener("keydown", onKeyDown);
|
|
3206
|
-
this.canvas.removeEventListener("paste", onCanvasPaste);
|
|
3207
3312
|
textarea.removeEventListener("keyup", onKeyUp);
|
|
3208
3313
|
textarea.removeEventListener("blur", onBlur);
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
onPaste = (event) => {
|
|
3317
|
+
const clipboard = event.clipboardData;
|
|
3318
|
+
event.preventDefault();
|
|
3319
|
+
const text = clipboard?.getData("text/plain") ?? "";
|
|
3320
|
+
const html = clipboard?.getData("text/html") ?? "";
|
|
3321
|
+
if (text.length > 0 || html.length > 0) this.sink.paste(text, html.length > 0 ? html : null);
|
|
3322
|
+
};
|
|
3323
|
+
listenWindow() {
|
|
3324
|
+
/**
|
|
3325
|
+
* A paste aimed at the canvas rather than at the field.
|
|
3326
|
+
*
|
|
3327
|
+
* When no editable has focus this proxy blurs and hands focus back
|
|
3328
|
+
* to the canvas, so the browser fires `paste` there — and the
|
|
3329
|
+
* listener on the field's element never hears it. The
|
|
3330
|
+
* text was dropped before it reached the application at all, which
|
|
3331
|
+
* is half of why a grid could not be pasted into; the other half
|
|
3332
|
+
* was `UiEditingController.paste` having nowhere to send it.
|
|
3333
|
+
*
|
|
3334
|
+
* The two listeners cannot both fire for one paste: the event goes
|
|
3335
|
+
* to whichever element has focus, and only one of them ever does.
|
|
3336
|
+
*/
|
|
3337
|
+
const onCanvasPaste = (event) => {
|
|
3338
|
+
if (this.owns(this.doc.activeElement)) return;
|
|
3339
|
+
this.onPaste(event);
|
|
3340
|
+
};
|
|
3341
|
+
const onReposition = () => {
|
|
3342
|
+
if (this.state !== null) this.position(this.state);
|
|
3343
|
+
};
|
|
3344
|
+
this.view?.addEventListener("scroll", onReposition, {
|
|
3345
|
+
capture: true,
|
|
3346
|
+
passive: true
|
|
3347
|
+
});
|
|
3348
|
+
this.view?.addEventListener("resize", onReposition);
|
|
3349
|
+
this.canvas.addEventListener("paste", onCanvasPaste);
|
|
3350
|
+
return () => {
|
|
3351
|
+
this.canvas.removeEventListener("paste", onCanvasPaste);
|
|
3209
3352
|
this.view?.removeEventListener("scroll", onReposition, { capture: true });
|
|
3210
3353
|
this.view?.removeEventListener("resize", onReposition);
|
|
3211
3354
|
};
|
|
3212
3355
|
}
|
|
3213
3356
|
};
|
|
3214
3357
|
/**
|
|
3358
|
+
* Reads the system clipboard's text from the main thread; null where
|
|
3359
|
+
* the browser has no async clipboard or refuses (no secure context, no
|
|
3360
|
+
* focus, or the person said no to its prompt). It never rejects.
|
|
3361
|
+
*/
|
|
3362
|
+
function readClipboard() {
|
|
3363
|
+
const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
3364
|
+
if (clipboard === void 0 || typeof clipboard.readText !== "function") return Promise.resolve(null);
|
|
3365
|
+
return clipboard.readText().then((text) => text, () => null);
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3215
3368
|
* Writes text to the system clipboard from the main thread, and answers
|
|
3216
3369
|
* whether it got there. The async API needs a secure context and, in
|
|
3217
3370
|
* some browsers, a recent user gesture; the `execCommand` fallback
|
|
@@ -4341,6 +4494,8 @@ function watchPixelRatio(onChange, view = globalThis.window) {
|
|
|
4341
4494
|
}
|
|
4342
4495
|
//#endregion
|
|
4343
4496
|
//#region src/app/worker/WorkerApp.ts
|
|
4497
|
+
/** Refreshes timed to tell the worker the display's interval. */
|
|
4498
|
+
const DISPLAY_SAMPLES = 12;
|
|
4344
4499
|
/**
|
|
4345
4500
|
* Main-thread half of a worker-hosted Gesso application.
|
|
4346
4501
|
*
|
|
@@ -4831,6 +4986,29 @@ var WorkerApp = class {
|
|
|
4831
4986
|
};
|
|
4832
4987
|
this.frameHandle = requestAnimationFrame(step);
|
|
4833
4988
|
}
|
|
4989
|
+
/**
|
|
4990
|
+
* Measures the display's interval on this thread's refreshes and tells
|
|
4991
|
+
* the worker, whose own may keep no display's time; see
|
|
4992
|
+
* `displayInterval` in the protocol. A dozen refreshes, once, at start.
|
|
4993
|
+
*/
|
|
4994
|
+
measureDisplay() {
|
|
4995
|
+
if (typeof requestAnimationFrame !== "function") return;
|
|
4996
|
+
const times = [];
|
|
4997
|
+
const step = (time) => {
|
|
4998
|
+
times.push(time);
|
|
4999
|
+
if (times.length < DISPLAY_SAMPLES) {
|
|
5000
|
+
requestAnimationFrame(step);
|
|
5001
|
+
return;
|
|
5002
|
+
}
|
|
5003
|
+
const gaps = times.slice(1).map((value, index) => value - times[index]).sort((a, b) => a - b);
|
|
5004
|
+
this.post({
|
|
5005
|
+
type: "displayInterval",
|
|
5006
|
+
ms: gaps[Math.floor(gaps.length / 2)],
|
|
5007
|
+
origin: performance.timeOrigin
|
|
5008
|
+
});
|
|
5009
|
+
};
|
|
5010
|
+
requestAnimationFrame(step);
|
|
5011
|
+
}
|
|
4834
5012
|
dispose() {
|
|
4835
5013
|
this.disconnectWebMcp?.();
|
|
4836
5014
|
this.disconnectWebMcp = null;
|
|
@@ -4905,6 +5083,8 @@ var WorkerApp = class {
|
|
|
4905
5083
|
layoutPasses: message.layoutPasses,
|
|
4906
5084
|
at: message.at,
|
|
4907
5085
|
inputLatencyMs: message.inputLatencyMs,
|
|
5086
|
+
time: message.time,
|
|
5087
|
+
wheelScroll: message.wheelScroll,
|
|
4908
5088
|
phases: message.phases,
|
|
4909
5089
|
renderer: message.renderer,
|
|
4910
5090
|
gpu: message.gpu
|
|
@@ -4917,6 +5097,7 @@ var WorkerApp = class {
|
|
|
4917
5097
|
}
|
|
4918
5098
|
if (message.type === "ready") {
|
|
4919
5099
|
this.ready = true;
|
|
5100
|
+
this.measureDisplay();
|
|
4920
5101
|
return;
|
|
4921
5102
|
}
|
|
4922
5103
|
if (message.type === "frameLoop") {
|
|
@@ -4956,6 +5137,14 @@ var WorkerApp = class {
|
|
|
4956
5137
|
}));
|
|
4957
5138
|
return;
|
|
4958
5139
|
}
|
|
5140
|
+
if (message.type === "clipboardRead") {
|
|
5141
|
+
readClipboard().then((text) => this.post({
|
|
5142
|
+
type: "clipboardReadResult",
|
|
5143
|
+
id: message.id,
|
|
5144
|
+
text
|
|
5145
|
+
}));
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
4959
5148
|
if (message.type === "openUrl") {
|
|
4960
5149
|
openUrlWith(this.options.onOpenUrl, message.url);
|
|
4961
5150
|
return;
|
|
@@ -6559,10 +6748,13 @@ var FocusService = class {
|
|
|
6559
6748
|
}
|
|
6560
6749
|
/**
|
|
6561
6750
|
* Ends the innermost trap and returns focus to whatever held it when
|
|
6562
|
-
* the trap was taken — the button that opened the dialog.
|
|
6751
|
+
* the trap was taken — the button that opened the dialog. Given the
|
|
6752
|
+
* node trapped in, only that trap ends, and nothing when it ended
|
|
6753
|
+
* already: a trap's subtree removed releases it, and a release after
|
|
6754
|
+
* that would otherwise end the trap under it.
|
|
6563
6755
|
*/
|
|
6564
|
-
releaseTrap() {
|
|
6565
|
-
this.run((manager) => manager.popScope());
|
|
6756
|
+
releaseTrap(scope) {
|
|
6757
|
+
this.run((manager) => manager.popScope(scope));
|
|
6566
6758
|
}
|
|
6567
6759
|
run(action) {
|
|
6568
6760
|
const manager = this.manager;
|
|
@@ -7365,6 +7557,12 @@ const MAX_ROOT_COMPONENT_DEPTH = 32;
|
|
|
7365
7557
|
*/
|
|
7366
7558
|
/** How many shell requests are held for a listener that has not attached, before the rest are dropped. */
|
|
7367
7559
|
const HELD_SHELL_REQUESTS = 256;
|
|
7560
|
+
/** The longest last frame after which an input's frame is still drawn early: half a 60 Hz refresh. See `GessoRuntime.answerInput`. */
|
|
7561
|
+
const EARLY_FRAME_MAX_MS = 8;
|
|
7562
|
+
/** The least time between two early frames: about half a refresh at 60 Hz. See `GessoRuntime.answerInput`. */
|
|
7563
|
+
const EARLY_FRAME_GAP_MS = 8;
|
|
7564
|
+
/** How long after a discrete input a channel's patch counts as its answer, and is drawn early. See `GessoRuntime.answerInput`. */
|
|
7565
|
+
const ANSWER_WINDOW_MS = 500;
|
|
7368
7566
|
var GessoRuntime = class {
|
|
7369
7567
|
services;
|
|
7370
7568
|
channels;
|
|
@@ -7410,6 +7608,16 @@ var GessoRuntime = class {
|
|
|
7410
7608
|
constraints;
|
|
7411
7609
|
pixelRatio;
|
|
7412
7610
|
lastFrameMs = 0;
|
|
7611
|
+
/** The timer that draws an input's frame early; see `answerInput`. */
|
|
7612
|
+
earlyFrame = null;
|
|
7613
|
+
/** When the last early frame was drawn, on this thread's clock. */
|
|
7614
|
+
lastEarlyFrame = -Infinity;
|
|
7615
|
+
/** When the last discrete input was routed; see `answerInput`. */
|
|
7616
|
+
lastDiscreteInput = -Infinity;
|
|
7617
|
+
/** The display time the frame being drawn is for; see `FrameMetrics.time`. */
|
|
7618
|
+
frameTime = null;
|
|
7619
|
+
/** When the frame's first phase began, before its changes were collected; see `FrameMetrics.durationMs`. */
|
|
7620
|
+
frameBegan = null;
|
|
7413
7621
|
frameListener = null;
|
|
7414
7622
|
rendererErrorListener = null;
|
|
7415
7623
|
frameErrorListener = null;
|
|
@@ -7757,7 +7965,10 @@ var GessoRuntime = class {
|
|
|
7757
7965
|
*/
|
|
7758
7966
|
deferPatchesFrom(sources) {
|
|
7759
7967
|
this.replicas = sources;
|
|
7760
|
-
for (const source of sources) source.deferPatches(() =>
|
|
7968
|
+
for (const source of sources) source.deferPatches(() => {
|
|
7969
|
+
this.scheduler.notifyDirty();
|
|
7970
|
+
if (now() - this.lastDiscreteInput < ANSWER_WINDOW_MS) this.drawEarly();
|
|
7971
|
+
});
|
|
7761
7972
|
}
|
|
7762
7973
|
/** Starts the frame scheduler. */
|
|
7763
7974
|
start() {
|
|
@@ -7932,6 +8143,51 @@ var GessoRuntime = class {
|
|
|
7932
8143
|
this.inputLatency.mark(at, this.scheduler.framePending);
|
|
7933
8144
|
}
|
|
7934
8145
|
/**
|
|
8146
|
+
* Draws the frame a discrete input armed (a key, a character typed, a
|
|
8147
|
+
* paste, a press) as soon as the input has been routed, rather than at
|
|
8148
|
+
* the next refresh.
|
|
8149
|
+
*
|
|
8150
|
+
* Waiting for the refresh is what typing cost. With keys at random
|
|
8151
|
+
* points in the frame, a key waited half a refresh on average and
|
|
8152
|
+
* nearly a whole one at worst before its frame began, so keystroke to
|
|
8153
|
+
* frame was p50 12.5 ms and p95 20.7 ms for about 5 ms of work:
|
|
8154
|
+
* a quarter of all keys over a frame at 60 Hz. A frame drawn in its
|
|
8155
|
+
* own task is committed when the task ends, and shown at the next
|
|
8156
|
+
* refresh the compositor has not yet taken, which is never later than
|
|
8157
|
+
* the frame the refresh would have drawn.
|
|
8158
|
+
*
|
|
8159
|
+
* It runs in a task of its own, a zero timer, not in the input's: a
|
|
8160
|
+
* key arrives as `keyDown` and then `beforeInput`, two messages posted
|
|
8161
|
+
* together, and one frame for both is the point. Continuous input
|
|
8162
|
+
* (pointer moves, the wheel) is left to the refresh, which already
|
|
8163
|
+
* coalesces it. Two guards keep it from costing more than it saves:
|
|
8164
|
+
* nothing early while the last frame took more than half a refresh,
|
|
8165
|
+
* when a second frame in the same refresh would take time the next
|
|
8166
|
+
* one needs; and no early frame within `EARLY_FRAME_GAP_MS` of the
|
|
8167
|
+
* last, so a burst of keys is still one frame per refresh or so.
|
|
8168
|
+
*
|
|
8169
|
+
* The answer to an input often comes later, from another thread: quick
|
|
8170
|
+
* open's results, a completion list. A channel's patch that arrives
|
|
8171
|
+
* within `ANSWER_WINDOW_MS` of a discrete input is drawn early the same
|
|
8172
|
+
* way (see `deferPatchesFrom`), so the answer does not wait for a
|
|
8173
|
+
* refresh either.
|
|
8174
|
+
*/
|
|
8175
|
+
answerInput() {
|
|
8176
|
+
this.lastDiscreteInput = now();
|
|
8177
|
+
this.drawEarly();
|
|
8178
|
+
}
|
|
8179
|
+
/** Draws the pending frame in a task of its own, if the guards in `answerInput` allow. */
|
|
8180
|
+
drawEarly() {
|
|
8181
|
+
if (!this.started || !this.visible || this.earlyFrame !== null || !this.scheduler.framePending) return;
|
|
8182
|
+
if (this.lastFrameMs > EARLY_FRAME_MAX_MS || now() - this.lastEarlyFrame < EARLY_FRAME_GAP_MS) return;
|
|
8183
|
+
this.earlyFrame = setTimeout(() => {
|
|
8184
|
+
this.earlyFrame = null;
|
|
8185
|
+
if (!this.started || !this.scheduler.framePending) return;
|
|
8186
|
+
this.lastEarlyFrame = now();
|
|
8187
|
+
this.scheduler.flush(now());
|
|
8188
|
+
}, 0);
|
|
8189
|
+
}
|
|
8190
|
+
/**
|
|
7935
8191
|
* Turns the layout inspector on or off. While on, every frame paints
|
|
7936
8192
|
* the hovered node's boxes and the measure heatmap over the scene, and
|
|
7937
8193
|
* the inspect listener receives the hovered node's explanation.
|
|
@@ -8344,6 +8600,10 @@ var GessoRuntime = class {
|
|
|
8344
8600
|
settleClipboard(id, copied) {
|
|
8345
8601
|
this.services.get(ShellService).settleClipboard(id, copied);
|
|
8346
8602
|
}
|
|
8603
|
+
/** Reports what a clipboard read found, settling the promise `ShellService.readText` returned. */
|
|
8604
|
+
settleClipboardRead(id, text) {
|
|
8605
|
+
this.services.get(ShellService).settleClipboardRead(id, text);
|
|
8606
|
+
}
|
|
8347
8607
|
/**
|
|
8348
8608
|
* Reports what the shell found in `localStorage`, settling the
|
|
8349
8609
|
* promise `ShellService.requestStorage` returned.
|
|
@@ -8653,6 +8913,10 @@ var GessoRuntime = class {
|
|
|
8653
8913
|
if (node === void 0 || !this.focusManager.focus(node)) this.focusManager.blur();
|
|
8654
8914
|
}
|
|
8655
8915
|
dispose() {
|
|
8916
|
+
if (this.earlyFrame !== null) {
|
|
8917
|
+
clearTimeout(this.earlyFrame);
|
|
8918
|
+
this.earlyFrame = null;
|
|
8919
|
+
}
|
|
8656
8920
|
if (this.scrollbarTimer !== null) {
|
|
8657
8921
|
clearTimeout(this.scrollbarTimer);
|
|
8658
8922
|
this.scrollbarTimer = null;
|
|
@@ -9492,6 +9756,15 @@ var GessoRuntime = class {
|
|
|
9492
9756
|
}
|
|
9493
9757
|
};
|
|
9494
9758
|
}
|
|
9759
|
+
/** Where the container the wheel last scrolled is; see `FrameMetrics.wheelScroll`. */
|
|
9760
|
+
wheelScroll() {
|
|
9761
|
+
const node = this.input.wheel.lastScrolled;
|
|
9762
|
+
const record = node === null ? void 0 : this.engine.recordFor(node);
|
|
9763
|
+
return record === void 0 ? null : {
|
|
9764
|
+
x: record.scrollX,
|
|
9765
|
+
y: record.scrollY
|
|
9766
|
+
};
|
|
9767
|
+
}
|
|
9495
9768
|
/**
|
|
9496
9769
|
* Work that must happen before the frame's dirty set is snapshotted.
|
|
9497
9770
|
*
|
|
@@ -9511,7 +9784,9 @@ var GessoRuntime = class {
|
|
|
9511
9784
|
* silently did nothing, so a theme change never reached descendants.
|
|
9512
9785
|
*/
|
|
9513
9786
|
runPreCollectPhases(time) {
|
|
9787
|
+
this.frameBegan = now();
|
|
9514
9788
|
this.phaseTimings = emptyPhaseTimings();
|
|
9789
|
+
this.frameTime = time;
|
|
9515
9790
|
this.phaseTimings.ticks = this.timePhase(() => this.visible && this.animations.isRunning, () => this.animations.advance(time));
|
|
9516
9791
|
this.scheduleAnimationTick(time);
|
|
9517
9792
|
this.phaseTimings.patches = this.timePhase(() => this.replicas.some((replica) => replica.hasPendingPatches), () => {
|
|
@@ -9749,10 +10024,10 @@ var GessoRuntime = class {
|
|
|
9749
10024
|
if (this.renderer.backend === "webgpu" && this.renderer.isLost) this.recoverWebGPU(this.renderer);
|
|
9750
10025
|
const overlay = this.inspector.hasOverlay ? this.inspector.overlay(started) : null;
|
|
9751
10026
|
this.gpuTimings = null;
|
|
9752
|
-
const changes =
|
|
9753
|
-
frame,
|
|
10027
|
+
const changes = {
|
|
10028
|
+
frame: withPending(frame, this.graph.getDirtyNodes()),
|
|
9754
10029
|
geometryVersion: this.engine.geometryVersion
|
|
9755
|
-
}
|
|
10030
|
+
};
|
|
9756
10031
|
this.phaseTimings.render = this.timePhase(() => this.renderer.isReady, () => this.renderer.render(root, {
|
|
9757
10032
|
layout: this.engine,
|
|
9758
10033
|
text: this.textMeasurer,
|
|
@@ -9766,7 +10041,8 @@ var GessoRuntime = class {
|
|
|
9766
10041
|
}
|
|
9767
10042
|
this.sendDevtoolsUpdates(frame);
|
|
9768
10043
|
const finished = now();
|
|
9769
|
-
const elapsed = finished - started;
|
|
10044
|
+
const elapsed = finished - (this.frameBegan ?? started);
|
|
10045
|
+
this.frameBegan = null;
|
|
9770
10046
|
this.lastFrameMs = elapsed;
|
|
9771
10047
|
this.scheduleScrollbarFade(finished);
|
|
9772
10048
|
this.scheduleCaretBlink(finished);
|
|
@@ -9781,6 +10057,8 @@ var GessoRuntime = class {
|
|
|
9781
10057
|
layoutPasses: settled.passes,
|
|
9782
10058
|
at: finished,
|
|
9783
10059
|
inputLatencyMs: this.inputLatency.take(epochAt(finished)),
|
|
10060
|
+
time: this.frameTime,
|
|
10061
|
+
wheelScroll: this.wheelScroll(),
|
|
9784
10062
|
phases: this.phaseTimings,
|
|
9785
10063
|
renderer: this.rendererState,
|
|
9786
10064
|
gpu: this.gpuTimings
|
|
@@ -10117,6 +10395,13 @@ function watchContextRestored(canvas, restored) {
|
|
|
10117
10395
|
target.addEventListener("contextrestored", restored);
|
|
10118
10396
|
return () => target.removeEventListener?.("contextrestored", restored);
|
|
10119
10397
|
}
|
|
10398
|
+
/** A frame's changes and those made since it was collected, together. */
|
|
10399
|
+
function withPending(frame, pending) {
|
|
10400
|
+
if (pending.isEmpty()) return frame;
|
|
10401
|
+
const merged = new Map(frame.entries());
|
|
10402
|
+
for (const node of pending.values()) merged.set(node, (merged.get(node) ?? DirtyFlags.None) | node.dirtyFlags);
|
|
10403
|
+
return new UiFrame(frame.id, frame.time, merged);
|
|
10404
|
+
}
|
|
10120
10405
|
//#endregion
|
|
10121
10406
|
//#region src/app/GessoApp.ts
|
|
10122
10407
|
/**
|
|
@@ -10518,6 +10803,11 @@ var GessoApp = class {
|
|
|
10518
10803
|
else if (request.type === "storage") this.runtime.settleStorage(request.id, shellStorageDenied());
|
|
10519
10804
|
else if (request.type === "file") this.runtime.settleFile(request.id, shellFilesUnsupported("There is no document to reach files through."));
|
|
10520
10805
|
else if (request.type === "clipboard") this.runtime.settleClipboard(request.id, false);
|
|
10806
|
+
else if (request.type === "clipboardRead") this.runtime.settleClipboardRead(request.id, null);
|
|
10807
|
+
return;
|
|
10808
|
+
}
|
|
10809
|
+
if (request.type === "clipboardRead") {
|
|
10810
|
+
readClipboard().then((text) => this.runtime.settleClipboardRead(request.id, text));
|
|
10521
10811
|
return;
|
|
10522
10812
|
}
|
|
10523
10813
|
if (request.type === "clipboard") {
|
|
@@ -11732,6 +12022,9 @@ var RenderWorkerApp = class {
|
|
|
11732
12022
|
case "clipboardResult":
|
|
11733
12023
|
runtime.settleClipboard(message.id, message.copied);
|
|
11734
12024
|
break;
|
|
12025
|
+
case "clipboardReadResult":
|
|
12026
|
+
runtime.settleClipboardRead(message.id, message.text);
|
|
12027
|
+
break;
|
|
11735
12028
|
case "storageResult":
|
|
11736
12029
|
runtime.settleStorage(message.id, message.result);
|
|
11737
12030
|
break;
|
|
@@ -11741,10 +12034,14 @@ var RenderWorkerApp = class {
|
|
|
11741
12034
|
case "tick":
|
|
11742
12035
|
this.clock?.tick(message.time);
|
|
11743
12036
|
break;
|
|
12037
|
+
case "displayInterval":
|
|
12038
|
+
this.clock?.displayInterval(message.ms, message.origin);
|
|
12039
|
+
break;
|
|
11744
12040
|
case "fullscreenChanged":
|
|
11745
12041
|
runtime.setFullscreen(message.active);
|
|
11746
12042
|
return;
|
|
11747
12043
|
case "visibility":
|
|
12044
|
+
this.clock?.setHidden(!message.visible);
|
|
11748
12045
|
runtime.setVisible(message.visible);
|
|
11749
12046
|
break;
|
|
11750
12047
|
case "inspector":
|
|
@@ -11773,7 +12070,10 @@ var RenderWorkerApp = class {
|
|
|
11773
12070
|
this.channels = void 0;
|
|
11774
12071
|
this.runtime = void 0;
|
|
11775
12072
|
}
|
|
11776
|
-
if (this.runtime !== void 0 && isInputMessage(message))
|
|
12073
|
+
if (this.runtime !== void 0 && isInputMessage(message)) {
|
|
12074
|
+
this.runtime.noteInput(message.at);
|
|
12075
|
+
if (isDiscreteInputMessage(message)) this.runtime.answerInput();
|
|
12076
|
+
}
|
|
11777
12077
|
}
|
|
11778
12078
|
/**
|
|
11779
12079
|
* Swaps the `APPLICATION_WORKER` placeholder for the shell's port.
|
|
@@ -11878,6 +12178,10 @@ var RenderWorkerApp = class {
|
|
|
11878
12178
|
id: request.id,
|
|
11879
12179
|
text: request.text
|
|
11880
12180
|
});
|
|
12181
|
+
else if (request.type === "clipboardRead") this.host.postMessage({
|
|
12182
|
+
type: "clipboardRead",
|
|
12183
|
+
id: request.id
|
|
12184
|
+
});
|
|
11881
12185
|
else if (request.type === "openUrl") this.host.postMessage({
|
|
11882
12186
|
type: "openUrl",
|
|
11883
12187
|
url: request.url
|
|
@@ -11969,6 +12273,8 @@ var RenderWorkerApp = class {
|
|
|
11969
12273
|
layoutPasses: metrics.layoutPasses,
|
|
11970
12274
|
at: metrics.at,
|
|
11971
12275
|
inputLatencyMs: metrics.inputLatencyMs,
|
|
12276
|
+
time: metrics.time,
|
|
12277
|
+
wheelScroll: metrics.wheelScroll,
|
|
11972
12278
|
phases: metrics.phases,
|
|
11973
12279
|
renderer: metrics.renderer,
|
|
11974
12280
|
gpu: metrics.gpu
|