gesso-framework 0.6.20 → 0.6.22
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 +26 -0
- package/dist/{index-Deqs0FCn.d.ts → index-C2nyeg5Y.d.ts} +120 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +319 -71
- 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
|
@@ -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
|
|
@@ -4984,6 +5137,14 @@ var WorkerApp = class {
|
|
|
4984
5137
|
}));
|
|
4985
5138
|
return;
|
|
4986
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
|
+
}
|
|
4987
5148
|
if (message.type === "openUrl") {
|
|
4988
5149
|
openUrlWith(this.options.onOpenUrl, message.url);
|
|
4989
5150
|
return;
|
|
@@ -6587,10 +6748,13 @@ var FocusService = class {
|
|
|
6587
6748
|
}
|
|
6588
6749
|
/**
|
|
6589
6750
|
* Ends the innermost trap and returns focus to whatever held it when
|
|
6590
|
-
* 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.
|
|
6591
6755
|
*/
|
|
6592
|
-
releaseTrap() {
|
|
6593
|
-
this.run((manager) => manager.popScope());
|
|
6756
|
+
releaseTrap(scope) {
|
|
6757
|
+
this.run((manager) => manager.popScope(scope));
|
|
6594
6758
|
}
|
|
6595
6759
|
run(action) {
|
|
6596
6760
|
const manager = this.manager;
|
|
@@ -7393,6 +7557,12 @@ const MAX_ROOT_COMPONENT_DEPTH = 32;
|
|
|
7393
7557
|
*/
|
|
7394
7558
|
/** How many shell requests are held for a listener that has not attached, before the rest are dropped. */
|
|
7395
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;
|
|
7396
7566
|
var GessoRuntime = class {
|
|
7397
7567
|
services;
|
|
7398
7568
|
channels;
|
|
@@ -7438,6 +7608,12 @@ var GessoRuntime = class {
|
|
|
7438
7608
|
constraints;
|
|
7439
7609
|
pixelRatio;
|
|
7440
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;
|
|
7441
7617
|
/** The display time the frame being drawn is for; see `FrameMetrics.time`. */
|
|
7442
7618
|
frameTime = null;
|
|
7443
7619
|
/** When the frame's first phase began, before its changes were collected; see `FrameMetrics.durationMs`. */
|
|
@@ -7789,7 +7965,10 @@ var GessoRuntime = class {
|
|
|
7789
7965
|
*/
|
|
7790
7966
|
deferPatchesFrom(sources) {
|
|
7791
7967
|
this.replicas = sources;
|
|
7792
|
-
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
|
+
});
|
|
7793
7972
|
}
|
|
7794
7973
|
/** Starts the frame scheduler. */
|
|
7795
7974
|
start() {
|
|
@@ -7964,6 +8143,51 @@ var GessoRuntime = class {
|
|
|
7964
8143
|
this.inputLatency.mark(at, this.scheduler.framePending);
|
|
7965
8144
|
}
|
|
7966
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
|
+
/**
|
|
7967
8191
|
* Turns the layout inspector on or off. While on, every frame paints
|
|
7968
8192
|
* the hovered node's boxes and the measure heatmap over the scene, and
|
|
7969
8193
|
* the inspect listener receives the hovered node's explanation.
|
|
@@ -8376,6 +8600,10 @@ var GessoRuntime = class {
|
|
|
8376
8600
|
settleClipboard(id, copied) {
|
|
8377
8601
|
this.services.get(ShellService).settleClipboard(id, copied);
|
|
8378
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
|
+
}
|
|
8379
8607
|
/**
|
|
8380
8608
|
* Reports what the shell found in `localStorage`, settling the
|
|
8381
8609
|
* promise `ShellService.requestStorage` returned.
|
|
@@ -8685,6 +8913,10 @@ var GessoRuntime = class {
|
|
|
8685
8913
|
if (node === void 0 || !this.focusManager.focus(node)) this.focusManager.blur();
|
|
8686
8914
|
}
|
|
8687
8915
|
dispose() {
|
|
8916
|
+
if (this.earlyFrame !== null) {
|
|
8917
|
+
clearTimeout(this.earlyFrame);
|
|
8918
|
+
this.earlyFrame = null;
|
|
8919
|
+
}
|
|
8688
8920
|
if (this.scrollbarTimer !== null) {
|
|
8689
8921
|
clearTimeout(this.scrollbarTimer);
|
|
8690
8922
|
this.scrollbarTimer = null;
|
|
@@ -10571,6 +10803,11 @@ var GessoApp = class {
|
|
|
10571
10803
|
else if (request.type === "storage") this.runtime.settleStorage(request.id, shellStorageDenied());
|
|
10572
10804
|
else if (request.type === "file") this.runtime.settleFile(request.id, shellFilesUnsupported("There is no document to reach files through."));
|
|
10573
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));
|
|
10574
10811
|
return;
|
|
10575
10812
|
}
|
|
10576
10813
|
if (request.type === "clipboard") {
|
|
@@ -11785,6 +12022,9 @@ var RenderWorkerApp = class {
|
|
|
11785
12022
|
case "clipboardResult":
|
|
11786
12023
|
runtime.settleClipboard(message.id, message.copied);
|
|
11787
12024
|
break;
|
|
12025
|
+
case "clipboardReadResult":
|
|
12026
|
+
runtime.settleClipboardRead(message.id, message.text);
|
|
12027
|
+
break;
|
|
11788
12028
|
case "storageResult":
|
|
11789
12029
|
runtime.settleStorage(message.id, message.result);
|
|
11790
12030
|
break;
|
|
@@ -11801,6 +12041,7 @@ var RenderWorkerApp = class {
|
|
|
11801
12041
|
runtime.setFullscreen(message.active);
|
|
11802
12042
|
return;
|
|
11803
12043
|
case "visibility":
|
|
12044
|
+
this.clock?.setHidden(!message.visible);
|
|
11804
12045
|
runtime.setVisible(message.visible);
|
|
11805
12046
|
break;
|
|
11806
12047
|
case "inspector":
|
|
@@ -11829,7 +12070,10 @@ var RenderWorkerApp = class {
|
|
|
11829
12070
|
this.channels = void 0;
|
|
11830
12071
|
this.runtime = void 0;
|
|
11831
12072
|
}
|
|
11832
|
-
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
|
+
}
|
|
11833
12077
|
}
|
|
11834
12078
|
/**
|
|
11835
12079
|
* Swaps the `APPLICATION_WORKER` placeholder for the shell's port.
|
|
@@ -11934,6 +12178,10 @@ var RenderWorkerApp = class {
|
|
|
11934
12178
|
id: request.id,
|
|
11935
12179
|
text: request.text
|
|
11936
12180
|
});
|
|
12181
|
+
else if (request.type === "clipboardRead") this.host.postMessage({
|
|
12182
|
+
type: "clipboardRead",
|
|
12183
|
+
id: request.id
|
|
12184
|
+
});
|
|
11937
12185
|
else if (request.type === "openUrl") this.host.postMessage({
|
|
11938
12186
|
type: "openUrl",
|
|
11939
12187
|
url: request.url
|