@swifttui/web 0.0.14 → 0.0.16

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.
Files changed (92) hide show
  1. package/README.md +24 -10
  2. package/dist/index.d.ts +9 -0
  3. package/dist/index.js +9 -0
  4. package/dist/manifest.d.ts +2 -0
  5. package/dist/manifest.js +2 -0
  6. package/dist/src/AccessibilityTree.js +156 -0
  7. package/dist/src/AccessibilityTree.js.map +1 -0
  8. package/dist/src/BoxDrawingRenderer.js +1106 -0
  9. package/dist/src/BoxDrawingRenderer.js.map +1 -0
  10. package/dist/src/WebHostApp.d.ts +41 -0
  11. package/dist/src/WebHostApp.js +135 -0
  12. package/dist/src/WebHostApp.js.map +1 -0
  13. package/dist/src/WebHostSceneManifest.d.ts +18 -0
  14. package/dist/src/WebHostSceneManifest.js +70 -0
  15. package/dist/src/WebHostSceneManifest.js.map +1 -0
  16. package/dist/src/WebHostSceneRuntime.d.ts +112 -0
  17. package/dist/src/WebHostSceneRuntime.js +651 -0
  18. package/dist/src/WebHostSceneRuntime.js.map +1 -0
  19. package/dist/src/WebHostSurfaceTransport.d.ts +166 -0
  20. package/dist/src/WebHostSurfaceTransport.js +252 -0
  21. package/dist/src/WebHostSurfaceTransport.js.map +1 -0
  22. package/dist/src/WebHostTerminalStyle.d.ts +92 -0
  23. package/dist/src/WebHostTerminalStyle.js +277 -0
  24. package/dist/src/WebHostTerminalStyle.js.map +1 -0
  25. package/dist/src/WebHostTestFixtures.d.ts +5 -0
  26. package/dist/src/WebHostTestFixtures.js +9 -0
  27. package/dist/src/WebHostTestFixtures.js.map +1 -0
  28. package/dist/src/WebSocketSceneBridge.d.ts +53 -0
  29. package/dist/src/WebSocketSceneBridge.js +124 -0
  30. package/dist/src/WebSocketSceneBridge.js.map +1 -0
  31. package/dist/src/wasi/BrowserWASIBridge.d.ts +33 -0
  32. package/dist/src/wasi/BrowserWASIBridge.js +97 -0
  33. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -0
  34. package/dist/src/wasi/SharedInputQueue.d.ts +31 -0
  35. package/dist/src/wasi/SharedInputQueue.js +102 -0
  36. package/dist/src/wasi/SharedInputQueue.js.map +1 -0
  37. package/dist/src/wasi/StdIOPipe.d.ts +15 -0
  38. package/dist/src/wasi/StdIOPipe.js +56 -0
  39. package/dist/src/wasi/StdIOPipe.js.map +1 -0
  40. package/dist/src/wasi/WasiPollScheduler.js +114 -0
  41. package/dist/src/wasi/WasiPollScheduler.js.map +1 -0
  42. package/dist/src/wasi/WasmSceneRuntime.d.ts +23 -0
  43. package/dist/src/wasi/WasmSceneRuntime.js +119 -0
  44. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -0
  45. package/dist/src/wasi/WasmSceneWorker.d.ts +27 -0
  46. package/dist/src/wasi/WasmSceneWorker.js +109 -0
  47. package/dist/src/wasi/WasmSceneWorker.js.map +1 -0
  48. package/dist/testing.d.ts +2 -0
  49. package/dist/testing.js +2 -0
  50. package/dist/wasi-worker.d.ts +2 -0
  51. package/dist/wasi-worker.js +2 -0
  52. package/dist/wasi.d.ts +6 -0
  53. package/dist/wasi.js +6 -0
  54. package/dist/websocket.d.ts +2 -0
  55. package/dist/websocket.js +2 -0
  56. package/package.json +49 -18
  57. package/AGENTS.md +0 -52
  58. package/cli.ts +0 -168
  59. package/index.html +0 -50
  60. package/index.ts +0 -8
  61. package/manifest.ts +0 -1
  62. package/src/AccessibilityTree.ts +0 -262
  63. package/src/BoxDrawingRenderer.ts +0 -585
  64. package/src/PublicEntrypointBoundary.test.ts +0 -20
  65. package/src/WebHostApp.test.ts +0 -222
  66. package/src/WebHostApp.ts +0 -269
  67. package/src/WebHostSceneManifest.test.ts +0 -38
  68. package/src/WebHostSceneManifest.ts +0 -156
  69. package/src/WebHostSceneRuntime.test.ts +0 -1982
  70. package/src/WebHostSceneRuntime.ts +0 -1142
  71. package/src/WebHostSurfaceTransport.test.ts +0 -362
  72. package/src/WebHostSurfaceTransport.ts +0 -691
  73. package/src/WebHostTerminalStyle.test.ts +0 -123
  74. package/src/WebHostTerminalStyle.ts +0 -471
  75. package/src/WebHostTestFixtures.ts +0 -10
  76. package/src/WebSocketSceneBridge.test.ts +0 -198
  77. package/src/WebSocketSceneBridge.ts +0 -233
  78. package/src/browser.ts +0 -59
  79. package/src/wasi/BrowserWASIBridge.test.ts +0 -168
  80. package/src/wasi/BrowserWASIBridge.ts +0 -167
  81. package/src/wasi/SharedInputQueue.test.ts +0 -146
  82. package/src/wasi/SharedInputQueue.ts +0 -199
  83. package/src/wasi/StdIOPipe.ts +0 -72
  84. package/src/wasi/WasiPollScheduler.test.ts +0 -176
  85. package/src/wasi/WasiPollScheduler.ts +0 -305
  86. package/src/wasi/WasmSceneRuntime.ts +0 -205
  87. package/src/wasi/WasmSceneWorker.ts +0 -182
  88. package/testing.ts +0 -1
  89. package/tsconfig.json +0 -29
  90. package/wasi-worker.ts +0 -1
  91. package/wasi.ts +0 -4
  92. package/websocket.ts +0 -1
@@ -0,0 +1,651 @@
1
+ import { applyWebHostTerminalStyle, normalizeWebHostTerminalStyle, webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
2
+ import { encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage } from "./WebHostSurfaceTransport.js";
3
+ import { canRenderBoxDrawing, drawBoxDrawing } from "./BoxDrawingRenderer.js";
4
+ import { AccessibilityTreeMounter } from "./AccessibilityTree.js";
5
+ //#region src/WebHostSceneRuntime.ts
6
+ var WebHostSceneRuntime = class {
7
+ descriptor;
8
+ element;
9
+ terminalMount;
10
+ bridge;
11
+ onInput;
12
+ onFrameDiagnostic;
13
+ synchronizeAccessibilityFocus;
14
+ wheelMode;
15
+ imageCache = /* @__PURE__ */ new Map();
16
+ currentStyle;
17
+ canvas;
18
+ accessibilityTree;
19
+ diagnosticText;
20
+ resizeObserver;
21
+ detachInputHandlers;
22
+ currentFrame;
23
+ columns = 80;
24
+ rows = 24;
25
+ cellWidth = 8;
26
+ cellHeight = 18;
27
+ activePointerButton = "primary";
28
+ hasCapturedPointer = false;
29
+ lastSentResize;
30
+ isVisible = false;
31
+ constructor(options) {
32
+ this.descriptor = options.descriptor;
33
+ this.currentStyle = normalizeWebHostTerminalStyle(options.style);
34
+ this.bridge = options.bridge;
35
+ this.onInput = options.onInput;
36
+ this.onFrameDiagnostic = options.onFrameDiagnostic;
37
+ this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;
38
+ this.wheelMode = options.wheelMode ?? (options.captureWheelInput === false ? "passive" : "capture");
39
+ this.element = document.createElement("section");
40
+ this.element.className = "webhost-scene";
41
+ this.element.dataset.sceneId = options.descriptor.id;
42
+ this.element.hidden = true;
43
+ const header = document.createElement("div");
44
+ header.className = "webhost-scene__header";
45
+ header.textContent = options.descriptor.title ?? options.descriptor.id;
46
+ this.terminalMount = document.createElement("div");
47
+ this.terminalMount.className = "webhost-scene__terminal";
48
+ this.terminalMount.tabIndex = 0;
49
+ this.element.append(header, this.terminalMount);
50
+ options.mount.appendChild(this.element);
51
+ this.applyVisibility();
52
+ }
53
+ async mount() {
54
+ if (this.canvas) return;
55
+ const canvas = document.createElement("canvas");
56
+ canvas.className = "webhost-scene__surface";
57
+ canvas.setAttribute("aria-hidden", "true");
58
+ this.canvas = canvas;
59
+ this.accessibilityTree = new AccessibilityTreeMounter();
60
+ this.terminalMount.replaceChildren(canvas, this.accessibilityTree.element, this.accessibilityTree.announcerElement);
61
+ this.installInputHandlers();
62
+ this.installResizeObserver();
63
+ this.bridge?.bindOutput({
64
+ presentSurface: (frame) => this.presentSurface(frame),
65
+ writeClipboard: (text) => this.writeClipboard(text),
66
+ notifyRuntimeIssue: (issue) => this.notifyRuntimeIssue(issue),
67
+ recordFrameDiagnostic: (diagnostic) => this.recordFrameDiagnostic(diagnostic),
68
+ writeOutput: (text) => this.writeOutput(text),
69
+ writeError: (text) => this.writeOutput(text)
70
+ });
71
+ this.applyStyle(this.currentStyle);
72
+ this.measureCells();
73
+ this.resizeToMount();
74
+ this.draw();
75
+ this.syncAccessibilityTree();
76
+ }
77
+ setVisible(visible) {
78
+ this.isVisible = visible;
79
+ this.applyVisibility();
80
+ if (visible) {
81
+ this.resizeToMount();
82
+ if (this.synchronizeAccessibilityFocus) this.terminalMount.focus?.({ preventScroll: true });
83
+ }
84
+ }
85
+ setStyle(style) {
86
+ this.currentStyle = normalizeWebHostTerminalStyle(style);
87
+ this.applyStyle(this.currentStyle);
88
+ this.bridge?.updateRenderStyle(this.currentStyle);
89
+ this.measureCells();
90
+ this.resizeToMount();
91
+ this.draw();
92
+ this.syncAccessibilityTree();
93
+ }
94
+ resize(columns, rows) {
95
+ this.columns = Math.max(1, Math.round(columns));
96
+ this.rows = Math.max(1, Math.round(rows));
97
+ this.resizeCanvas();
98
+ this.draw();
99
+ this.syncAccessibilityTree();
100
+ }
101
+ writeOutput(text) {
102
+ if (!this.diagnosticText) {
103
+ const diagnosticText = document.createElement("pre");
104
+ diagnosticText.className = "webhost-scene__diagnostic";
105
+ this.diagnosticText = diagnosticText;
106
+ this.terminalMount.appendChild(diagnosticText);
107
+ }
108
+ this.diagnosticText.textContent = `${this.diagnosticText.textContent ?? ""}${text}`;
109
+ }
110
+ notifyRuntimeIssue(issue) {
111
+ console.log(issue.description);
112
+ }
113
+ recordFrameDiagnostic(diagnostic) {
114
+ this.onFrameDiagnostic?.(diagnostic);
115
+ }
116
+ async writeClipboard(text) {
117
+ const clipboard = globalThis.navigator?.clipboard;
118
+ if (!clipboard?.writeText) return;
119
+ try {
120
+ await clipboard.writeText(text);
121
+ } catch {}
122
+ }
123
+ sendInput(chunk) {
124
+ this.onInput(chunk);
125
+ }
126
+ dispose() {
127
+ this.detachInputHandlers?.();
128
+ this.resizeObserver?.disconnect();
129
+ this.element.remove();
130
+ }
131
+ presentSurface(frame) {
132
+ const previousFrame = this.currentFrame;
133
+ this.currentFrame = frame;
134
+ this.columns = Math.max(1, Math.round(frame.width));
135
+ this.rows = Math.max(1, Math.round(frame.height));
136
+ const resized = this.resizeCanvas();
137
+ this.draw(previousFrame && !resized ? frame.damage : void 0);
138
+ this.syncAccessibilityTree();
139
+ }
140
+ applyStyle(style) {
141
+ applyWebHostTerminalStyle(this.element, style);
142
+ this.element.style.padding = "0.75rem";
143
+ this.element.style.borderRadius = "16px";
144
+ this.element.style.boxShadow = "0 20px 50px rgba(0, 0, 0, 0.28)";
145
+ this.element.style.overflow = "hidden";
146
+ this.element.style.gap = "0.5rem";
147
+ this.element.style.gridTemplateRows = "auto 1fr";
148
+ this.terminalMount.style.position = "relative";
149
+ this.terminalMount.style.overflow = "hidden";
150
+ this.terminalMount.style.overscrollBehavior = "contain";
151
+ this.terminalMount.style.outline = "none";
152
+ this.terminalMount.style.background = webTUITerminalBackgroundColor(this.currentStyle);
153
+ this.terminalMount.style.minHeight = `${this.cellHeight * 8}px`;
154
+ if (this.canvas) {
155
+ this.canvas.style.display = "block";
156
+ this.canvas.style.width = "100%";
157
+ this.canvas.style.height = "100%";
158
+ }
159
+ }
160
+ applyVisibility() {
161
+ this.element.hidden = !this.isVisible;
162
+ this.element.style.setProperty("display", this.isVisible ? "grid" : "none", "important");
163
+ }
164
+ installResizeObserver() {
165
+ if (typeof ResizeObserver === "undefined") return;
166
+ this.resizeObserver = new ResizeObserver(() => {
167
+ this.resizeToMount();
168
+ });
169
+ this.resizeObserver.observe(this.terminalMount);
170
+ }
171
+ installInputHandlers() {
172
+ const handleKeyDown = (event) => {
173
+ if (event.metaKey || event.isComposing) return;
174
+ const key = keyInputFromKeyboardEvent(event);
175
+ if (!key) return;
176
+ this.onInput(encodeKeyInputMessage({
177
+ ...key,
178
+ modifiers: modifierMask(event)
179
+ }));
180
+ event.preventDefault();
181
+ };
182
+ const handlePaste = (event) => {
183
+ const text = event.clipboardData?.getData("text/plain") ?? "";
184
+ if (!text) return;
185
+ this.onInput(encodePasteInputMessage(text));
186
+ event.preventDefault();
187
+ };
188
+ const handlePointerDown = (event) => {
189
+ const location = this.cellLocation(event);
190
+ if (!location) return;
191
+ const button = pointerButton(event.button);
192
+ this.activePointerButton = button;
193
+ this.hasCapturedPointer = true;
194
+ this.terminalMount.focus?.({ preventScroll: true });
195
+ this.terminalMount.setPointerCapture?.(event.pointerId);
196
+ this.onInput(encodeMouseInputMessage({
197
+ kind: "down",
198
+ x: location.x,
199
+ y: location.y,
200
+ button,
201
+ modifiers: modifierMask(event)
202
+ }));
203
+ event.preventDefault();
204
+ };
205
+ const handlePointerUp = (event) => {
206
+ const location = this.hasCapturedPointer ? this.rawCellLocation(event) : this.cellLocation(event);
207
+ this.terminalMount.releasePointerCapture?.(event.pointerId);
208
+ this.hasCapturedPointer = false;
209
+ if (!location) return;
210
+ this.onInput(encodeMouseInputMessage({
211
+ kind: "up",
212
+ x: location.x,
213
+ y: location.y,
214
+ button: pointerButton(event.button) ?? this.activePointerButton,
215
+ modifiers: modifierMask(event)
216
+ }));
217
+ event.preventDefault();
218
+ };
219
+ const handlePointerMove = (event) => {
220
+ const location = event.buttons && this.hasCapturedPointer ? this.rawCellLocation(event) : this.cellLocation(event);
221
+ if (!location) return;
222
+ this.onInput(encodeMouseInputMessage({
223
+ kind: event.buttons ? "dragged" : "moved",
224
+ x: location.x,
225
+ y: location.y,
226
+ button: this.activePointerButton,
227
+ modifiers: modifierMask(event)
228
+ }));
229
+ };
230
+ const handleWheel = (event) => {
231
+ if (this.wheelMode === "passive") return;
232
+ const location = this.cellLocation(event);
233
+ if (!location) return;
234
+ if (this.wheelMode === "chain" && !this.wheelTargetCanScroll(location, event.deltaX, event.deltaY)) return;
235
+ this.onInput(encodeMouseInputMessage({
236
+ kind: "scrolled",
237
+ x: location.x,
238
+ y: location.y,
239
+ deltaX: normalizedWheelDelta(event.deltaX),
240
+ deltaY: normalizedWheelDelta(event.deltaY),
241
+ modifiers: modifierMask(event)
242
+ }));
243
+ event.preventDefault();
244
+ };
245
+ this.terminalMount.addEventListener("keydown", handleKeyDown);
246
+ this.terminalMount.addEventListener("paste", handlePaste);
247
+ this.terminalMount.addEventListener("pointerdown", handlePointerDown);
248
+ this.terminalMount.addEventListener("pointerup", handlePointerUp);
249
+ this.terminalMount.addEventListener("pointermove", handlePointerMove);
250
+ this.terminalMount.addEventListener("wheel", handleWheel, { passive: false });
251
+ this.detachInputHandlers = () => {
252
+ this.terminalMount.removeEventListener("keydown", handleKeyDown);
253
+ this.terminalMount.removeEventListener("paste", handlePaste);
254
+ this.terminalMount.removeEventListener("pointerdown", handlePointerDown);
255
+ this.terminalMount.removeEventListener("pointerup", handlePointerUp);
256
+ this.terminalMount.removeEventListener("pointermove", handlePointerMove);
257
+ this.terminalMount.removeEventListener("wheel", handleWheel);
258
+ };
259
+ }
260
+ resizeToMount() {
261
+ this.measureCells();
262
+ const rect = this.terminalMount.getBoundingClientRect?.();
263
+ const width = rect?.width && rect.width > 0 ? rect.width : this.columns * this.cellWidth;
264
+ const height = rect?.height && rect.height > 0 ? rect.height : this.rows * this.cellHeight;
265
+ const nextColumns = Math.max(1, Math.floor(width / this.cellWidth));
266
+ const nextRows = Math.max(1, Math.floor(height / this.cellHeight));
267
+ this.columns = nextColumns;
268
+ this.rows = nextRows;
269
+ this.sendResizeIfNeeded();
270
+ this.resizeCanvas();
271
+ }
272
+ sendResizeIfNeeded() {
273
+ const current = {
274
+ columns: this.columns,
275
+ rows: this.rows,
276
+ cellWidth: this.cellWidth,
277
+ cellHeight: this.cellHeight
278
+ };
279
+ if (this.lastSentResize && this.lastSentResize.columns === current.columns && this.lastSentResize.rows === current.rows && this.lastSentResize.cellWidth === current.cellWidth && this.lastSentResize.cellHeight === current.cellHeight) return;
280
+ this.lastSentResize = current;
281
+ this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);
282
+ }
283
+ resizeCanvas() {
284
+ if (!this.canvas) return false;
285
+ const cssWidth = Math.max(1, this.columns * this.cellWidth);
286
+ const cssHeight = Math.max(1, this.rows * this.cellHeight);
287
+ const scale = globalThis.window?.devicePixelRatio || 1;
288
+ const width = Math.ceil(cssWidth * scale);
289
+ const height = Math.ceil(cssHeight * scale);
290
+ const styleWidth = `${cssWidth}px`;
291
+ const styleHeight = `${cssHeight}px`;
292
+ if (this.canvas.width === width && this.canvas.height === height && this.canvas.style.width === styleWidth && this.canvas.style.height === styleHeight) return false;
293
+ this.canvas.width = width;
294
+ this.canvas.height = height;
295
+ this.canvas.style.width = styleWidth;
296
+ this.canvas.style.height = styleHeight;
297
+ return true;
298
+ }
299
+ measureCells() {
300
+ const context = (this.canvas ?? document.createElement("canvas")).getContext?.("2d");
301
+ if (!context) {
302
+ this.cellWidth = Math.max(1, Math.round(this.currentStyle.fontSize * .62));
303
+ this.cellHeight = Math.max(1, Math.round(this.currentStyle.fontSize * 1.35));
304
+ return;
305
+ }
306
+ context.font = this.fontForStyle();
307
+ this.cellWidth = Math.max(1, Math.ceil(context.measureText("W").width));
308
+ this.cellHeight = Math.max(1, Math.ceil(this.currentStyle.fontSize * 1.35));
309
+ }
310
+ draw(damage) {
311
+ const canvas = this.canvas;
312
+ const context = canvas?.getContext("2d");
313
+ if (!canvas || !context) return;
314
+ const frame = this.currentFrame;
315
+ const dirtyRegion = frame ? this.dirtyRegionForDamage(damage, frame) : void 0;
316
+ if (dirtyRegion?.rects.length === 0) return;
317
+ const scale = globalThis.window?.devicePixelRatio || 1;
318
+ context.setTransform(scale, 0, 0, scale, 0, 0);
319
+ context.textBaseline = "alphabetic";
320
+ context.fillStyle = webTUITerminalBackgroundColor(this.currentStyle);
321
+ if (dirtyRegion) for (const rect of dirtyRegion.rects) {
322
+ context.clearRect(rect.x, rect.y, rect.width, rect.height);
323
+ context.fillRect(rect.x, rect.y, rect.width, rect.height);
324
+ }
325
+ else {
326
+ context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);
327
+ context.fillRect(0, 0, this.columns * this.cellWidth, this.rows * this.cellHeight);
328
+ }
329
+ if (!frame) return;
330
+ this.drawRows(context, frame, dirtyRegion);
331
+ this.drawImages(context, frame.images ?? [], dirtyRegion);
332
+ }
333
+ drawRows(context, frame, dirtyRegion) {
334
+ if (dirtyRegion) {
335
+ for (const [y, ranges] of dirtyRegion.rows) {
336
+ const row = frame.rows[y] ?? [];
337
+ this.drawRow(context, frame, row, y, ranges);
338
+ }
339
+ return;
340
+ }
341
+ for (let y = 0; y < frame.rows.length; y += 1) {
342
+ const row = frame.rows[y] ?? [];
343
+ this.drawRow(context, frame, row, y);
344
+ }
345
+ }
346
+ drawRow(context, frame, row, y, ranges) {
347
+ for (const cell of row) {
348
+ const [x, text, span, styleIndex] = cell;
349
+ if (ranges !== void 0 && !cellIntersectsRanges(x, span, ranges)) continue;
350
+ const style = frame.styles[styleIndex] ?? void 0;
351
+ this.drawCell(context, x, y, text, span, style);
352
+ }
353
+ }
354
+ syncAccessibilityTree() {
355
+ const tree = this.accessibilityTree;
356
+ if (!tree || !this.currentFrame) return;
357
+ tree.present(this.currentFrame.accessibilityTree ?? [], {
358
+ cellWidth: this.cellWidth,
359
+ cellHeight: this.cellHeight
360
+ }, this.currentFrame.accessibilityAnnouncements ?? [], { synchronizeFocus: this.synchronizeAccessibilityFocus });
361
+ }
362
+ drawImages(context, images, dirtyRegion) {
363
+ for (const image of images) this.drawImage(context, image, dirtyRegion);
364
+ }
365
+ drawImage(context, image, dirtyRegion) {
366
+ const decodedImage = this.cachedImage(image);
367
+ if (!decodedImage) return;
368
+ const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
369
+ const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
370
+ if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) return;
371
+ if (dirtyRegion && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)) return;
372
+ context.save();
373
+ context.beginPath();
374
+ context.rect(clipX * this.cellWidth, clipY * this.cellHeight, clipWidth * this.cellWidth, clipHeight * this.cellHeight);
375
+ context.clip();
376
+ context.drawImage(decodedImage, boundsX * this.cellWidth, boundsY * this.cellHeight, boundsWidth * this.cellWidth, boundsHeight * this.cellHeight);
377
+ context.restore();
378
+ }
379
+ cachedImage(image) {
380
+ const cached = this.imageCache.get(image.id);
381
+ if (cached?.image) return cached.image;
382
+ if (!cached?.promise && image.dataBase64) {
383
+ const promise = decodeImage(image.dataBase64, image.format);
384
+ this.imageCache.set(image.id, { promise });
385
+ promise.then((decodedImage) => {
386
+ if (this.imageCache.get(image.id)?.promise !== promise) return;
387
+ this.imageCache.set(image.id, { image: decodedImage });
388
+ this.draw();
389
+ }).catch(() => {
390
+ this.imageCache.delete(image.id);
391
+ });
392
+ }
393
+ }
394
+ drawCell(context, x, y, text, span, style) {
395
+ const rectX = x * this.cellWidth;
396
+ const rectY = y * this.cellHeight;
397
+ const width = Math.max(1, span) * this.cellWidth;
398
+ const background = resolvedBackground(style, this.currentStyle);
399
+ const foreground = resolvedForeground(style, this.currentStyle);
400
+ const opacity = style?.opacity ?? 1;
401
+ if (background) {
402
+ context.globalAlpha = opacity;
403
+ context.fillStyle = background;
404
+ context.fillRect(rectX, rectY, width, this.cellHeight);
405
+ }
406
+ if (text !== " ") {
407
+ context.globalAlpha = opacity;
408
+ context.fillStyle = foreground;
409
+ context.strokeStyle = foreground;
410
+ if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {
411
+ x: rectX,
412
+ y: rectY,
413
+ width,
414
+ height: this.cellHeight
415
+ })) {
416
+ context.font = this.fontForStyle(style);
417
+ context.fillText(text, rectX, rectY + Math.floor((this.cellHeight + this.currentStyle.fontSize) / 2) - 2);
418
+ }
419
+ }
420
+ this.drawTextLine(context, rectX, rectY, width, style?.underline, "underline", foreground);
421
+ this.drawTextLine(context, rectX, rectY, width, style?.strikethrough, "strike", foreground);
422
+ context.globalAlpha = 1;
423
+ }
424
+ dirtyRegionForDamage(damage, frame) {
425
+ if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) return;
426
+ const rects = [];
427
+ const rows = /* @__PURE__ */ new Map();
428
+ for (const [row, ranges] of damage.textRows) {
429
+ if (row < 0 || row >= frame.height) continue;
430
+ if (ranges.length === 0) {
431
+ rects.push(this.cellRect(0, row, frame.width));
432
+ rows.set(row, "full");
433
+ continue;
434
+ }
435
+ const rowRanges = rows.get(row) === "full" ? [] : [...rows.get(row) ?? []];
436
+ for (const [start, end] of ranges) {
437
+ const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));
438
+ const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));
439
+ if (lowerBound >= upperBound) continue;
440
+ rects.push(this.cellRect(lowerBound, row, upperBound - lowerBound));
441
+ rowRanges.push({
442
+ start: lowerBound,
443
+ end: upperBound
444
+ });
445
+ }
446
+ if (rows.get(row) !== "full" && rowRanges.length > 0) rows.set(row, normalizeCellRanges(rowRanges));
447
+ }
448
+ return {
449
+ rects,
450
+ rows
451
+ };
452
+ }
453
+ cellRect(x, y, span) {
454
+ return {
455
+ x: x * this.cellWidth,
456
+ y: y * this.cellHeight,
457
+ width: Math.max(1, span) * this.cellWidth,
458
+ height: this.cellHeight
459
+ };
460
+ }
461
+ drawTextLine(context, x, y, width, line, placement, fallbackColor) {
462
+ if (!line) return;
463
+ context.strokeStyle = line.color ?? fallbackColor;
464
+ context.lineWidth = line.pattern === "double" ? 2 : 1;
465
+ if (line.pattern === "dot") context.setLineDash([1, 3]);
466
+ else if (line.pattern === "dash") context.setLineDash([4, 3]);
467
+ else context.setLineDash([]);
468
+ const lineY = placement === "underline" ? y + this.cellHeight - 2 : y + Math.floor(this.cellHeight / 2);
469
+ context.beginPath();
470
+ context.moveTo(x, lineY);
471
+ context.lineTo(x + width, lineY);
472
+ context.stroke();
473
+ context.setLineDash([]);
474
+ }
475
+ fontForStyle(style) {
476
+ const emphasis = style?.em ?? 0;
477
+ return `${(emphasis & 2) !== 0 ? "italic " : ""}${(emphasis & 1) !== 0 ? "700 " : ""}${this.currentStyle.fontSize}px ${this.currentStyle.fontFamily}`;
478
+ }
479
+ /**
480
+ * Whether any scrollable region under `location` can still scroll in the
481
+ * wheel's direction. Mirrors the Swift host's scroll hit-test: a region
482
+ * qualifies when its viewport contains the cell AND it has remaining headroom
483
+ * in the delta's direction. Used by "chain" wheel mode to decide capture vs.
484
+ * fall-through. With no published `scrollRegions`, nothing can scroll, so the
485
+ * wheel chains to the page (a scene with no ScrollView stays fully passive).
486
+ */
487
+ wheelTargetCanScroll(location, deltaX, deltaY) {
488
+ const regions = this.currentFrame?.scrollRegions;
489
+ if (!regions || regions.length === 0) return false;
490
+ const cellX = Math.floor(location.x);
491
+ const cellY = Math.floor(location.y);
492
+ for (const region of regions) {
493
+ const [rx, ry, rw, rh] = region.rect;
494
+ if (cellX < rx || cellY < ry || cellX >= rx + rw || cellY >= ry + rh) continue;
495
+ if (regionCanScrollInDirection(region, deltaX, deltaY)) return true;
496
+ }
497
+ return false;
498
+ }
499
+ cellLocation(event) {
500
+ const location = this.rawCellLocation(event);
501
+ if (!location) return;
502
+ const cellX = Math.floor(location.x);
503
+ const cellY = Math.floor(location.y);
504
+ if (cellX < 0 || cellY < 0 || cellX >= this.columns || cellY >= this.rows) return;
505
+ return location;
506
+ }
507
+ rawCellLocation(event) {
508
+ const rect = this.canvas?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.();
509
+ if (!rect) return;
510
+ return {
511
+ x: (event.clientX - rect.left) / this.cellWidth,
512
+ y: (event.clientY - rect.top) / this.cellHeight
513
+ };
514
+ }
515
+ };
516
+ async function decodeImage(dataBase64, format) {
517
+ const bytes = decodeBase64Bytes(dataBase64);
518
+ const blob = new Blob([bytes], { type: `image/${format}` });
519
+ if (typeof createImageBitmap === "function") return createImageBitmap(blob);
520
+ return new Promise((resolve, reject) => {
521
+ const image = new Image();
522
+ const url = URL.createObjectURL(blob);
523
+ image.onload = () => {
524
+ URL.revokeObjectURL(url);
525
+ resolve(image);
526
+ };
527
+ image.onerror = () => {
528
+ URL.revokeObjectURL(url);
529
+ reject(/* @__PURE__ */ new Error(`Failed to decode ${format} image`));
530
+ };
531
+ image.src = url;
532
+ });
533
+ }
534
+ function decodeBase64Bytes(value) {
535
+ if (typeof atob === "function") {
536
+ const binary = atob(value);
537
+ const bytes = new Uint8Array(binary.length);
538
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
539
+ return bytes;
540
+ }
541
+ return new Uint8Array(Buffer.from(value, "base64"));
542
+ }
543
+ function keyInputFromKeyboardEvent(event) {
544
+ switch (event.key) {
545
+ case "Enter": return { key: "return" };
546
+ case " ": return { key: "space" };
547
+ case "Tab": return { key: "tab" };
548
+ case "ArrowLeft": return { key: "arrowLeft" };
549
+ case "ArrowRight": return { key: "arrowRight" };
550
+ case "ArrowUp": return { key: "arrowUp" };
551
+ case "ArrowDown": return { key: "arrowDown" };
552
+ case "Backspace": return { key: "backspace" };
553
+ case "Escape": return { key: "escape" };
554
+ case "Home": return { key: "home" };
555
+ case "End": return { key: "end" };
556
+ default: {
557
+ const characters = Array.from(event.key);
558
+ if (characters.length !== 1) return;
559
+ return {
560
+ key: "character",
561
+ character: characters[0]
562
+ };
563
+ }
564
+ }
565
+ }
566
+ function pointerButton(button) {
567
+ switch (button) {
568
+ case 1: return "middle";
569
+ case 2: return "secondary";
570
+ default: return "primary";
571
+ }
572
+ }
573
+ function modifierMask(event) {
574
+ let mask = 0;
575
+ if (event.shiftKey) mask |= 1;
576
+ if (event.altKey) mask |= 2;
577
+ if (event.ctrlKey) mask |= 4;
578
+ return mask;
579
+ }
580
+ function normalizedWheelDelta(delta) {
581
+ if (delta > 0) return 1;
582
+ if (delta < 0) return -1;
583
+ return 0;
584
+ }
585
+ /**
586
+ * Whether a published scroll region has remaining headroom in the wheel's
587
+ * direction, recomputing the per-direction extent from offset/content/viewport.
588
+ * Mirrors SwiftTUI's clamp (`min(max(0, offset), max(0, content - viewport))`)
589
+ * so the host and the app agree on "at edge". Wheel sign convention matches the
590
+ * app: `deltaY > 0` scrolls down (offset grows toward the content bottom).
591
+ * Diagonal wheels qualify if either axis has headroom.
592
+ */
593
+ function regionCanScrollInDirection(region, deltaX, deltaY) {
594
+ const [, , viewportWidth, viewportHeight] = region.rect;
595
+ const [offsetX, offsetY] = region.offset;
596
+ const [contentWidth, contentHeight] = region.content;
597
+ const maxX = Math.max(0, contentWidth - viewportWidth);
598
+ const maxY = Math.max(0, contentHeight - viewportHeight);
599
+ const clampedX = Math.min(Math.max(0, offsetX), maxX);
600
+ const clampedY = Math.min(Math.max(0, offsetY), maxY);
601
+ if (deltaY > 0 && clampedY < maxY) return true;
602
+ if (deltaY < 0 && clampedY > 0) return true;
603
+ if (deltaX > 0 && clampedX < maxX) return true;
604
+ if (deltaX < 0 && clampedX > 0) return true;
605
+ return false;
606
+ }
607
+ function normalizeCellRanges(ranges) {
608
+ const sorted = ranges.filter((range) => range.end > range.start).sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);
609
+ const normalized = [];
610
+ for (const range of sorted) {
611
+ const previous = normalized[normalized.length - 1];
612
+ if (previous && range.start <= previous.end) {
613
+ previous.end = Math.max(previous.end, range.end);
614
+ continue;
615
+ }
616
+ normalized.push({ ...range });
617
+ }
618
+ return normalized;
619
+ }
620
+ function cellIntersectsRanges(x, span, ranges) {
621
+ if (ranges === "full") return true;
622
+ const start = Math.floor(x);
623
+ const end = start + Math.max(1, Math.ceil(span));
624
+ return ranges.some((range) => start < range.end && end > range.start);
625
+ }
626
+ function dirtyRegionIntersectsCellRect(region, x, y, width, height) {
627
+ const startRow = Math.max(0, Math.floor(y));
628
+ const endRow = Math.max(startRow, Math.ceil(y + height));
629
+ const rectRange = {
630
+ start: Math.floor(x),
631
+ end: Math.floor(x) + Math.max(1, Math.ceil(width))
632
+ };
633
+ for (let row = startRow; row < endRow; row += 1) {
634
+ const ranges = region.rows.get(row);
635
+ if (!ranges) continue;
636
+ if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) return true;
637
+ }
638
+ return false;
639
+ }
640
+ function resolvedForeground(style, terminalStyle) {
641
+ if ((style?.em ?? 0) & 16) return style?.bg ?? terminalStyle.theme.background;
642
+ return style?.fg ?? terminalStyle.theme.foreground;
643
+ }
644
+ function resolvedBackground(style, terminalStyle) {
645
+ if ((style?.em ?? 0) & 16) return style?.fg ?? terminalStyle.theme.foreground;
646
+ return style?.bg;
647
+ }
648
+ //#endregion
649
+ export { WebHostSceneRuntime };
650
+
651
+ //# sourceMappingURL=WebHostSceneRuntime.js.map