@swifttui/web 0.1.13 → 0.1.14
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/README.md +31 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/src/CanvasSurfacePainter.js +3 -10
- package/dist/src/CanvasSurfacePainter.js.map +1 -1
- package/dist/src/DomSurfacePainter.d.ts +51 -0
- package/dist/src/DomSurfacePainter.js +267 -0
- package/dist/src/DomSurfacePainter.js.map +1 -0
- package/dist/src/SurfaceRenderer.d.ts +50 -0
- package/dist/src/SurfaceRenderer.js +21 -0
- package/dist/src/SurfaceRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +8 -0
- package/dist/src/WebHostApp.js +6 -2
- package/dist/src/WebHostApp.js.map +1 -1
- package/dist/src/WebHostSceneRuntime.d.ts +22 -1
- package/dist/src/WebHostSceneRuntime.js +59 -14
- package/dist/src/WebHostSceneRuntime.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,12 +84,41 @@ The page that hosts the WASI runtime must serve
|
|
|
84
84
|
`Cross-Origin-Embedder-Policy: require-corp` so the `SharedArrayBuffer`-backed
|
|
85
85
|
stdin works.
|
|
86
86
|
|
|
87
|
+
## Renderers
|
|
88
|
+
|
|
89
|
+
Two surface presenters ship behind one option; both consume the same frames:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
await createWebHostApp({
|
|
93
|
+
mount,
|
|
94
|
+
manifestUrl,
|
|
95
|
+
renderer: "dom", // "canvas" is the default
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- **`"canvas"`** (default) paints cells onto a 2D `<canvas>`: one DOM node,
|
|
100
|
+
pixel-exact box-drawing seams and decoration patterns.
|
|
101
|
+
- **`"dom"`** renders cells as absolutely positioned text elements: the
|
|
102
|
+
browser's own font shaping and fallback (emoji, CJK), crisp text at any
|
|
103
|
+
page zoom, an inspectable element tree, and — uniquely — native text
|
|
104
|
+
selection: **hold Alt/Option and drag** to select and copy the app's text
|
|
105
|
+
(plain drags remain pointer input for the app). Box-drawing characters
|
|
106
|
+
render as font glyphs, and underline/strikethrough patterns map onto CSS
|
|
107
|
+
`text-decoration`, so hairline details can differ slightly from the canvas
|
|
108
|
+
painter. Grid alignment is kept exact by stretching each glyph advance to
|
|
109
|
+
the cell width via `letter-spacing`.
|
|
110
|
+
|
|
111
|
+
The option is also available per scene runtime (`WebHostSceneRuntimeOptions.renderer`)
|
|
112
|
+
and both painters are exported (`CanvasSurfacePainter`, `DomSurfacePainter`)
|
|
113
|
+
for hosts that compose their own runtimes.
|
|
114
|
+
|
|
87
115
|
## Surface transport
|
|
88
116
|
|
|
89
117
|
This package uses SwiftTUI's `web-surface` WASI transport. The Swift runner
|
|
90
118
|
emits structured raster-surface records on stdout, and the browser host draws
|
|
91
|
-
|
|
92
|
-
not depend on `ghostty-web` or
|
|
119
|
+
them with the configured renderer — canvas rects/text or DOM elements. It does
|
|
120
|
+
not load a terminal emulator and does not depend on `ghostty-web` or
|
|
121
|
+
`ghostty-vt.wasm`.
|
|
93
122
|
|
|
94
123
|
`web-surface` is the default `SwiftTUIWASI` browser transport. WebHost still
|
|
95
124
|
sets `TUIGUI_TRANSPORT=surface` explicitly so generated app environments are
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, WebHostANSIColors, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
|
|
2
2
|
import { SUPPORTED_SURFACE_VERSION, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
|
|
3
|
+
import { SurfaceMetrics, WebHostSurfacePainter, WebHostSurfaceRendererKind, resolvedSurfaceBackground, resolvedSurfaceForeground } from "./src/SurfaceRenderer.js";
|
|
3
4
|
import { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
4
5
|
import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WheelMode } from "./src/WebHostSceneRuntime.js";
|
|
5
6
|
import { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
|
|
6
7
|
import { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostVisibilityDocument, createWebHostApp } from "./src/WebHostApp.js";
|
|
8
|
+
import { DomSurfacePainter } from "./src/DomSurfacePainter.js";
|
|
7
9
|
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
8
10
|
import { JSPIConstructors, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
9
11
|
import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
|
|
10
|
-
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, JSPIConstructors, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, SUPPORTED_SURFACE_VERSION, StdIOPipe, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebHostVisibilityDocument, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
12
|
+
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, DomSurfacePainter, JSPIConstructors, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, SUPPORTED_SURFACE_VERSION, StdIOPipe, SurfaceMetrics, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfacePainter, WebHostSurfaceRect, WebHostSurfaceRendererKind, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebHostVisibilityDocument, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { SUPPORTED_SURFACE_VERSION, WebHostOutputDecoder, encodeKeyInputMessage,
|
|
|
5
5
|
import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
|
|
6
6
|
import { WebSocketSceneBridge, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
|
|
7
7
|
import { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
8
|
+
import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./src/SurfaceRenderer.js";
|
|
9
|
+
import { DomSurfacePainter } from "./src/DomSurfacePainter.js";
|
|
8
10
|
import { WebHostSceneRuntime } from "./src/WebHostSceneRuntime.js";
|
|
9
11
|
import { createWebHostApp } from "./src/WebHostApp.js";
|
|
10
|
-
export { BrowserWASIBridge, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
12
|
+
export { BrowserWASIBridge, DomSurfacePainter, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
|
|
2
2
|
import { canRenderBoxDrawing, drawBoxDrawing } from "./BoxDrawingRenderer.js";
|
|
3
|
+
import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./SurfaceRenderer.js";
|
|
3
4
|
//#region src/CanvasSurfacePainter.ts
|
|
4
5
|
/**
|
|
5
6
|
* Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell
|
|
@@ -102,8 +103,8 @@ var CanvasSurfacePainter = class {
|
|
|
102
103
|
const rectX = x * metrics.cellWidth;
|
|
103
104
|
const rectY = y * metrics.cellHeight;
|
|
104
105
|
const width = Math.max(1, span) * metrics.cellWidth;
|
|
105
|
-
const background =
|
|
106
|
-
const foreground =
|
|
106
|
+
const background = resolvedSurfaceBackground(style, metrics.style);
|
|
107
|
+
const foreground = resolvedSurfaceForeground(style, metrics.style);
|
|
107
108
|
const opacity = style?.opacity ?? 1;
|
|
108
109
|
if (background) {
|
|
109
110
|
context.globalAlpha = opacity;
|
|
@@ -249,14 +250,6 @@ function dirtyRegionIntersectsCellRect(region, x, y, width, height) {
|
|
|
249
250
|
}
|
|
250
251
|
return false;
|
|
251
252
|
}
|
|
252
|
-
function resolvedForeground(style, terminalStyle) {
|
|
253
|
-
if ((style?.em ?? 0) & 16) return style?.bg ?? terminalStyle.theme.background;
|
|
254
|
-
return style?.fg ?? terminalStyle.theme.foreground;
|
|
255
|
-
}
|
|
256
|
-
function resolvedBackground(style, terminalStyle) {
|
|
257
|
-
if ((style?.em ?? 0) & 16) return style?.fg ?? terminalStyle.theme.foreground;
|
|
258
|
-
return style?.bg;
|
|
259
|
-
}
|
|
260
253
|
//#endregion
|
|
261
254
|
export { CanvasSurfacePainter, fontForStyle };
|
|
262
255
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CanvasSurfacePainter.js","names":[],"sources":["../../src/CanvasSurfacePainter.ts"],"sourcesContent":["import {\n canRenderBoxDrawing,\n drawBoxDrawing,\n} from \"./BoxDrawingRenderer.ts\";\nimport {\n type ResolvedWebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\n WebHostSurfaceImageFormat,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/**\n * A read-only snapshot of the cell grid geometry and active style the painter\n * needs for a single paint pass. The runtime owns this state and mutates it as\n * the surface resizes or restyles; passing a fresh snapshot per `paint` keeps\n * the painter stateless about geometry and avoids stale reads.\n */\nexport interface CanvasSurfaceMetrics {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n style: ResolvedWebHostTerminalStyle;\n}\n\ninterface CachedWebHostImage {\n image?: CanvasImageSource;\n promise?: Promise<CanvasImageSource>;\n}\n\ninterface DirtyRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface DirtyCellRange {\n start: number;\n end: number;\n}\n\ntype DirtyRowRanges = \"full\" | DirtyCellRange[];\n\ninterface DirtyRegion {\n rects: DirtyRect[];\n rows: Map<number, DirtyRowRanges>;\n}\n\n/**\n * Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell\n * text/box-drawing/decorations, surface images, and damage-scoped dirty-region\n * painting. The painter caches decoded images and asks the host to repaint once\n * an image finishes decoding (via the `requestRedraw` callback).\n *\n * Geometry and style are supplied per paint via {@link CanvasSurfaceMetrics};\n * the painter holds only the canvas handle, the image cache, and the redraw\n * callback as durable state.\n */\nexport class CanvasSurfacePainter {\n private readonly imageCache = new Map<string, CachedWebHostImage>();\n private canvas?: HTMLCanvasElement;\n private requestRedraw: () => void = () => {};\n\n /**\n * Binds the canvas the painter draws into and the callback used to request a\n * full repaint after an asynchronous image decode completes.\n */\n attach(\n canvas: HTMLCanvasElement,\n requestRedraw: () => void\n ): void {\n this.canvas = canvas;\n this.requestRedraw = requestRedraw;\n }\n\n paint(\n metrics: CanvasSurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void {\n const canvas = this.canvas;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) {\n return;\n }\n\n const dirtyRegion = frame\n ? this.dirtyRegionForDamage(damage, frame, metrics)\n : undefined;\n if (dirtyRegion?.rects.length === 0) {\n return;\n }\n\n const scale = globalThis.window?.devicePixelRatio || 1;\n context.setTransform(scale, 0, 0, scale, 0, 0);\n context.textBaseline = \"alphabetic\";\n\n context.fillStyle = webTUITerminalBackgroundColor(metrics.style);\n if (dirtyRegion) {\n for (const rect of dirtyRegion.rects) {\n context.clearRect(rect.x, rect.y, rect.width, rect.height);\n context.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n } else {\n context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);\n context.fillRect(0, 0, metrics.columns * metrics.cellWidth, metrics.rows * metrics.cellHeight);\n }\n\n if (!frame) {\n return;\n }\n\n this.drawRows(context, frame, metrics, dirtyRegion);\n this.drawImages(context, frame.images ?? [], metrics, dirtyRegion);\n }\n\n private drawRows(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n if (dirtyRegion) {\n for (const [y, ranges] of dirtyRegion.rows) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y, ranges);\n }\n return;\n }\n\n for (let y = 0; y < frame.rows.length; y += 1) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y);\n }\n }\n\n private drawRow(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n row: WebHostSurfaceFrame[\"rows\"][number],\n y: number,\n ranges?: DirtyRowRanges\n ): void {\n for (const cell of row) {\n const [x, text, span, styleIndex] = cell;\n if (ranges !== undefined && !cellIntersectsRanges(x, span, ranges)) {\n continue;\n }\n const style = frame.styles[styleIndex] ?? undefined;\n this.drawCell(context, metrics, x, y, text, span, style);\n }\n }\n\n private drawImages(\n context: CanvasRenderingContext2D,\n images: WebHostSurfaceImage[],\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n for (const image of images) {\n this.drawImage(context, image, metrics, dirtyRegion);\n }\n }\n\n private drawImage(\n context: CanvasRenderingContext2D,\n image: WebHostSurfaceImage,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n const decodedImage = this.cachedImage(image);\n if (!decodedImage) {\n return;\n }\n\n const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;\n const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;\n if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) {\n return;\n }\n if (\n dirtyRegion\n && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)\n ) {\n return;\n }\n\n context.save();\n context.beginPath();\n context.rect(\n clipX * metrics.cellWidth,\n clipY * metrics.cellHeight,\n clipWidth * metrics.cellWidth,\n clipHeight * metrics.cellHeight\n );\n context.clip();\n context.drawImage(\n decodedImage,\n boundsX * metrics.cellWidth,\n boundsY * metrics.cellHeight,\n boundsWidth * metrics.cellWidth,\n boundsHeight * metrics.cellHeight\n );\n context.restore();\n }\n\n private cachedImage(\n image: WebHostSurfaceImage\n ): CanvasImageSource | undefined {\n const cached = this.imageCache.get(image.id);\n if (cached?.image) {\n return cached.image;\n }\n\n if (!cached?.promise && image.dataBase64) {\n const promise = decodeImage(image.dataBase64, image.format);\n this.imageCache.set(image.id, { promise });\n void promise.then((decodedImage) => {\n const latest = this.imageCache.get(image.id);\n if (latest?.promise !== promise) {\n return;\n }\n this.imageCache.set(image.id, { image: decodedImage });\n this.requestRedraw();\n }).catch(() => {\n this.imageCache.delete(image.id);\n });\n }\n\n return undefined;\n }\n\n private drawCell(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n text: string,\n span: number,\n style?: WebHostSurfaceStyle | null\n ): void {\n const rectX = x * metrics.cellWidth;\n const rectY = y * metrics.cellHeight;\n const width = Math.max(1, span) * metrics.cellWidth;\n const background = resolvedBackground(style, metrics.style);\n const foreground = resolvedForeground(style, metrics.style);\n const opacity = style?.opacity ?? 1;\n\n if (background) {\n context.globalAlpha = opacity;\n context.fillStyle = background;\n context.fillRect(rectX, rectY, width, metrics.cellHeight);\n }\n\n if (text !== \" \") {\n context.globalAlpha = opacity;\n context.fillStyle = foreground;\n context.strokeStyle = foreground;\n if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {\n x: rectX,\n y: rectY,\n width,\n height: metrics.cellHeight,\n })) {\n context.font = fontForStyle(metrics.style, style);\n context.fillText(\n text,\n rectX,\n rectY + Math.floor((metrics.cellHeight + metrics.style.fontSize) / 2) - 2\n );\n }\n }\n\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.underline, \"underline\", foreground);\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.strikethrough, \"strike\", foreground);\n context.globalAlpha = 1;\n }\n\n private dirtyRegionForDamage(\n damage: WebHostSurfaceDamage | undefined,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics\n ): DirtyRegion | undefined {\n if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) {\n return undefined;\n }\n\n const rects: DirtyRect[] = [];\n const rows = new Map<number, DirtyRowRanges>();\n for (const [row, ranges] of damage.textRows) {\n if (row < 0 || row >= frame.height) {\n continue;\n }\n if (ranges.length === 0) {\n rects.push(cellRect(metrics, 0, row, frame.width));\n rows.set(row, \"full\");\n continue;\n }\n const rowRanges: DirtyCellRange[] = rows.get(row) === \"full\"\n ? []\n : [...(rows.get(row) as DirtyCellRange[] | undefined ?? [])];\n for (const [start, end] of ranges) {\n const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));\n const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));\n if (lowerBound >= upperBound) {\n continue;\n }\n rects.push(cellRect(metrics, lowerBound, row, upperBound - lowerBound));\n rowRanges.push({ start: lowerBound, end: upperBound });\n }\n if (rows.get(row) !== \"full\" && rowRanges.length > 0) {\n rows.set(row, normalizeCellRanges(rowRanges));\n }\n }\n return { rects, rows };\n }\n\n private drawTextLine(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n width: number,\n line: WebHostSurfaceStyle[\"underline\"],\n placement: \"underline\" | \"strike\",\n fallbackColor: string\n ): void {\n if (!line) {\n return;\n }\n context.strokeStyle = line.color ?? fallbackColor;\n context.lineWidth = line.pattern === \"double\" ? 2 : 1;\n if (line.pattern === \"dot\") {\n context.setLineDash([1, 3]);\n } else if (line.pattern === \"dash\") {\n context.setLineDash([4, 3]);\n } else {\n context.setLineDash([]);\n }\n\n const lineY = placement === \"underline\"\n ? y + metrics.cellHeight - 2\n : y + Math.floor(metrics.cellHeight / 2);\n context.beginPath();\n context.moveTo(x, lineY);\n context.lineTo(x + width, lineY);\n context.stroke();\n context.setLineDash([]);\n }\n}\n\n/**\n * The CSS font string for a cell, folding the surface emphasis bits (bold,\n * italic) over the host terminal's font size/family. Exposed so the host can\n * reuse the exact same metric when measuring cell dimensions.\n */\nexport function fontForStyle(\n terminalStyle: ResolvedWebHostTerminalStyle,\n style?: WebHostSurfaceStyle | null\n): string {\n const emphasis = style?.em ?? 0;\n const italic = (emphasis & 2) !== 0 ? \"italic \" : \"\";\n const weight = (emphasis & 1) !== 0 ? \"700 \" : \"\";\n return `${italic}${weight}${terminalStyle.fontSize}px ${terminalStyle.fontFamily}`;\n}\n\nfunction cellRect(\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n span: number\n): DirtyRect {\n return {\n x: x * metrics.cellWidth,\n y: y * metrics.cellHeight,\n width: Math.max(1, span) * metrics.cellWidth,\n height: metrics.cellHeight,\n };\n}\n\nasync function decodeImage(\n dataBase64: string,\n format: WebHostSurfaceImageFormat\n): Promise<CanvasImageSource> {\n const bytes = decodeBase64Bytes(dataBase64);\n const blob = new Blob([bytes], { type: `image/${format}` });\n\n if (typeof createImageBitmap === \"function\") {\n // Animated GIFs collapse to their first frame in createImageBitmap\n // — that matches the Kitty path's first-frame composite. Phase 7\n // will replace this with a frame ticker.\n return createImageBitmap(blob);\n }\n\n return new Promise((resolve, reject) => {\n const image = new Image();\n const url = URL.createObjectURL(blob);\n image.onload = () => {\n URL.revokeObjectURL(url);\n resolve(image);\n };\n image.onerror = () => {\n URL.revokeObjectURL(url);\n reject(new Error(`Failed to decode ${format} image`));\n };\n image.src = url;\n });\n}\n\nfunction decodeBase64Bytes(\n value: string\n): Uint8Array {\n if (typeof atob === \"function\") {\n const binary = atob(value);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n }\n\n return new Uint8Array(Buffer.from(value, \"base64\"));\n}\n\nfunction normalizeCellRanges(\n ranges: DirtyCellRange[]\n): DirtyCellRange[] {\n const sorted = ranges\n .filter((range) => range.end > range.start)\n .sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);\n const normalized: DirtyCellRange[] = [];\n for (const range of sorted) {\n const previous = normalized[normalized.length - 1];\n if (previous && range.start <= previous.end) {\n previous.end = Math.max(previous.end, range.end);\n continue;\n }\n normalized.push({ ...range });\n }\n return normalized;\n}\n\nfunction cellIntersectsRanges(\n x: number,\n span: number,\n ranges: DirtyRowRanges\n): boolean {\n if (ranges === \"full\") {\n return true;\n }\n const start = Math.floor(x);\n const end = start + Math.max(1, Math.ceil(span));\n return ranges.some((range) => start < range.end && end > range.start);\n}\n\nfunction dirtyRegionIntersectsCellRect(\n region: DirtyRegion,\n x: number,\n y: number,\n width: number,\n height: number\n): boolean {\n const startRow = Math.max(0, Math.floor(y));\n const endRow = Math.max(startRow, Math.ceil(y + height));\n const rectRange = {\n start: Math.floor(x),\n end: Math.floor(x) + Math.max(1, Math.ceil(width)),\n };\n for (let row = startRow; row < endRow; row += 1) {\n const ranges = region.rows.get(row);\n if (!ranges) {\n continue;\n }\n if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) {\n return true;\n }\n }\n return false;\n}\n\nfunction resolvedForeground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string {\n if ((style?.em ?? 0) & 16) {\n return style?.bg ?? terminalStyle.theme.background;\n }\n return style?.fg ?? terminalStyle.theme.foreground;\n}\n\nfunction resolvedBackground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string | undefined {\n if ((style?.em ?? 0) & 16) {\n return style?.fg ?? terminalStyle.theme.foreground;\n }\n return style?.bg;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CanvasSurfacePainter.js","names":[],"sources":["../../src/CanvasSurfacePainter.ts"],"sourcesContent":["import {\n canRenderBoxDrawing,\n drawBoxDrawing,\n} from \"./BoxDrawingRenderer.ts\";\nimport {\n resolvedSurfaceBackground,\n resolvedSurfaceForeground,\n type SurfaceMetrics,\n type WebHostSurfacePainter,\n} from \"./SurfaceRenderer.ts\";\nimport {\n type ResolvedWebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\n WebHostSurfaceImageFormat,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/**\n * A read-only snapshot of the cell grid geometry and active style the painter\n * needs for a single paint pass — see {@link SurfaceMetrics}, shared with the\n * DOM painter. The alias keeps this painter's original public name stable.\n */\nexport type CanvasSurfaceMetrics = SurfaceMetrics;\n\ninterface CachedWebHostImage {\n image?: CanvasImageSource;\n promise?: Promise<CanvasImageSource>;\n}\n\ninterface DirtyRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface DirtyCellRange {\n start: number;\n end: number;\n}\n\ntype DirtyRowRanges = \"full\" | DirtyCellRange[];\n\ninterface DirtyRegion {\n rects: DirtyRect[];\n rows: Map<number, DirtyRowRanges>;\n}\n\n/**\n * Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell\n * text/box-drawing/decorations, surface images, and damage-scoped dirty-region\n * painting. The painter caches decoded images and asks the host to repaint once\n * an image finishes decoding (via the `requestRedraw` callback).\n *\n * Geometry and style are supplied per paint via {@link CanvasSurfaceMetrics};\n * the painter holds only the canvas handle, the image cache, and the redraw\n * callback as durable state.\n */\nexport class CanvasSurfacePainter implements WebHostSurfacePainter {\n private readonly imageCache = new Map<string, CachedWebHostImage>();\n private canvas?: HTMLCanvasElement;\n private requestRedraw: () => void = () => {};\n\n /**\n * Binds the canvas the painter draws into and the callback used to request a\n * full repaint after an asynchronous image decode completes.\n */\n attach(\n canvas: HTMLCanvasElement,\n requestRedraw: () => void\n ): void {\n this.canvas = canvas;\n this.requestRedraw = requestRedraw;\n }\n\n paint(\n metrics: CanvasSurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void {\n const canvas = this.canvas;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) {\n return;\n }\n\n const dirtyRegion = frame\n ? this.dirtyRegionForDamage(damage, frame, metrics)\n : undefined;\n if (dirtyRegion?.rects.length === 0) {\n return;\n }\n\n const scale = globalThis.window?.devicePixelRatio || 1;\n context.setTransform(scale, 0, 0, scale, 0, 0);\n context.textBaseline = \"alphabetic\";\n\n context.fillStyle = webTUITerminalBackgroundColor(metrics.style);\n if (dirtyRegion) {\n for (const rect of dirtyRegion.rects) {\n context.clearRect(rect.x, rect.y, rect.width, rect.height);\n context.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n } else {\n context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);\n context.fillRect(0, 0, metrics.columns * metrics.cellWidth, metrics.rows * metrics.cellHeight);\n }\n\n if (!frame) {\n return;\n }\n\n this.drawRows(context, frame, metrics, dirtyRegion);\n this.drawImages(context, frame.images ?? [], metrics, dirtyRegion);\n }\n\n private drawRows(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n if (dirtyRegion) {\n for (const [y, ranges] of dirtyRegion.rows) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y, ranges);\n }\n return;\n }\n\n for (let y = 0; y < frame.rows.length; y += 1) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y);\n }\n }\n\n private drawRow(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n row: WebHostSurfaceFrame[\"rows\"][number],\n y: number,\n ranges?: DirtyRowRanges\n ): void {\n for (const cell of row) {\n const [x, text, span, styleIndex] = cell;\n if (ranges !== undefined && !cellIntersectsRanges(x, span, ranges)) {\n continue;\n }\n const style = frame.styles[styleIndex] ?? undefined;\n this.drawCell(context, metrics, x, y, text, span, style);\n }\n }\n\n private drawImages(\n context: CanvasRenderingContext2D,\n images: WebHostSurfaceImage[],\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n for (const image of images) {\n this.drawImage(context, image, metrics, dirtyRegion);\n }\n }\n\n private drawImage(\n context: CanvasRenderingContext2D,\n image: WebHostSurfaceImage,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n const decodedImage = this.cachedImage(image);\n if (!decodedImage) {\n return;\n }\n\n const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;\n const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;\n if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) {\n return;\n }\n if (\n dirtyRegion\n && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)\n ) {\n return;\n }\n\n context.save();\n context.beginPath();\n context.rect(\n clipX * metrics.cellWidth,\n clipY * metrics.cellHeight,\n clipWidth * metrics.cellWidth,\n clipHeight * metrics.cellHeight\n );\n context.clip();\n context.drawImage(\n decodedImage,\n boundsX * metrics.cellWidth,\n boundsY * metrics.cellHeight,\n boundsWidth * metrics.cellWidth,\n boundsHeight * metrics.cellHeight\n );\n context.restore();\n }\n\n private cachedImage(\n image: WebHostSurfaceImage\n ): CanvasImageSource | undefined {\n const cached = this.imageCache.get(image.id);\n if (cached?.image) {\n return cached.image;\n }\n\n if (!cached?.promise && image.dataBase64) {\n const promise = decodeImage(image.dataBase64, image.format);\n this.imageCache.set(image.id, { promise });\n void promise.then((decodedImage) => {\n const latest = this.imageCache.get(image.id);\n if (latest?.promise !== promise) {\n return;\n }\n this.imageCache.set(image.id, { image: decodedImage });\n this.requestRedraw();\n }).catch(() => {\n this.imageCache.delete(image.id);\n });\n }\n\n return undefined;\n }\n\n private drawCell(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n text: string,\n span: number,\n style?: WebHostSurfaceStyle | null\n ): void {\n const rectX = x * metrics.cellWidth;\n const rectY = y * metrics.cellHeight;\n const width = Math.max(1, span) * metrics.cellWidth;\n const background = resolvedSurfaceBackground(style, metrics.style);\n const foreground = resolvedSurfaceForeground(style, metrics.style);\n const opacity = style?.opacity ?? 1;\n\n if (background) {\n context.globalAlpha = opacity;\n context.fillStyle = background;\n context.fillRect(rectX, rectY, width, metrics.cellHeight);\n }\n\n if (text !== \" \") {\n context.globalAlpha = opacity;\n context.fillStyle = foreground;\n context.strokeStyle = foreground;\n if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {\n x: rectX,\n y: rectY,\n width,\n height: metrics.cellHeight,\n })) {\n context.font = fontForStyle(metrics.style, style);\n context.fillText(\n text,\n rectX,\n rectY + Math.floor((metrics.cellHeight + metrics.style.fontSize) / 2) - 2\n );\n }\n }\n\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.underline, \"underline\", foreground);\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.strikethrough, \"strike\", foreground);\n context.globalAlpha = 1;\n }\n\n private dirtyRegionForDamage(\n damage: WebHostSurfaceDamage | undefined,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics\n ): DirtyRegion | undefined {\n if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) {\n return undefined;\n }\n\n const rects: DirtyRect[] = [];\n const rows = new Map<number, DirtyRowRanges>();\n for (const [row, ranges] of damage.textRows) {\n if (row < 0 || row >= frame.height) {\n continue;\n }\n if (ranges.length === 0) {\n rects.push(cellRect(metrics, 0, row, frame.width));\n rows.set(row, \"full\");\n continue;\n }\n const rowRanges: DirtyCellRange[] = rows.get(row) === \"full\"\n ? []\n : [...(rows.get(row) as DirtyCellRange[] | undefined ?? [])];\n for (const [start, end] of ranges) {\n const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));\n const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));\n if (lowerBound >= upperBound) {\n continue;\n }\n rects.push(cellRect(metrics, lowerBound, row, upperBound - lowerBound));\n rowRanges.push({ start: lowerBound, end: upperBound });\n }\n if (rows.get(row) !== \"full\" && rowRanges.length > 0) {\n rows.set(row, normalizeCellRanges(rowRanges));\n }\n }\n return { rects, rows };\n }\n\n private drawTextLine(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n width: number,\n line: WebHostSurfaceStyle[\"underline\"],\n placement: \"underline\" | \"strike\",\n fallbackColor: string\n ): void {\n if (!line) {\n return;\n }\n context.strokeStyle = line.color ?? fallbackColor;\n context.lineWidth = line.pattern === \"double\" ? 2 : 1;\n if (line.pattern === \"dot\") {\n context.setLineDash([1, 3]);\n } else if (line.pattern === \"dash\") {\n context.setLineDash([4, 3]);\n } else {\n context.setLineDash([]);\n }\n\n const lineY = placement === \"underline\"\n ? y + metrics.cellHeight - 2\n : y + Math.floor(metrics.cellHeight / 2);\n context.beginPath();\n context.moveTo(x, lineY);\n context.lineTo(x + width, lineY);\n context.stroke();\n context.setLineDash([]);\n }\n}\n\n/**\n * The CSS font string for a cell, folding the surface emphasis bits (bold,\n * italic) over the host terminal's font size/family. Exposed so the host can\n * reuse the exact same metric when measuring cell dimensions.\n */\nexport function fontForStyle(\n terminalStyle: ResolvedWebHostTerminalStyle,\n style?: WebHostSurfaceStyle | null\n): string {\n const emphasis = style?.em ?? 0;\n const italic = (emphasis & 2) !== 0 ? \"italic \" : \"\";\n const weight = (emphasis & 1) !== 0 ? \"700 \" : \"\";\n return `${italic}${weight}${terminalStyle.fontSize}px ${terminalStyle.fontFamily}`;\n}\n\nfunction cellRect(\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n span: number\n): DirtyRect {\n return {\n x: x * metrics.cellWidth,\n y: y * metrics.cellHeight,\n width: Math.max(1, span) * metrics.cellWidth,\n height: metrics.cellHeight,\n };\n}\n\nasync function decodeImage(\n dataBase64: string,\n format: WebHostSurfaceImageFormat\n): Promise<CanvasImageSource> {\n const bytes = decodeBase64Bytes(dataBase64);\n const blob = new Blob([bytes], { type: `image/${format}` });\n\n if (typeof createImageBitmap === \"function\") {\n // Animated GIFs collapse to their first frame in createImageBitmap\n // — that matches the Kitty path's first-frame composite. Phase 7\n // will replace this with a frame ticker.\n return createImageBitmap(blob);\n }\n\n return new Promise((resolve, reject) => {\n const image = new Image();\n const url = URL.createObjectURL(blob);\n image.onload = () => {\n URL.revokeObjectURL(url);\n resolve(image);\n };\n image.onerror = () => {\n URL.revokeObjectURL(url);\n reject(new Error(`Failed to decode ${format} image`));\n };\n image.src = url;\n });\n}\n\nfunction decodeBase64Bytes(\n value: string\n): Uint8Array {\n if (typeof atob === \"function\") {\n const binary = atob(value);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n }\n\n return new Uint8Array(Buffer.from(value, \"base64\"));\n}\n\nfunction normalizeCellRanges(\n ranges: DirtyCellRange[]\n): DirtyCellRange[] {\n const sorted = ranges\n .filter((range) => range.end > range.start)\n .sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);\n const normalized: DirtyCellRange[] = [];\n for (const range of sorted) {\n const previous = normalized[normalized.length - 1];\n if (previous && range.start <= previous.end) {\n previous.end = Math.max(previous.end, range.end);\n continue;\n }\n normalized.push({ ...range });\n }\n return normalized;\n}\n\nfunction cellIntersectsRanges(\n x: number,\n span: number,\n ranges: DirtyRowRanges\n): boolean {\n if (ranges === \"full\") {\n return true;\n }\n const start = Math.floor(x);\n const end = start + Math.max(1, Math.ceil(span));\n return ranges.some((range) => start < range.end && end > range.start);\n}\n\nfunction dirtyRegionIntersectsCellRect(\n region: DirtyRegion,\n x: number,\n y: number,\n width: number,\n height: number\n): boolean {\n const startRow = Math.max(0, Math.floor(y));\n const endRow = Math.max(startRow, Math.ceil(y + height));\n const rectRange = {\n start: Math.floor(x),\n end: Math.floor(x) + Math.max(1, Math.ceil(width)),\n };\n for (let row = startRow; row < endRow; row += 1) {\n const ranges = region.rows.get(row);\n if (!ranges) {\n continue;\n }\n if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) {\n return true;\n }\n }\n return false;\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { WebHostSurfaceDamage, WebHostSurfaceFrame } from "./WebHostSurfaceTransport.js";
|
|
2
|
+
import { SurfaceMetrics, WebHostSurfacePainter } from "./SurfaceRenderer.js";
|
|
3
|
+
//#region src/DomSurfacePainter.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Draws SwiftTUI surface frames as a DOM element tree instead of canvas
|
|
6
|
+
* pixels: one absolutely positioned row container per grid row, one `<span>`
|
|
7
|
+
* per styled cell run, and `<img>` elements for surface images.
|
|
8
|
+
*
|
|
9
|
+
* Rendering cells as real text buys what canvas cannot offer — the browser's
|
|
10
|
+
* own font shaping and fallback (emoji, CJK), crisp text at any page zoom, an
|
|
11
|
+
* inspectable tree, and native text selection — at the cost of pixel-exact
|
|
12
|
+
* box-drawing seams, which render as font glyphs here rather than the canvas
|
|
13
|
+
* painter's hand-drawn strokes.
|
|
14
|
+
*
|
|
15
|
+
* Damage handling mirrors the canvas painter at row granularity: a frame
|
|
16
|
+
* carrying scoped damage rebuilds only the touched rows' elements; geometry
|
|
17
|
+
* or style changes force a full rebuild. Grid alignment across a run is kept
|
|
18
|
+
* exact by stretching each glyph advance to the cell width via
|
|
19
|
+
* `letter-spacing`, measured once per font/size pair.
|
|
20
|
+
*/
|
|
21
|
+
declare class DomSurfacePainter implements WebHostSurfacePainter {
|
|
22
|
+
private root?;
|
|
23
|
+
private rowsLayer?;
|
|
24
|
+
private imagesLayer?;
|
|
25
|
+
private rowElements;
|
|
26
|
+
private renderedImages;
|
|
27
|
+
private appliedMetricsKey?;
|
|
28
|
+
private renderedGridKey?;
|
|
29
|
+
private hasRenderedFrame;
|
|
30
|
+
private letterSpacing?;
|
|
31
|
+
/**
|
|
32
|
+
* Binds the container the painter renders into. The runtime owns the
|
|
33
|
+
* container's size; the painter owns everything inside it.
|
|
34
|
+
*/
|
|
35
|
+
attach(root: HTMLElement): void;
|
|
36
|
+
paint(metrics: SurfaceMetrics, frame: WebHostSurfaceFrame | undefined, damage?: WebHostSurfaceDamage): void;
|
|
37
|
+
private rebuildRow;
|
|
38
|
+
private ensureRowElement;
|
|
39
|
+
private applyRootStyle;
|
|
40
|
+
/**
|
|
41
|
+
* The per-glyph advance correction that stretches the font's natural
|
|
42
|
+
* monospace advance to exactly `cellWidth`, so long runs stay on the cell
|
|
43
|
+
* grid instead of drifting by the sub-pixel remainder of the runtime's
|
|
44
|
+
* ceil'd cell measurement.
|
|
45
|
+
*/
|
|
46
|
+
private letterSpacingFor;
|
|
47
|
+
private reconcileImages;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { DomSurfacePainter };
|
|
51
|
+
//# sourceMappingURL=DomSurfacePainter.d.ts.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
|
|
2
|
+
import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./SurfaceRenderer.js";
|
|
3
|
+
import { fontForStyle } from "./CanvasSurfacePainter.js";
|
|
4
|
+
//#region src/DomSurfacePainter.ts
|
|
5
|
+
/**
|
|
6
|
+
* Draws SwiftTUI surface frames as a DOM element tree instead of canvas
|
|
7
|
+
* pixels: one absolutely positioned row container per grid row, one `<span>`
|
|
8
|
+
* per styled cell run, and `<img>` elements for surface images.
|
|
9
|
+
*
|
|
10
|
+
* Rendering cells as real text buys what canvas cannot offer — the browser's
|
|
11
|
+
* own font shaping and fallback (emoji, CJK), crisp text at any page zoom, an
|
|
12
|
+
* inspectable tree, and native text selection — at the cost of pixel-exact
|
|
13
|
+
* box-drawing seams, which render as font glyphs here rather than the canvas
|
|
14
|
+
* painter's hand-drawn strokes.
|
|
15
|
+
*
|
|
16
|
+
* Damage handling mirrors the canvas painter at row granularity: a frame
|
|
17
|
+
* carrying scoped damage rebuilds only the touched rows' elements; geometry
|
|
18
|
+
* or style changes force a full rebuild. Grid alignment across a run is kept
|
|
19
|
+
* exact by stretching each glyph advance to the cell width via
|
|
20
|
+
* `letter-spacing`, measured once per font/size pair.
|
|
21
|
+
*/
|
|
22
|
+
var DomSurfacePainter = class {
|
|
23
|
+
root;
|
|
24
|
+
rowsLayer;
|
|
25
|
+
imagesLayer;
|
|
26
|
+
rowElements = [];
|
|
27
|
+
renderedImages = /* @__PURE__ */ new Map();
|
|
28
|
+
appliedMetricsKey;
|
|
29
|
+
renderedGridKey;
|
|
30
|
+
hasRenderedFrame = false;
|
|
31
|
+
letterSpacing;
|
|
32
|
+
/**
|
|
33
|
+
* Binds the container the painter renders into. The runtime owns the
|
|
34
|
+
* container's size; the painter owns everything inside it.
|
|
35
|
+
*/
|
|
36
|
+
attach(root) {
|
|
37
|
+
this.root = root;
|
|
38
|
+
const rowsLayer = createElement("div");
|
|
39
|
+
rowsLayer.className = "webhost-scene__surface-rows";
|
|
40
|
+
fillContainer(rowsLayer.style);
|
|
41
|
+
const imagesLayer = createElement("div");
|
|
42
|
+
imagesLayer.className = "webhost-scene__surface-images";
|
|
43
|
+
fillContainer(imagesLayer.style);
|
|
44
|
+
imagesLayer.style.pointerEvents = "none";
|
|
45
|
+
this.rowsLayer = rowsLayer;
|
|
46
|
+
this.imagesLayer = imagesLayer;
|
|
47
|
+
root.replaceChildren(rowsLayer, imagesLayer);
|
|
48
|
+
this.rowElements = [];
|
|
49
|
+
this.renderedImages = /* @__PURE__ */ new Map();
|
|
50
|
+
this.appliedMetricsKey = void 0;
|
|
51
|
+
this.renderedGridKey = void 0;
|
|
52
|
+
this.hasRenderedFrame = false;
|
|
53
|
+
}
|
|
54
|
+
paint(metrics, frame, damage) {
|
|
55
|
+
const root = this.root;
|
|
56
|
+
const rowsLayer = this.rowsLayer;
|
|
57
|
+
if (!root || !rowsLayer) return;
|
|
58
|
+
const metricsKey = metricsKeyFor(metrics);
|
|
59
|
+
const metricsChanged = metricsKey !== this.appliedMetricsKey;
|
|
60
|
+
if (metricsChanged) {
|
|
61
|
+
this.applyRootStyle(root, metrics);
|
|
62
|
+
this.appliedMetricsKey = metricsKey;
|
|
63
|
+
}
|
|
64
|
+
if (!frame) {
|
|
65
|
+
this.rowElements = [];
|
|
66
|
+
rowsLayer.replaceChildren();
|
|
67
|
+
this.reconcileImages([], metrics);
|
|
68
|
+
this.renderedGridKey = void 0;
|
|
69
|
+
this.hasRenderedFrame = false;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const gridKey = `${frame.width}x${frame.height}x${frame.rows.length}`;
|
|
73
|
+
const fullRepaint = metricsChanged || !this.hasRenderedFrame || gridKey !== this.renderedGridKey || !damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay;
|
|
74
|
+
this.renderedGridKey = gridKey;
|
|
75
|
+
if (fullRepaint) {
|
|
76
|
+
for (let y = this.rowElements.length; y > frame.rows.length; y -= 1) this.rowElements[y - 1]?.remove();
|
|
77
|
+
this.rowElements.length = Math.min(this.rowElements.length, frame.rows.length);
|
|
78
|
+
for (let y = 0; y < frame.rows.length; y += 1) this.rebuildRow(y, frame, metrics);
|
|
79
|
+
} else for (const [row] of damage.textRows) {
|
|
80
|
+
if (row < 0 || row >= frame.rows.length) continue;
|
|
81
|
+
this.rebuildRow(row, frame, metrics);
|
|
82
|
+
}
|
|
83
|
+
this.reconcileImages(frame.images ?? [], metrics);
|
|
84
|
+
this.hasRenderedFrame = true;
|
|
85
|
+
}
|
|
86
|
+
rebuildRow(y, frame, metrics) {
|
|
87
|
+
const rowElement = this.ensureRowElement(y, metrics);
|
|
88
|
+
const children = [];
|
|
89
|
+
for (const [x, text, span, styleIndex] of frame.rows[y] ?? []) {
|
|
90
|
+
const cellElement = buildCellElement(x, text, span, frame.styles[styleIndex] ?? void 0, metrics);
|
|
91
|
+
if (cellElement) children.push(cellElement);
|
|
92
|
+
}
|
|
93
|
+
rowElement.replaceChildren(...children);
|
|
94
|
+
}
|
|
95
|
+
ensureRowElement(y, metrics) {
|
|
96
|
+
let rowElement = this.rowElements[y];
|
|
97
|
+
if (!rowElement) {
|
|
98
|
+
rowElement = createElement("div");
|
|
99
|
+
rowElement.className = "webhost-scene__surface-row";
|
|
100
|
+
rowElement.style.position = "absolute";
|
|
101
|
+
rowElement.style.left = "0";
|
|
102
|
+
this.rowElements[y] = rowElement;
|
|
103
|
+
this.rowsLayer?.appendChild(rowElement);
|
|
104
|
+
}
|
|
105
|
+
rowElement.style.top = `${y * metrics.cellHeight}px`;
|
|
106
|
+
rowElement.style.height = `${metrics.cellHeight}px`;
|
|
107
|
+
rowElement.style.width = `${metrics.columns * metrics.cellWidth}px`;
|
|
108
|
+
return rowElement;
|
|
109
|
+
}
|
|
110
|
+
applyRootStyle(root, metrics) {
|
|
111
|
+
const style = root.style;
|
|
112
|
+
style.position = "relative";
|
|
113
|
+
style.overflow = "hidden";
|
|
114
|
+
style.background = webTUITerminalBackgroundColor(metrics.style);
|
|
115
|
+
style.font = fontForStyle(metrics.style);
|
|
116
|
+
style.lineHeight = `${metrics.cellHeight}px`;
|
|
117
|
+
style.letterSpacing = this.letterSpacingFor(metrics);
|
|
118
|
+
style.fontVariantLigatures = "none";
|
|
119
|
+
style.userSelect = "text";
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The per-glyph advance correction that stretches the font's natural
|
|
123
|
+
* monospace advance to exactly `cellWidth`, so long runs stay on the cell
|
|
124
|
+
* grid instead of drifting by the sub-pixel remainder of the runtime's
|
|
125
|
+
* ceil'd cell measurement.
|
|
126
|
+
*/
|
|
127
|
+
letterSpacingFor(metrics) {
|
|
128
|
+
const font = fontForStyle(metrics.style);
|
|
129
|
+
const key = `${font}|${metrics.cellWidth}`;
|
|
130
|
+
if (this.letterSpacing?.key === key) return this.letterSpacing.value;
|
|
131
|
+
let value = "0px";
|
|
132
|
+
const context = createElement("canvas").getContext?.("2d");
|
|
133
|
+
if (context) {
|
|
134
|
+
context.font = font;
|
|
135
|
+
const advance = context.measureText("W").width;
|
|
136
|
+
const correction = metrics.cellWidth - advance;
|
|
137
|
+
if (advance > 0 && Math.abs(correction) >= .01) value = `${Math.round(correction * 1e3) / 1e3}px`;
|
|
138
|
+
}
|
|
139
|
+
this.letterSpacing = {
|
|
140
|
+
key,
|
|
141
|
+
value
|
|
142
|
+
};
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
reconcileImages(images, metrics) {
|
|
146
|
+
const layer = this.imagesLayer;
|
|
147
|
+
if (!layer) return;
|
|
148
|
+
const next = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const image of images) {
|
|
150
|
+
const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
|
|
151
|
+
const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
|
|
152
|
+
if (!image.dataBase64 || boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) continue;
|
|
153
|
+
const existing = this.renderedImages.get(image.id);
|
|
154
|
+
const entry = existing ?? makeImageEntry();
|
|
155
|
+
entry.container.style.left = `${clipX * metrics.cellWidth}px`;
|
|
156
|
+
entry.container.style.top = `${clipY * metrics.cellHeight}px`;
|
|
157
|
+
entry.container.style.width = `${clipWidth * metrics.cellWidth}px`;
|
|
158
|
+
entry.container.style.height = `${clipHeight * metrics.cellHeight}px`;
|
|
159
|
+
entry.image.style.left = `${(boundsX - clipX) * metrics.cellWidth}px`;
|
|
160
|
+
entry.image.style.top = `${(boundsY - clipY) * metrics.cellHeight}px`;
|
|
161
|
+
entry.image.style.width = `${boundsWidth * metrics.cellWidth}px`;
|
|
162
|
+
entry.image.style.height = `${boundsHeight * metrics.cellHeight}px`;
|
|
163
|
+
const source = `data:image/${image.format};base64,${image.dataBase64}`;
|
|
164
|
+
if (entry.source !== source) {
|
|
165
|
+
entry.image.setAttribute("src", source);
|
|
166
|
+
entry.source = source;
|
|
167
|
+
}
|
|
168
|
+
if (!existing) layer.appendChild(entry.container);
|
|
169
|
+
next.set(image.id, entry);
|
|
170
|
+
}
|
|
171
|
+
for (const [id, entry] of this.renderedImages) if (!next.has(id)) entry.container.remove();
|
|
172
|
+
this.renderedImages = next;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* A change key over everything the rendered tree bakes into element styles —
|
|
177
|
+
* grid geometry, font, and theme colors. A key change invalidates every
|
|
178
|
+
* rendered row, so the next paint restyles the root and rebuilds in full.
|
|
179
|
+
*/
|
|
180
|
+
function metricsKeyFor(metrics) {
|
|
181
|
+
return [
|
|
182
|
+
metrics.columns,
|
|
183
|
+
metrics.rows,
|
|
184
|
+
metrics.cellWidth,
|
|
185
|
+
metrics.cellHeight,
|
|
186
|
+
fontForStyle(metrics.style),
|
|
187
|
+
metrics.style.theme.foreground,
|
|
188
|
+
metrics.style.theme.background,
|
|
189
|
+
metrics.style.theme.windowBackground,
|
|
190
|
+
metrics.style.backgroundOpacity
|
|
191
|
+
].join("|");
|
|
192
|
+
}
|
|
193
|
+
function buildCellElement(x, text, span, style, metrics) {
|
|
194
|
+
const background = resolvedSurfaceBackground(style, metrics.style);
|
|
195
|
+
const hasDecoration = Boolean(style?.underline || style?.strikethrough);
|
|
196
|
+
if (!background && !hasDecoration && text.trim() === "") return;
|
|
197
|
+
const element = createElement("span");
|
|
198
|
+
element.textContent = text;
|
|
199
|
+
const elementStyle = element.style;
|
|
200
|
+
elementStyle.position = "absolute";
|
|
201
|
+
elementStyle.left = `${x * metrics.cellWidth}px`;
|
|
202
|
+
elementStyle.top = "0";
|
|
203
|
+
elementStyle.width = `${Math.max(1, span) * metrics.cellWidth}px`;
|
|
204
|
+
elementStyle.height = "100%";
|
|
205
|
+
elementStyle.whiteSpace = "pre";
|
|
206
|
+
elementStyle.color = resolvedSurfaceForeground(style, metrics.style);
|
|
207
|
+
if (background) elementStyle.backgroundColor = background;
|
|
208
|
+
const emphasis = style?.em ?? 0;
|
|
209
|
+
if (emphasis & 1) elementStyle.fontWeight = "700";
|
|
210
|
+
if (emphasis & 2) elementStyle.fontStyle = "italic";
|
|
211
|
+
const opacity = style?.opacity ?? 1;
|
|
212
|
+
if (opacity !== 1) elementStyle.opacity = String(opacity);
|
|
213
|
+
applyTextDecoration(elementStyle, style);
|
|
214
|
+
return element;
|
|
215
|
+
}
|
|
216
|
+
function applyTextDecoration(elementStyle, style) {
|
|
217
|
+
const lines = [];
|
|
218
|
+
if (style?.underline) lines.push("underline");
|
|
219
|
+
if (style?.strikethrough) lines.push("line-through");
|
|
220
|
+
if (lines.length === 0) return;
|
|
221
|
+
elementStyle.textDecorationLine = lines.join(" ");
|
|
222
|
+
elementStyle.textDecorationStyle = decorationStyleFor(style?.underline?.pattern ?? style?.strikethrough?.pattern);
|
|
223
|
+
const color = style?.underline?.color ?? style?.strikethrough?.color;
|
|
224
|
+
if (color) elementStyle.textDecorationColor = color;
|
|
225
|
+
}
|
|
226
|
+
function decorationStyleFor(pattern) {
|
|
227
|
+
switch (pattern) {
|
|
228
|
+
case "dot": return "dotted";
|
|
229
|
+
case "dash":
|
|
230
|
+
case "dashDot":
|
|
231
|
+
case "dashDotDot": return "dashed";
|
|
232
|
+
case "double": return "double";
|
|
233
|
+
case "curly": return "wavy";
|
|
234
|
+
default: return "solid";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function fillContainer(style) {
|
|
238
|
+
style.position = "absolute";
|
|
239
|
+
style.left = "0";
|
|
240
|
+
style.top = "0";
|
|
241
|
+
style.width = "100%";
|
|
242
|
+
style.height = "100%";
|
|
243
|
+
}
|
|
244
|
+
function makeImageEntry() {
|
|
245
|
+
const container = createElement("div");
|
|
246
|
+
container.className = "webhost-scene__surface-image";
|
|
247
|
+
container.style.position = "absolute";
|
|
248
|
+
container.style.overflow = "hidden";
|
|
249
|
+
const image = createElement("img");
|
|
250
|
+
image.style.position = "absolute";
|
|
251
|
+
image.setAttribute("alt", "");
|
|
252
|
+
image.setAttribute("draggable", "false");
|
|
253
|
+
container.appendChild(image);
|
|
254
|
+
return {
|
|
255
|
+
container,
|
|
256
|
+
image,
|
|
257
|
+
source: ""
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function createElement(tagName) {
|
|
261
|
+
if (typeof document === "undefined") throw new Error("document is not available");
|
|
262
|
+
return document.createElement(tagName);
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { DomSurfacePainter };
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=DomSurfacePainter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DomSurfacePainter.js","names":[],"sources":["../../src/DomSurfacePainter.ts"],"sourcesContent":["import { fontForStyle } from \"./CanvasSurfacePainter.ts\";\nimport {\n resolvedSurfaceBackground,\n resolvedSurfaceForeground,\n type SurfaceMetrics,\n type WebHostSurfacePainter,\n} from \"./SurfaceRenderer.ts\";\nimport { webTUITerminalBackgroundColor } from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\n WebHostSurfaceLineStyle,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\ninterface RenderedImage {\n container: HTMLElement;\n image: HTMLElement;\n source: string;\n}\n\n/**\n * Draws SwiftTUI surface frames as a DOM element tree instead of canvas\n * pixels: one absolutely positioned row container per grid row, one `<span>`\n * per styled cell run, and `<img>` elements for surface images.\n *\n * Rendering cells as real text buys what canvas cannot offer — the browser's\n * own font shaping and fallback (emoji, CJK), crisp text at any page zoom, an\n * inspectable tree, and native text selection — at the cost of pixel-exact\n * box-drawing seams, which render as font glyphs here rather than the canvas\n * painter's hand-drawn strokes.\n *\n * Damage handling mirrors the canvas painter at row granularity: a frame\n * carrying scoped damage rebuilds only the touched rows' elements; geometry\n * or style changes force a full rebuild. Grid alignment across a run is kept\n * exact by stretching each glyph advance to the cell width via\n * `letter-spacing`, measured once per font/size pair.\n */\nexport class DomSurfacePainter implements WebHostSurfacePainter {\n private root?: HTMLElement;\n private rowsLayer?: HTMLElement;\n private imagesLayer?: HTMLElement;\n private rowElements: HTMLElement[] = [];\n private renderedImages = new Map<string, RenderedImage>();\n private appliedMetricsKey?: string;\n private renderedGridKey?: string;\n private hasRenderedFrame = false;\n private letterSpacing?: { key: string; value: string };\n\n /**\n * Binds the container the painter renders into. The runtime owns the\n * container's size; the painter owns everything inside it.\n */\n attach(\n root: HTMLElement\n ): void {\n this.root = root;\n\n const rowsLayer = createElement(\"div\");\n rowsLayer.className = \"webhost-scene__surface-rows\";\n fillContainer(rowsLayer.style);\n\n const imagesLayer = createElement(\"div\");\n imagesLayer.className = \"webhost-scene__surface-images\";\n fillContainer(imagesLayer.style);\n imagesLayer.style.pointerEvents = \"none\";\n\n this.rowsLayer = rowsLayer;\n this.imagesLayer = imagesLayer;\n root.replaceChildren(rowsLayer, imagesLayer);\n this.rowElements = [];\n this.renderedImages = new Map();\n this.appliedMetricsKey = undefined;\n this.renderedGridKey = undefined;\n this.hasRenderedFrame = false;\n }\n\n paint(\n metrics: SurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void {\n const root = this.root;\n const rowsLayer = this.rowsLayer;\n if (!root || !rowsLayer) {\n return;\n }\n\n const metricsKey = metricsKeyFor(metrics);\n const metricsChanged = metricsKey !== this.appliedMetricsKey;\n if (metricsChanged) {\n this.applyRootStyle(root, metrics);\n this.appliedMetricsKey = metricsKey;\n }\n\n if (!frame) {\n this.rowElements = [];\n rowsLayer.replaceChildren();\n this.reconcileImages([], metrics);\n this.renderedGridKey = undefined;\n this.hasRenderedFrame = false;\n return;\n }\n\n const gridKey = `${frame.width}x${frame.height}x${frame.rows.length}`;\n const fullRepaint = metricsChanged\n || !this.hasRenderedFrame\n || gridKey !== this.renderedGridKey\n || !damage\n || damage.requiresFullTextRepaint\n || damage.requiresFullGraphicsReplay;\n this.renderedGridKey = gridKey;\n\n if (fullRepaint) {\n for (let y = this.rowElements.length; y > frame.rows.length; y -= 1) {\n this.rowElements[y - 1]?.remove();\n }\n this.rowElements.length = Math.min(this.rowElements.length, frame.rows.length);\n for (let y = 0; y < frame.rows.length; y += 1) {\n this.rebuildRow(y, frame, metrics);\n }\n } else {\n for (const [row] of damage.textRows) {\n if (row < 0 || row >= frame.rows.length) {\n continue;\n }\n this.rebuildRow(row, frame, metrics);\n }\n }\n\n this.reconcileImages(frame.images ?? [], metrics);\n this.hasRenderedFrame = true;\n }\n\n private rebuildRow(\n y: number,\n frame: WebHostSurfaceFrame,\n metrics: SurfaceMetrics\n ): void {\n const rowElement = this.ensureRowElement(y, metrics);\n const children: HTMLElement[] = [];\n for (const [x, text, span, styleIndex] of frame.rows[y] ?? []) {\n const cellElement = buildCellElement(\n x,\n text,\n span,\n frame.styles[styleIndex] ?? undefined,\n metrics\n );\n if (cellElement) {\n children.push(cellElement);\n }\n }\n rowElement.replaceChildren(...children);\n }\n\n private ensureRowElement(\n y: number,\n metrics: SurfaceMetrics\n ): HTMLElement {\n let rowElement = this.rowElements[y];\n if (!rowElement) {\n rowElement = createElement(\"div\");\n rowElement.className = \"webhost-scene__surface-row\";\n rowElement.style.position = \"absolute\";\n rowElement.style.left = \"0\";\n this.rowElements[y] = rowElement;\n this.rowsLayer?.appendChild(rowElement);\n }\n rowElement.style.top = `${y * metrics.cellHeight}px`;\n rowElement.style.height = `${metrics.cellHeight}px`;\n rowElement.style.width = `${metrics.columns * metrics.cellWidth}px`;\n return rowElement;\n }\n\n private applyRootStyle(\n root: HTMLElement,\n metrics: SurfaceMetrics\n ): void {\n const style = root.style;\n style.position = \"relative\";\n style.overflow = \"hidden\";\n style.background = webTUITerminalBackgroundColor(metrics.style);\n style.font = fontForStyle(metrics.style);\n // Set after `font`: the shorthand resets line-height, and the grid needs\n // every row to be exactly one cell tall.\n style.lineHeight = `${metrics.cellHeight}px`;\n style.letterSpacing = this.letterSpacingFor(metrics);\n // Ligature-capable monospace fonts would merge runs like \"->\" into one\n // glyph and break the column grid.\n style.fontVariantLigatures = \"none\";\n style.userSelect = \"text\";\n }\n\n /**\n * The per-glyph advance correction that stretches the font's natural\n * monospace advance to exactly `cellWidth`, so long runs stay on the cell\n * grid instead of drifting by the sub-pixel remainder of the runtime's\n * ceil'd cell measurement.\n */\n private letterSpacingFor(\n metrics: SurfaceMetrics\n ): string {\n const font = fontForStyle(metrics.style);\n const key = `${font}|${metrics.cellWidth}`;\n if (this.letterSpacing?.key === key) {\n return this.letterSpacing.value;\n }\n\n let value = \"0px\";\n const canvas = createElement(\"canvas\") as HTMLCanvasElement;\n const context = canvas.getContext?.(\"2d\");\n if (context) {\n context.font = font;\n const advance = context.measureText(\"W\").width;\n const correction = metrics.cellWidth - advance;\n if (advance > 0 && Math.abs(correction) >= 0.01) {\n value = `${Math.round(correction * 1000) / 1000}px`;\n }\n }\n\n this.letterSpacing = { key, value };\n return value;\n }\n\n private reconcileImages(\n images: WebHostSurfaceImage[],\n metrics: SurfaceMetrics\n ): void {\n const layer = this.imagesLayer;\n if (!layer) {\n return;\n }\n\n const next = new Map<string, RenderedImage>();\n for (const image of images) {\n const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;\n const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;\n if (\n !image.dataBase64\n || boundsWidth <= 0\n || boundsHeight <= 0\n || clipWidth <= 0\n || clipHeight <= 0\n ) {\n continue;\n }\n\n const existing = this.renderedImages.get(image.id);\n const entry = existing ?? makeImageEntry();\n entry.container.style.left = `${clipX * metrics.cellWidth}px`;\n entry.container.style.top = `${clipY * metrics.cellHeight}px`;\n entry.container.style.width = `${clipWidth * metrics.cellWidth}px`;\n entry.container.style.height = `${clipHeight * metrics.cellHeight}px`;\n entry.image.style.left = `${(boundsX - clipX) * metrics.cellWidth}px`;\n entry.image.style.top = `${(boundsY - clipY) * metrics.cellHeight}px`;\n entry.image.style.width = `${boundsWidth * metrics.cellWidth}px`;\n entry.image.style.height = `${boundsHeight * metrics.cellHeight}px`;\n\n const source = `data:image/${image.format};base64,${image.dataBase64}`;\n if (entry.source !== source) {\n entry.image.setAttribute(\"src\", source);\n entry.source = source;\n }\n if (!existing) {\n layer.appendChild(entry.container);\n }\n next.set(image.id, entry);\n }\n\n for (const [id, entry] of this.renderedImages) {\n if (!next.has(id)) {\n entry.container.remove();\n }\n }\n this.renderedImages = next;\n }\n}\n\n/**\n * A change key over everything the rendered tree bakes into element styles —\n * grid geometry, font, and theme colors. A key change invalidates every\n * rendered row, so the next paint restyles the root and rebuilds in full.\n */\nfunction metricsKeyFor(\n metrics: SurfaceMetrics\n): string {\n return [\n metrics.columns,\n metrics.rows,\n metrics.cellWidth,\n metrics.cellHeight,\n fontForStyle(metrics.style),\n metrics.style.theme.foreground,\n metrics.style.theme.background,\n metrics.style.theme.windowBackground,\n metrics.style.backgroundOpacity,\n ].join(\"|\");\n}\n\nfunction buildCellElement(\n x: number,\n text: string,\n span: number,\n style: WebHostSurfaceStyle | undefined,\n metrics: SurfaceMetrics\n): HTMLElement | undefined {\n const background = resolvedSurfaceBackground(style, metrics.style);\n const hasDecoration = Boolean(style?.underline || style?.strikethrough);\n if (!background && !hasDecoration && text.trim() === \"\") {\n return undefined;\n }\n\n const element = createElement(\"span\");\n element.textContent = text;\n const elementStyle = element.style;\n elementStyle.position = \"absolute\";\n elementStyle.left = `${x * metrics.cellWidth}px`;\n elementStyle.top = \"0\";\n elementStyle.width = `${Math.max(1, span) * metrics.cellWidth}px`;\n elementStyle.height = \"100%\";\n elementStyle.whiteSpace = \"pre\";\n elementStyle.color = resolvedSurfaceForeground(style, metrics.style);\n if (background) {\n elementStyle.backgroundColor = background;\n }\n\n const emphasis = style?.em ?? 0;\n if (emphasis & 1) {\n elementStyle.fontWeight = \"700\";\n }\n if (emphasis & 2) {\n elementStyle.fontStyle = \"italic\";\n }\n\n const opacity = style?.opacity ?? 1;\n if (opacity !== 1) {\n elementStyle.opacity = String(opacity);\n }\n\n applyTextDecoration(elementStyle, style);\n return element;\n}\n\nfunction applyTextDecoration(\n elementStyle: CSSStyleDeclaration,\n style: WebHostSurfaceStyle | undefined\n): void {\n const lines: string[] = [];\n if (style?.underline) {\n lines.push(\"underline\");\n }\n if (style?.strikethrough) {\n lines.push(\"line-through\");\n }\n if (lines.length === 0) {\n return;\n }\n\n elementStyle.textDecorationLine = lines.join(\" \");\n const pattern = style?.underline?.pattern ?? style?.strikethrough?.pattern;\n elementStyle.textDecorationStyle = decorationStyleFor(pattern);\n // CSS shares one decoration color across both lines; the underline's color\n // wins when the app styles them differently.\n const color = style?.underline?.color ?? style?.strikethrough?.color;\n if (color) {\n elementStyle.textDecorationColor = color;\n }\n}\n\nfunction decorationStyleFor(\n pattern: WebHostSurfaceLineStyle[\"pattern\"] | undefined\n): string {\n switch (pattern) {\n case \"dot\":\n return \"dotted\";\n case \"dash\":\n case \"dashDot\":\n case \"dashDotDot\":\n return \"dashed\";\n case \"double\":\n return \"double\";\n case \"curly\":\n return \"wavy\";\n default:\n return \"solid\";\n }\n}\n\nfunction fillContainer(\n style: CSSStyleDeclaration\n): void {\n style.position = \"absolute\";\n style.left = \"0\";\n style.top = \"0\";\n style.width = \"100%\";\n style.height = \"100%\";\n}\n\nfunction makeImageEntry(): RenderedImage {\n const container = createElement(\"div\");\n container.className = \"webhost-scene__surface-image\";\n container.style.position = \"absolute\";\n container.style.overflow = \"hidden\";\n\n const image = createElement(\"img\");\n image.style.position = \"absolute\";\n image.setAttribute(\"alt\", \"\");\n image.setAttribute(\"draggable\", \"false\");\n container.appendChild(image);\n return { container, image, source: \"\" };\n}\n\nfunction createElement(\n tagName: string\n): HTMLElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"document is not available\");\n }\n return document.createElement(tagName);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ResolvedWebHostTerminalStyle } from "./WebHostTerminalStyle.js";
|
|
2
|
+
import { WebHostSurfaceDamage, WebHostSurfaceFrame, WebHostSurfaceStyle } from "./WebHostSurfaceTransport.js";
|
|
3
|
+
//#region src/SurfaceRenderer.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Which presenter draws surface frames into the scene mount.
|
|
6
|
+
*
|
|
7
|
+
* - `"canvas"` (default): paints cells onto a 2D `<canvas>` — pixel-exact
|
|
8
|
+
* box-drawing seams and decoration patterns, one DOM node total.
|
|
9
|
+
* - `"dom"`: renders cells as absolutely positioned text elements — native
|
|
10
|
+
* font rendering (fallback glyphs, subpixel AA, crisp zoom), an
|
|
11
|
+
* inspectable element tree, and real selectable text (hold Alt/Option and
|
|
12
|
+
* drag). Box drawing and decoration patterns render via font glyphs and
|
|
13
|
+
* CSS `text-decoration`, so hairline seams may differ from the canvas
|
|
14
|
+
* painter.
|
|
15
|
+
*/
|
|
16
|
+
type WebHostSurfaceRendererKind = "canvas" | "dom";
|
|
17
|
+
/**
|
|
18
|
+
* A read-only snapshot of the cell grid geometry and active style a surface
|
|
19
|
+
* painter needs for a single paint pass. The runtime owns this state and
|
|
20
|
+
* mutates it as the surface resizes or restyles; passing a fresh snapshot per
|
|
21
|
+
* `paint` keeps painters stateless about geometry and avoids stale reads.
|
|
22
|
+
*/
|
|
23
|
+
interface SurfaceMetrics {
|
|
24
|
+
columns: number;
|
|
25
|
+
rows: number;
|
|
26
|
+
cellWidth: number;
|
|
27
|
+
cellHeight: number;
|
|
28
|
+
style: ResolvedWebHostTerminalStyle;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The paint seam shared by the canvas and DOM painters. The runtime calls
|
|
32
|
+
* `paint` with the latest metrics snapshot, the current frame (or `undefined`
|
|
33
|
+
* before the first frame), and optional damage scoping the repaint.
|
|
34
|
+
*/
|
|
35
|
+
interface WebHostSurfacePainter {
|
|
36
|
+
paint(metrics: SurfaceMetrics, frame: WebHostSurfaceFrame | undefined, damage?: WebHostSurfaceDamage): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The effective text color for a cell, folding the reverse-video emphasis bit
|
|
40
|
+
* (`em & 16`) over the host terminal theme's defaults.
|
|
41
|
+
*/
|
|
42
|
+
declare function resolvedSurfaceForeground(style: WebHostSurfaceStyle | null | undefined, terminalStyle: ResolvedWebHostTerminalStyle): string;
|
|
43
|
+
/**
|
|
44
|
+
* The effective background fill for a cell (or `undefined` for the terminal's
|
|
45
|
+
* base background), folding the reverse-video emphasis bit (`em & 16`).
|
|
46
|
+
*/
|
|
47
|
+
declare function resolvedSurfaceBackground(style: WebHostSurfaceStyle | null | undefined, terminalStyle: ResolvedWebHostTerminalStyle): string | undefined;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { SurfaceMetrics, WebHostSurfacePainter, WebHostSurfaceRendererKind, resolvedSurfaceBackground, resolvedSurfaceForeground };
|
|
50
|
+
//# sourceMappingURL=SurfaceRenderer.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/SurfaceRenderer.ts
|
|
2
|
+
/**
|
|
3
|
+
* The effective text color for a cell, folding the reverse-video emphasis bit
|
|
4
|
+
* (`em & 16`) over the host terminal theme's defaults.
|
|
5
|
+
*/
|
|
6
|
+
function resolvedSurfaceForeground(style, terminalStyle) {
|
|
7
|
+
if ((style?.em ?? 0) & 16) return style?.bg ?? terminalStyle.theme.background;
|
|
8
|
+
return style?.fg ?? terminalStyle.theme.foreground;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The effective background fill for a cell (or `undefined` for the terminal's
|
|
12
|
+
* base background), folding the reverse-video emphasis bit (`em & 16`).
|
|
13
|
+
*/
|
|
14
|
+
function resolvedSurfaceBackground(style, terminalStyle) {
|
|
15
|
+
if ((style?.em ?? 0) & 16) return style?.fg ?? terminalStyle.theme.foreground;
|
|
16
|
+
return style?.bg;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { resolvedSurfaceBackground, resolvedSurfaceForeground };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=SurfaceRenderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SurfaceRenderer.js","names":[],"sources":["../../src/SurfaceRenderer.ts"],"sourcesContent":["import type { ResolvedWebHostTerminalStyle } from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/**\n * Which presenter draws surface frames into the scene mount.\n *\n * - `\"canvas\"` (default): paints cells onto a 2D `<canvas>` — pixel-exact\n * box-drawing seams and decoration patterns, one DOM node total.\n * - `\"dom\"`: renders cells as absolutely positioned text elements — native\n * font rendering (fallback glyphs, subpixel AA, crisp zoom), an\n * inspectable element tree, and real selectable text (hold Alt/Option and\n * drag). Box drawing and decoration patterns render via font glyphs and\n * CSS `text-decoration`, so hairline seams may differ from the canvas\n * painter.\n */\nexport type WebHostSurfaceRendererKind = \"canvas\" | \"dom\";\n\n/**\n * A read-only snapshot of the cell grid geometry and active style a surface\n * painter needs for a single paint pass. The runtime owns this state and\n * mutates it as the surface resizes or restyles; passing a fresh snapshot per\n * `paint` keeps painters stateless about geometry and avoids stale reads.\n */\nexport interface SurfaceMetrics {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n style: ResolvedWebHostTerminalStyle;\n}\n\n/**\n * The paint seam shared by the canvas and DOM painters. The runtime calls\n * `paint` with the latest metrics snapshot, the current frame (or `undefined`\n * before the first frame), and optional damage scoping the repaint.\n */\nexport interface WebHostSurfacePainter {\n paint(\n metrics: SurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void;\n}\n\n/**\n * The effective text color for a cell, folding the reverse-video emphasis bit\n * (`em & 16`) over the host terminal theme's defaults.\n */\nexport function resolvedSurfaceForeground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string {\n if ((style?.em ?? 0) & 16) {\n return style?.bg ?? terminalStyle.theme.background;\n }\n return style?.fg ?? terminalStyle.theme.foreground;\n}\n\n/**\n * The effective background fill for a cell (or `undefined` for the terminal's\n * base background), folding the reverse-video emphasis bit (`em & 16`).\n */\nexport function resolvedSurfaceBackground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string | undefined {\n if ((style?.em ?? 0) & 16) {\n return style?.fg ?? terminalStyle.theme.foreground;\n }\n return style?.bg;\n}\n"],"mappings":";;;;;AAoDA,SAAgB,0BACd,OACA,eACQ;CACR,KAAK,OAAO,MAAM,KAAK,IACrB,OAAO,OAAO,MAAM,cAAc,MAAM;CAE1C,OAAO,OAAO,MAAM,cAAc,MAAM;AAC1C;;;;;AAMA,SAAgB,0BACd,OACA,eACoB;CACpB,KAAK,OAAO,MAAM,KAAK,IACrB,OAAO,OAAO,MAAM,cAAc,MAAM;CAE1C,OAAO,OAAO;AAChB"}
|
package/dist/src/WebHostApp.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WebHostTerminalStyle } from "./WebHostTerminalStyle.js";
|
|
2
|
+
import { WebHostSurfaceRendererKind } from "./SurfaceRenderer.js";
|
|
2
3
|
import { WebHostSceneDescriptor, WebHostSceneManifestSource } from "./WebHostSceneManifest.js";
|
|
3
4
|
import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions } from "./WebHostSceneRuntime.js";
|
|
4
5
|
import { WebSocketSceneBridgeOptions } from "./WebSocketSceneBridge.js";
|
|
@@ -45,6 +46,13 @@ interface WebHostAppOptions {
|
|
|
45
46
|
suspendHiddenScenes?: boolean;
|
|
46
47
|
/** Visibility source override; defaults to the global `document`. */
|
|
47
48
|
visibilityDocument?: WebHostVisibilityDocument;
|
|
49
|
+
/**
|
|
50
|
+
* Which surface presenter every scene runtime uses: `"canvas"` (default)
|
|
51
|
+
* paints frames onto a 2D `<canvas>`; `"dom"` renders them as absolutely
|
|
52
|
+
* positioned text elements. Forwarded to each scene runtime as `renderer`.
|
|
53
|
+
* See {@link WebHostSurfaceRendererKind}.
|
|
54
|
+
*/
|
|
55
|
+
renderer?: WebHostSurfaceRendererKind;
|
|
48
56
|
}
|
|
49
57
|
interface WebHostAppController {
|
|
50
58
|
scenes: WebHostSceneDescriptor[];
|
package/dist/src/WebHostApp.js
CHANGED
|
@@ -17,7 +17,8 @@ async function createWebHostApp(options) {
|
|
|
17
17
|
createElement: options.createElement,
|
|
18
18
|
sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions)),
|
|
19
19
|
suspendHiddenScenes: options.suspendHiddenScenes,
|
|
20
|
-
visibilityDocument: options.visibilityDocument ?? defaultVisibilityDocument()
|
|
20
|
+
visibilityDocument: options.visibilityDocument ?? defaultVisibilityDocument(),
|
|
21
|
+
renderer: options.renderer
|
|
21
22
|
});
|
|
22
23
|
await controller.initialize();
|
|
23
24
|
return controller;
|
|
@@ -35,6 +36,7 @@ var InternalWebHostAppController = class {
|
|
|
35
36
|
runtimes = /* @__PURE__ */ new Map();
|
|
36
37
|
bridges = /* @__PURE__ */ new Map();
|
|
37
38
|
suspendHiddenScenes;
|
|
39
|
+
renderer;
|
|
38
40
|
visibilityDocument;
|
|
39
41
|
detachVisibilityListener;
|
|
40
42
|
constructor(options) {
|
|
@@ -45,6 +47,7 @@ var InternalWebHostAppController = class {
|
|
|
45
47
|
this.bridgeFactory = options.bridgeFactory;
|
|
46
48
|
this.sceneRuntimeFactory = options.sceneRuntimeFactory;
|
|
47
49
|
this.suspendHiddenScenes = options.suspendHiddenScenes;
|
|
50
|
+
this.renderer = options.renderer;
|
|
48
51
|
this.visibilityDocument = options.visibilityDocument;
|
|
49
52
|
this.scenes = options.manifest.scenes;
|
|
50
53
|
this.selectedSceneId = options.initialSceneId && options.manifest.scenes.some((scene) => scene.id === options.initialSceneId) ? options.initialSceneId : options.manifest.scenes.find((scene) => scene.id === options.manifest.defaultSceneId)?.id ?? options.manifest.defaultSceneId;
|
|
@@ -103,7 +106,8 @@ var InternalWebHostAppController = class {
|
|
|
103
106
|
style: this.style,
|
|
104
107
|
bridge,
|
|
105
108
|
onInput: (chunk) => bridge.sendInput(chunk),
|
|
106
|
-
suspendWhenHidden: this.suspendHiddenScenes
|
|
109
|
+
suspendWhenHidden: this.suspendHiddenScenes,
|
|
110
|
+
renderer: this.renderer
|
|
107
111
|
});
|
|
108
112
|
this.bridges.set(id, bridge);
|
|
109
113
|
this.runtimes.set(id, runtime);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebHostApp.js","names":[],"sources":["../../src/WebHostApp.ts"],"sourcesContent":["import { BrowserWASIBridge } from \"./wasi/BrowserWASIBridge.ts\";\nimport {\n WebSocketSceneBridge,\n type WebSocketSceneBridgeOptions,\n} from \"./WebSocketSceneBridge.ts\";\nimport {\n loadWebHostSceneManifest,\n normalizeWebHostSceneManifest,\n type WebHostSceneDescriptor,\n type WebHostSceneManifest,\n type WebHostSceneManifestSource,\n} from \"./WebHostSceneManifest.ts\";\nimport {\n mergeWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n WebHostSceneRuntime,\n type WebHostSceneBridge,\n type WebHostSceneRuntimeOptions,\n} from \"./WebHostSceneRuntime.ts\";\n\nexport interface WebHostEmbeddedHostConfig {\n token: string;\n webSocketBaseURL?: string | URL;\n webSocketFactory?: WebSocketSceneBridgeOptions[\"webSocketFactory\"];\n}\n\nexport interface WebHostBridgeFactoryOptions {\n sceneId: string;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n environment?: Record<string, string>;\n}\n\nexport type WebHostBridgeFactory = (options: WebHostBridgeFactoryOptions) => WebHostSceneBridge;\n\n/**\n * The slice of `Document` the app controller needs to track page visibility.\n * Injectable for tests and non-browser hosts; defaults to the global\n * `document` when one exists.\n */\nexport interface WebHostVisibilityDocument {\n readonly hidden: boolean;\n addEventListener(type: \"visibilitychange\", listener: () => void): void;\n removeEventListener(type: \"visibilitychange\", listener: () => void): void;\n}\n\nexport interface WebHostAppOptions {\n mount: HTMLElement;\n manifest?: WebHostSceneManifestSource;\n manifestUrl?: string | URL;\n initialSceneId?: string;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory?: (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n /**\n * Whether scenes that cannot be seen — background scenes after a switch,\n * or every scene while the document is hidden — suspend their apps (run\n * loop parked, monotonic clock frozen) instead of burning CPU. Forwarded to\n * each scene runtime as `suspendWhenHidden`. Defaults to `true`.\n */\n suspendHiddenScenes?: boolean;\n /** Visibility source override; defaults to the global `document`. */\n visibilityDocument?: WebHostVisibilityDocument;\n}\n\nexport interface WebHostAppController {\n scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n switchScene(id: string): Promise<void>;\n setStyle(style: WebHostTerminalStyle): void;\n dispose(): Promise<void>;\n}\n\ntype RuntimeFactory = (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n\nexport async function createWebHostApp(\n options: WebHostAppOptions\n): Promise<WebHostAppController> {\n const manifest = await resolveManifest(options);\n const controller = new InternalWebHostAppController({\n mount: options.mount,\n manifest,\n style: options.style,\n environment: options.environment,\n embeddedHost: options.embeddedHost,\n bridgeFactory: options.bridgeFactory,\n initialSceneId: options.initialSceneId,\n createElement: options.createElement,\n sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions)),\n suspendHiddenScenes: options.suspendHiddenScenes,\n visibilityDocument: options.visibilityDocument ?? defaultVisibilityDocument(),\n });\n await controller.initialize();\n return controller;\n}\n\nclass InternalWebHostAppController implements WebHostAppController {\n readonly scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n\n private readonly mount: HTMLElement;\n private readonly sceneRoot: HTMLElement;\n private style: ResolvedWebHostTerminalStyle;\n private readonly environment?: Record<string, string>;\n private readonly embeddedHost?: WebHostEmbeddedHostConfig;\n private readonly bridgeFactory?: WebHostBridgeFactory;\n private readonly sceneRuntimeFactory: RuntimeFactory;\n private readonly runtimes = new Map<string, WebHostSceneRuntime>();\n private readonly bridges = new Map<string, WebHostSceneBridge>();\n private readonly suspendHiddenScenes?: boolean;\n private readonly visibilityDocument?: WebHostVisibilityDocument;\n private detachVisibilityListener?: () => void;\n\n constructor(options: {\n mount: HTMLElement;\n manifest: WebHostSceneManifest;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n initialSceneId?: string;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory: RuntimeFactory;\n suspendHiddenScenes?: boolean;\n visibilityDocument?: WebHostVisibilityDocument;\n }) {\n this.mount = options.mount;\n this.style = normalizeWebHostTerminalStyle(options.style ?? {});\n this.environment = options.environment;\n this.embeddedHost = options.embeddedHost;\n this.bridgeFactory = options.bridgeFactory;\n this.sceneRuntimeFactory = options.sceneRuntimeFactory;\n this.suspendHiddenScenes = options.suspendHiddenScenes;\n this.visibilityDocument = options.visibilityDocument;\n this.scenes = options.manifest.scenes;\n this.selectedSceneId =\n options.initialSceneId &&\n options.manifest.scenes.some((scene) => scene.id === options.initialSceneId)\n ? options.initialSceneId\n : options.manifest.scenes.find((scene) => scene.id === options.manifest.defaultSceneId)?.id ??\n options.manifest.defaultSceneId;\n\n this.sceneRoot = (options.createElement ?? defaultCreateElement)(\"div\");\n this.sceneRoot.className = \"webhost-scene-root\";\n this.mount.replaceChildren(this.sceneRoot);\n this.applyHostFrameStyle();\n }\n\n async initialize(): Promise<void> {\n this.installVisibilityListener();\n await this.ensureRuntime(this.selectedSceneId);\n await this.switchScene(this.selectedSceneId);\n }\n\n async switchScene(\n id: string\n ): Promise<void> {\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n for (const [sceneId, runtime] of this.runtimes) {\n runtime.setVisible(sceneId === id);\n }\n\n const runtime = await this.ensureRuntime(id);\n runtime.setVisible(true);\n this.selectedSceneId = id;\n }\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n const merged = mergeWebHostTerminalStyle(this.style, style);\n this.style = merged;\n\n for (const runtime of this.runtimes.values()) {\n runtime.setStyle(this.style);\n }\n this.applyHostFrameStyle();\n }\n\n async dispose(): Promise<void> {\n this.detachVisibilityListener?.();\n this.detachVisibilityListener = undefined;\n for (const runtime of this.runtimes.values()) {\n runtime.dispose();\n }\n for (const bridge of this.bridges.values()) {\n bridge.dispose();\n }\n this.runtimes.clear();\n this.bridges.clear();\n this.mount.replaceChildren();\n }\n\n private installVisibilityListener(): void {\n const visibilityDocument = this.visibilityDocument;\n if (!visibilityDocument) {\n return;\n }\n const listener = (): void => {\n const visible = !visibilityDocument.hidden;\n for (const runtime of this.runtimes.values()) {\n runtime.setDocumentVisible(visible);\n }\n };\n visibilityDocument.addEventListener(\"visibilitychange\", listener);\n this.detachVisibilityListener = () => {\n visibilityDocument.removeEventListener(\"visibilitychange\", listener);\n };\n }\n\n private async ensureRuntime(\n id: string\n ): Promise<WebHostSceneRuntime> {\n const existing = this.runtimes.get(id);\n if (existing) {\n return existing;\n }\n\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n const bridge = this.makeBridge(id, descriptor);\n const runtime = this.sceneRuntimeFactory({\n mount: this.sceneRoot,\n descriptor,\n style: this.style,\n bridge,\n onInput: (chunk) => bridge.sendInput(chunk),\n suspendWhenHidden: this.suspendHiddenScenes,\n });\n\n this.bridges.set(id, bridge);\n this.runtimes.set(id, runtime);\n await runtime.mount();\n runtime.setVisible(id === this.selectedSceneId);\n if (this.visibilityDocument) {\n runtime.setDocumentVisible(!this.visibilityDocument.hidden);\n }\n return runtime;\n }\n\n private makeBridge(\n sceneId: string,\n descriptor: WebHostSceneDescriptor\n ): WebHostSceneBridge {\n if (this.bridgeFactory) {\n return this.bridgeFactory({\n sceneId,\n descriptor,\n style: this.style,\n environment: this.environment,\n });\n }\n\n if (this.embeddedHost) {\n return new WebSocketSceneBridge({\n sceneId,\n token: this.embeddedHost.token,\n baseURL: this.embeddedHost.webSocketBaseURL,\n webSocketFactory: this.embeddedHost.webSocketFactory,\n });\n }\n\n return new BrowserWASIBridge({\n sceneId,\n columns: 80,\n rows: 24,\n environment: this.environment,\n renderStyle: this.style,\n });\n }\n\n private applyHostFrameStyle(): void {\n this.mount.style.background = \"linear-gradient(180deg, #0f172a 0%, #111827 100%)\";\n this.mount.style.minHeight = \"100%\";\n this.mount.style.display = \"block\";\n this.mount.style.padding = \"1rem\";\n }\n}\n\nfunction defaultVisibilityDocument(): WebHostVisibilityDocument | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n return document;\n}\n\nfunction defaultCreateElement(\n tagName: string\n): HTMLElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"document is not available\");\n }\n\n return document.createElement(tagName);\n}\n\nasync function resolveManifest(\n options: WebHostAppOptions\n): Promise<WebHostSceneManifest> {\n if (options.manifest) {\n return loadWebHostSceneManifest(options.manifest);\n }\n\n if (options.manifestUrl) {\n return loadWebHostSceneManifest(options.manifestUrl);\n }\n\n return normalizeWebHostSceneManifest([\n {\n id: \"main\",\n title: \"Main\",\n isDefault: true,\n },\n ]);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WebHostApp.js","names":[],"sources":["../../src/WebHostApp.ts"],"sourcesContent":["import { BrowserWASIBridge } from \"./wasi/BrowserWASIBridge.ts\";\nimport {\n WebSocketSceneBridge,\n type WebSocketSceneBridgeOptions,\n} from \"./WebSocketSceneBridge.ts\";\nimport {\n loadWebHostSceneManifest,\n normalizeWebHostSceneManifest,\n type WebHostSceneDescriptor,\n type WebHostSceneManifest,\n type WebHostSceneManifestSource,\n} from \"./WebHostSceneManifest.ts\";\nimport {\n mergeWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n WebHostSceneRuntime,\n type WebHostSceneBridge,\n type WebHostSceneRuntimeOptions,\n} from \"./WebHostSceneRuntime.ts\";\nimport type { WebHostSurfaceRendererKind } from \"./SurfaceRenderer.ts\";\n\nexport interface WebHostEmbeddedHostConfig {\n token: string;\n webSocketBaseURL?: string | URL;\n webSocketFactory?: WebSocketSceneBridgeOptions[\"webSocketFactory\"];\n}\n\nexport interface WebHostBridgeFactoryOptions {\n sceneId: string;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n environment?: Record<string, string>;\n}\n\nexport type WebHostBridgeFactory = (options: WebHostBridgeFactoryOptions) => WebHostSceneBridge;\n\n/**\n * The slice of `Document` the app controller needs to track page visibility.\n * Injectable for tests and non-browser hosts; defaults to the global\n * `document` when one exists.\n */\nexport interface WebHostVisibilityDocument {\n readonly hidden: boolean;\n addEventListener(type: \"visibilitychange\", listener: () => void): void;\n removeEventListener(type: \"visibilitychange\", listener: () => void): void;\n}\n\nexport interface WebHostAppOptions {\n mount: HTMLElement;\n manifest?: WebHostSceneManifestSource;\n manifestUrl?: string | URL;\n initialSceneId?: string;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory?: (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n /**\n * Whether scenes that cannot be seen — background scenes after a switch,\n * or every scene while the document is hidden — suspend their apps (run\n * loop parked, monotonic clock frozen) instead of burning CPU. Forwarded to\n * each scene runtime as `suspendWhenHidden`. Defaults to `true`.\n */\n suspendHiddenScenes?: boolean;\n /** Visibility source override; defaults to the global `document`. */\n visibilityDocument?: WebHostVisibilityDocument;\n /**\n * Which surface presenter every scene runtime uses: `\"canvas\"` (default)\n * paints frames onto a 2D `<canvas>`; `\"dom\"` renders them as absolutely\n * positioned text elements. Forwarded to each scene runtime as `renderer`.\n * See {@link WebHostSurfaceRendererKind}.\n */\n renderer?: WebHostSurfaceRendererKind;\n}\n\nexport interface WebHostAppController {\n scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n switchScene(id: string): Promise<void>;\n setStyle(style: WebHostTerminalStyle): void;\n dispose(): Promise<void>;\n}\n\ntype RuntimeFactory = (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n\nexport async function createWebHostApp(\n options: WebHostAppOptions\n): Promise<WebHostAppController> {\n const manifest = await resolveManifest(options);\n const controller = new InternalWebHostAppController({\n mount: options.mount,\n manifest,\n style: options.style,\n environment: options.environment,\n embeddedHost: options.embeddedHost,\n bridgeFactory: options.bridgeFactory,\n initialSceneId: options.initialSceneId,\n createElement: options.createElement,\n sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions)),\n suspendHiddenScenes: options.suspendHiddenScenes,\n visibilityDocument: options.visibilityDocument ?? defaultVisibilityDocument(),\n renderer: options.renderer,\n });\n await controller.initialize();\n return controller;\n}\n\nclass InternalWebHostAppController implements WebHostAppController {\n readonly scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n\n private readonly mount: HTMLElement;\n private readonly sceneRoot: HTMLElement;\n private style: ResolvedWebHostTerminalStyle;\n private readonly environment?: Record<string, string>;\n private readonly embeddedHost?: WebHostEmbeddedHostConfig;\n private readonly bridgeFactory?: WebHostBridgeFactory;\n private readonly sceneRuntimeFactory: RuntimeFactory;\n private readonly runtimes = new Map<string, WebHostSceneRuntime>();\n private readonly bridges = new Map<string, WebHostSceneBridge>();\n private readonly suspendHiddenScenes?: boolean;\n private readonly renderer?: WebHostSurfaceRendererKind;\n private readonly visibilityDocument?: WebHostVisibilityDocument;\n private detachVisibilityListener?: () => void;\n\n constructor(options: {\n mount: HTMLElement;\n manifest: WebHostSceneManifest;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n initialSceneId?: string;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory: RuntimeFactory;\n suspendHiddenScenes?: boolean;\n visibilityDocument?: WebHostVisibilityDocument;\n renderer?: WebHostSurfaceRendererKind;\n }) {\n this.mount = options.mount;\n this.style = normalizeWebHostTerminalStyle(options.style ?? {});\n this.environment = options.environment;\n this.embeddedHost = options.embeddedHost;\n this.bridgeFactory = options.bridgeFactory;\n this.sceneRuntimeFactory = options.sceneRuntimeFactory;\n this.suspendHiddenScenes = options.suspendHiddenScenes;\n this.renderer = options.renderer;\n this.visibilityDocument = options.visibilityDocument;\n this.scenes = options.manifest.scenes;\n this.selectedSceneId =\n options.initialSceneId &&\n options.manifest.scenes.some((scene) => scene.id === options.initialSceneId)\n ? options.initialSceneId\n : options.manifest.scenes.find((scene) => scene.id === options.manifest.defaultSceneId)?.id ??\n options.manifest.defaultSceneId;\n\n this.sceneRoot = (options.createElement ?? defaultCreateElement)(\"div\");\n this.sceneRoot.className = \"webhost-scene-root\";\n this.mount.replaceChildren(this.sceneRoot);\n this.applyHostFrameStyle();\n }\n\n async initialize(): Promise<void> {\n this.installVisibilityListener();\n await this.ensureRuntime(this.selectedSceneId);\n await this.switchScene(this.selectedSceneId);\n }\n\n async switchScene(\n id: string\n ): Promise<void> {\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n for (const [sceneId, runtime] of this.runtimes) {\n runtime.setVisible(sceneId === id);\n }\n\n const runtime = await this.ensureRuntime(id);\n runtime.setVisible(true);\n this.selectedSceneId = id;\n }\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n const merged = mergeWebHostTerminalStyle(this.style, style);\n this.style = merged;\n\n for (const runtime of this.runtimes.values()) {\n runtime.setStyle(this.style);\n }\n this.applyHostFrameStyle();\n }\n\n async dispose(): Promise<void> {\n this.detachVisibilityListener?.();\n this.detachVisibilityListener = undefined;\n for (const runtime of this.runtimes.values()) {\n runtime.dispose();\n }\n for (const bridge of this.bridges.values()) {\n bridge.dispose();\n }\n this.runtimes.clear();\n this.bridges.clear();\n this.mount.replaceChildren();\n }\n\n private installVisibilityListener(): void {\n const visibilityDocument = this.visibilityDocument;\n if (!visibilityDocument) {\n return;\n }\n const listener = (): void => {\n const visible = !visibilityDocument.hidden;\n for (const runtime of this.runtimes.values()) {\n runtime.setDocumentVisible(visible);\n }\n };\n visibilityDocument.addEventListener(\"visibilitychange\", listener);\n this.detachVisibilityListener = () => {\n visibilityDocument.removeEventListener(\"visibilitychange\", listener);\n };\n }\n\n private async ensureRuntime(\n id: string\n ): Promise<WebHostSceneRuntime> {\n const existing = this.runtimes.get(id);\n if (existing) {\n return existing;\n }\n\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n const bridge = this.makeBridge(id, descriptor);\n const runtime = this.sceneRuntimeFactory({\n mount: this.sceneRoot,\n descriptor,\n style: this.style,\n bridge,\n onInput: (chunk) => bridge.sendInput(chunk),\n suspendWhenHidden: this.suspendHiddenScenes,\n renderer: this.renderer,\n });\n\n this.bridges.set(id, bridge);\n this.runtimes.set(id, runtime);\n await runtime.mount();\n runtime.setVisible(id === this.selectedSceneId);\n if (this.visibilityDocument) {\n runtime.setDocumentVisible(!this.visibilityDocument.hidden);\n }\n return runtime;\n }\n\n private makeBridge(\n sceneId: string,\n descriptor: WebHostSceneDescriptor\n ): WebHostSceneBridge {\n if (this.bridgeFactory) {\n return this.bridgeFactory({\n sceneId,\n descriptor,\n style: this.style,\n environment: this.environment,\n });\n }\n\n if (this.embeddedHost) {\n return new WebSocketSceneBridge({\n sceneId,\n token: this.embeddedHost.token,\n baseURL: this.embeddedHost.webSocketBaseURL,\n webSocketFactory: this.embeddedHost.webSocketFactory,\n });\n }\n\n return new BrowserWASIBridge({\n sceneId,\n columns: 80,\n rows: 24,\n environment: this.environment,\n renderStyle: this.style,\n });\n }\n\n private applyHostFrameStyle(): void {\n this.mount.style.background = \"linear-gradient(180deg, #0f172a 0%, #111827 100%)\";\n this.mount.style.minHeight = \"100%\";\n this.mount.style.display = \"block\";\n this.mount.style.padding = \"1rem\";\n }\n}\n\nfunction defaultVisibilityDocument(): WebHostVisibilityDocument | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n return document;\n}\n\nfunction defaultCreateElement(\n tagName: string\n): HTMLElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"document is not available\");\n }\n\n return document.createElement(tagName);\n}\n\nasync function resolveManifest(\n options: WebHostAppOptions\n): Promise<WebHostSceneManifest> {\n if (options.manifest) {\n return loadWebHostSceneManifest(options.manifest);\n }\n\n if (options.manifestUrl) {\n return loadWebHostSceneManifest(options.manifestUrl);\n }\n\n return normalizeWebHostSceneManifest([\n {\n id: \"main\",\n title: \"Main\",\n isDefault: true,\n },\n ]);\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WebHostTerminalStyle } from "./WebHostTerminalStyle.js";
|
|
2
2
|
import { WebHostFocusPresentation, WebHostFrameDiagnosticRecord, WebHostOutputSink, WebHostRuntimeIssue } from "./WebHostSurfaceTransport.js";
|
|
3
|
+
import { WebHostSurfaceRendererKind } from "./SurfaceRenderer.js";
|
|
3
4
|
import { WebHostSceneDescriptor } from "./WebHostSceneManifest.js";
|
|
4
5
|
//#region src/WebHostSceneRuntime.d.ts
|
|
5
6
|
interface WebHostSceneBridge {
|
|
@@ -54,6 +55,14 @@ interface WebHostSceneRuntimeOptions {
|
|
|
54
55
|
* `false` to let background scenes keep running (pre-suspension behavior).
|
|
55
56
|
*/
|
|
56
57
|
suspendWhenHidden?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Which surface presenter draws the scene's frames. `"canvas"` (default)
|
|
60
|
+
* paints onto a 2D `<canvas>`; `"dom"` renders cells as absolutely
|
|
61
|
+
* positioned text elements — native font rendering and, uniquely, real
|
|
62
|
+
* text selection: hold Alt/Option and drag to select instead of sending
|
|
63
|
+
* pointer input to the app. See {@link WebHostSurfaceRendererKind}.
|
|
64
|
+
*/
|
|
65
|
+
renderer?: WebHostSurfaceRendererKind;
|
|
57
66
|
}
|
|
58
67
|
type WheelMode = "capture" | "chain" | "passive";
|
|
59
68
|
/**
|
|
@@ -72,10 +81,13 @@ declare class WebHostSceneRuntime {
|
|
|
72
81
|
private readonly onFrameDiagnostic?;
|
|
73
82
|
private readonly synchronizeAccessibilityFocus;
|
|
74
83
|
private readonly wheelMode;
|
|
84
|
+
private readonly rendererKind;
|
|
75
85
|
private readonly painter;
|
|
76
86
|
private readonly inputEncoder;
|
|
77
87
|
private currentStyle;
|
|
78
88
|
private canvas?;
|
|
89
|
+
private domSurfaceRoot?;
|
|
90
|
+
private lastDomSurfaceSize?;
|
|
79
91
|
private accessibilityTree?;
|
|
80
92
|
private diagnosticText?;
|
|
81
93
|
private resizeObserver?;
|
|
@@ -138,17 +150,26 @@ declare class WebHostSceneRuntime {
|
|
|
138
150
|
private linkTarget;
|
|
139
151
|
private openHyperlink;
|
|
140
152
|
private applyStyle;
|
|
153
|
+
/** The element the active painter presents frames into. */
|
|
154
|
+
private get surfaceElement();
|
|
141
155
|
private applyVisibility;
|
|
142
156
|
private installResizeObserver;
|
|
143
157
|
private installInputHandlers;
|
|
144
158
|
private resizeToMount;
|
|
145
159
|
private sendResizeIfNeeded;
|
|
146
|
-
private
|
|
160
|
+
private resizeSurface;
|
|
147
161
|
private measureCells;
|
|
148
162
|
private draw;
|
|
149
163
|
private syncAccessibilityTree;
|
|
150
164
|
private surfaceMetrics;
|
|
151
165
|
private pointerMetrics;
|
|
166
|
+
/**
|
|
167
|
+
* Whether this pointer event should be left to the browser for native text
|
|
168
|
+
* selection instead of being forwarded to the app. Only the DOM renderer
|
|
169
|
+
* has real text nodes to select, and only while Alt/Option is held — plain
|
|
170
|
+
* pointer input still belongs to the app.
|
|
171
|
+
*/
|
|
172
|
+
private allowsNativeTextSelection;
|
|
152
173
|
private cellLocation;
|
|
153
174
|
private rawCellLocation;
|
|
154
175
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyWebHostTerminalStyle, normalizeWebHostTerminalStyle, webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
|
|
2
2
|
import { CanvasSurfacePainter, fontForStyle } from "./CanvasSurfacePainter.js";
|
|
3
|
+
import { DomSurfacePainter } from "./DomSurfacePainter.js";
|
|
3
4
|
import { InputEventEncoder } from "./InputEventEncoder.js";
|
|
4
5
|
import { cellLocationForEvent, linkTargetAt, rawCellLocationForEvent, wheelTargetCanScroll } from "./PointerGeometry.js";
|
|
5
6
|
import { AccessibilityTreeMounter } from "./AccessibilityTree.js";
|
|
@@ -30,10 +31,13 @@ var WebHostSceneRuntime = class {
|
|
|
30
31
|
onFrameDiagnostic;
|
|
31
32
|
synchronizeAccessibilityFocus;
|
|
32
33
|
wheelMode;
|
|
33
|
-
|
|
34
|
+
rendererKind;
|
|
35
|
+
painter;
|
|
34
36
|
inputEncoder = new InputEventEncoder();
|
|
35
37
|
currentStyle;
|
|
36
38
|
canvas;
|
|
39
|
+
domSurfaceRoot;
|
|
40
|
+
lastDomSurfaceSize;
|
|
37
41
|
accessibilityTree;
|
|
38
42
|
diagnosticText;
|
|
39
43
|
resizeObserver;
|
|
@@ -60,6 +64,8 @@ var WebHostSceneRuntime = class {
|
|
|
60
64
|
this.onFrameDiagnostic = options.onFrameDiagnostic;
|
|
61
65
|
this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;
|
|
62
66
|
this.wheelMode = options.wheelMode ?? legacyWheelMode(options.captureWheelInput);
|
|
67
|
+
this.rendererKind = options.renderer ?? "canvas";
|
|
68
|
+
this.painter = this.rendererKind === "dom" ? new DomSurfacePainter() : new CanvasSurfacePainter();
|
|
63
69
|
this.onOpenHyperlink = options.onOpenHyperlink;
|
|
64
70
|
this.suspendWhenHidden = options.suspendWhenHidden ?? true;
|
|
65
71
|
this.element = document.createElement("section");
|
|
@@ -77,14 +83,22 @@ var WebHostSceneRuntime = class {
|
|
|
77
83
|
this.applyVisibility();
|
|
78
84
|
}
|
|
79
85
|
async mount() {
|
|
80
|
-
if (this.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
if (this.surfaceElement) return;
|
|
87
|
+
if (this.painter instanceof DomSurfacePainter) {
|
|
88
|
+
const surfaceRoot = document.createElement("div");
|
|
89
|
+
surfaceRoot.className = "webhost-scene__surface webhost-scene__surface--dom";
|
|
90
|
+
surfaceRoot.setAttribute("aria-hidden", "true");
|
|
91
|
+
this.domSurfaceRoot = surfaceRoot;
|
|
92
|
+
this.painter.attach(surfaceRoot);
|
|
93
|
+
} else {
|
|
94
|
+
const canvas = document.createElement("canvas");
|
|
95
|
+
canvas.className = "webhost-scene__surface";
|
|
96
|
+
canvas.setAttribute("aria-hidden", "true");
|
|
97
|
+
this.canvas = canvas;
|
|
98
|
+
this.painter.attach(canvas, () => this.draw());
|
|
99
|
+
}
|
|
86
100
|
this.accessibilityTree = new AccessibilityTreeMounter();
|
|
87
|
-
this.terminalMount.replaceChildren(
|
|
101
|
+
this.terminalMount.replaceChildren(this.surfaceElement, this.accessibilityTree.element, this.accessibilityTree.announcerElement);
|
|
88
102
|
this.installInputHandlers();
|
|
89
103
|
this.installResizeObserver();
|
|
90
104
|
this.bridge?.bindOutput({
|
|
@@ -144,7 +158,7 @@ var WebHostSceneRuntime = class {
|
|
|
144
158
|
resize(columns, rows) {
|
|
145
159
|
this.columns = Math.max(1, Math.round(columns));
|
|
146
160
|
this.rows = Math.max(1, Math.round(rows));
|
|
147
|
-
this.
|
|
161
|
+
this.resizeSurface();
|
|
148
162
|
this.draw();
|
|
149
163
|
this.syncAccessibilityTree();
|
|
150
164
|
}
|
|
@@ -183,7 +197,7 @@ var WebHostSceneRuntime = class {
|
|
|
183
197
|
this.currentFrame = frame;
|
|
184
198
|
this.columns = Math.max(1, Math.round(frame.width));
|
|
185
199
|
this.rows = Math.max(1, Math.round(frame.height));
|
|
186
|
-
const resized = this.
|
|
200
|
+
const resized = this.resizeSurface();
|
|
187
201
|
this.draw(previousFrame && !resized ? frame.damage : void 0);
|
|
188
202
|
this.syncAccessibilityTree();
|
|
189
203
|
}
|
|
@@ -238,6 +252,14 @@ var WebHostSceneRuntime = class {
|
|
|
238
252
|
this.canvas.style.width = "100%";
|
|
239
253
|
this.canvas.style.height = "100%";
|
|
240
254
|
}
|
|
255
|
+
if (this.domSurfaceRoot) {
|
|
256
|
+
this.domSurfaceRoot.style.display = "block";
|
|
257
|
+
this.domSurfaceRoot.style.position = "relative";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** The element the active painter presents frames into. */
|
|
261
|
+
get surfaceElement() {
|
|
262
|
+
return this.canvas ?? this.domSurfaceRoot;
|
|
241
263
|
}
|
|
242
264
|
applyVisibility() {
|
|
243
265
|
this.element.hidden = !this.isVisible;
|
|
@@ -265,6 +287,7 @@ var WebHostSceneRuntime = class {
|
|
|
265
287
|
event.preventDefault();
|
|
266
288
|
};
|
|
267
289
|
const handlePointerDown = (event) => {
|
|
290
|
+
if (this.allowsNativeTextSelection(event)) return;
|
|
268
291
|
const location = this.cellLocation(event);
|
|
269
292
|
if (!location) return;
|
|
270
293
|
const button = this.inputEncoder.pointerButton(event.button);
|
|
@@ -277,6 +300,7 @@ var WebHostSceneRuntime = class {
|
|
|
277
300
|
event.preventDefault();
|
|
278
301
|
};
|
|
279
302
|
const handlePointerUp = (event) => {
|
|
303
|
+
if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) return;
|
|
280
304
|
const location = this.hasCapturedPointer ? this.rawCellLocation(event) : this.cellLocation(event);
|
|
281
305
|
this.terminalMount.releasePointerCapture?.(event.pointerId);
|
|
282
306
|
this.hasCapturedPointer = false;
|
|
@@ -289,6 +313,7 @@ var WebHostSceneRuntime = class {
|
|
|
289
313
|
event.preventDefault();
|
|
290
314
|
};
|
|
291
315
|
const handlePointerMove = (event) => {
|
|
316
|
+
if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) return;
|
|
292
317
|
const location = event.buttons && this.hasCapturedPointer ? this.rawCellLocation(event) : this.cellLocation(event);
|
|
293
318
|
if (!location) return;
|
|
294
319
|
if (!this.hasCapturedPointer) this.terminalMount.style.cursor = this.linkTarget(location) !== void 0 ? "pointer" : "";
|
|
@@ -327,7 +352,7 @@ var WebHostSceneRuntime = class {
|
|
|
327
352
|
this.columns = nextColumns;
|
|
328
353
|
this.rows = nextRows;
|
|
329
354
|
this.sendResizeIfNeeded();
|
|
330
|
-
this.
|
|
355
|
+
this.resizeSurface();
|
|
331
356
|
}
|
|
332
357
|
sendResizeIfNeeded() {
|
|
333
358
|
const current = {
|
|
@@ -340,10 +365,21 @@ var WebHostSceneRuntime = class {
|
|
|
340
365
|
this.lastSentResize = current;
|
|
341
366
|
this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);
|
|
342
367
|
}
|
|
343
|
-
|
|
344
|
-
if (!this.canvas) return false;
|
|
368
|
+
resizeSurface() {
|
|
345
369
|
const cssWidth = Math.max(1, this.columns * this.cellWidth);
|
|
346
370
|
const cssHeight = Math.max(1, this.rows * this.cellHeight);
|
|
371
|
+
if (this.domSurfaceRoot) {
|
|
372
|
+
const last = this.lastDomSurfaceSize;
|
|
373
|
+
if (last && last.width === cssWidth && last.height === cssHeight) return false;
|
|
374
|
+
this.lastDomSurfaceSize = {
|
|
375
|
+
width: cssWidth,
|
|
376
|
+
height: cssHeight
|
|
377
|
+
};
|
|
378
|
+
this.domSurfaceRoot.style.width = `${cssWidth}px`;
|
|
379
|
+
this.domSurfaceRoot.style.height = `${cssHeight}px`;
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
if (!this.canvas) return false;
|
|
347
383
|
const scale = globalThis.window?.devicePixelRatio || 1;
|
|
348
384
|
const width = Math.ceil(cssWidth * scale);
|
|
349
385
|
const height = Math.ceil(cssHeight * scale);
|
|
@@ -389,13 +425,22 @@ var WebHostSceneRuntime = class {
|
|
|
389
425
|
}
|
|
390
426
|
pointerMetrics() {
|
|
391
427
|
return {
|
|
392
|
-
rect: this.
|
|
428
|
+
rect: this.surfaceElement?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),
|
|
393
429
|
cellWidth: this.cellWidth,
|
|
394
430
|
cellHeight: this.cellHeight,
|
|
395
431
|
columns: this.columns,
|
|
396
432
|
rows: this.rows
|
|
397
433
|
};
|
|
398
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Whether this pointer event should be left to the browser for native text
|
|
437
|
+
* selection instead of being forwarded to the app. Only the DOM renderer
|
|
438
|
+
* has real text nodes to select, and only while Alt/Option is held — plain
|
|
439
|
+
* pointer input still belongs to the app.
|
|
440
|
+
*/
|
|
441
|
+
allowsNativeTextSelection(event) {
|
|
442
|
+
return this.rendererKind === "dom" && event.altKey;
|
|
443
|
+
}
|
|
399
444
|
cellLocation(event) {
|
|
400
445
|
return cellLocationForEvent(event, this.pointerMetrics());
|
|
401
446
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WebHostSceneRuntime.js","names":[],"sources":["../../src/WebHostSceneRuntime.ts"],"sourcesContent":["import {\n applyWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n CanvasSurfacePainter,\n fontForStyle,\n type CanvasSurfaceMetrics,\n} from \"./CanvasSurfacePainter.ts\";\nimport {\n InputEventEncoder,\n type CellLocation,\n type PointerButton,\n} from \"./InputEventEncoder.ts\";\nimport {\n cellLocationForEvent,\n linkTargetAt,\n rawCellLocationForEvent,\n wheelTargetCanScroll,\n type PointerGeometryMetrics,\n} from \"./PointerGeometry.ts\";\nimport { AccessibilityTreeMounter } from \"./AccessibilityTree.ts\";\nimport {\n type WebHostFocusPresentation,\n type WebHostFrameDiagnosticRecord,\n type WebHostOutputSink,\n type WebHostRuntimeIssue,\n type WebHostSurfaceDamage,\n type WebHostSurfaceFrame,\n} from \"./WebHostSurfaceTransport.ts\";\nimport type { WebHostSceneDescriptor } from \"./WebHostSceneManifest.ts\";\n\nexport interface WebHostSceneBridge {\n bindOutput(sink: WebHostOutputSink): void;\n resize(columns: number, rows: number, cellWidth?: number, cellHeight?: number): void;\n updateRenderStyle(style: WebHostTerminalStyle): void;\n sendInput(chunk: Uint8Array): void;\n dispose(): void;\n}\n\nexport interface WebHostSceneRuntimeOptions {\n mount: HTMLElement;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n bridge?: WebHostSceneBridge;\n onInput(chunk: Uint8Array): void;\n onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n synchronizeAccessibilityFocus?: boolean;\n /**\n * How the embedded view treats mouse-wheel input.\n * - `\"chain\"` (default): forward the wheel only while a scrollable region\n * under the pointer can still scroll in that direction; otherwise let it\n * fall through so the page (or parent iframe) scrolls — iframe-like nested\n * scrolling. A scene with no `ScrollView` never traps the wheel. Uses the\n * `scrollRegions` the app publishes in its frames.\n * - `\"capture\"`: always forward the wheel to the app while the pointer is over\n * the surface (and `preventDefault` page scroll). Best for full-screen apps\n * where there is no page to scroll past.\n * - `\"passive\"`: never capture; the page always scrolls.\n *\n * Takes precedence over the legacy `captureWheelInput` flag.\n */\n wheelMode?: WheelMode;\n /**\n * Legacy boolean wheel gate. `true` → `\"capture\"`, `false` → `\"passive\"`.\n * Prefer `wheelMode`. Ignored when `wheelMode` is set. When neither is set the\n * mode defaults to `\"chain\"`.\n */\n captureWheelInput?: boolean;\n /**\n * Called when the user clicks a hyperlink cell (a click is a pointer-down\n * and pointer-up over the same link target). When unset, `http(s)` targets\n * open in a new tab via `window.open(url, \"_blank\", \"noopener,noreferrer\")`\n * and other schemes are ignored. Mirrors the Android host's tap-to-open.\n */\n onOpenHyperlink?: (url: string) => void;\n /**\n * Whether to suspend the scene's app while it cannot be seen — when the\n * scene is switched to the background (`setVisible(false)`) or the whole\n * document is hidden (`setDocumentVisible(false)`). Suspension parks the\n * app's run loop and freezes its monotonic clock, so a hidden scene costs\n * no CPU and resumes exactly where it left off. Defaults to `true`; set\n * `false` to let background scenes keep running (pre-suspension behavior).\n */\n suspendWhenHidden?: boolean;\n}\n\nexport type WheelMode = \"capture\" | \"chain\" | \"passive\";\n\n/**\n * Resolves the legacy `captureWheelInput` flag to a {@link WheelMode}. When the\n * flag is unset the mode defaults to `\"chain\"`, so embeds never trap a visitor\n * who is merely scrolling past the view; `true` maps to `\"capture\"` and `false`\n * to `\"passive\"` to preserve the old boolean behavior.\n */\nfunction legacyWheelMode(captureWheelInput: boolean | undefined): WheelMode {\n if (captureWheelInput === undefined) {\n return \"chain\";\n }\n return captureWheelInput ? \"capture\" : \"passive\";\n}\n\n/**\n * Coordinates a single SwiftTUI scene's browser presentation: it owns the DOM\n * mount, canvas, accessibility tree, and bridge wiring, and delegates the heavy\n * responsibilities to focused collaborators — {@link CanvasSurfacePainter} for\n * canvas drawing, {@link InputEventEncoder} for wire-message encoding, and the\n * {@link PointerGeometry} helpers for pixel→cell hit-testing and wheel chaining.\n */\nexport class WebHostSceneRuntime {\n readonly descriptor: WebHostSceneDescriptor;\n readonly element: HTMLElement;\n readonly terminalMount: HTMLElement;\n\n private readonly bridge?: WebHostSceneBridge;\n private readonly onInput: (chunk: Uint8Array) => void;\n private readonly onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n private readonly synchronizeAccessibilityFocus: boolean;\n private readonly wheelMode: WheelMode;\n private readonly painter = new CanvasSurfacePainter();\n private readonly inputEncoder = new InputEventEncoder();\n private currentStyle: ResolvedWebHostTerminalStyle;\n private canvas?: HTMLCanvasElement;\n private accessibilityTree?: AccessibilityTreeMounter;\n private diagnosticText?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private detachInputHandlers?: () => void;\n private currentFrame?: WebHostSurfaceFrame;\n private columns = 80;\n private rows = 24;\n private cellWidth = 8;\n private cellHeight = 18;\n private activePointerButton: PointerButton = \"primary\";\n private hasCapturedPointer = false;\n private readonly onOpenHyperlink?: (url: string) => void;\n private pointerDownLinkTarget?: string;\n private lastSentResize?: {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n };\n private isVisible = false;\n private documentVisible = true;\n private runtimeSuspended = false;\n private readonly suspendWhenHidden: boolean;\n\n constructor(options: WebHostSceneRuntimeOptions) {\n this.descriptor = options.descriptor;\n this.currentStyle = normalizeWebHostTerminalStyle(options.style);\n this.bridge = options.bridge;\n this.onInput = options.onInput;\n this.onFrameDiagnostic = options.onFrameDiagnostic;\n this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;\n this.wheelMode = options.wheelMode ?? legacyWheelMode(options.captureWheelInput);\n this.onOpenHyperlink = options.onOpenHyperlink;\n this.suspendWhenHidden = options.suspendWhenHidden ?? true;\n this.element = document.createElement(\"section\");\n this.element.className = \"webhost-scene\";\n this.element.dataset.sceneId = options.descriptor.id;\n this.element.hidden = true;\n\n const header = document.createElement(\"div\");\n header.className = \"webhost-scene__header\";\n header.textContent = options.descriptor.title ?? options.descriptor.id;\n\n this.terminalMount = document.createElement(\"div\");\n this.terminalMount.className = \"webhost-scene__terminal\";\n this.terminalMount.tabIndex = 0;\n\n this.element.append(header, this.terminalMount);\n options.mount.appendChild(this.element);\n this.applyVisibility();\n }\n\n async mount(): Promise<void> {\n if (this.canvas) {\n return;\n }\n\n const canvas = document.createElement(\"canvas\");\n canvas.className = \"webhost-scene__surface\";\n canvas.setAttribute(\"aria-hidden\", \"true\");\n this.canvas = canvas;\n this.painter.attach(canvas, () => this.draw());\n this.accessibilityTree = new AccessibilityTreeMounter();\n this.terminalMount.replaceChildren(\n canvas,\n this.accessibilityTree.element,\n this.accessibilityTree.announcerElement\n );\n this.installInputHandlers();\n this.installResizeObserver();\n\n this.bridge?.bindOutput({\n presentSurface: (frame) => this.presentSurface(frame),\n writeClipboard: (text) => this.writeClipboard(text),\n notifyRuntimeIssue: (issue) => this.notifyRuntimeIssue(issue),\n recordFrameDiagnostic: (diagnostic) => this.recordFrameDiagnostic(diagnostic),\n writeOutput: (text) => this.writeOutput(text),\n writeError: (text) => this.writeOutput(text),\n });\n\n this.applyStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n setVisible(\n visible: boolean\n ): void {\n this.isVisible = visible;\n this.applyVisibility();\n if (visible) {\n this.resizeToMount();\n if (this.synchronizeAccessibilityFocus) {\n this.terminalMount.focus?.({ preventScroll: true });\n }\n }\n this.updateRuntimeSuspension();\n }\n\n /**\n * Reports whether the surrounding document can be seen at all (browser tab\n * visible, iframe on-screen, …). Combined with the scene-level\n * `setVisible`: the app is suspended while either says hidden, unless\n * `suspendWhenHidden` is `false`.\n */\n setDocumentVisible(\n visible: boolean\n ): void {\n this.documentVisible = visible;\n this.updateRuntimeSuspension();\n }\n\n private updateRuntimeSuspension(): void {\n const suspended = this.suspendWhenHidden && (!this.isVisible || !this.documentVisible);\n if (suspended === this.runtimeSuspended) {\n return;\n }\n this.runtimeSuspended = suspended;\n this.onRuntimeSuspensionChange(suspended);\n }\n\n /**\n * Suspension hook for subclasses that own an app execution vehicle (the\n * WASI worker / JSPI executor). The base runtime only presents frames, so\n * it has nothing to suspend.\n */\n protected onRuntimeSuspensionChange(\n _suspended: boolean\n ): void {}\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n this.currentStyle = normalizeWebHostTerminalStyle(style);\n this.applyStyle(this.currentStyle);\n this.bridge?.updateRenderStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n resize(\n columns: number,\n rows: number\n ): void {\n this.columns = Math.max(1, Math.round(columns));\n this.rows = Math.max(1, Math.round(rows));\n this.resizeCanvas();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n writeOutput(\n text: string\n ): void {\n if (!this.diagnosticText) {\n const diagnosticText = document.createElement(\"pre\");\n diagnosticText.className = \"webhost-scene__diagnostic\";\n this.diagnosticText = diagnosticText;\n this.terminalMount.appendChild(diagnosticText);\n }\n this.diagnosticText.textContent = `${this.diagnosticText.textContent ?? \"\"}${text}`;\n }\n\n notifyRuntimeIssue(\n issue: WebHostRuntimeIssue\n ): void {\n console.log(issue.description);\n }\n\n private recordFrameDiagnostic(\n diagnostic: WebHostFrameDiagnosticRecord\n ): void {\n this.onFrameDiagnostic?.(diagnostic);\n }\n\n async writeClipboard(\n text: string\n ): Promise<void> {\n const clipboard = globalThis.navigator?.clipboard;\n if (!clipboard?.writeText) {\n return;\n }\n\n try {\n await clipboard.writeText(text);\n } catch {\n // Clipboard permissions are browser/user-gesture dependent; hosts treat\n // rejection as a best-effort no-op rather than surfacing diagnostics.\n }\n }\n\n sendInput(\n chunk: Uint8Array\n ): void {\n this.onInput(chunk);\n }\n\n dispose(): void {\n this.detachInputHandlers?.();\n this.resizeObserver?.disconnect();\n this.element.remove();\n }\n\n private presentSurface(\n frame: WebHostSurfaceFrame\n ): void {\n const previousFrame = this.currentFrame;\n this.currentFrame = frame;\n this.columns = Math.max(1, Math.round(frame.width));\n this.rows = Math.max(1, Math.round(frame.height));\n const resized = this.resizeCanvas();\n this.draw(previousFrame && !resized ? frame.damage : undefined);\n this.syncAccessibilityTree();\n }\n\n /**\n * The current frame's preferred grid size in cells, when the app published\n * one — the measured pre-minimum content size, for embedders negotiating\n * with an outer layout system (the Android host's preferred columns/rows).\n */\n get preferredGridSize(): { width: number; height: number } | undefined {\n const frame = this.currentFrame;\n if (frame?.preferredGridWidth === undefined || frame.preferredGridHeight === undefined) {\n return undefined;\n }\n return { width: frame.preferredGridWidth, height: frame.preferredGridHeight };\n }\n\n /**\n * The current frame's settled focus presentation, when the app published\n * one. `prefersTextInput` is what the Android host uses to gate its IME;\n * embedders can drive virtual-keyboard or focus affordances from it.\n */\n get focusPresentation(): WebHostFocusPresentation | undefined {\n return this.currentFrame?.focusPresentation;\n }\n\n private linkTarget(\n location: CellLocation\n ): string | undefined {\n return linkTargetAt(\n this.currentFrame?.links,\n this.currentFrame?.linkTargets,\n location\n );\n }\n\n private openHyperlink(\n url: string\n ): void {\n if (this.onOpenHyperlink) {\n this.onOpenHyperlink(url);\n return;\n }\n // Defense in depth on top of the app-side OSC-8 destination sanitization:\n // the default handler only opens web schemes.\n if (!/^https?:/i.test(url)) {\n return;\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n }\n\n private applyStyle(\n style: WebHostTerminalStyle\n ): void {\n applyWebHostTerminalStyle(this.element, style);\n this.element.style.padding = \"0.75rem\";\n this.element.style.borderRadius = \"16px\";\n this.element.style.boxShadow = \"0 20px 50px rgba(0, 0, 0, 0.28)\";\n this.element.style.overflow = \"hidden\";\n this.element.style.gap = \"0.5rem\";\n this.element.style.gridTemplateRows = \"auto 1fr\";\n\n this.terminalMount.style.position = \"relative\";\n this.terminalMount.style.overflow = \"hidden\";\n // Keep a captured wheel from rubber-banding/chaining the page; the wheel\n // capture vs. fall-through decision lives in handleWheel.\n this.terminalMount.style.overscrollBehavior = \"contain\";\n this.terminalMount.style.outline = \"none\";\n this.terminalMount.style.background = webTUITerminalBackgroundColor(this.currentStyle);\n this.terminalMount.style.minHeight = `${this.cellHeight * 8}px`;\n\n if (this.canvas) {\n this.canvas.style.display = \"block\";\n this.canvas.style.width = \"100%\";\n this.canvas.style.height = \"100%\";\n }\n }\n\n private applyVisibility(): void {\n this.element.hidden = !this.isVisible;\n this.element.style.setProperty(\n \"display\",\n this.isVisible ? \"grid\" : \"none\",\n \"important\"\n );\n }\n\n private installResizeObserver(): void {\n if (typeof ResizeObserver === \"undefined\") {\n return;\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n this.resizeToMount();\n });\n this.resizeObserver.observe(this.terminalMount);\n }\n\n private installInputHandlers(): void {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.metaKey || event.isComposing) {\n return;\n }\n const message = this.inputEncoder.encodeKey(event);\n if (!message) {\n return;\n }\n\n this.onInput(message);\n event.preventDefault();\n };\n\n const handlePaste = (event: ClipboardEvent) => {\n const text = event.clipboardData?.getData(\"text/plain\") ?? \"\";\n if (!text) {\n return;\n }\n this.onInput(this.inputEncoder.encodePaste(text));\n event.preventDefault();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n const location = this.cellLocation(event);\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button);\n this.activePointerButton = button;\n this.hasCapturedPointer = true;\n this.pointerDownLinkTarget = button === \"primary\"\n ? this.linkTarget(location)\n : undefined;\n this.terminalMount.focus?.({ preventScroll: true });\n this.terminalMount.setPointerCapture?.(event.pointerId);\n this.onInput(this.inputEncoder.encodePointerDown(location, button, event));\n event.preventDefault();\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n const location = this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n this.terminalMount.releasePointerCapture?.(event.pointerId);\n this.hasCapturedPointer = false;\n const downLinkTarget = this.pointerDownLinkTarget;\n this.pointerDownLinkTarget = undefined;\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button) ?? this.activePointerButton;\n this.onInput(this.inputEncoder.encodePointerUp(location, button, event));\n // A click — down and up over the same link target — opens the link,\n // mirroring the Android host's tap-to-open. The app still receives the\n // pointer messages above.\n if (downLinkTarget !== undefined && this.linkTarget(location) === downLinkTarget) {\n this.openHyperlink(downLinkTarget);\n }\n event.preventDefault();\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const location = event.buttons && this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n if (!location) {\n return;\n }\n\n if (!this.hasCapturedPointer) {\n this.terminalMount.style.cursor =\n this.linkTarget(location) !== undefined ? \"pointer\" : \"\";\n }\n this.onInput(this.inputEncoder.encodePointerMove(location, this.activePointerButton, event));\n };\n\n const handleWheel = (event: WheelEvent) => {\n if (this.wheelMode === \"passive\") {\n return;\n }\n\n const location = this.cellLocation(event);\n if (!location) {\n // Pointer is outside the cell grid (sub-cell margin / gutter). Don't\n // capture — let the wheel fall through to the page.\n return;\n }\n\n // In \"chain\" mode, capture only while a scrollable region under the\n // pointer can still move in this direction; otherwise let the wheel fall\n // through so the page (or parent iframe) scrolls — iframe-like behavior.\n // \"capture\" mode always forwards while over the surface (legacy).\n if (this.wheelMode === \"chain\"\n && !wheelTargetCanScroll(this.currentFrame?.scrollRegions, location, event.deltaX, event.deltaY)) {\n return;\n }\n\n this.onInput(this.inputEncoder.encodeWheel(location, event));\n event.preventDefault();\n };\n\n this.terminalMount.addEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.addEventListener(\"paste\", handlePaste);\n this.terminalMount.addEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.addEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.addEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.addEventListener(\"wheel\", handleWheel, { passive: false });\n\n this.detachInputHandlers = () => {\n this.terminalMount.removeEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.removeEventListener(\"paste\", handlePaste);\n this.terminalMount.removeEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.removeEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.removeEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.removeEventListener(\"wheel\", handleWheel);\n };\n }\n\n private resizeToMount(): void {\n this.measureCells();\n const rect = this.terminalMount.getBoundingClientRect?.();\n const width = rect?.width && rect.width > 0 ? rect.width : this.columns * this.cellWidth;\n const height = rect?.height && rect.height > 0 ? rect.height : this.rows * this.cellHeight;\n const nextColumns = Math.max(1, Math.floor(width / this.cellWidth));\n const nextRows = Math.max(1, Math.floor(height / this.cellHeight));\n\n this.columns = nextColumns;\n this.rows = nextRows;\n this.sendResizeIfNeeded();\n this.resizeCanvas();\n }\n\n private sendResizeIfNeeded(): void {\n const current = {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n };\n if (this.lastSentResize\n && this.lastSentResize.columns === current.columns\n && this.lastSentResize.rows === current.rows\n && this.lastSentResize.cellWidth === current.cellWidth\n && this.lastSentResize.cellHeight === current.cellHeight\n ) {\n return;\n }\n\n this.lastSentResize = current;\n this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);\n }\n\n private resizeCanvas(): boolean {\n if (!this.canvas) {\n return false;\n }\n\n const cssWidth = Math.max(1, this.columns * this.cellWidth);\n const cssHeight = Math.max(1, this.rows * this.cellHeight);\n const scale = globalThis.window?.devicePixelRatio || 1;\n const width = Math.ceil(cssWidth * scale);\n const height = Math.ceil(cssHeight * scale);\n const styleWidth = `${cssWidth}px`;\n const styleHeight = `${cssHeight}px`;\n if (this.canvas.width === width\n && this.canvas.height === height\n && this.canvas.style.width === styleWidth\n && this.canvas.style.height === styleHeight\n ) {\n return false;\n }\n\n this.canvas.width = width;\n this.canvas.height = height;\n this.canvas.style.width = styleWidth;\n this.canvas.style.height = styleHeight;\n return true;\n }\n\n private measureCells(): void {\n const canvas = this.canvas ?? document.createElement(\"canvas\");\n const context = canvas.getContext?.(\"2d\");\n if (!context) {\n this.cellWidth = Math.max(1, Math.round(this.currentStyle.fontSize * 0.62));\n this.cellHeight = Math.max(1, Math.round(this.currentStyle.fontSize * 1.35));\n return;\n }\n\n context.font = fontForStyle(this.currentStyle);\n this.cellWidth = Math.max(1, Math.ceil(context.measureText(\"W\").width));\n this.cellHeight = Math.max(1, Math.ceil(this.currentStyle.fontSize * 1.35));\n }\n\n private draw(\n damage?: WebHostSurfaceDamage\n ): void {\n this.painter.paint(this.surfaceMetrics(), this.currentFrame, damage);\n }\n\n private syncAccessibilityTree(): void {\n const tree = this.accessibilityTree;\n if (!tree || !this.currentFrame) {\n return;\n }\n\n tree.present(this.currentFrame.accessibilityTree ?? [], {\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n }, this.currentFrame.accessibilityAnnouncements ?? [], {\n synchronizeFocus: this.synchronizeAccessibilityFocus,\n });\n }\n\n private surfaceMetrics(): CanvasSurfaceMetrics {\n return {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n style: this.currentStyle,\n };\n }\n\n private pointerMetrics(): PointerGeometryMetrics {\n return {\n rect: this.canvas?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n columns: this.columns,\n rows: this.rows,\n };\n }\n\n private cellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return cellLocationForEvent(event, this.pointerMetrics());\n }\n\n private rawCellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return rawCellLocationForEvent(event, this.pointerMetrics());\n }\n}\n"],"mappings":";;;;;;;;;;;;AAkGA,SAAS,gBAAgB,mBAAmD;CAC1E,IAAI,sBAAsB,KAAA,GACxB,OAAO;CAET,OAAO,oBAAoB,YAAY;AACzC;;;;;;;;AASA,IAAa,sBAAb,MAAiC;CAC/B;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA,UAA2B,IAAI,qBAAqB;CACpD,eAAgC,IAAI,kBAAkB;CACtD;CACA;CACA;CACA;CACA;CACA;CACA;CACA,UAAkB;CAClB,OAAe;CACf,YAAoB;CACpB,aAAqB;CACrB,sBAA6C;CAC7C,qBAA6B;CAC7B;CACA;CACA;CAMA,YAAoB;CACpB,kBAA0B;CAC1B,mBAA2B;CAC3B;CAEA,YAAY,SAAqC;EAC/C,KAAK,aAAa,QAAQ;EAC1B,KAAK,eAAe,8BAA8B,QAAQ,KAAK;EAC/D,KAAK,SAAS,QAAQ;EACtB,KAAK,UAAU,QAAQ;EACvB,KAAK,oBAAoB,QAAQ;EACjC,KAAK,gCAAgC,QAAQ,iCAAiC;EAC9E,KAAK,YAAY,QAAQ,aAAa,gBAAgB,QAAQ,iBAAiB;EAC/E,KAAK,kBAAkB,QAAQ;EAC/B,KAAK,oBAAoB,QAAQ,qBAAqB;EACtD,KAAK,UAAU,SAAS,cAAc,SAAS;EAC/C,KAAK,QAAQ,YAAY;EACzB,KAAK,QAAQ,QAAQ,UAAU,QAAQ,WAAW;EAClD,KAAK,QAAQ,SAAS;EAEtB,MAAM,SAAS,SAAS,cAAc,KAAK;EAC3C,OAAO,YAAY;EACnB,OAAO,cAAc,QAAQ,WAAW,SAAS,QAAQ,WAAW;EAEpE,KAAK,gBAAgB,SAAS,cAAc,KAAK;EACjD,KAAK,cAAc,YAAY;EAC/B,KAAK,cAAc,WAAW;EAE9B,KAAK,QAAQ,OAAO,QAAQ,KAAK,aAAa;EAC9C,QAAQ,MAAM,YAAY,KAAK,OAAO;EACtC,KAAK,gBAAgB;CACvB;CAEA,MAAM,QAAuB;EAC3B,IAAI,KAAK,QACP;EAGF,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,YAAY;EACnB,OAAO,aAAa,eAAe,MAAM;EACzC,KAAK,SAAS;EACd,KAAK,QAAQ,OAAO,cAAc,KAAK,KAAK,CAAC;EAC7C,KAAK,oBAAoB,IAAI,yBAAyB;EACtD,KAAK,cAAc,gBACjB,QACA,KAAK,kBAAkB,SACvB,KAAK,kBAAkB,gBACzB;EACA,KAAK,qBAAqB;EAC1B,KAAK,sBAAsB;EAE3B,KAAK,QAAQ,WAAW;GACtB,iBAAiB,UAAU,KAAK,eAAe,KAAK;GACpD,iBAAiB,SAAS,KAAK,eAAe,IAAI;GAClD,qBAAqB,UAAU,KAAK,mBAAmB,KAAK;GAC5D,wBAAwB,eAAe,KAAK,sBAAsB,UAAU;GAC5E,cAAc,SAAS,KAAK,YAAY,IAAI;GAC5C,aAAa,SAAS,KAAK,YAAY,IAAI;EAC7C,CAAC;EAED,KAAK,WAAW,KAAK,YAAY;EACjC,KAAK,aAAa;EAClB,KAAK,cAAc;EACnB,KAAK,KAAK;EACV,KAAK,sBAAsB;CAC7B;CAEA,WACE,SACM;EACN,KAAK,YAAY;EACjB,KAAK,gBAAgB;EACrB,IAAI,SAAS;GACX,KAAK,cAAc;GACnB,IAAI,KAAK,+BACP,KAAK,cAAc,QAAQ,EAAE,eAAe,KAAK,CAAC;EAEtD;EACA,KAAK,wBAAwB;CAC/B;;;;;;;CAQA,mBACE,SACM;EACN,KAAK,kBAAkB;EACvB,KAAK,wBAAwB;CAC/B;CAEA,0BAAwC;EACtC,MAAM,YAAY,KAAK,sBAAsB,CAAC,KAAK,aAAa,CAAC,KAAK;EACtE,IAAI,cAAc,KAAK,kBACrB;EAEF,KAAK,mBAAmB;EACxB,KAAK,0BAA0B,SAAS;CAC1C;;;;;;CAOA,0BACE,YACM,CAAC;CAET,SACE,OACM;EACN,KAAK,eAAe,8BAA8B,KAAK;EACvD,KAAK,WAAW,KAAK,YAAY;EACjC,KAAK,QAAQ,kBAAkB,KAAK,YAAY;EAChD,KAAK,aAAa;EAClB,KAAK,cAAc;EACnB,KAAK,KAAK;EACV,KAAK,sBAAsB;CAC7B;CAEA,OACE,SACA,MACM;EACN,KAAK,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,CAAC;EAC9C,KAAK,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC;EACxC,KAAK,aAAa;EAClB,KAAK,KAAK;EACV,KAAK,sBAAsB;CAC7B;CAEA,YACE,MACM;EACN,IAAI,CAAC,KAAK,gBAAgB;GACxB,MAAM,iBAAiB,SAAS,cAAc,KAAK;GACnD,eAAe,YAAY;GAC3B,KAAK,iBAAiB;GACtB,KAAK,cAAc,YAAY,cAAc;EAC/C;EACA,KAAK,eAAe,cAAc,GAAG,KAAK,eAAe,eAAe,KAAK;CAC/E;CAEA,mBACE,OACM;EACN,QAAQ,IAAI,MAAM,WAAW;CAC/B;CAEA,sBACE,YACM;EACN,KAAK,oBAAoB,UAAU;CACrC;CAEA,MAAM,eACJ,MACe;EACf,MAAM,YAAY,WAAW,WAAW;EACxC,IAAI,CAAC,WAAW,WACd;EAGF,IAAI;GACF,MAAM,UAAU,UAAU,IAAI;EAChC,QAAQ,CAGR;CACF;CAEA,UACE,OACM;EACN,KAAK,QAAQ,KAAK;CACpB;CAEA,UAAgB;EACd,KAAK,sBAAsB;EAC3B,KAAK,gBAAgB,WAAW;EAChC,KAAK,QAAQ,OAAO;CACtB;CAEA,eACE,OACM;EACN,MAAM,gBAAgB,KAAK;EAC3B,KAAK,eAAe;EACpB,KAAK,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,KAAK,CAAC;EAClD,KAAK,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,MAAM,MAAM,CAAC;EAChD,MAAM,UAAU,KAAK,aAAa;EAClC,KAAK,KAAK,iBAAiB,CAAC,UAAU,MAAM,SAAS,KAAA,CAAS;EAC9D,KAAK,sBAAsB;CAC7B;;;;;;CAOA,IAAI,oBAAmE;EACrE,MAAM,QAAQ,KAAK;EACnB,IAAI,OAAO,uBAAuB,KAAA,KAAa,MAAM,wBAAwB,KAAA,GAC3E;EAEF,OAAO;GAAE,OAAO,MAAM;GAAoB,QAAQ,MAAM;EAAoB;CAC9E;;;;;;CAOA,IAAI,oBAA0D;EAC5D,OAAO,KAAK,cAAc;CAC5B;CAEA,WACE,UACoB;EACpB,OAAO,aACL,KAAK,cAAc,OACnB,KAAK,cAAc,aACnB,QACF;CACF;CAEA,cACE,KACM;EACN,IAAI,KAAK,iBAAiB;GACxB,KAAK,gBAAgB,GAAG;GACxB;EACF;EAGA,IAAI,CAAC,YAAY,KAAK,GAAG,GACvB;EAEF,OAAO,KAAK,KAAK,UAAU,qBAAqB;CAClD;CAEA,WACE,OACM;EACN,0BAA0B,KAAK,SAAS,KAAK;EAC7C,KAAK,QAAQ,MAAM,UAAU;EAC7B,KAAK,QAAQ,MAAM,eAAe;EAClC,KAAK,QAAQ,MAAM,YAAY;EAC/B,KAAK,QAAQ,MAAM,WAAW;EAC9B,KAAK,QAAQ,MAAM,MAAM;EACzB,KAAK,QAAQ,MAAM,mBAAmB;EAEtC,KAAK,cAAc,MAAM,WAAW;EACpC,KAAK,cAAc,MAAM,WAAW;EAGpC,KAAK,cAAc,MAAM,qBAAqB;EAC9C,KAAK,cAAc,MAAM,UAAU;EACnC,KAAK,cAAc,MAAM,aAAa,8BAA8B,KAAK,YAAY;EACrF,KAAK,cAAc,MAAM,YAAY,GAAG,KAAK,aAAa,EAAE;EAE5D,IAAI,KAAK,QAAQ;GACf,KAAK,OAAO,MAAM,UAAU;GAC5B,KAAK,OAAO,MAAM,QAAQ;GAC1B,KAAK,OAAO,MAAM,SAAS;EAC7B;CACF;CAEA,kBAAgC;EAC9B,KAAK,QAAQ,SAAS,CAAC,KAAK;EAC5B,KAAK,QAAQ,MAAM,YACjB,WACA,KAAK,YAAY,SAAS,QAC1B,WACF;CACF;CAEA,wBAAsC;EACpC,IAAI,OAAO,mBAAmB,aAC5B;EAGF,KAAK,iBAAiB,IAAI,qBAAqB;GAC7C,KAAK,cAAc;EACrB,CAAC;EACD,KAAK,eAAe,QAAQ,KAAK,aAAa;CAChD;CAEA,uBAAqC;EACnC,MAAM,iBAAiB,UAAyB;GAC9C,IAAI,MAAM,WAAW,MAAM,aACzB;GAEF,MAAM,UAAU,KAAK,aAAa,UAAU,KAAK;GACjD,IAAI,CAAC,SACH;GAGF,KAAK,QAAQ,OAAO;GACpB,MAAM,eAAe;EACvB;EAEA,MAAM,eAAe,UAA0B;GAC7C,MAAM,OAAO,MAAM,eAAe,QAAQ,YAAY,KAAK;GAC3D,IAAI,CAAC,MACH;GAEF,KAAK,QAAQ,KAAK,aAAa,YAAY,IAAI,CAAC;GAChD,MAAM,eAAe;EACvB;EAEA,MAAM,qBAAqB,UAAwB;GACjD,MAAM,WAAW,KAAK,aAAa,KAAK;GACxC,IAAI,CAAC,UACH;GAGF,MAAM,SAAS,KAAK,aAAa,cAAc,MAAM,MAAM;GAC3D,KAAK,sBAAsB;GAC3B,KAAK,qBAAqB;GAC1B,KAAK,wBAAwB,WAAW,YACpC,KAAK,WAAW,QAAQ,IACxB,KAAA;GACJ,KAAK,cAAc,QAAQ,EAAE,eAAe,KAAK,CAAC;GAClD,KAAK,cAAc,oBAAoB,MAAM,SAAS;GACtD,KAAK,QAAQ,KAAK,aAAa,kBAAkB,UAAU,QAAQ,KAAK,CAAC;GACzE,MAAM,eAAe;EACvB;EAEA,MAAM,mBAAmB,UAAwB;GAC/C,MAAM,WAAW,KAAK,qBAClB,KAAK,gBAAgB,KAAK,IAC1B,KAAK,aAAa,KAAK;GAC3B,KAAK,cAAc,wBAAwB,MAAM,SAAS;GAC1D,KAAK,qBAAqB;GAC1B,MAAM,iBAAiB,KAAK;GAC5B,KAAK,wBAAwB,KAAA;GAC7B,IAAI,CAAC,UACH;GAGF,MAAM,SAAS,KAAK,aAAa,cAAc,MAAM,MAAM,KAAK,KAAK;GACrE,KAAK,QAAQ,KAAK,aAAa,gBAAgB,UAAU,QAAQ,KAAK,CAAC;GAIvE,IAAI,mBAAmB,KAAA,KAAa,KAAK,WAAW,QAAQ,MAAM,gBAChE,KAAK,cAAc,cAAc;GAEnC,MAAM,eAAe;EACvB;EAEA,MAAM,qBAAqB,UAAwB;GACjD,MAAM,WAAW,MAAM,WAAW,KAAK,qBACnC,KAAK,gBAAgB,KAAK,IAC1B,KAAK,aAAa,KAAK;GAC3B,IAAI,CAAC,UACH;GAGF,IAAI,CAAC,KAAK,oBACR,KAAK,cAAc,MAAM,SACvB,KAAK,WAAW,QAAQ,MAAM,KAAA,IAAY,YAAY;GAE1D,KAAK,QAAQ,KAAK,aAAa,kBAAkB,UAAU,KAAK,qBAAqB,KAAK,CAAC;EAC7F;EAEA,MAAM,eAAe,UAAsB;GACzC,IAAI,KAAK,cAAc,WACrB;GAGF,MAAM,WAAW,KAAK,aAAa,KAAK;GACxC,IAAI,CAAC,UAGH;GAOF,IAAI,KAAK,cAAc,WAClB,CAAC,qBAAqB,KAAK,cAAc,eAAe,UAAU,MAAM,QAAQ,MAAM,MAAM,GAC/F;GAGF,KAAK,QAAQ,KAAK,aAAa,YAAY,UAAU,KAAK,CAAC;GAC3D,MAAM,eAAe;EACvB;EAEA,KAAK,cAAc,iBAAiB,WAAW,aAAa;EAC5D,KAAK,cAAc,iBAAiB,SAAS,WAAW;EACxD,KAAK,cAAc,iBAAiB,eAAe,iBAAiB;EACpE,KAAK,cAAc,iBAAiB,aAAa,eAAe;EAChE,KAAK,cAAc,iBAAiB,eAAe,iBAAiB;EACpE,KAAK,cAAc,iBAAiB,SAAS,aAAa,EAAE,SAAS,MAAM,CAAC;EAE5E,KAAK,4BAA4B;GAC/B,KAAK,cAAc,oBAAoB,WAAW,aAAa;GAC/D,KAAK,cAAc,oBAAoB,SAAS,WAAW;GAC3D,KAAK,cAAc,oBAAoB,eAAe,iBAAiB;GACvE,KAAK,cAAc,oBAAoB,aAAa,eAAe;GACnE,KAAK,cAAc,oBAAoB,eAAe,iBAAiB;GACvE,KAAK,cAAc,oBAAoB,SAAS,WAAW;EAC7D;CACF;CAEA,gBAA8B;EAC5B,KAAK,aAAa;EAClB,MAAM,OAAO,KAAK,cAAc,wBAAwB;EACxD,MAAM,QAAQ,MAAM,SAAS,KAAK,QAAQ,IAAI,KAAK,QAAQ,KAAK,UAAU,KAAK;EAC/E,MAAM,SAAS,MAAM,UAAU,KAAK,SAAS,IAAI,KAAK,SAAS,KAAK,OAAO,KAAK;EAChF,MAAM,cAAc,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,KAAK,SAAS,CAAC;EAClE,MAAM,WAAW,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,KAAK,UAAU,CAAC;EAEjE,KAAK,UAAU;EACf,KAAK,OAAO;EACZ,KAAK,mBAAmB;EACxB,KAAK,aAAa;CACpB;CAEA,qBAAmC;EACjC,MAAM,UAAU;GACd,SAAS,KAAK;GACd,MAAM,KAAK;GACX,WAAW,KAAK;GAChB,YAAY,KAAK;EACnB;EACA,IAAI,KAAK,kBACJ,KAAK,eAAe,YAAY,QAAQ,WACxC,KAAK,eAAe,SAAS,QAAQ,QACrC,KAAK,eAAe,cAAc,QAAQ,aAC1C,KAAK,eAAe,eAAe,QAAQ,YAE9C;EAGF,KAAK,iBAAiB;EACtB,KAAK,QAAQ,OAAO,QAAQ,SAAS,QAAQ,MAAM,QAAQ,WAAW,QAAQ,UAAU;CAC1F;CAEA,eAAgC;EAC9B,IAAI,CAAC,KAAK,QACR,OAAO;EAGT,MAAM,WAAW,KAAK,IAAI,GAAG,KAAK,UAAU,KAAK,SAAS;EAC1D,MAAM,YAAY,KAAK,IAAI,GAAG,KAAK,OAAO,KAAK,UAAU;EACzD,MAAM,QAAQ,WAAW,QAAQ,oBAAoB;EACrD,MAAM,QAAQ,KAAK,KAAK,WAAW,KAAK;EACxC,MAAM,SAAS,KAAK,KAAK,YAAY,KAAK;EAC1C,MAAM,aAAa,GAAG,SAAS;EAC/B,MAAM,cAAc,GAAG,UAAU;EACjC,IAAI,KAAK,OAAO,UAAU,SACrB,KAAK,OAAO,WAAW,UACvB,KAAK,OAAO,MAAM,UAAU,cAC5B,KAAK,OAAO,MAAM,WAAW,aAEhC,OAAO;EAGT,KAAK,OAAO,QAAQ;EACpB,KAAK,OAAO,SAAS;EACrB,KAAK,OAAO,MAAM,QAAQ;EAC1B,KAAK,OAAO,MAAM,SAAS;EAC3B,OAAO;CACT;CAEA,eAA6B;EAE3B,MAAM,WADS,KAAK,UAAU,SAAS,cAAc,QAAQ,EAAA,CACtC,aAAa,IAAI;EACxC,IAAI,CAAC,SAAS;GACZ,KAAK,YAAY,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,aAAa,WAAW,GAAI,CAAC;GAC1E,KAAK,aAAa,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,aAAa,WAAW,IAAI,CAAC;GAC3E;EACF;EAEA,QAAQ,OAAO,aAAa,KAAK,YAAY;EAC7C,KAAK,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,QAAQ,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC;EACtE,KAAK,aAAa,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,aAAa,WAAW,IAAI,CAAC;CAC5E;CAEA,KACE,QACM;EACN,KAAK,QAAQ,MAAM,KAAK,eAAe,GAAG,KAAK,cAAc,MAAM;CACrE;CAEA,wBAAsC;EACpC,MAAM,OAAO,KAAK;EAClB,IAAI,CAAC,QAAQ,CAAC,KAAK,cACjB;EAGF,KAAK,QAAQ,KAAK,aAAa,qBAAqB,CAAC,GAAG;GACtD,WAAW,KAAK;GAChB,YAAY,KAAK;EACnB,GAAG,KAAK,aAAa,8BAA8B,CAAC,GAAG,EACrD,kBAAkB,KAAK,8BACzB,CAAC;CACH;CAEA,iBAA+C;EAC7C,OAAO;GACL,SAAS,KAAK;GACd,MAAM,KAAK;GACX,WAAW,KAAK;GAChB,YAAY,KAAK;GACjB,OAAO,KAAK;EACd;CACF;CAEA,iBAAiD;EAC/C,OAAO;GACL,MAAM,KAAK,QAAQ,wBAAwB,KAAK,KAAK,cAAc,wBAAwB;GAC3F,WAAW,KAAK;GAChB,YAAY,KAAK;GACjB,SAAS,KAAK;GACd,MAAM,KAAK;EACb;CACF;CAEA,aACE,OAC0B;EAC1B,OAAO,qBAAqB,OAAO,KAAK,eAAe,CAAC;CAC1D;CAEA,gBACE,OAC0B;EAC1B,OAAO,wBAAwB,OAAO,KAAK,eAAe,CAAC;CAC7D;AACF"}
|
|
1
|
+
{"version":3,"file":"WebHostSceneRuntime.js","names":[],"sources":["../../src/WebHostSceneRuntime.ts"],"sourcesContent":["import {\n applyWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n CanvasSurfacePainter,\n fontForStyle,\n type CanvasSurfaceMetrics,\n} from \"./CanvasSurfacePainter.ts\";\nimport { DomSurfacePainter } from \"./DomSurfacePainter.ts\";\nimport type { WebHostSurfaceRendererKind } from \"./SurfaceRenderer.ts\";\nimport {\n InputEventEncoder,\n type CellLocation,\n type PointerButton,\n} from \"./InputEventEncoder.ts\";\nimport {\n cellLocationForEvent,\n linkTargetAt,\n rawCellLocationForEvent,\n wheelTargetCanScroll,\n type PointerGeometryMetrics,\n} from \"./PointerGeometry.ts\";\nimport { AccessibilityTreeMounter } from \"./AccessibilityTree.ts\";\nimport {\n type WebHostFocusPresentation,\n type WebHostFrameDiagnosticRecord,\n type WebHostOutputSink,\n type WebHostRuntimeIssue,\n type WebHostSurfaceDamage,\n type WebHostSurfaceFrame,\n} from \"./WebHostSurfaceTransport.ts\";\nimport type { WebHostSceneDescriptor } from \"./WebHostSceneManifest.ts\";\n\nexport interface WebHostSceneBridge {\n bindOutput(sink: WebHostOutputSink): void;\n resize(columns: number, rows: number, cellWidth?: number, cellHeight?: number): void;\n updateRenderStyle(style: WebHostTerminalStyle): void;\n sendInput(chunk: Uint8Array): void;\n dispose(): void;\n}\n\nexport interface WebHostSceneRuntimeOptions {\n mount: HTMLElement;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n bridge?: WebHostSceneBridge;\n onInput(chunk: Uint8Array): void;\n onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n synchronizeAccessibilityFocus?: boolean;\n /**\n * How the embedded view treats mouse-wheel input.\n * - `\"chain\"` (default): forward the wheel only while a scrollable region\n * under the pointer can still scroll in that direction; otherwise let it\n * fall through so the page (or parent iframe) scrolls — iframe-like nested\n * scrolling. A scene with no `ScrollView` never traps the wheel. Uses the\n * `scrollRegions` the app publishes in its frames.\n * - `\"capture\"`: always forward the wheel to the app while the pointer is over\n * the surface (and `preventDefault` page scroll). Best for full-screen apps\n * where there is no page to scroll past.\n * - `\"passive\"`: never capture; the page always scrolls.\n *\n * Takes precedence over the legacy `captureWheelInput` flag.\n */\n wheelMode?: WheelMode;\n /**\n * Legacy boolean wheel gate. `true` → `\"capture\"`, `false` → `\"passive\"`.\n * Prefer `wheelMode`. Ignored when `wheelMode` is set. When neither is set the\n * mode defaults to `\"chain\"`.\n */\n captureWheelInput?: boolean;\n /**\n * Called when the user clicks a hyperlink cell (a click is a pointer-down\n * and pointer-up over the same link target). When unset, `http(s)` targets\n * open in a new tab via `window.open(url, \"_blank\", \"noopener,noreferrer\")`\n * and other schemes are ignored. Mirrors the Android host's tap-to-open.\n */\n onOpenHyperlink?: (url: string) => void;\n /**\n * Whether to suspend the scene's app while it cannot be seen — when the\n * scene is switched to the background (`setVisible(false)`) or the whole\n * document is hidden (`setDocumentVisible(false)`). Suspension parks the\n * app's run loop and freezes its monotonic clock, so a hidden scene costs\n * no CPU and resumes exactly where it left off. Defaults to `true`; set\n * `false` to let background scenes keep running (pre-suspension behavior).\n */\n suspendWhenHidden?: boolean;\n /**\n * Which surface presenter draws the scene's frames. `\"canvas\"` (default)\n * paints onto a 2D `<canvas>`; `\"dom\"` renders cells as absolutely\n * positioned text elements — native font rendering and, uniquely, real\n * text selection: hold Alt/Option and drag to select instead of sending\n * pointer input to the app. See {@link WebHostSurfaceRendererKind}.\n */\n renderer?: WebHostSurfaceRendererKind;\n}\n\nexport type WheelMode = \"capture\" | \"chain\" | \"passive\";\n\n/**\n * Resolves the legacy `captureWheelInput` flag to a {@link WheelMode}. When the\n * flag is unset the mode defaults to `\"chain\"`, so embeds never trap a visitor\n * who is merely scrolling past the view; `true` maps to `\"capture\"` and `false`\n * to `\"passive\"` to preserve the old boolean behavior.\n */\nfunction legacyWheelMode(captureWheelInput: boolean | undefined): WheelMode {\n if (captureWheelInput === undefined) {\n return \"chain\";\n }\n return captureWheelInput ? \"capture\" : \"passive\";\n}\n\n/**\n * Coordinates a single SwiftTUI scene's browser presentation: it owns the DOM\n * mount, canvas, accessibility tree, and bridge wiring, and delegates the heavy\n * responsibilities to focused collaborators — {@link CanvasSurfacePainter} for\n * canvas drawing, {@link InputEventEncoder} for wire-message encoding, and the\n * {@link PointerGeometry} helpers for pixel→cell hit-testing and wheel chaining.\n */\nexport class WebHostSceneRuntime {\n readonly descriptor: WebHostSceneDescriptor;\n readonly element: HTMLElement;\n readonly terminalMount: HTMLElement;\n\n private readonly bridge?: WebHostSceneBridge;\n private readonly onInput: (chunk: Uint8Array) => void;\n private readonly onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;\n private readonly synchronizeAccessibilityFocus: boolean;\n private readonly wheelMode: WheelMode;\n private readonly rendererKind: WebHostSurfaceRendererKind;\n private readonly painter: CanvasSurfacePainter | DomSurfacePainter;\n private readonly inputEncoder = new InputEventEncoder();\n private currentStyle: ResolvedWebHostTerminalStyle;\n private canvas?: HTMLCanvasElement;\n private domSurfaceRoot?: HTMLElement;\n private lastDomSurfaceSize?: { width: number; height: number };\n private accessibilityTree?: AccessibilityTreeMounter;\n private diagnosticText?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private detachInputHandlers?: () => void;\n private currentFrame?: WebHostSurfaceFrame;\n private columns = 80;\n private rows = 24;\n private cellWidth = 8;\n private cellHeight = 18;\n private activePointerButton: PointerButton = \"primary\";\n private hasCapturedPointer = false;\n private readonly onOpenHyperlink?: (url: string) => void;\n private pointerDownLinkTarget?: string;\n private lastSentResize?: {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n };\n private isVisible = false;\n private documentVisible = true;\n private runtimeSuspended = false;\n private readonly suspendWhenHidden: boolean;\n\n constructor(options: WebHostSceneRuntimeOptions) {\n this.descriptor = options.descriptor;\n this.currentStyle = normalizeWebHostTerminalStyle(options.style);\n this.bridge = options.bridge;\n this.onInput = options.onInput;\n this.onFrameDiagnostic = options.onFrameDiagnostic;\n this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;\n this.wheelMode = options.wheelMode ?? legacyWheelMode(options.captureWheelInput);\n this.rendererKind = options.renderer ?? \"canvas\";\n this.painter = this.rendererKind === \"dom\"\n ? new DomSurfacePainter()\n : new CanvasSurfacePainter();\n this.onOpenHyperlink = options.onOpenHyperlink;\n this.suspendWhenHidden = options.suspendWhenHidden ?? true;\n this.element = document.createElement(\"section\");\n this.element.className = \"webhost-scene\";\n this.element.dataset.sceneId = options.descriptor.id;\n this.element.hidden = true;\n\n const header = document.createElement(\"div\");\n header.className = \"webhost-scene__header\";\n header.textContent = options.descriptor.title ?? options.descriptor.id;\n\n this.terminalMount = document.createElement(\"div\");\n this.terminalMount.className = \"webhost-scene__terminal\";\n this.terminalMount.tabIndex = 0;\n\n this.element.append(header, this.terminalMount);\n options.mount.appendChild(this.element);\n this.applyVisibility();\n }\n\n async mount(): Promise<void> {\n if (this.surfaceElement) {\n return;\n }\n\n if (this.painter instanceof DomSurfacePainter) {\n const surfaceRoot = document.createElement(\"div\");\n surfaceRoot.className = \"webhost-scene__surface webhost-scene__surface--dom\";\n surfaceRoot.setAttribute(\"aria-hidden\", \"true\");\n this.domSurfaceRoot = surfaceRoot;\n this.painter.attach(surfaceRoot);\n } else {\n const canvas = document.createElement(\"canvas\");\n canvas.className = \"webhost-scene__surface\";\n canvas.setAttribute(\"aria-hidden\", \"true\");\n this.canvas = canvas;\n this.painter.attach(canvas, () => this.draw());\n }\n this.accessibilityTree = new AccessibilityTreeMounter();\n this.terminalMount.replaceChildren(\n this.surfaceElement as HTMLElement,\n this.accessibilityTree.element,\n this.accessibilityTree.announcerElement\n );\n this.installInputHandlers();\n this.installResizeObserver();\n\n this.bridge?.bindOutput({\n presentSurface: (frame) => this.presentSurface(frame),\n writeClipboard: (text) => this.writeClipboard(text),\n notifyRuntimeIssue: (issue) => this.notifyRuntimeIssue(issue),\n recordFrameDiagnostic: (diagnostic) => this.recordFrameDiagnostic(diagnostic),\n writeOutput: (text) => this.writeOutput(text),\n writeError: (text) => this.writeOutput(text),\n });\n\n this.applyStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n setVisible(\n visible: boolean\n ): void {\n this.isVisible = visible;\n this.applyVisibility();\n if (visible) {\n this.resizeToMount();\n if (this.synchronizeAccessibilityFocus) {\n this.terminalMount.focus?.({ preventScroll: true });\n }\n }\n this.updateRuntimeSuspension();\n }\n\n /**\n * Reports whether the surrounding document can be seen at all (browser tab\n * visible, iframe on-screen, …). Combined with the scene-level\n * `setVisible`: the app is suspended while either says hidden, unless\n * `suspendWhenHidden` is `false`.\n */\n setDocumentVisible(\n visible: boolean\n ): void {\n this.documentVisible = visible;\n this.updateRuntimeSuspension();\n }\n\n private updateRuntimeSuspension(): void {\n const suspended = this.suspendWhenHidden && (!this.isVisible || !this.documentVisible);\n if (suspended === this.runtimeSuspended) {\n return;\n }\n this.runtimeSuspended = suspended;\n this.onRuntimeSuspensionChange(suspended);\n }\n\n /**\n * Suspension hook for subclasses that own an app execution vehicle (the\n * WASI worker / JSPI executor). The base runtime only presents frames, so\n * it has nothing to suspend.\n */\n protected onRuntimeSuspensionChange(\n _suspended: boolean\n ): void {}\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n this.currentStyle = normalizeWebHostTerminalStyle(style);\n this.applyStyle(this.currentStyle);\n this.bridge?.updateRenderStyle(this.currentStyle);\n this.measureCells();\n this.resizeToMount();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n resize(\n columns: number,\n rows: number\n ): void {\n this.columns = Math.max(1, Math.round(columns));\n this.rows = Math.max(1, Math.round(rows));\n this.resizeSurface();\n this.draw();\n this.syncAccessibilityTree();\n }\n\n writeOutput(\n text: string\n ): void {\n if (!this.diagnosticText) {\n const diagnosticText = document.createElement(\"pre\");\n diagnosticText.className = \"webhost-scene__diagnostic\";\n this.diagnosticText = diagnosticText;\n this.terminalMount.appendChild(diagnosticText);\n }\n this.diagnosticText.textContent = `${this.diagnosticText.textContent ?? \"\"}${text}`;\n }\n\n notifyRuntimeIssue(\n issue: WebHostRuntimeIssue\n ): void {\n console.log(issue.description);\n }\n\n private recordFrameDiagnostic(\n diagnostic: WebHostFrameDiagnosticRecord\n ): void {\n this.onFrameDiagnostic?.(diagnostic);\n }\n\n async writeClipboard(\n text: string\n ): Promise<void> {\n const clipboard = globalThis.navigator?.clipboard;\n if (!clipboard?.writeText) {\n return;\n }\n\n try {\n await clipboard.writeText(text);\n } catch {\n // Clipboard permissions are browser/user-gesture dependent; hosts treat\n // rejection as a best-effort no-op rather than surfacing diagnostics.\n }\n }\n\n sendInput(\n chunk: Uint8Array\n ): void {\n this.onInput(chunk);\n }\n\n dispose(): void {\n this.detachInputHandlers?.();\n this.resizeObserver?.disconnect();\n this.element.remove();\n }\n\n private presentSurface(\n frame: WebHostSurfaceFrame\n ): void {\n const previousFrame = this.currentFrame;\n this.currentFrame = frame;\n this.columns = Math.max(1, Math.round(frame.width));\n this.rows = Math.max(1, Math.round(frame.height));\n const resized = this.resizeSurface();\n this.draw(previousFrame && !resized ? frame.damage : undefined);\n this.syncAccessibilityTree();\n }\n\n /**\n * The current frame's preferred grid size in cells, when the app published\n * one — the measured pre-minimum content size, for embedders negotiating\n * with an outer layout system (the Android host's preferred columns/rows).\n */\n get preferredGridSize(): { width: number; height: number } | undefined {\n const frame = this.currentFrame;\n if (frame?.preferredGridWidth === undefined || frame.preferredGridHeight === undefined) {\n return undefined;\n }\n return { width: frame.preferredGridWidth, height: frame.preferredGridHeight };\n }\n\n /**\n * The current frame's settled focus presentation, when the app published\n * one. `prefersTextInput` is what the Android host uses to gate its IME;\n * embedders can drive virtual-keyboard or focus affordances from it.\n */\n get focusPresentation(): WebHostFocusPresentation | undefined {\n return this.currentFrame?.focusPresentation;\n }\n\n private linkTarget(\n location: CellLocation\n ): string | undefined {\n return linkTargetAt(\n this.currentFrame?.links,\n this.currentFrame?.linkTargets,\n location\n );\n }\n\n private openHyperlink(\n url: string\n ): void {\n if (this.onOpenHyperlink) {\n this.onOpenHyperlink(url);\n return;\n }\n // Defense in depth on top of the app-side OSC-8 destination sanitization:\n // the default handler only opens web schemes.\n if (!/^https?:/i.test(url)) {\n return;\n }\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n }\n\n private applyStyle(\n style: WebHostTerminalStyle\n ): void {\n applyWebHostTerminalStyle(this.element, style);\n this.element.style.padding = \"0.75rem\";\n this.element.style.borderRadius = \"16px\";\n this.element.style.boxShadow = \"0 20px 50px rgba(0, 0, 0, 0.28)\";\n this.element.style.overflow = \"hidden\";\n this.element.style.gap = \"0.5rem\";\n this.element.style.gridTemplateRows = \"auto 1fr\";\n\n this.terminalMount.style.position = \"relative\";\n this.terminalMount.style.overflow = \"hidden\";\n // Keep a captured wheel from rubber-banding/chaining the page; the wheel\n // capture vs. fall-through decision lives in handleWheel.\n this.terminalMount.style.overscrollBehavior = \"contain\";\n this.terminalMount.style.outline = \"none\";\n this.terminalMount.style.background = webTUITerminalBackgroundColor(this.currentStyle);\n this.terminalMount.style.minHeight = `${this.cellHeight * 8}px`;\n\n if (this.canvas) {\n this.canvas.style.display = \"block\";\n this.canvas.style.width = \"100%\";\n this.canvas.style.height = \"100%\";\n }\n if (this.domSurfaceRoot) {\n this.domSurfaceRoot.style.display = \"block\";\n this.domSurfaceRoot.style.position = \"relative\";\n }\n }\n\n /** The element the active painter presents frames into. */\n private get surfaceElement(): HTMLElement | undefined {\n return this.canvas ?? this.domSurfaceRoot;\n }\n\n private applyVisibility(): void {\n this.element.hidden = !this.isVisible;\n this.element.style.setProperty(\n \"display\",\n this.isVisible ? \"grid\" : \"none\",\n \"important\"\n );\n }\n\n private installResizeObserver(): void {\n if (typeof ResizeObserver === \"undefined\") {\n return;\n }\n\n this.resizeObserver = new ResizeObserver(() => {\n this.resizeToMount();\n });\n this.resizeObserver.observe(this.terminalMount);\n }\n\n private installInputHandlers(): void {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.metaKey || event.isComposing) {\n return;\n }\n const message = this.inputEncoder.encodeKey(event);\n if (!message) {\n return;\n }\n\n this.onInput(message);\n event.preventDefault();\n };\n\n const handlePaste = (event: ClipboardEvent) => {\n const text = event.clipboardData?.getData(\"text/plain\") ?? \"\";\n if (!text) {\n return;\n }\n this.onInput(this.inputEncoder.encodePaste(text));\n event.preventDefault();\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (this.allowsNativeTextSelection(event)) {\n // DOM renderer + Alt/Option: leave the event to the browser so the\n // drag becomes a native text selection instead of app pointer input.\n return;\n }\n const location = this.cellLocation(event);\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button);\n this.activePointerButton = button;\n this.hasCapturedPointer = true;\n this.pointerDownLinkTarget = button === \"primary\"\n ? this.linkTarget(location)\n : undefined;\n this.terminalMount.focus?.({ preventScroll: true });\n this.terminalMount.setPointerCapture?.(event.pointerId);\n this.onInput(this.inputEncoder.encodePointerDown(location, button, event));\n event.preventDefault();\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) {\n return;\n }\n const location = this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n this.terminalMount.releasePointerCapture?.(event.pointerId);\n this.hasCapturedPointer = false;\n const downLinkTarget = this.pointerDownLinkTarget;\n this.pointerDownLinkTarget = undefined;\n if (!location) {\n return;\n }\n\n const button = this.inputEncoder.pointerButton(event.button) ?? this.activePointerButton;\n this.onInput(this.inputEncoder.encodePointerUp(location, button, event));\n // A click — down and up over the same link target — opens the link,\n // mirroring the Android host's tap-to-open. The app still receives the\n // pointer messages above.\n if (downLinkTarget !== undefined && this.linkTarget(location) === downLinkTarget) {\n this.openHyperlink(downLinkTarget);\n }\n event.preventDefault();\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) {\n return;\n }\n const location = event.buttons && this.hasCapturedPointer\n ? this.rawCellLocation(event)\n : this.cellLocation(event);\n if (!location) {\n return;\n }\n\n if (!this.hasCapturedPointer) {\n this.terminalMount.style.cursor =\n this.linkTarget(location) !== undefined ? \"pointer\" : \"\";\n }\n this.onInput(this.inputEncoder.encodePointerMove(location, this.activePointerButton, event));\n };\n\n const handleWheel = (event: WheelEvent) => {\n if (this.wheelMode === \"passive\") {\n return;\n }\n\n const location = this.cellLocation(event);\n if (!location) {\n // Pointer is outside the cell grid (sub-cell margin / gutter). Don't\n // capture — let the wheel fall through to the page.\n return;\n }\n\n // In \"chain\" mode, capture only while a scrollable region under the\n // pointer can still move in this direction; otherwise let the wheel fall\n // through so the page (or parent iframe) scrolls — iframe-like behavior.\n // \"capture\" mode always forwards while over the surface (legacy).\n if (this.wheelMode === \"chain\"\n && !wheelTargetCanScroll(this.currentFrame?.scrollRegions, location, event.deltaX, event.deltaY)) {\n return;\n }\n\n this.onInput(this.inputEncoder.encodeWheel(location, event));\n event.preventDefault();\n };\n\n this.terminalMount.addEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.addEventListener(\"paste\", handlePaste);\n this.terminalMount.addEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.addEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.addEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.addEventListener(\"wheel\", handleWheel, { passive: false });\n\n this.detachInputHandlers = () => {\n this.terminalMount.removeEventListener(\"keydown\", handleKeyDown);\n this.terminalMount.removeEventListener(\"paste\", handlePaste);\n this.terminalMount.removeEventListener(\"pointerdown\", handlePointerDown);\n this.terminalMount.removeEventListener(\"pointerup\", handlePointerUp);\n this.terminalMount.removeEventListener(\"pointermove\", handlePointerMove);\n this.terminalMount.removeEventListener(\"wheel\", handleWheel);\n };\n }\n\n private resizeToMount(): void {\n this.measureCells();\n const rect = this.terminalMount.getBoundingClientRect?.();\n const width = rect?.width && rect.width > 0 ? rect.width : this.columns * this.cellWidth;\n const height = rect?.height && rect.height > 0 ? rect.height : this.rows * this.cellHeight;\n const nextColumns = Math.max(1, Math.floor(width / this.cellWidth));\n const nextRows = Math.max(1, Math.floor(height / this.cellHeight));\n\n this.columns = nextColumns;\n this.rows = nextRows;\n this.sendResizeIfNeeded();\n this.resizeSurface();\n }\n\n private sendResizeIfNeeded(): void {\n const current = {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n };\n if (this.lastSentResize\n && this.lastSentResize.columns === current.columns\n && this.lastSentResize.rows === current.rows\n && this.lastSentResize.cellWidth === current.cellWidth\n && this.lastSentResize.cellHeight === current.cellHeight\n ) {\n return;\n }\n\n this.lastSentResize = current;\n this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);\n }\n\n private resizeSurface(): boolean {\n const cssWidth = Math.max(1, this.columns * this.cellWidth);\n const cssHeight = Math.max(1, this.rows * this.cellHeight);\n\n if (this.domSurfaceRoot) {\n const last = this.lastDomSurfaceSize;\n if (last && last.width === cssWidth && last.height === cssHeight) {\n return false;\n }\n this.lastDomSurfaceSize = { width: cssWidth, height: cssHeight };\n this.domSurfaceRoot.style.width = `${cssWidth}px`;\n this.domSurfaceRoot.style.height = `${cssHeight}px`;\n return true;\n }\n\n if (!this.canvas) {\n return false;\n }\n\n const scale = globalThis.window?.devicePixelRatio || 1;\n const width = Math.ceil(cssWidth * scale);\n const height = Math.ceil(cssHeight * scale);\n const styleWidth = `${cssWidth}px`;\n const styleHeight = `${cssHeight}px`;\n if (this.canvas.width === width\n && this.canvas.height === height\n && this.canvas.style.width === styleWidth\n && this.canvas.style.height === styleHeight\n ) {\n return false;\n }\n\n this.canvas.width = width;\n this.canvas.height = height;\n this.canvas.style.width = styleWidth;\n this.canvas.style.height = styleHeight;\n return true;\n }\n\n private measureCells(): void {\n const canvas = this.canvas ?? document.createElement(\"canvas\");\n const context = canvas.getContext?.(\"2d\");\n if (!context) {\n this.cellWidth = Math.max(1, Math.round(this.currentStyle.fontSize * 0.62));\n this.cellHeight = Math.max(1, Math.round(this.currentStyle.fontSize * 1.35));\n return;\n }\n\n context.font = fontForStyle(this.currentStyle);\n this.cellWidth = Math.max(1, Math.ceil(context.measureText(\"W\").width));\n this.cellHeight = Math.max(1, Math.ceil(this.currentStyle.fontSize * 1.35));\n }\n\n private draw(\n damage?: WebHostSurfaceDamage\n ): void {\n this.painter.paint(this.surfaceMetrics(), this.currentFrame, damage);\n }\n\n private syncAccessibilityTree(): void {\n const tree = this.accessibilityTree;\n if (!tree || !this.currentFrame) {\n return;\n }\n\n tree.present(this.currentFrame.accessibilityTree ?? [], {\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n }, this.currentFrame.accessibilityAnnouncements ?? [], {\n synchronizeFocus: this.synchronizeAccessibilityFocus,\n });\n }\n\n private surfaceMetrics(): CanvasSurfaceMetrics {\n return {\n columns: this.columns,\n rows: this.rows,\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n style: this.currentStyle,\n };\n }\n\n private pointerMetrics(): PointerGeometryMetrics {\n return {\n rect: this.surfaceElement?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),\n cellWidth: this.cellWidth,\n cellHeight: this.cellHeight,\n columns: this.columns,\n rows: this.rows,\n };\n }\n\n /**\n * Whether this pointer event should be left to the browser for native text\n * selection instead of being forwarded to the app. Only the DOM renderer\n * has real text nodes to select, and only while Alt/Option is held — plain\n * pointer input still belongs to the app.\n */\n private allowsNativeTextSelection(\n event: MouseEvent\n ): boolean {\n return this.rendererKind === \"dom\" && event.altKey;\n }\n\n private cellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return cellLocationForEvent(event, this.pointerMetrics());\n }\n\n private rawCellLocation(\n event: MouseEvent\n ): CellLocation | undefined {\n return rawCellLocationForEvent(event, this.pointerMetrics());\n }\n}\n"],"mappings":"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"}
|