@hex1b/web-terminal 0.1.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/LICENSE +21 -0
- package/README.md +243 -0
- package/dist/fonts/cascadia-mono-nf/CascadiaMonoNF.woff2 +0 -0
- package/dist/fonts/cascadia-mono-nf/LICENSE.txt +94 -0
- package/dist/fonts/cascadia-mono-nf/README.md +29 -0
- package/dist/history-state.d.ts +27 -0
- package/dist/history-state.d.ts.map +1 -0
- package/dist/history-state.js +194 -0
- package/dist/history-state.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/input-policy.d.ts +42 -0
- package/dist/input-policy.d.ts.map +1 -0
- package/dist/input-policy.js +143 -0
- package/dist/input-policy.js.map +1 -0
- package/dist/mouse-input.d.ts +26 -0
- package/dist/mouse-input.d.ts.map +1 -0
- package/dist/mouse-input.js +356 -0
- package/dist/mouse-input.js.map +1 -0
- package/dist/protocol.d.ts +17 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +270 -0
- package/dist/protocol.js.map +1 -0
- package/dist/renderer.d.ts +113 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +595 -0
- package/dist/renderer.js.map +1 -0
- package/dist/selection-input.d.ts +36 -0
- package/dist/selection-input.d.ts.map +1 -0
- package/dist/selection-input.js +69 -0
- package/dist/selection-input.js.map +1 -0
- package/dist/selection-ui.d.ts +24 -0
- package/dist/selection-ui.d.ts.map +1 -0
- package/dist/selection-ui.js +113 -0
- package/dist/selection-ui.js.map +1 -0
- package/dist/terminal-font.d.ts +23 -0
- package/dist/terminal-font.d.ts.map +1 -0
- package/dist/terminal-font.js +85 -0
- package/dist/terminal-font.js.map +1 -0
- package/dist/terminal-sizing.d.ts +8 -0
- package/dist/terminal-sizing.d.ts.map +1 -0
- package/dist/terminal-sizing.js +38 -0
- package/dist/terminal-sizing.js.map +1 -0
- package/dist/terminal-theme.d.ts +2 -0
- package/dist/terminal-theme.d.ts.map +1 -0
- package/dist/terminal-theme.js +39 -0
- package/dist/terminal-theme.js.map +1 -0
- package/dist/terminal-worker.d.ts +2 -0
- package/dist/terminal-worker.d.ts.map +1 -0
- package/dist/terminal-worker.js +285 -0
- package/dist/terminal-worker.js.map +1 -0
- package/dist/types.d.ts +312 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +3 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +7 -0
- package/dist/validation.js.map +1 -0
- package/dist/web-terminal.d.ts +51 -0
- package/dist/web-terminal.d.ts.map +1 -0
- package/dist/web-terminal.js +728 -0
- package/dist/web-terminal.js.map +1 -0
- package/dist/wire-types.d.ts +218 -0
- package/dist/wire-types.d.ts.map +1 -0
- package/dist/wire-types.js +2 -0
- package/dist/wire-types.js.map +1 -0
- package/package.json +42 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACpE,mBAAmB,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC","sourcesContent":["export { WebTerminal } from \"./web-terminal.js\";\nexport { InputRoute, TerminalAction, defaultInputBindings } from \"./input-policy.js\";\nexport { MIN_FONT_SIZE, MAX_FONT_SIZE } from \"./terminal-sizing.js\";\nexport type * from \"./types.js\";\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare const InputRoute: Readonly<{
|
|
2
|
+
Continue: "continue";
|
|
3
|
+
Consume: "consume";
|
|
4
|
+
Application: "application";
|
|
5
|
+
Browser: "browser";
|
|
6
|
+
}>;
|
|
7
|
+
export declare const TerminalAction: Readonly<{
|
|
8
|
+
CopySelection: "copySelection";
|
|
9
|
+
PasteClipboard: "pasteClipboard";
|
|
10
|
+
CopyOrPaste: "copyOrPaste";
|
|
11
|
+
ClearSelection: "clearSelection";
|
|
12
|
+
ScrollToLive: "scrollToLive";
|
|
13
|
+
ScrollLines: "scrollLines";
|
|
14
|
+
}>;
|
|
15
|
+
/** Fresh, inspectable defaults. Overrides are per mounted view, never global. */
|
|
16
|
+
export declare function defaultInputBindings(): InputBinding[];
|
|
17
|
+
/** Resolves intent only. DOM cancellation and action execution belong to the mounted view. */
|
|
18
|
+
export declare class InputPolicy {
|
|
19
|
+
#private;
|
|
20
|
+
constructor({ inputBindings, onInput, actions }?: InputPolicyOptions);
|
|
21
|
+
get bindings(): ({
|
|
22
|
+
id: string;
|
|
23
|
+
match: (input: Readonly<TerminalInput>, context: TerminalInputContext) => boolean;
|
|
24
|
+
when?: (context: TerminalInputContext, input: Readonly<TerminalInput>) => boolean;
|
|
25
|
+
remove?: false;
|
|
26
|
+
route: InputRouteValue;
|
|
27
|
+
action?: never;
|
|
28
|
+
args?: never;
|
|
29
|
+
} | {
|
|
30
|
+
id: string;
|
|
31
|
+
match: (input: Readonly<TerminalInput>, context: TerminalInputContext) => boolean;
|
|
32
|
+
when?: (context: TerminalInputContext, input: Readonly<TerminalInput>) => boolean;
|
|
33
|
+
remove?: false;
|
|
34
|
+
action: string | InputActionHandler;
|
|
35
|
+
args?: unknown;
|
|
36
|
+
route?: never;
|
|
37
|
+
})[];
|
|
38
|
+
resolve(input: Readonly<TerminalInput>, context: TerminalInputContext): InputDecision;
|
|
39
|
+
}
|
|
40
|
+
export declare function inputModifiers(event: Pick<KeyboardEvent, "ctrlKey" | "altKey" | "shiftKey" | "metaKey">): InputModifiers;
|
|
41
|
+
import type { InputActionHandler, InputBinding, InputDecision, InputModifiers, InputPolicyOptions, InputRouteValue, TerminalInput, TerminalInputContext } from "./types.js";
|
|
42
|
+
//# sourceMappingURL=input-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-policy.d.ts","sourceRoot":"","sources":["../src/input-policy.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU;;;;;EAErB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;EAGzB,CAAC;AASH,iFAAiF;AACjF,wBAAgB,oBAAoB,IAAI,YAAY,EAAE,CA2BrD;AAOD,8FAA8F;AAC9F,qBAAa,WAAW;;gBAKV,EAAE,aAAkB,EAAE,OAAO,EAAE,OAAY,EAAE,GAAE,kBAAuB;IAgClF,IAAI,QAAQ;;;;;;;;;;;;;;;;SAA8D;IAkB1E,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,oBAAoB,GAAG,aAAa;CAoBtF;AAUD,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,GAAG,cAAc,CAExH;AACD,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAwB,aAAa,EAAE,cAAc,EACjG,kBAAkB,EAAE,eAAe,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export const InputRoute = Object.freeze({
|
|
2
|
+
Continue: "continue", Consume: "consume", Application: "application", Browser: "browser"
|
|
3
|
+
});
|
|
4
|
+
export const TerminalAction = Object.freeze({
|
|
5
|
+
CopySelection: "copySelection", PasteClipboard: "pasteClipboard", CopyOrPaste: "copyOrPaste",
|
|
6
|
+
ClearSelection: "clearSelection", ScrollToLive: "scrollToLive", ScrollLines: "scrollLines"
|
|
7
|
+
});
|
|
8
|
+
const browserCtrlKeys = new Set(["l", "t", "v", "w", "+", "-", "=", "0"]);
|
|
9
|
+
const browserFunctionKeys = new Set(["F1", "F3", "F5", "F6", "F7", "F10", "F11", "F12"]);
|
|
10
|
+
const terminalKeys = new Set(["Tab", "Enter", "Backspace", "Delete", "Escape", "ArrowUp", "ArrowDown",
|
|
11
|
+
"ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown", "Insert",
|
|
12
|
+
...Array.from({ length: 12 }, (_, index) => `F${index + 1}`)]);
|
|
13
|
+
const routes = new Set(Object.values(InputRoute));
|
|
14
|
+
/** Fresh, inspectable defaults. Overrides are per mounted view, never global. */
|
|
15
|
+
export function defaultInputBindings() {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
id: "clipboard.copy-key",
|
|
19
|
+
match: input => input.type === "key" && input.key.toLowerCase() === "c" && !input.alt &&
|
|
20
|
+
((input.meta && !input.ctrl) || (input.ctrl && input.shift && !input.meta)),
|
|
21
|
+
action: TerminalAction.CopySelection
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "browser.shortcuts",
|
|
25
|
+
match: input => input.type === "key" && (input.meta || browserFunctionKeys.has(input.key) ||
|
|
26
|
+
(input.ctrl && (input.key === "Tab" || input.key === "F4" || browserCtrlKeys.has(input.key.toLowerCase()) ||
|
|
27
|
+
(input.shift && input.key.length === 1))) || (input.alt && !input.ctrl)),
|
|
28
|
+
route: InputRoute.Browser
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "terminal.keys",
|
|
32
|
+
match: input => input.type === "key" && (terminalKeys.has(input.key) || (input.ctrl && input.key.length === 1)),
|
|
33
|
+
route: InputRoute.Application
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "clipboard.context-click",
|
|
37
|
+
match: (input, context) => input.type === "pointer" && input.button === "right" && !input.meta &&
|
|
38
|
+
(!context.mouseCaptured || input.shift || context.historical || context.readOnly),
|
|
39
|
+
action: TerminalAction.CopyOrPaste
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
function synchronous(callback, name) {
|
|
44
|
+
if (typeof callback !== "function" || callback.constructor.name === "AsyncFunction")
|
|
45
|
+
throw new TypeError(`${name} must be a synchronous function`);
|
|
46
|
+
}
|
|
47
|
+
/** Resolves intent only. DOM cancellation and action execution belong to the mounted view. */
|
|
48
|
+
export class InputPolicy {
|
|
49
|
+
#bindings;
|
|
50
|
+
#intercept;
|
|
51
|
+
#actions;
|
|
52
|
+
constructor({ inputBindings = [], onInput, actions = {} } = {}) {
|
|
53
|
+
if (!Array.isArray(inputBindings))
|
|
54
|
+
throw new TypeError("inputBindings must be an array");
|
|
55
|
+
if (!actions || typeof actions !== "object" || Array.isArray(actions))
|
|
56
|
+
throw new TypeError("actions must be an object");
|
|
57
|
+
this.#actions = new Set(Object.values(TerminalAction));
|
|
58
|
+
for (const [name, handler] of Object.entries(actions)) {
|
|
59
|
+
if (!name || this.#actions.has(name) || typeof handler !== "function")
|
|
60
|
+
throw new TypeError(`Invalid or reserved action: ${name}`);
|
|
61
|
+
this.#actions.add(name);
|
|
62
|
+
}
|
|
63
|
+
if (onInput !== undefined)
|
|
64
|
+
synchronous(onInput, "onInput");
|
|
65
|
+
this.#intercept = onInput;
|
|
66
|
+
const defaults = defaultInputBindings();
|
|
67
|
+
const ids = new Set();
|
|
68
|
+
const overrides = inputBindings.map((binding) => {
|
|
69
|
+
if (!binding || typeof binding.id !== "string" || !binding.id || ids.has(binding.id))
|
|
70
|
+
throw new TypeError("Each input binding needs a unique, nonempty id");
|
|
71
|
+
ids.add(binding.id);
|
|
72
|
+
if (binding.remove === true) {
|
|
73
|
+
if (!defaults.some(item => item.id === binding.id))
|
|
74
|
+
throw new TypeError(`Cannot remove unknown default binding: ${binding.id}`);
|
|
75
|
+
if (Object.keys(binding).some(key => !["id", "remove"].includes(key)))
|
|
76
|
+
throw new TypeError(`Removed binding ${binding.id} must contain only id and remove`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
synchronous(binding.match, `Binding ${binding.id}.match`);
|
|
80
|
+
if (binding.when !== undefined)
|
|
81
|
+
synchronous(binding.when, `Binding ${binding.id}.when`);
|
|
82
|
+
this.#decision(binding);
|
|
83
|
+
return Object.freeze({ ...binding });
|
|
84
|
+
}).filter((binding) => binding !== null);
|
|
85
|
+
this.#bindings = [...overrides, ...defaults.filter(binding => !ids.has(binding.id))];
|
|
86
|
+
}
|
|
87
|
+
get bindings() { return this.#bindings.map(binding => ({ ...binding })); }
|
|
88
|
+
#decision(value) {
|
|
89
|
+
if (isRoute(value))
|
|
90
|
+
return { route: value };
|
|
91
|
+
if (!isRecord(value) || typeof value.then === "function")
|
|
92
|
+
throw new TypeError("Input routing must synchronously return a route or action");
|
|
93
|
+
const hasAction = Object.hasOwn(value, "action");
|
|
94
|
+
const hasRoute = Object.hasOwn(value, "route");
|
|
95
|
+
if (hasAction === hasRoute)
|
|
96
|
+
throw new TypeError("Specify exactly one action or route");
|
|
97
|
+
if (hasRoute) {
|
|
98
|
+
if (!isRoute(value.route))
|
|
99
|
+
throw new TypeError(`Unknown input route: ${String(value.route)}`);
|
|
100
|
+
return { route: value.route };
|
|
101
|
+
}
|
|
102
|
+
if (!isActionHandler(value.action) && (typeof value.action !== "string" || !this.#actions.has(value.action)))
|
|
103
|
+
throw new TypeError(`Unknown terminal action: ${String(value.action)}`);
|
|
104
|
+
return { action: value.action, args: value.args };
|
|
105
|
+
}
|
|
106
|
+
resolve(input, context) {
|
|
107
|
+
const intercepted = this.#intercept?.(input, context);
|
|
108
|
+
if (intercepted !== undefined) {
|
|
109
|
+
const decision = this.#decision(intercepted);
|
|
110
|
+
if (decision.route !== InputRoute.Continue)
|
|
111
|
+
return decision;
|
|
112
|
+
}
|
|
113
|
+
for (const binding of this.#bindings) {
|
|
114
|
+
const matched = binding.match(input, context);
|
|
115
|
+
if (typeof matched !== "boolean")
|
|
116
|
+
throw new TypeError(`Binding ${binding.id}.match must return a boolean`);
|
|
117
|
+
if (!matched)
|
|
118
|
+
continue;
|
|
119
|
+
if (binding.when) {
|
|
120
|
+
const enabled = binding.when(context, input);
|
|
121
|
+
if (typeof enabled !== "boolean")
|
|
122
|
+
throw new TypeError(`Binding ${binding.id}.when must return a boolean`);
|
|
123
|
+
if (!enabled)
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const decision = this.#decision(binding);
|
|
127
|
+
if (decision.route !== InputRoute.Continue)
|
|
128
|
+
return decision;
|
|
129
|
+
}
|
|
130
|
+
return { route: InputRoute.Continue };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function isRoute(value) {
|
|
134
|
+
return typeof value === "string" && [...routes].some(route => route === value);
|
|
135
|
+
}
|
|
136
|
+
function isActionHandler(value) {
|
|
137
|
+
return typeof value === "function";
|
|
138
|
+
}
|
|
139
|
+
export function inputModifiers(event) {
|
|
140
|
+
return { ctrl: !!event.ctrlKey, alt: !!event.altKey, shift: !!event.shiftKey, meta: !!event.metaKey };
|
|
141
|
+
}
|
|
142
|
+
import { isRecord } from "./validation.js";
|
|
143
|
+
//# sourceMappingURL=input-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-policy.js","sourceRoot":"","sources":["../src/input-policy.ts"],"names":[],"mappings":"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","sourcesContent":["export const InputRoute = Object.freeze({\n Continue: \"continue\", Consume: \"consume\", Application: \"application\", Browser: \"browser\"\n});\n\nexport const TerminalAction = Object.freeze({\n CopySelection: \"copySelection\", PasteClipboard: \"pasteClipboard\", CopyOrPaste: \"copyOrPaste\",\n ClearSelection: \"clearSelection\", ScrollToLive: \"scrollToLive\", ScrollLines: \"scrollLines\"\n});\n\nconst browserCtrlKeys = new Set([\"l\", \"t\", \"v\", \"w\", \"+\", \"-\", \"=\", \"0\"]);\nconst browserFunctionKeys = new Set([\"F1\", \"F3\", \"F5\", \"F6\", \"F7\", \"F10\", \"F11\", \"F12\"]);\nconst terminalKeys = new Set([\"Tab\", \"Enter\", \"Backspace\", \"Delete\", \"Escape\", \"ArrowUp\", \"ArrowDown\",\n \"ArrowLeft\", \"ArrowRight\", \"Home\", \"End\", \"PageUp\", \"PageDown\", \"Insert\",\n ...Array.from({ length: 12 }, (_, index) => `F${index + 1}`)]);\nconst routes = new Set(Object.values(InputRoute));\n\n/** Fresh, inspectable defaults. Overrides are per mounted view, never global. */\nexport function defaultInputBindings(): InputBinding[] {\n return [\n {\n id: \"clipboard.copy-key\",\n match: input => input.type === \"key\" && input.key.toLowerCase() === \"c\" && !input.alt &&\n ((input.meta && !input.ctrl) || (input.ctrl && input.shift && !input.meta)),\n action: TerminalAction.CopySelection\n },\n {\n id: \"browser.shortcuts\",\n match: input => input.type === \"key\" && (input.meta || browserFunctionKeys.has(input.key) ||\n (input.ctrl && (input.key === \"Tab\" || input.key === \"F4\" || browserCtrlKeys.has(input.key.toLowerCase()) ||\n (input.shift && input.key.length === 1))) || (input.alt && !input.ctrl)),\n route: InputRoute.Browser\n },\n {\n id: \"terminal.keys\",\n match: input => input.type === \"key\" && (terminalKeys.has(input.key) || (input.ctrl && input.key.length === 1)),\n route: InputRoute.Application\n },\n {\n id: \"clipboard.context-click\",\n match: (input, context) => input.type === \"pointer\" && input.button === \"right\" && !input.meta &&\n (!context.mouseCaptured || input.shift || context.historical || context.readOnly),\n action: TerminalAction.CopyOrPaste\n }\n ];\n}\n\nfunction synchronous(callback: unknown, name: string): void {\n if (typeof callback !== \"function\" || callback.constructor.name === \"AsyncFunction\")\n throw new TypeError(`${name} must be a synchronous function`);\n}\n\n/** Resolves intent only. DOM cancellation and action execution belong to the mounted view. */\nexport class InputPolicy {\n #bindings: InputBinding[];\n #intercept: InputPolicyOptions[\"onInput\"];\n #actions: Set<string>;\n\n constructor({ inputBindings = [], onInput, actions = {} }: InputPolicyOptions = {}) {\n if (!Array.isArray(inputBindings)) throw new TypeError(\"inputBindings must be an array\");\n if (!actions || typeof actions !== \"object\" || Array.isArray(actions)) throw new TypeError(\"actions must be an object\");\n this.#actions = new Set(Object.values(TerminalAction));\n for (const [name, handler] of Object.entries(actions)) {\n if (!name || this.#actions.has(name) || typeof handler !== \"function\")\n throw new TypeError(`Invalid or reserved action: ${name}`);\n this.#actions.add(name);\n }\n if (onInput !== undefined) synchronous(onInput, \"onInput\");\n this.#intercept = onInput;\n const defaults = defaultInputBindings();\n const ids = new Set();\n const overrides = inputBindings.map((binding: InputBindingOverride): InputBinding | null => {\n if (!binding || typeof binding.id !== \"string\" || !binding.id || ids.has(binding.id))\n throw new TypeError(\"Each input binding needs a unique, nonempty id\");\n ids.add(binding.id);\n if (binding.remove === true) {\n if (!defaults.some(item => item.id === binding.id))\n throw new TypeError(`Cannot remove unknown default binding: ${binding.id}`);\n if (Object.keys(binding).some(key => ![\"id\", \"remove\"].includes(key)))\n throw new TypeError(`Removed binding ${binding.id} must contain only id and remove`);\n return null;\n }\n synchronous(binding.match, `Binding ${binding.id}.match`);\n if (binding.when !== undefined) synchronous(binding.when, `Binding ${binding.id}.when`);\n this.#decision(binding);\n return Object.freeze({ ...binding });\n }).filter((binding): binding is InputBinding => binding !== null);\n this.#bindings = [...overrides, ...defaults.filter(binding => !ids.has(binding.id))];\n }\n\n get bindings() { return this.#bindings.map(binding => ({ ...binding })); }\n\n #decision(value: unknown): InputDecision {\n if (isRoute(value)) return { route: value };\n if (!isRecord(value) || typeof value.then === \"function\")\n throw new TypeError(\"Input routing must synchronously return a route or action\");\n const hasAction = Object.hasOwn(value, \"action\");\n const hasRoute = Object.hasOwn(value, \"route\");\n if (hasAction === hasRoute) throw new TypeError(\"Specify exactly one action or route\");\n if (hasRoute) {\n if (!isRoute(value.route)) throw new TypeError(`Unknown input route: ${String(value.route)}`);\n return { route: value.route };\n }\n if (!isActionHandler(value.action) && (typeof value.action !== \"string\" || !this.#actions.has(value.action)))\n throw new TypeError(`Unknown terminal action: ${String(value.action)}`);\n return { action: value.action, args: value.args };\n }\n\n resolve(input: Readonly<TerminalInput>, context: TerminalInputContext): InputDecision {\n const intercepted = this.#intercept?.(input, context);\n if (intercepted !== undefined) {\n const decision = this.#decision(intercepted);\n if (decision.route !== InputRoute.Continue) return decision;\n }\n for (const binding of this.#bindings) {\n const matched = binding.match(input, context);\n if (typeof matched !== \"boolean\") throw new TypeError(`Binding ${binding.id}.match must return a boolean`);\n if (!matched) continue;\n if (binding.when) {\n const enabled = binding.when(context, input);\n if (typeof enabled !== \"boolean\") throw new TypeError(`Binding ${binding.id}.when must return a boolean`);\n if (!enabled) continue;\n }\n const decision = this.#decision(binding);\n if (decision.route !== InputRoute.Continue) return decision;\n }\n return { route: InputRoute.Continue };\n }\n}\n\nfunction isRoute(value: unknown): value is InputRouteValue {\n return typeof value === \"string\" && [...routes].some(route => route === value);\n}\n\nfunction isActionHandler(value: unknown): value is InputActionHandler {\n return typeof value === \"function\";\n}\n\nexport function inputModifiers(event: Pick<KeyboardEvent, \"ctrlKey\" | \"altKey\" | \"shiftKey\" | \"metaKey\">): InputModifiers {\n return { ctrl: !!event.ctrlKey, alt: !!event.altKey, shift: !!event.shiftKey, meta: !!event.metaKey };\n}\nimport type { InputActionHandler, InputBinding, InputBindingOverride, InputDecision, InputModifiers,\n InputPolicyOptions, InputRouteValue, TerminalInput, TerminalInputContext } from \"./types.js\";\nimport { isRecord } from \"./validation.js\";\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GestureState } from "./selection-input.js";
|
|
2
|
+
import type { InputDecision, MouseTrackingMode, SelectionMode, TerminalInput, TerminalPoint } from "./types.js";
|
|
3
|
+
import type { MouseCommand } from "./wire-types.js";
|
|
4
|
+
interface MouseInspection {
|
|
5
|
+
state?: () => Omit<GestureState, "tracking">;
|
|
6
|
+
begin?: (point: TerminalPoint, selection: {
|
|
7
|
+
mode: SelectionMode;
|
|
8
|
+
extend: boolean;
|
|
9
|
+
}) => void;
|
|
10
|
+
extend?: (point: TerminalPoint) => void;
|
|
11
|
+
scroll?: (delta: number, endpoint?: TerminalPoint) => void;
|
|
12
|
+
end?: (cancelled: boolean) => void;
|
|
13
|
+
resolve?: (input: TerminalInput) => InputDecision;
|
|
14
|
+
execute?: (decision: Extract<InputDecision, {
|
|
15
|
+
action: unknown;
|
|
16
|
+
}>, input: TerminalInput) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface MouseCapture {
|
|
19
|
+
update(columns: number, rows: number, tracking: MouseTrackingMode): void;
|
|
20
|
+
cancel(): void;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
}
|
|
23
|
+
/** Capture input intent only; the server chooses and encodes the mouse protocol. */
|
|
24
|
+
export declare function captureMouse(canvas: HTMLCanvasElement, send: (command: MouseCommand) => void, focus: () => void, inspection?: MouseInspection): MouseCapture;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=mouse-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mouse-input.d.ts","sourceRoot":"","sources":["../src/mouse-input.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAmB,iBAAiB,EAAiB,aAAa,EAC3F,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAK/E,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;IAC7C,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC5F,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IACxC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,aAAa,KAAK,IAAI,CAAC;IAC3D,GAAG,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,aAAa,CAAC;IAClD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CACjG;AACD,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACzE,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,MAAM,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,EAC3F,KAAK,EAAE,MAAM,IAAI,EAAE,UAAU,GAAE,eAAoB,GAAG,YAAY,CA+SnE"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { cellPoint, WheelAccumulator, SelectionGesture } from "./selection-input.js";
|
|
2
|
+
import { InputRoute, inputModifiers } from "./input-policy.js";
|
|
3
|
+
const buttons = [["left", 1], ["middle", 4], ["right", 2]];
|
|
4
|
+
const pointerButtons = ["left", "middle", "right"];
|
|
5
|
+
/** Capture input intent only; the server chooses and encodes the mouse protocol. */
|
|
6
|
+
export function captureMouse(canvas, send, focus, inspection = {}) {
|
|
7
|
+
const listeners = new AbortController();
|
|
8
|
+
const options = { signal: listeners.signal };
|
|
9
|
+
let columns = 1, rows = 1;
|
|
10
|
+
let tracking = 0;
|
|
11
|
+
let pointerId = null;
|
|
12
|
+
const pressed = new Set();
|
|
13
|
+
let lastPoint;
|
|
14
|
+
let pendingMove;
|
|
15
|
+
let lastMove;
|
|
16
|
+
let scheduled;
|
|
17
|
+
const wheel = new WheelAccumulator();
|
|
18
|
+
const gesture = new SelectionGesture();
|
|
19
|
+
let wheelOwner;
|
|
20
|
+
let pointerEvent;
|
|
21
|
+
let autoScroll;
|
|
22
|
+
let click = { count: 0, time: 0, x: -1, y: -1 };
|
|
23
|
+
let selectionClick;
|
|
24
|
+
let completedClick;
|
|
25
|
+
let routedGesture;
|
|
26
|
+
let contextMenuRoute;
|
|
27
|
+
const state = () => ({ tracking, ...inspection.state?.() });
|
|
28
|
+
function point(event, clamp = false) {
|
|
29
|
+
return cellPoint(event, canvas.getBoundingClientRect(), columns, rows, clamp);
|
|
30
|
+
}
|
|
31
|
+
function updateAutoScroll() {
|
|
32
|
+
clearTimeout(autoScroll);
|
|
33
|
+
autoScroll = undefined;
|
|
34
|
+
if (gesture.owner !== "local" || !pointerEvent)
|
|
35
|
+
return;
|
|
36
|
+
const bounds = canvas.getBoundingClientRect();
|
|
37
|
+
const distance = pointerEvent.clientY < bounds.top ? pointerEvent.clientY - bounds.top
|
|
38
|
+
: pointerEvent.clientY >= bounds.bottom ? pointerEvent.clientY - bounds.bottom + 1 : 0;
|
|
39
|
+
if (!distance)
|
|
40
|
+
return;
|
|
41
|
+
autoScroll = setTimeout(() => {
|
|
42
|
+
autoScroll = undefined;
|
|
43
|
+
if (!pointerEvent)
|
|
44
|
+
return;
|
|
45
|
+
const position = point(pointerEvent, true);
|
|
46
|
+
if (gesture.owner === "local" && position) {
|
|
47
|
+
if (selectionClick)
|
|
48
|
+
selectionClick.dragged = true;
|
|
49
|
+
click.count = 0;
|
|
50
|
+
const lines = Math.sign(distance) * Math.min(8, Math.max(1, Math.ceil(Math.abs(distance) / (bounds.height / rows))));
|
|
51
|
+
inspection.scroll?.(lines, gesture.scrollPoint(position));
|
|
52
|
+
updateAutoScroll();
|
|
53
|
+
}
|
|
54
|
+
}, 60);
|
|
55
|
+
}
|
|
56
|
+
function flushMove() {
|
|
57
|
+
if (scheduled !== undefined)
|
|
58
|
+
cancelAnimationFrame(scheduled);
|
|
59
|
+
scheduled = undefined;
|
|
60
|
+
if (pendingMove)
|
|
61
|
+
send(pendingMove);
|
|
62
|
+
pendingMove = undefined;
|
|
63
|
+
}
|
|
64
|
+
function changeButtons(event, position) {
|
|
65
|
+
for (const [button, mask] of buttons) {
|
|
66
|
+
const down = (event.buttons & mask) !== 0;
|
|
67
|
+
if (down === pressed.has(button))
|
|
68
|
+
continue;
|
|
69
|
+
flushMove();
|
|
70
|
+
if (down)
|
|
71
|
+
pressed.add(button);
|
|
72
|
+
else
|
|
73
|
+
pressed.delete(button);
|
|
74
|
+
if (down || tracking !== 9)
|
|
75
|
+
send({ type: "mouse", action: down ? "down" : "up", button, ...position });
|
|
76
|
+
lastMove = undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function cancel(report = true, cancelled = true) {
|
|
80
|
+
inspection.end?.(cancelled);
|
|
81
|
+
clearTimeout(autoScroll);
|
|
82
|
+
autoScroll = undefined;
|
|
83
|
+
pointerEvent = undefined;
|
|
84
|
+
selectionClick = undefined;
|
|
85
|
+
completedClick = undefined;
|
|
86
|
+
gesture.end();
|
|
87
|
+
routedGesture = undefined;
|
|
88
|
+
if (report)
|
|
89
|
+
flushMove();
|
|
90
|
+
else {
|
|
91
|
+
if (scheduled !== undefined)
|
|
92
|
+
cancelAnimationFrame(scheduled);
|
|
93
|
+
scheduled = undefined;
|
|
94
|
+
pendingMove = undefined;
|
|
95
|
+
}
|
|
96
|
+
if (report && tracking !== 9 && lastPoint) {
|
|
97
|
+
for (const button of pressed)
|
|
98
|
+
send({ type: "mouse", action: "up", button, ...lastPoint });
|
|
99
|
+
}
|
|
100
|
+
pressed.clear();
|
|
101
|
+
const captured = pointerId;
|
|
102
|
+
pointerId = null;
|
|
103
|
+
if (captured !== null && canvas.hasPointerCapture(captured))
|
|
104
|
+
canvas.releasePointerCapture(captured);
|
|
105
|
+
lastMove = undefined;
|
|
106
|
+
wheel.reset();
|
|
107
|
+
wheelOwner = undefined;
|
|
108
|
+
}
|
|
109
|
+
canvas.addEventListener("pointerdown", event => {
|
|
110
|
+
if (event.defaultPrevented && pointerId === null)
|
|
111
|
+
return;
|
|
112
|
+
if (event.pointerType !== "mouse" || ![0, 1, 2].includes(event.button))
|
|
113
|
+
return;
|
|
114
|
+
const position = point(event);
|
|
115
|
+
if (!position)
|
|
116
|
+
return;
|
|
117
|
+
if (pointerId !== null) {
|
|
118
|
+
if (routedGesture !== InputRoute.Browser)
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
if ((gesture.owner === "app" || routedGesture === InputRoute.Application) && pointerId === event.pointerId)
|
|
121
|
+
changeButtons(event, position);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
focus();
|
|
125
|
+
const input = { type: "pointer", button: pointerButtons[event.button],
|
|
126
|
+
point: Object.freeze({ ...position }), ...inputModifiers(event) };
|
|
127
|
+
const decision = inspection.resolve?.(input) ?? { route: InputRoute.Continue };
|
|
128
|
+
const route = decision.action !== undefined ? InputRoute.Consume : decision.route;
|
|
129
|
+
contextMenuRoute = event.button === 2 ? route : undefined;
|
|
130
|
+
if (route !== InputRoute.Continue) {
|
|
131
|
+
completedClick = selectionClick = undefined;
|
|
132
|
+
click.count = 0;
|
|
133
|
+
routedGesture = route;
|
|
134
|
+
pointerId = event.pointerId;
|
|
135
|
+
lastPoint = position;
|
|
136
|
+
canvas.setPointerCapture(pointerId);
|
|
137
|
+
if (route !== InputRoute.Browser)
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
if (route === InputRoute.Application)
|
|
140
|
+
flushMove();
|
|
141
|
+
else {
|
|
142
|
+
if (scheduled !== undefined)
|
|
143
|
+
cancelAnimationFrame(scheduled);
|
|
144
|
+
scheduled = undefined;
|
|
145
|
+
pendingMove = undefined;
|
|
146
|
+
}
|
|
147
|
+
if (route === InputRoute.Application)
|
|
148
|
+
changeButtons(event, position);
|
|
149
|
+
if (decision.action !== undefined)
|
|
150
|
+
inspection.execute?.(decision, input);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Keep focus on the hidden keyboard input instead of the canvas.
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
if (event.metaKey)
|
|
156
|
+
return;
|
|
157
|
+
const now = performance.now();
|
|
158
|
+
click.count = now - click.time < 500 && position.x === click.x && position.y === click.y
|
|
159
|
+
? click.count % 3 + 1 : 1;
|
|
160
|
+
click = { count: click.count, time: now, x: position.x, y: position.y };
|
|
161
|
+
const start = gesture.begin({ button: event.button, shiftKey: event.shiftKey, altKey: event.altKey,
|
|
162
|
+
detail: event.detail || click.count }, position, state());
|
|
163
|
+
if (!start)
|
|
164
|
+
return;
|
|
165
|
+
pointerId = event.pointerId;
|
|
166
|
+
pointerEvent = event;
|
|
167
|
+
lastPoint = position;
|
|
168
|
+
completedClick = undefined;
|
|
169
|
+
canvas.setPointerCapture(pointerId);
|
|
170
|
+
if (start.owner === "local") {
|
|
171
|
+
if (scheduled !== undefined)
|
|
172
|
+
cancelAnimationFrame(scheduled);
|
|
173
|
+
scheduled = undefined;
|
|
174
|
+
pendingMove = undefined;
|
|
175
|
+
lastMove = undefined;
|
|
176
|
+
selectionClick = { point: position, mode: start.mode, extend: start.extend, dragged: false };
|
|
177
|
+
inspection.begin?.(position, start);
|
|
178
|
+
}
|
|
179
|
+
else
|
|
180
|
+
changeButtons(event, position);
|
|
181
|
+
}, options);
|
|
182
|
+
canvas.addEventListener("pointermove", event => {
|
|
183
|
+
if (event.pointerType !== "mouse")
|
|
184
|
+
return;
|
|
185
|
+
if (pointerId === null && event.buttons)
|
|
186
|
+
return;
|
|
187
|
+
if (pointerId !== null && pointerId !== event.pointerId)
|
|
188
|
+
return;
|
|
189
|
+
const position = point(event, pointerId !== null);
|
|
190
|
+
if (!position)
|
|
191
|
+
return;
|
|
192
|
+
lastPoint = position;
|
|
193
|
+
if (routedGesture && routedGesture !== InputRoute.Application)
|
|
194
|
+
return;
|
|
195
|
+
if (gesture.owner === "local") {
|
|
196
|
+
pointerEvent = event;
|
|
197
|
+
const previous = gesture.endpoint;
|
|
198
|
+
const endpoint = gesture.move(position);
|
|
199
|
+
if (endpoint && previous && (endpoint.x !== previous.x || endpoint.y !== previous.y)) {
|
|
200
|
+
click.count = 0;
|
|
201
|
+
if (selectionClick)
|
|
202
|
+
selectionClick.dragged = true;
|
|
203
|
+
inspection.extend?.(endpoint);
|
|
204
|
+
}
|
|
205
|
+
updateAutoScroll();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (!tracking || (!routedGesture && gesture.owner === null && (state().historical || state().readOnly || event.shiftKey)))
|
|
209
|
+
return;
|
|
210
|
+
if (pointerId === event.pointerId)
|
|
211
|
+
changeButtons(event, position);
|
|
212
|
+
if (event.metaKey || (tracking !== 1003 && !(tracking === 1002 && pressed.size)))
|
|
213
|
+
return;
|
|
214
|
+
const button = buttons.find(([name]) => pressed.has(name))?.[0] ?? "none";
|
|
215
|
+
const move = { type: "mouse", action: "move", button, ...position };
|
|
216
|
+
const key = JSON.stringify(move);
|
|
217
|
+
if (key === lastMove)
|
|
218
|
+
return;
|
|
219
|
+
lastMove = key;
|
|
220
|
+
pendingMove = move;
|
|
221
|
+
if (scheduled === undefined)
|
|
222
|
+
scheduled = requestAnimationFrame(flushMove);
|
|
223
|
+
}, options);
|
|
224
|
+
canvas.addEventListener("pointerup", event => {
|
|
225
|
+
if (pointerId !== event.pointerId)
|
|
226
|
+
return;
|
|
227
|
+
const position = point(event, true) ?? lastPoint;
|
|
228
|
+
if (routedGesture) {
|
|
229
|
+
if (routedGesture === InputRoute.Application && position)
|
|
230
|
+
changeButtons(event, position);
|
|
231
|
+
if (event.buttons === 0)
|
|
232
|
+
cancel(routedGesture === InputRoute.Application);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (gesture.owner === "local") {
|
|
236
|
+
if (position && gesture.endpoint && (position.x !== gesture.endpoint.x || position.y !== gesture.endpoint.y)) {
|
|
237
|
+
if (selectionClick)
|
|
238
|
+
selectionClick.dragged = true;
|
|
239
|
+
const endpoint = gesture.move(position);
|
|
240
|
+
if (endpoint)
|
|
241
|
+
inspection.extend?.(endpoint);
|
|
242
|
+
}
|
|
243
|
+
const completed = selectionClick;
|
|
244
|
+
cancel(false, false);
|
|
245
|
+
completedClick = completed;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (position)
|
|
249
|
+
changeButtons(event, position);
|
|
250
|
+
if (!pressed.size)
|
|
251
|
+
cancel();
|
|
252
|
+
}, options);
|
|
253
|
+
canvas.addEventListener("click", event => {
|
|
254
|
+
const completed = completedClick;
|
|
255
|
+
completedClick = undefined;
|
|
256
|
+
if (!completed || completed.dragged || !Number.isInteger(event.detail) || event.detail < 1)
|
|
257
|
+
return;
|
|
258
|
+
click.count = event.detail;
|
|
259
|
+
// Some browsers expose native multiclick counts only on click, not pointerdown.
|
|
260
|
+
const mode = event.detail >= 3 ? "line" : event.detail === 2 ? "word" : "character";
|
|
261
|
+
if (!completed.extend && completed.mode !== "rectangle" && mode !== completed.mode)
|
|
262
|
+
inspection.begin?.(completed.point, { mode, extend: false });
|
|
263
|
+
}, options);
|
|
264
|
+
canvas.addEventListener("pointercancel", () => cancel(), options);
|
|
265
|
+
canvas.addEventListener("lostpointercapture", () => {
|
|
266
|
+
if (pointerId !== null)
|
|
267
|
+
cancel();
|
|
268
|
+
}, options);
|
|
269
|
+
window.addEventListener("blur", () => cancel(), options);
|
|
270
|
+
canvas.addEventListener("contextmenu", event => {
|
|
271
|
+
if (contextMenuRoute !== undefined && contextMenuRoute !== InputRoute.Continue) {
|
|
272
|
+
const route = contextMenuRoute;
|
|
273
|
+
contextMenuRoute = undefined;
|
|
274
|
+
if (route !== InputRoute.Browser)
|
|
275
|
+
event.preventDefault();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (tracking || gesture.owner === "local")
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
}, options);
|
|
281
|
+
canvas.addEventListener("wheel", event => {
|
|
282
|
+
const input = { type: "wheel", deltaX: event.deltaX, deltaY: event.deltaY,
|
|
283
|
+
deltaMode: event.deltaMode, point: point(event), ...inputModifiers(event) };
|
|
284
|
+
const decision = pointerId === null
|
|
285
|
+
? inspection.resolve?.(input) ?? { route: InputRoute.Continue }
|
|
286
|
+
: { route: routedGesture ?? InputRoute.Continue };
|
|
287
|
+
if (decision.route === InputRoute.Browser)
|
|
288
|
+
return;
|
|
289
|
+
if (decision.action !== undefined || decision.route === InputRoute.Consume) {
|
|
290
|
+
event.preventDefault();
|
|
291
|
+
if (decision.action !== undefined)
|
|
292
|
+
inspection.execute?.(decision, input);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Ctrl+wheel (including trackpad pinch) remains browser zoom.
|
|
296
|
+
if (decision.route !== InputRoute.Application && (event.ctrlKey || event.metaKey))
|
|
297
|
+
return;
|
|
298
|
+
const owner = decision.route === InputRoute.Application ? "app" : gesture.wheelOwner(event, state());
|
|
299
|
+
if (owner === "app" && tracking === 9)
|
|
300
|
+
return;
|
|
301
|
+
const position = point(gesture.owner === "local" && pointerEvent ? pointerEvent : event, gesture.owner === "local");
|
|
302
|
+
if (!position)
|
|
303
|
+
return;
|
|
304
|
+
event.preventDefault();
|
|
305
|
+
flushMove();
|
|
306
|
+
const bounds = canvas.getBoundingClientRect();
|
|
307
|
+
if (wheelOwner !== owner)
|
|
308
|
+
wheel.reset();
|
|
309
|
+
wheelOwner = owner;
|
|
310
|
+
const { x: horizontal, y: vertical } = wheel.take(event, bounds, rows);
|
|
311
|
+
if (owner === "local") {
|
|
312
|
+
if (vertical) {
|
|
313
|
+
if (selectionClick)
|
|
314
|
+
selectionClick.dragged = true;
|
|
315
|
+
click.count = 0;
|
|
316
|
+
inspection.scroll?.(vertical, gesture.scrollPoint(position));
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const wheelDirections = [
|
|
321
|
+
[vertical, "wheelUp", "wheelDown"],
|
|
322
|
+
[horizontal, "wheelLeft", "wheelRight"]
|
|
323
|
+
];
|
|
324
|
+
for (const [steps, negative, positive] of wheelDirections) {
|
|
325
|
+
if (steps)
|
|
326
|
+
send({
|
|
327
|
+
type: "mouse", action: "wheel", button: steps < 0 ? negative : positive,
|
|
328
|
+
count: Math.min(32, Math.abs(steps)), ...position
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}, { ...options, passive: false });
|
|
332
|
+
return {
|
|
333
|
+
update(nextColumns, nextRows, nextTracking) {
|
|
334
|
+
if (columns !== nextColumns || rows !== nextRows || tracking !== nextTracking) {
|
|
335
|
+
if (lastPoint) {
|
|
336
|
+
lastPoint = { ...lastPoint,
|
|
337
|
+
x: Math.min(lastPoint.x, nextColumns - 1),
|
|
338
|
+
y: Math.min(lastPoint.y, nextRows - 1)
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// Application mode changes do not transfer ownership mid-gesture.
|
|
342
|
+
if (columns !== nextColumns || rows !== nextRows)
|
|
343
|
+
cancel(nextTracking !== 0);
|
|
344
|
+
columns = nextColumns;
|
|
345
|
+
rows = nextRows;
|
|
346
|
+
tracking = nextTracking;
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
cancel() { cancel(); },
|
|
350
|
+
dispose() {
|
|
351
|
+
cancel(false);
|
|
352
|
+
listeners.abort();
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=mouse-input.js.map
|