@swifttui/web 0.0.6
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/AGENTS.md +52 -0
- package/README.md +116 -0
- package/cli.ts +168 -0
- package/index.html +50 -0
- package/index.ts +8 -0
- package/manifest.ts +1 -0
- package/package.json +33 -0
- package/src/AccessibilityTree.ts +262 -0
- package/src/BoxDrawingRenderer.ts +585 -0
- package/src/PublicEntrypointBoundary.test.ts +20 -0
- package/src/WebHostApp.test.ts +222 -0
- package/src/WebHostApp.ts +269 -0
- package/src/WebHostSceneManifest.test.ts +38 -0
- package/src/WebHostSceneManifest.ts +156 -0
- package/src/WebHostSceneRuntime.test.ts +1752 -0
- package/src/WebHostSceneRuntime.ts +955 -0
- package/src/WebHostSurfaceTransport.test.ts +362 -0
- package/src/WebHostSurfaceTransport.ts +648 -0
- package/src/WebHostTerminalStyle.test.ts +123 -0
- package/src/WebHostTerminalStyle.ts +471 -0
- package/src/WebHostTestFixtures.ts +10 -0
- package/src/WebSocketSceneBridge.test.ts +198 -0
- package/src/WebSocketSceneBridge.ts +233 -0
- package/src/browser.ts +59 -0
- package/src/wasi/BrowserWASIBridge.test.ts +168 -0
- package/src/wasi/BrowserWASIBridge.ts +167 -0
- package/src/wasi/SharedInputQueue.test.ts +146 -0
- package/src/wasi/SharedInputQueue.ts +199 -0
- package/src/wasi/StdIOPipe.ts +72 -0
- package/src/wasi/WasiPollScheduler.test.ts +176 -0
- package/src/wasi/WasiPollScheduler.ts +305 -0
- package/src/wasi/WasmSceneRuntime.ts +205 -0
- package/src/wasi/WasmSceneWorker.ts +182 -0
- package/style.css +15 -0
- package/testing.ts +1 -0
- package/tsconfig.json +29 -0
- package/wasi-worker.ts +1 -0
- package/wasi.ts +4 -0
- package/websocket.ts +1 -0
|
@@ -0,0 +1,955 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyWebHostTerminalStyle,
|
|
3
|
+
normalizeWebHostTerminalStyle,
|
|
4
|
+
type ResolvedWebHostTerminalStyle,
|
|
5
|
+
type WebHostTerminalStyle,
|
|
6
|
+
webTUITerminalBackgroundColor,
|
|
7
|
+
} from "./WebHostTerminalStyle.ts";
|
|
8
|
+
import {
|
|
9
|
+
canRenderBoxDrawing,
|
|
10
|
+
drawBoxDrawing,
|
|
11
|
+
} from "./BoxDrawingRenderer.ts";
|
|
12
|
+
import { AccessibilityTreeMounter } from "./AccessibilityTree.ts";
|
|
13
|
+
import {
|
|
14
|
+
encodeKeyInputMessage,
|
|
15
|
+
encodeMouseInputMessage,
|
|
16
|
+
encodePasteInputMessage,
|
|
17
|
+
type WebHostFrameDiagnosticRecord,
|
|
18
|
+
type WebHostOutputSink,
|
|
19
|
+
type WebHostKeyInput,
|
|
20
|
+
type WebHostRuntimeIssue,
|
|
21
|
+
type WebHostSurfaceDamage,
|
|
22
|
+
type WebHostSurfaceFrame,
|
|
23
|
+
type WebHostSurfaceImage,
|
|
24
|
+
type WebHostSurfaceImageFormat,
|
|
25
|
+
type WebHostSurfaceStyle,
|
|
26
|
+
} from "./WebHostSurfaceTransport.ts";
|
|
27
|
+
import type { WebHostSceneDescriptor } from "./WebHostSceneManifest.ts";
|
|
28
|
+
|
|
29
|
+
export interface WebHostSceneBridge {
|
|
30
|
+
bindOutput(sink: WebHostOutputSink): void;
|
|
31
|
+
resize(columns: number, rows: number, cellWidth?: number, cellHeight?: number): void;
|
|
32
|
+
updateRenderStyle(style: WebHostTerminalStyle): void;
|
|
33
|
+
sendInput(chunk: Uint8Array): void;
|
|
34
|
+
dispose(): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WebHostSceneRuntimeOptions {
|
|
38
|
+
mount: HTMLElement;
|
|
39
|
+
descriptor: WebHostSceneDescriptor;
|
|
40
|
+
style: WebHostTerminalStyle;
|
|
41
|
+
bridge?: WebHostSceneBridge;
|
|
42
|
+
onInput(chunk: Uint8Array): void;
|
|
43
|
+
onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;
|
|
44
|
+
synchronizeAccessibilityFocus?: boolean;
|
|
45
|
+
captureWheelInput?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface CachedWebHostImage {
|
|
49
|
+
image?: CanvasImageSource;
|
|
50
|
+
promise?: Promise<CanvasImageSource>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface DirtyRect {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class WebHostSceneRuntime {
|
|
61
|
+
readonly descriptor: WebHostSceneDescriptor;
|
|
62
|
+
readonly element: HTMLElement;
|
|
63
|
+
readonly terminalMount: HTMLElement;
|
|
64
|
+
|
|
65
|
+
private readonly bridge?: WebHostSceneBridge;
|
|
66
|
+
private readonly onInput: (chunk: Uint8Array) => void;
|
|
67
|
+
private readonly onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;
|
|
68
|
+
private readonly synchronizeAccessibilityFocus: boolean;
|
|
69
|
+
private readonly captureWheelInput: boolean;
|
|
70
|
+
private readonly imageCache = new Map<string, CachedWebHostImage>();
|
|
71
|
+
private currentStyle: ResolvedWebHostTerminalStyle;
|
|
72
|
+
private canvas?: HTMLCanvasElement;
|
|
73
|
+
private accessibilityTree?: AccessibilityTreeMounter;
|
|
74
|
+
private diagnosticText?: HTMLElement;
|
|
75
|
+
private resizeObserver?: ResizeObserver;
|
|
76
|
+
private detachInputHandlers?: () => void;
|
|
77
|
+
private currentFrame?: WebHostSurfaceFrame;
|
|
78
|
+
private columns = 80;
|
|
79
|
+
private rows = 24;
|
|
80
|
+
private cellWidth = 8;
|
|
81
|
+
private cellHeight = 18;
|
|
82
|
+
private activePointerButton: "primary" | "middle" | "secondary" = "primary";
|
|
83
|
+
private hasCapturedPointer = false;
|
|
84
|
+
private lastSentResize?: {
|
|
85
|
+
columns: number;
|
|
86
|
+
rows: number;
|
|
87
|
+
cellWidth: number;
|
|
88
|
+
cellHeight: number;
|
|
89
|
+
};
|
|
90
|
+
private isVisible = false;
|
|
91
|
+
|
|
92
|
+
constructor(options: WebHostSceneRuntimeOptions) {
|
|
93
|
+
this.descriptor = options.descriptor;
|
|
94
|
+
this.currentStyle = normalizeWebHostTerminalStyle(options.style);
|
|
95
|
+
this.bridge = options.bridge;
|
|
96
|
+
this.onInput = options.onInput;
|
|
97
|
+
this.onFrameDiagnostic = options.onFrameDiagnostic;
|
|
98
|
+
this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;
|
|
99
|
+
this.captureWheelInput = options.captureWheelInput ?? true;
|
|
100
|
+
this.element = document.createElement("section");
|
|
101
|
+
this.element.className = "webhost-scene";
|
|
102
|
+
this.element.dataset.sceneId = options.descriptor.id;
|
|
103
|
+
this.element.hidden = true;
|
|
104
|
+
|
|
105
|
+
const header = document.createElement("div");
|
|
106
|
+
header.className = "webhost-scene__header";
|
|
107
|
+
header.textContent = options.descriptor.title ?? options.descriptor.id;
|
|
108
|
+
|
|
109
|
+
this.terminalMount = document.createElement("div");
|
|
110
|
+
this.terminalMount.className = "webhost-scene__terminal";
|
|
111
|
+
this.terminalMount.tabIndex = 0;
|
|
112
|
+
|
|
113
|
+
this.element.append(header, this.terminalMount);
|
|
114
|
+
options.mount.appendChild(this.element);
|
|
115
|
+
this.applyVisibility();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async mount(): Promise<void> {
|
|
119
|
+
if (this.canvas) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const canvas = document.createElement("canvas");
|
|
124
|
+
canvas.className = "webhost-scene__surface";
|
|
125
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
126
|
+
this.canvas = canvas;
|
|
127
|
+
this.accessibilityTree = new AccessibilityTreeMounter();
|
|
128
|
+
this.terminalMount.replaceChildren(
|
|
129
|
+
canvas,
|
|
130
|
+
this.accessibilityTree.element,
|
|
131
|
+
this.accessibilityTree.announcerElement
|
|
132
|
+
);
|
|
133
|
+
this.installInputHandlers();
|
|
134
|
+
this.installResizeObserver();
|
|
135
|
+
|
|
136
|
+
this.bridge?.bindOutput({
|
|
137
|
+
presentSurface: (frame) => this.presentSurface(frame),
|
|
138
|
+
writeClipboard: (text) => this.writeClipboard(text),
|
|
139
|
+
notifyRuntimeIssue: (issue) => this.notifyRuntimeIssue(issue),
|
|
140
|
+
recordFrameDiagnostic: (diagnostic) => this.recordFrameDiagnostic(diagnostic),
|
|
141
|
+
writeOutput: (text) => this.writeOutput(text),
|
|
142
|
+
writeError: (text) => this.writeOutput(text),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
this.applyStyle(this.currentStyle);
|
|
146
|
+
this.measureCells();
|
|
147
|
+
this.resizeToMount();
|
|
148
|
+
this.draw();
|
|
149
|
+
this.syncAccessibilityTree();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setVisible(
|
|
153
|
+
visible: boolean
|
|
154
|
+
): void {
|
|
155
|
+
this.isVisible = visible;
|
|
156
|
+
this.applyVisibility();
|
|
157
|
+
if (visible) {
|
|
158
|
+
this.resizeToMount();
|
|
159
|
+
if (this.synchronizeAccessibilityFocus) {
|
|
160
|
+
this.terminalMount.focus?.({ preventScroll: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setStyle(
|
|
166
|
+
style: WebHostTerminalStyle
|
|
167
|
+
): void {
|
|
168
|
+
this.currentStyle = normalizeWebHostTerminalStyle(style);
|
|
169
|
+
this.applyStyle(this.currentStyle);
|
|
170
|
+
this.bridge?.updateRenderStyle(this.currentStyle);
|
|
171
|
+
this.measureCells();
|
|
172
|
+
this.resizeToMount();
|
|
173
|
+
this.draw();
|
|
174
|
+
this.syncAccessibilityTree();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
resize(
|
|
178
|
+
columns: number,
|
|
179
|
+
rows: number
|
|
180
|
+
): void {
|
|
181
|
+
this.columns = Math.max(1, Math.round(columns));
|
|
182
|
+
this.rows = Math.max(1, Math.round(rows));
|
|
183
|
+
this.resizeCanvas();
|
|
184
|
+
this.draw();
|
|
185
|
+
this.syncAccessibilityTree();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
writeOutput(
|
|
189
|
+
text: string
|
|
190
|
+
): void {
|
|
191
|
+
if (!this.diagnosticText) {
|
|
192
|
+
const diagnosticText = document.createElement("pre");
|
|
193
|
+
diagnosticText.className = "webhost-scene__diagnostic";
|
|
194
|
+
this.diagnosticText = diagnosticText;
|
|
195
|
+
this.terminalMount.appendChild(diagnosticText);
|
|
196
|
+
}
|
|
197
|
+
this.diagnosticText.textContent = `${this.diagnosticText.textContent ?? ""}${text}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
notifyRuntimeIssue(
|
|
201
|
+
issue: WebHostRuntimeIssue
|
|
202
|
+
): void {
|
|
203
|
+
console.log(issue.description);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private recordFrameDiagnostic(
|
|
207
|
+
diagnostic: WebHostFrameDiagnosticRecord
|
|
208
|
+
): void {
|
|
209
|
+
this.onFrameDiagnostic?.(diagnostic);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async writeClipboard(
|
|
213
|
+
text: string
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
216
|
+
if (!clipboard?.writeText) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
await clipboard.writeText(text);
|
|
222
|
+
} catch {
|
|
223
|
+
// Clipboard permissions are browser/user-gesture dependent; hosts treat
|
|
224
|
+
// rejection as a best-effort no-op rather than surfacing diagnostics.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
sendInput(
|
|
229
|
+
chunk: Uint8Array
|
|
230
|
+
): void {
|
|
231
|
+
this.onInput(chunk);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
dispose(): void {
|
|
235
|
+
this.detachInputHandlers?.();
|
|
236
|
+
this.resizeObserver?.disconnect();
|
|
237
|
+
this.element.remove();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private presentSurface(
|
|
241
|
+
frame: WebHostSurfaceFrame
|
|
242
|
+
): void {
|
|
243
|
+
const previousFrame = this.currentFrame;
|
|
244
|
+
this.currentFrame = frame;
|
|
245
|
+
this.columns = Math.max(1, Math.round(frame.width));
|
|
246
|
+
this.rows = Math.max(1, Math.round(frame.height));
|
|
247
|
+
const resized = this.resizeCanvas();
|
|
248
|
+
this.draw(previousFrame && !resized ? frame.damage : undefined);
|
|
249
|
+
this.syncAccessibilityTree();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private applyStyle(
|
|
253
|
+
style: WebHostTerminalStyle
|
|
254
|
+
): void {
|
|
255
|
+
applyWebHostTerminalStyle(this.element, style);
|
|
256
|
+
this.element.style.padding = "0.75rem";
|
|
257
|
+
this.element.style.borderRadius = "16px";
|
|
258
|
+
this.element.style.boxShadow = "0 20px 50px rgba(0, 0, 0, 0.28)";
|
|
259
|
+
this.element.style.overflow = "hidden";
|
|
260
|
+
this.element.style.gap = "0.5rem";
|
|
261
|
+
this.element.style.gridTemplateRows = "auto 1fr";
|
|
262
|
+
|
|
263
|
+
this.terminalMount.style.position = "relative";
|
|
264
|
+
this.terminalMount.style.overflow = "hidden";
|
|
265
|
+
this.terminalMount.style.outline = "none";
|
|
266
|
+
this.terminalMount.style.background = webTUITerminalBackgroundColor(this.currentStyle);
|
|
267
|
+
this.terminalMount.style.minHeight = `${this.cellHeight * 8}px`;
|
|
268
|
+
|
|
269
|
+
if (this.canvas) {
|
|
270
|
+
this.canvas.style.display = "block";
|
|
271
|
+
this.canvas.style.width = "100%";
|
|
272
|
+
this.canvas.style.height = "100%";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private applyVisibility(): void {
|
|
277
|
+
this.element.hidden = !this.isVisible;
|
|
278
|
+
this.element.style.setProperty(
|
|
279
|
+
"display",
|
|
280
|
+
this.isVisible ? "grid" : "none",
|
|
281
|
+
"important"
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private installResizeObserver(): void {
|
|
286
|
+
if (typeof ResizeObserver === "undefined") {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
291
|
+
this.resizeToMount();
|
|
292
|
+
});
|
|
293
|
+
this.resizeObserver.observe(this.terminalMount);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private installInputHandlers(): void {
|
|
297
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
298
|
+
if (event.metaKey || event.isComposing) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const key = keyInputFromKeyboardEvent(event);
|
|
302
|
+
if (!key) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.onInput(encodeKeyInputMessage({
|
|
307
|
+
...key,
|
|
308
|
+
modifiers: modifierMask(event),
|
|
309
|
+
}));
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const handlePaste = (event: ClipboardEvent) => {
|
|
314
|
+
const text = event.clipboardData?.getData("text/plain") ?? "";
|
|
315
|
+
if (!text) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
this.onInput(encodePasteInputMessage(text));
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
323
|
+
const location = this.cellLocation(event);
|
|
324
|
+
if (!location) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const button = pointerButton(event.button);
|
|
329
|
+
this.activePointerButton = button;
|
|
330
|
+
this.hasCapturedPointer = true;
|
|
331
|
+
this.terminalMount.focus?.({ preventScroll: true });
|
|
332
|
+
this.terminalMount.setPointerCapture?.(event.pointerId);
|
|
333
|
+
this.onInput(encodeMouseInputMessage({
|
|
334
|
+
kind: "down",
|
|
335
|
+
x: location.x,
|
|
336
|
+
y: location.y,
|
|
337
|
+
button,
|
|
338
|
+
modifiers: modifierMask(event),
|
|
339
|
+
}));
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const handlePointerUp = (event: PointerEvent) => {
|
|
344
|
+
const location = this.hasCapturedPointer
|
|
345
|
+
? this.rawCellLocation(event)
|
|
346
|
+
: this.cellLocation(event);
|
|
347
|
+
this.terminalMount.releasePointerCapture?.(event.pointerId);
|
|
348
|
+
this.hasCapturedPointer = false;
|
|
349
|
+
if (!location) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.onInput(encodeMouseInputMessage({
|
|
354
|
+
kind: "up",
|
|
355
|
+
x: location.x,
|
|
356
|
+
y: location.y,
|
|
357
|
+
button: pointerButton(event.button) ?? this.activePointerButton,
|
|
358
|
+
modifiers: modifierMask(event),
|
|
359
|
+
}));
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const handlePointerMove = (event: PointerEvent) => {
|
|
364
|
+
const location = event.buttons && this.hasCapturedPointer
|
|
365
|
+
? this.rawCellLocation(event)
|
|
366
|
+
: this.cellLocation(event);
|
|
367
|
+
if (!location) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.onInput(encodeMouseInputMessage({
|
|
372
|
+
kind: event.buttons ? "dragged" : "moved",
|
|
373
|
+
x: location.x,
|
|
374
|
+
y: location.y,
|
|
375
|
+
button: this.activePointerButton,
|
|
376
|
+
modifiers: modifierMask(event),
|
|
377
|
+
}));
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const handleWheel = (event: WheelEvent) => {
|
|
381
|
+
if (!this.captureWheelInput) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const location = this.cellLocation(event);
|
|
386
|
+
if (!location) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
this.onInput(encodeMouseInputMessage({
|
|
391
|
+
kind: "scrolled",
|
|
392
|
+
x: location.x,
|
|
393
|
+
y: location.y,
|
|
394
|
+
deltaX: normalizedWheelDelta(event.deltaX),
|
|
395
|
+
deltaY: normalizedWheelDelta(event.deltaY),
|
|
396
|
+
modifiers: modifierMask(event),
|
|
397
|
+
}));
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
this.terminalMount.addEventListener("keydown", handleKeyDown);
|
|
402
|
+
this.terminalMount.addEventListener("paste", handlePaste);
|
|
403
|
+
this.terminalMount.addEventListener("pointerdown", handlePointerDown);
|
|
404
|
+
this.terminalMount.addEventListener("pointerup", handlePointerUp);
|
|
405
|
+
this.terminalMount.addEventListener("pointermove", handlePointerMove);
|
|
406
|
+
this.terminalMount.addEventListener("wheel", handleWheel, { passive: false });
|
|
407
|
+
|
|
408
|
+
this.detachInputHandlers = () => {
|
|
409
|
+
this.terminalMount.removeEventListener("keydown", handleKeyDown);
|
|
410
|
+
this.terminalMount.removeEventListener("paste", handlePaste);
|
|
411
|
+
this.terminalMount.removeEventListener("pointerdown", handlePointerDown);
|
|
412
|
+
this.terminalMount.removeEventListener("pointerup", handlePointerUp);
|
|
413
|
+
this.terminalMount.removeEventListener("pointermove", handlePointerMove);
|
|
414
|
+
this.terminalMount.removeEventListener("wheel", handleWheel);
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private resizeToMount(): void {
|
|
419
|
+
this.measureCells();
|
|
420
|
+
const rect = this.terminalMount.getBoundingClientRect?.();
|
|
421
|
+
const width = rect?.width && rect.width > 0 ? rect.width : this.columns * this.cellWidth;
|
|
422
|
+
const height = rect?.height && rect.height > 0 ? rect.height : this.rows * this.cellHeight;
|
|
423
|
+
const nextColumns = Math.max(1, Math.floor(width / this.cellWidth));
|
|
424
|
+
const nextRows = Math.max(1, Math.floor(height / this.cellHeight));
|
|
425
|
+
|
|
426
|
+
this.columns = nextColumns;
|
|
427
|
+
this.rows = nextRows;
|
|
428
|
+
this.sendResizeIfNeeded();
|
|
429
|
+
this.resizeCanvas();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private sendResizeIfNeeded(): void {
|
|
433
|
+
const current = {
|
|
434
|
+
columns: this.columns,
|
|
435
|
+
rows: this.rows,
|
|
436
|
+
cellWidth: this.cellWidth,
|
|
437
|
+
cellHeight: this.cellHeight,
|
|
438
|
+
};
|
|
439
|
+
if (this.lastSentResize
|
|
440
|
+
&& this.lastSentResize.columns === current.columns
|
|
441
|
+
&& this.lastSentResize.rows === current.rows
|
|
442
|
+
&& this.lastSentResize.cellWidth === current.cellWidth
|
|
443
|
+
&& this.lastSentResize.cellHeight === current.cellHeight
|
|
444
|
+
) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.lastSentResize = current;
|
|
449
|
+
this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private resizeCanvas(): boolean {
|
|
453
|
+
if (!this.canvas) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const cssWidth = Math.max(1, this.columns * this.cellWidth);
|
|
458
|
+
const cssHeight = Math.max(1, this.rows * this.cellHeight);
|
|
459
|
+
const scale = globalThis.window?.devicePixelRatio || 1;
|
|
460
|
+
const width = Math.ceil(cssWidth * scale);
|
|
461
|
+
const height = Math.ceil(cssHeight * scale);
|
|
462
|
+
const styleWidth = `${cssWidth}px`;
|
|
463
|
+
const styleHeight = `${cssHeight}px`;
|
|
464
|
+
if (this.canvas.width === width
|
|
465
|
+
&& this.canvas.height === height
|
|
466
|
+
&& this.canvas.style.width === styleWidth
|
|
467
|
+
&& this.canvas.style.height === styleHeight
|
|
468
|
+
) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.canvas.width = width;
|
|
473
|
+
this.canvas.height = height;
|
|
474
|
+
this.canvas.style.width = styleWidth;
|
|
475
|
+
this.canvas.style.height = styleHeight;
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private measureCells(): void {
|
|
480
|
+
const canvas = this.canvas ?? document.createElement("canvas");
|
|
481
|
+
const context = canvas.getContext?.("2d");
|
|
482
|
+
if (!context) {
|
|
483
|
+
this.cellWidth = Math.max(1, Math.round(this.currentStyle.fontSize * 0.62));
|
|
484
|
+
this.cellHeight = Math.max(1, Math.round(this.currentStyle.fontSize * 1.35));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
context.font = this.fontForStyle();
|
|
489
|
+
this.cellWidth = Math.max(1, Math.ceil(context.measureText("W").width));
|
|
490
|
+
this.cellHeight = Math.max(1, Math.ceil(this.currentStyle.fontSize * 1.35));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private draw(
|
|
494
|
+
damage?: WebHostSurfaceDamage
|
|
495
|
+
): void {
|
|
496
|
+
const canvas = this.canvas;
|
|
497
|
+
const context = canvas?.getContext("2d");
|
|
498
|
+
if (!canvas || !context) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const frame = this.currentFrame;
|
|
503
|
+
const dirtyRects = frame ? this.dirtyRectsForDamage(damage, frame) : undefined;
|
|
504
|
+
if (dirtyRects?.length === 0) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const scale = globalThis.window?.devicePixelRatio || 1;
|
|
509
|
+
context.setTransform(scale, 0, 0, scale, 0, 0);
|
|
510
|
+
context.textBaseline = "alphabetic";
|
|
511
|
+
|
|
512
|
+
context.fillStyle = webTUITerminalBackgroundColor(this.currentStyle);
|
|
513
|
+
if (dirtyRects) {
|
|
514
|
+
for (const rect of dirtyRects) {
|
|
515
|
+
context.clearRect(rect.x, rect.y, rect.width, rect.height);
|
|
516
|
+
context.fillRect(rect.x, rect.y, rect.width, rect.height);
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);
|
|
520
|
+
context.fillRect(0, 0, this.columns * this.cellWidth, this.rows * this.cellHeight);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!frame) {
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this.drawRows(context, frame, dirtyRects);
|
|
528
|
+
this.drawImages(context, frame.images ?? [], dirtyRects);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private drawRows(
|
|
532
|
+
context: CanvasRenderingContext2D,
|
|
533
|
+
frame: WebHostSurfaceFrame,
|
|
534
|
+
dirtyRects?: DirtyRect[]
|
|
535
|
+
): void {
|
|
536
|
+
for (let y = 0; y < frame.rows.length; y += 1) {
|
|
537
|
+
const row = frame.rows[y] ?? [];
|
|
538
|
+
for (const cell of row) {
|
|
539
|
+
const [x, text, span, styleIndex] = cell;
|
|
540
|
+
const cellRect = this.cellRect(x, y, span);
|
|
541
|
+
if (dirtyRects && !dirtyRects.some((rect) => rectsIntersect(rect, cellRect))) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const style = frame.styles[styleIndex] ?? undefined;
|
|
545
|
+
this.drawCell(context, x, y, text, span, style);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private syncAccessibilityTree(): void {
|
|
551
|
+
const tree = this.accessibilityTree;
|
|
552
|
+
if (!tree || !this.currentFrame) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
tree.present(this.currentFrame.accessibilityTree ?? [], {
|
|
557
|
+
cellWidth: this.cellWidth,
|
|
558
|
+
cellHeight: this.cellHeight,
|
|
559
|
+
}, this.currentFrame.accessibilityAnnouncements ?? [], {
|
|
560
|
+
synchronizeFocus: this.synchronizeAccessibilityFocus,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private drawImages(
|
|
565
|
+
context: CanvasRenderingContext2D,
|
|
566
|
+
images: WebHostSurfaceImage[],
|
|
567
|
+
dirtyRects?: DirtyRect[]
|
|
568
|
+
): void {
|
|
569
|
+
for (const image of images) {
|
|
570
|
+
this.drawImage(context, image, dirtyRects);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private drawImage(
|
|
575
|
+
context: CanvasRenderingContext2D,
|
|
576
|
+
image: WebHostSurfaceImage,
|
|
577
|
+
dirtyRects?: DirtyRect[]
|
|
578
|
+
): void {
|
|
579
|
+
const decodedImage = this.cachedImage(image);
|
|
580
|
+
if (!decodedImage) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
|
|
585
|
+
const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
|
|
586
|
+
if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const imageRect = {
|
|
590
|
+
x: clipX * this.cellWidth,
|
|
591
|
+
y: clipY * this.cellHeight,
|
|
592
|
+
width: clipWidth * this.cellWidth,
|
|
593
|
+
height: clipHeight * this.cellHeight,
|
|
594
|
+
};
|
|
595
|
+
if (dirtyRects && !dirtyRects.some((rect) => rectsIntersect(rect, imageRect))) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
context.save();
|
|
600
|
+
context.beginPath();
|
|
601
|
+
context.rect(
|
|
602
|
+
clipX * this.cellWidth,
|
|
603
|
+
clipY * this.cellHeight,
|
|
604
|
+
clipWidth * this.cellWidth,
|
|
605
|
+
clipHeight * this.cellHeight
|
|
606
|
+
);
|
|
607
|
+
context.clip();
|
|
608
|
+
context.drawImage(
|
|
609
|
+
decodedImage,
|
|
610
|
+
boundsX * this.cellWidth,
|
|
611
|
+
boundsY * this.cellHeight,
|
|
612
|
+
boundsWidth * this.cellWidth,
|
|
613
|
+
boundsHeight * this.cellHeight
|
|
614
|
+
);
|
|
615
|
+
context.restore();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private cachedImage(
|
|
619
|
+
image: WebHostSurfaceImage
|
|
620
|
+
): CanvasImageSource | undefined {
|
|
621
|
+
const cached = this.imageCache.get(image.id);
|
|
622
|
+
if (cached?.image) {
|
|
623
|
+
return cached.image;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (!cached?.promise && image.dataBase64) {
|
|
627
|
+
const promise = decodeImage(image.dataBase64, image.format);
|
|
628
|
+
this.imageCache.set(image.id, { promise });
|
|
629
|
+
void promise.then((decodedImage) => {
|
|
630
|
+
const latest = this.imageCache.get(image.id);
|
|
631
|
+
if (latest?.promise !== promise) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
this.imageCache.set(image.id, { image: decodedImage });
|
|
635
|
+
this.draw();
|
|
636
|
+
}).catch(() => {
|
|
637
|
+
this.imageCache.delete(image.id);
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private drawCell(
|
|
645
|
+
context: CanvasRenderingContext2D,
|
|
646
|
+
x: number,
|
|
647
|
+
y: number,
|
|
648
|
+
text: string,
|
|
649
|
+
span: number,
|
|
650
|
+
style?: WebHostSurfaceStyle | null
|
|
651
|
+
): void {
|
|
652
|
+
const rectX = x * this.cellWidth;
|
|
653
|
+
const rectY = y * this.cellHeight;
|
|
654
|
+
const width = Math.max(1, span) * this.cellWidth;
|
|
655
|
+
const background = resolvedBackground(style, this.currentStyle);
|
|
656
|
+
const foreground = resolvedForeground(style, this.currentStyle);
|
|
657
|
+
const opacity = style?.opacity ?? 1;
|
|
658
|
+
|
|
659
|
+
if (background) {
|
|
660
|
+
context.globalAlpha = opacity;
|
|
661
|
+
context.fillStyle = background;
|
|
662
|
+
context.fillRect(rectX, rectY, width, this.cellHeight);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (text !== " ") {
|
|
666
|
+
context.globalAlpha = opacity;
|
|
667
|
+
context.fillStyle = foreground;
|
|
668
|
+
context.strokeStyle = foreground;
|
|
669
|
+
if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {
|
|
670
|
+
x: rectX,
|
|
671
|
+
y: rectY,
|
|
672
|
+
width,
|
|
673
|
+
height: this.cellHeight,
|
|
674
|
+
})) {
|
|
675
|
+
context.font = this.fontForStyle(style);
|
|
676
|
+
context.fillText(
|
|
677
|
+
text,
|
|
678
|
+
rectX,
|
|
679
|
+
rectY + Math.floor((this.cellHeight + this.currentStyle.fontSize) / 2) - 2
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
this.drawTextLine(context, rectX, rectY, width, style?.underline, "underline", foreground);
|
|
685
|
+
this.drawTextLine(context, rectX, rectY, width, style?.strikethrough, "strike", foreground);
|
|
686
|
+
context.globalAlpha = 1;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private dirtyRectsForDamage(
|
|
690
|
+
damage: WebHostSurfaceDamage | undefined,
|
|
691
|
+
frame: WebHostSurfaceFrame
|
|
692
|
+
): DirtyRect[] | undefined {
|
|
693
|
+
if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const rects: DirtyRect[] = [];
|
|
698
|
+
for (const [row, ranges] of damage.textRows) {
|
|
699
|
+
if (row < 0 || row >= frame.height) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (ranges.length === 0) {
|
|
703
|
+
rects.push(this.cellRect(0, row, frame.width));
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
for (const [start, end] of ranges) {
|
|
707
|
+
const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));
|
|
708
|
+
const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));
|
|
709
|
+
if (lowerBound >= upperBound) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
rects.push(this.cellRect(lowerBound, row, upperBound - lowerBound));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return rects;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
private cellRect(
|
|
719
|
+
x: number,
|
|
720
|
+
y: number,
|
|
721
|
+
span: number
|
|
722
|
+
): DirtyRect {
|
|
723
|
+
return {
|
|
724
|
+
x: x * this.cellWidth,
|
|
725
|
+
y: y * this.cellHeight,
|
|
726
|
+
width: Math.max(1, span) * this.cellWidth,
|
|
727
|
+
height: this.cellHeight,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private drawTextLine(
|
|
732
|
+
context: CanvasRenderingContext2D,
|
|
733
|
+
x: number,
|
|
734
|
+
y: number,
|
|
735
|
+
width: number,
|
|
736
|
+
line: WebHostSurfaceStyle["underline"],
|
|
737
|
+
placement: "underline" | "strike",
|
|
738
|
+
fallbackColor: string
|
|
739
|
+
): void {
|
|
740
|
+
if (!line) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
context.strokeStyle = line.color ?? fallbackColor;
|
|
744
|
+
context.lineWidth = line.pattern === "double" ? 2 : 1;
|
|
745
|
+
if (line.pattern === "dot") {
|
|
746
|
+
context.setLineDash([1, 3]);
|
|
747
|
+
} else if (line.pattern === "dash") {
|
|
748
|
+
context.setLineDash([4, 3]);
|
|
749
|
+
} else {
|
|
750
|
+
context.setLineDash([]);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const lineY = placement === "underline"
|
|
754
|
+
? y + this.cellHeight - 2
|
|
755
|
+
: y + Math.floor(this.cellHeight / 2);
|
|
756
|
+
context.beginPath();
|
|
757
|
+
context.moveTo(x, lineY);
|
|
758
|
+
context.lineTo(x + width, lineY);
|
|
759
|
+
context.stroke();
|
|
760
|
+
context.setLineDash([]);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private fontForStyle(
|
|
764
|
+
style?: WebHostSurfaceStyle | null
|
|
765
|
+
): string {
|
|
766
|
+
const emphasis = style?.em ?? 0;
|
|
767
|
+
const italic = (emphasis & 2) !== 0 ? "italic " : "";
|
|
768
|
+
const weight = (emphasis & 1) !== 0 ? "700 " : "";
|
|
769
|
+
return `${italic}${weight}${this.currentStyle.fontSize}px ${this.currentStyle.fontFamily}`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private cellLocation(
|
|
773
|
+
event: MouseEvent
|
|
774
|
+
): { x: number; y: number } | undefined {
|
|
775
|
+
const location = this.rawCellLocation(event);
|
|
776
|
+
if (!location) {
|
|
777
|
+
return undefined;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const cellX = Math.floor(location.x);
|
|
781
|
+
const cellY = Math.floor(location.y);
|
|
782
|
+
if (cellX < 0 || cellY < 0 || cellX >= this.columns || cellY >= this.rows) {
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
return location;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private rawCellLocation(
|
|
789
|
+
event: MouseEvent
|
|
790
|
+
): { x: number; y: number } | undefined {
|
|
791
|
+
const rect = this.canvas?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.();
|
|
792
|
+
if (!rect) {
|
|
793
|
+
return undefined;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const x = (event.clientX - rect.left) / this.cellWidth;
|
|
797
|
+
const y = (event.clientY - rect.top) / this.cellHeight;
|
|
798
|
+
return { x, y };
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function decodeImage(
|
|
803
|
+
dataBase64: string,
|
|
804
|
+
format: WebHostSurfaceImageFormat
|
|
805
|
+
): Promise<CanvasImageSource> {
|
|
806
|
+
const bytes = decodeBase64Bytes(dataBase64);
|
|
807
|
+
const blob = new Blob([bytes], { type: `image/${format}` });
|
|
808
|
+
|
|
809
|
+
if (typeof createImageBitmap === "function") {
|
|
810
|
+
// Animated GIFs collapse to their first frame in createImageBitmap
|
|
811
|
+
// — that matches the Kitty path's first-frame composite. Phase 7
|
|
812
|
+
// will replace this with a frame ticker.
|
|
813
|
+
return createImageBitmap(blob);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return new Promise((resolve, reject) => {
|
|
817
|
+
const image = new Image();
|
|
818
|
+
const url = URL.createObjectURL(blob);
|
|
819
|
+
image.onload = () => {
|
|
820
|
+
URL.revokeObjectURL(url);
|
|
821
|
+
resolve(image);
|
|
822
|
+
};
|
|
823
|
+
image.onerror = () => {
|
|
824
|
+
URL.revokeObjectURL(url);
|
|
825
|
+
reject(new Error(`Failed to decode ${format} image`));
|
|
826
|
+
};
|
|
827
|
+
image.src = url;
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function decodeBase64Bytes(
|
|
832
|
+
value: string
|
|
833
|
+
): Uint8Array {
|
|
834
|
+
if (typeof atob === "function") {
|
|
835
|
+
const binary = atob(value);
|
|
836
|
+
const bytes = new Uint8Array(binary.length);
|
|
837
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
838
|
+
bytes[index] = binary.charCodeAt(index);
|
|
839
|
+
}
|
|
840
|
+
return bytes;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function keyInputFromKeyboardEvent(
|
|
847
|
+
event: KeyboardEvent
|
|
848
|
+
): Pick<WebHostKeyInput, "key" | "character"> | undefined {
|
|
849
|
+
switch (event.key) {
|
|
850
|
+
case "Enter":
|
|
851
|
+
return { key: "return" };
|
|
852
|
+
case " ":
|
|
853
|
+
return { key: "space" };
|
|
854
|
+
case "Tab":
|
|
855
|
+
return { key: "tab" };
|
|
856
|
+
case "ArrowLeft":
|
|
857
|
+
return { key: "arrowLeft" };
|
|
858
|
+
case "ArrowRight":
|
|
859
|
+
return { key: "arrowRight" };
|
|
860
|
+
case "ArrowUp":
|
|
861
|
+
return { key: "arrowUp" };
|
|
862
|
+
case "ArrowDown":
|
|
863
|
+
return { key: "arrowDown" };
|
|
864
|
+
case "Backspace":
|
|
865
|
+
return { key: "backspace" };
|
|
866
|
+
case "Escape":
|
|
867
|
+
return { key: "escape" };
|
|
868
|
+
case "Home":
|
|
869
|
+
return { key: "home" };
|
|
870
|
+
case "End":
|
|
871
|
+
return { key: "end" };
|
|
872
|
+
default:
|
|
873
|
+
{
|
|
874
|
+
const characters = Array.from(event.key);
|
|
875
|
+
if (characters.length !== 1) {
|
|
876
|
+
return undefined;
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
key: "character",
|
|
880
|
+
character: characters[0],
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function pointerButton(
|
|
887
|
+
button: number
|
|
888
|
+
): "primary" | "middle" | "secondary" {
|
|
889
|
+
switch (button) {
|
|
890
|
+
case 1:
|
|
891
|
+
return "middle";
|
|
892
|
+
case 2:
|
|
893
|
+
return "secondary";
|
|
894
|
+
default:
|
|
895
|
+
return "primary";
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function modifierMask(
|
|
900
|
+
event: MouseEvent | KeyboardEvent
|
|
901
|
+
): number {
|
|
902
|
+
let mask = 0;
|
|
903
|
+
if (event.shiftKey) {
|
|
904
|
+
mask |= 1;
|
|
905
|
+
}
|
|
906
|
+
if (event.altKey) {
|
|
907
|
+
mask |= 2;
|
|
908
|
+
}
|
|
909
|
+
if (event.ctrlKey) {
|
|
910
|
+
mask |= 4;
|
|
911
|
+
}
|
|
912
|
+
return mask;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function normalizedWheelDelta(
|
|
916
|
+
delta: number
|
|
917
|
+
): number {
|
|
918
|
+
if (delta > 0) {
|
|
919
|
+
return 1;
|
|
920
|
+
}
|
|
921
|
+
if (delta < 0) {
|
|
922
|
+
return -1;
|
|
923
|
+
}
|
|
924
|
+
return 0;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function rectsIntersect(
|
|
928
|
+
lhs: DirtyRect,
|
|
929
|
+
rhs: DirtyRect
|
|
930
|
+
): boolean {
|
|
931
|
+
return lhs.x < rhs.x + rhs.width
|
|
932
|
+
&& lhs.x + lhs.width > rhs.x
|
|
933
|
+
&& lhs.y < rhs.y + rhs.height
|
|
934
|
+
&& lhs.y + lhs.height > rhs.y;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function resolvedForeground(
|
|
938
|
+
style: WebHostSurfaceStyle | null | undefined,
|
|
939
|
+
terminalStyle: ResolvedWebHostTerminalStyle
|
|
940
|
+
): string {
|
|
941
|
+
if ((style?.em ?? 0) & 16) {
|
|
942
|
+
return style?.bg ?? terminalStyle.theme.background;
|
|
943
|
+
}
|
|
944
|
+
return style?.fg ?? terminalStyle.theme.foreground;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function resolvedBackground(
|
|
948
|
+
style: WebHostSurfaceStyle | null | undefined,
|
|
949
|
+
terminalStyle: ResolvedWebHostTerminalStyle
|
|
950
|
+
): string | undefined {
|
|
951
|
+
if ((style?.em ?? 0) & 16) {
|
|
952
|
+
return style?.fg ?? terminalStyle.theme.foreground;
|
|
953
|
+
}
|
|
954
|
+
return style?.bg;
|
|
955
|
+
}
|