@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 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
- rectangles and text into a canvas. It does not load a terminal emulator and does
92
- not depend on `ghostty-web` or `ghostty-vt.wasm`.
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 = resolvedBackground(style, metrics.style);
106
- const foreground = resolvedForeground(style, metrics.style);
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"}
@@ -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[];
@@ -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 resizeCanvas;
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
- painter = new CanvasSurfacePainter();
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.canvas) return;
81
- const canvas = document.createElement("canvas");
82
- canvas.className = "webhost-scene__surface";
83
- canvas.setAttribute("aria-hidden", "true");
84
- this.canvas = canvas;
85
- this.painter.attach(canvas, () => this.draw());
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(canvas, this.accessibilityTree.element, this.accessibilityTree.announcerElement);
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.resizeCanvas();
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.resizeCanvas();
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.resizeCanvas();
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
- resizeCanvas() {
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.canvas?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swifttui/web",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",