@swifttui/web 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/src/CanvasSurfacePainter.js +3 -10
- package/dist/src/CanvasSurfacePainter.js.map +1 -1
- package/dist/src/DomSurfacePainter.d.ts +51 -0
- package/dist/src/DomSurfacePainter.js +267 -0
- package/dist/src/DomSurfacePainter.js.map +1 -0
- package/dist/src/SurfaceRenderer.d.ts +50 -0
- package/dist/src/SurfaceRenderer.js +21 -0
- package/dist/src/SurfaceRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +8 -0
- package/dist/src/WebHostApp.js +6 -2
- package/dist/src/WebHostApp.js.map +1 -1
- package/dist/src/WebHostSceneRuntime.d.ts +22 -1
- package/dist/src/WebHostSceneRuntime.js +59 -14
- package/dist/src/WebHostSceneRuntime.js.map +1 -1
- package/dist/src/wasi/WasmEngineCapabilities.js +1 -1
- package/dist/src/wasi/WasmEngineCapabilities.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,12 +84,41 @@ The page that hosts the WASI runtime must serve
|
|
|
84
84
|
`Cross-Origin-Embedder-Policy: require-corp` so the `SharedArrayBuffer`-backed
|
|
85
85
|
stdin works.
|
|
86
86
|
|
|
87
|
+
## Renderers
|
|
88
|
+
|
|
89
|
+
Two surface presenters ship behind one option; both consume the same frames:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
await createWebHostApp({
|
|
93
|
+
mount,
|
|
94
|
+
manifestUrl,
|
|
95
|
+
renderer: "dom", // "canvas" is the default
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- **`"canvas"`** (default) paints cells onto a 2D `<canvas>`: one DOM node,
|
|
100
|
+
pixel-exact box-drawing seams and decoration patterns.
|
|
101
|
+
- **`"dom"`** renders cells as absolutely positioned text elements: the
|
|
102
|
+
browser's own font shaping and fallback (emoji, CJK), crisp text at any
|
|
103
|
+
page zoom, an inspectable element tree, and — uniquely — native text
|
|
104
|
+
selection: **hold Alt/Option and drag** to select and copy the app's text
|
|
105
|
+
(plain drags remain pointer input for the app). Box-drawing characters
|
|
106
|
+
render as font glyphs, and underline/strikethrough patterns map onto CSS
|
|
107
|
+
`text-decoration`, so hairline details can differ slightly from the canvas
|
|
108
|
+
painter. Grid alignment is kept exact by stretching each glyph advance to
|
|
109
|
+
the cell width via `letter-spacing`.
|
|
110
|
+
|
|
111
|
+
The option is also available per scene runtime (`WebHostSceneRuntimeOptions.renderer`)
|
|
112
|
+
and both painters are exported (`CanvasSurfacePainter`, `DomSurfacePainter`)
|
|
113
|
+
for hosts that compose their own runtimes.
|
|
114
|
+
|
|
87
115
|
## Surface transport
|
|
88
116
|
|
|
89
117
|
This package uses SwiftTUI's `web-surface` WASI transport. The Swift runner
|
|
90
118
|
emits structured raster-surface records on stdout, and the browser host draws
|
|
91
|
-
|
|
92
|
-
not depend on `ghostty-web` or
|
|
119
|
+
them with the configured renderer — canvas rects/text or DOM elements. It does
|
|
120
|
+
not load a terminal emulator and does not depend on `ghostty-web` or
|
|
121
|
+
`ghostty-vt.wasm`.
|
|
93
122
|
|
|
94
123
|
`web-surface` is the default `SwiftTUIWASI` browser transport. WebHost still
|
|
95
124
|
sets `TUIGUI_TRANSPORT=surface` explicitly so generated app environments are
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, WebHostANSIColors, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
|
|
2
2
|
import { SUPPORTED_SURFACE_VERSION, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage } from "./src/WebHostSurfaceTransport.js";
|
|
3
|
+
import { SurfaceMetrics, WebHostSurfacePainter, WebHostSurfaceRendererKind, resolvedSurfaceBackground, resolvedSurfaceForeground } from "./src/SurfaceRenderer.js";
|
|
3
4
|
import { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
4
5
|
import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WheelMode } from "./src/WebHostSceneRuntime.js";
|
|
5
6
|
import { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
|
|
6
7
|
import { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostVisibilityDocument, createWebHostApp } from "./src/WebHostApp.js";
|
|
8
|
+
import { DomSurfacePainter } from "./src/DomSurfacePainter.js";
|
|
7
9
|
import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
|
|
8
10
|
import { JSPIConstructors, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
|
|
9
11
|
import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
|
|
10
|
-
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, JSPIConstructors, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, SUPPORTED_SURFACE_VERSION, StdIOPipe, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebHostVisibilityDocument, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
12
|
+
export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, DomSurfacePainter, JSPIConstructors, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, SUPPORTED_SURFACE_VERSION, StdIOPipe, SurfaceMetrics, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostRuntimeIssue, WebHostSceneBridge, WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfacePainter, WebHostSurfaceRect, WebHostSurfaceRendererKind, WebHostSurfaceSize, WebHostSurfaceStyle, WebHostTerminalAppearance, WebHostTerminalCursorStyle, WebHostTerminalPalette, WebHostTerminalRenderStyle, WebHostTerminalStyle, WebHostTerminalTheme, WebHostVisibilityDocument, WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, WheelMode, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { SUPPORTED_SURFACE_VERSION, WebHostOutputDecoder, encodeKeyInputMessage,
|
|
|
5
5
|
import { BrowserWASIBridge } from "./src/wasi/BrowserWASIBridge.js";
|
|
6
6
|
import { WebSocketSceneBridge, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
|
|
7
7
|
import { loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
|
|
8
|
+
import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./src/SurfaceRenderer.js";
|
|
9
|
+
import { DomSurfacePainter } from "./src/DomSurfacePainter.js";
|
|
8
10
|
import { WebHostSceneRuntime } from "./src/WebHostSceneRuntime.js";
|
|
9
11
|
import { createWebHostApp } from "./src/WebHostApp.js";
|
|
10
|
-
export { BrowserWASIBridge, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
12
|
+
export { BrowserWASIBridge, DomSurfacePainter, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
|
|
2
2
|
import { canRenderBoxDrawing, drawBoxDrawing } from "./BoxDrawingRenderer.js";
|
|
3
|
+
import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./SurfaceRenderer.js";
|
|
3
4
|
//#region src/CanvasSurfacePainter.ts
|
|
4
5
|
/**
|
|
5
6
|
* Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell
|
|
@@ -102,8 +103,8 @@ var CanvasSurfacePainter = class {
|
|
|
102
103
|
const rectX = x * metrics.cellWidth;
|
|
103
104
|
const rectY = y * metrics.cellHeight;
|
|
104
105
|
const width = Math.max(1, span) * metrics.cellWidth;
|
|
105
|
-
const background =
|
|
106
|
-
const foreground =
|
|
106
|
+
const background = resolvedSurfaceBackground(style, metrics.style);
|
|
107
|
+
const foreground = resolvedSurfaceForeground(style, metrics.style);
|
|
107
108
|
const opacity = style?.opacity ?? 1;
|
|
108
109
|
if (background) {
|
|
109
110
|
context.globalAlpha = opacity;
|
|
@@ -249,14 +250,6 @@ function dirtyRegionIntersectsCellRect(region, x, y, width, height) {
|
|
|
249
250
|
}
|
|
250
251
|
return false;
|
|
251
252
|
}
|
|
252
|
-
function resolvedForeground(style, terminalStyle) {
|
|
253
|
-
if ((style?.em ?? 0) & 16) return style?.bg ?? terminalStyle.theme.background;
|
|
254
|
-
return style?.fg ?? terminalStyle.theme.foreground;
|
|
255
|
-
}
|
|
256
|
-
function resolvedBackground(style, terminalStyle) {
|
|
257
|
-
if ((style?.em ?? 0) & 16) return style?.fg ?? terminalStyle.theme.foreground;
|
|
258
|
-
return style?.bg;
|
|
259
|
-
}
|
|
260
253
|
//#endregion
|
|
261
254
|
export { CanvasSurfacePainter, fontForStyle };
|
|
262
255
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CanvasSurfacePainter.js","names":[],"sources":["../../src/CanvasSurfacePainter.ts"],"sourcesContent":["import {\n canRenderBoxDrawing,\n drawBoxDrawing,\n} from \"./BoxDrawingRenderer.ts\";\nimport {\n type ResolvedWebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\n WebHostSurfaceImageFormat,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/**\n * A read-only snapshot of the cell grid geometry and active style the painter\n * needs for a single paint pass. The runtime owns this state and mutates it as\n * the surface resizes or restyles; passing a fresh snapshot per `paint` keeps\n * the painter stateless about geometry and avoids stale reads.\n */\nexport interface CanvasSurfaceMetrics {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n style: ResolvedWebHostTerminalStyle;\n}\n\ninterface CachedWebHostImage {\n image?: CanvasImageSource;\n promise?: Promise<CanvasImageSource>;\n}\n\ninterface DirtyRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface DirtyCellRange {\n start: number;\n end: number;\n}\n\ntype DirtyRowRanges = \"full\" | DirtyCellRange[];\n\ninterface DirtyRegion {\n rects: DirtyRect[];\n rows: Map<number, DirtyRowRanges>;\n}\n\n/**\n * Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell\n * text/box-drawing/decorations, surface images, and damage-scoped dirty-region\n * painting. The painter caches decoded images and asks the host to repaint once\n * an image finishes decoding (via the `requestRedraw` callback).\n *\n * Geometry and style are supplied per paint via {@link CanvasSurfaceMetrics};\n * the painter holds only the canvas handle, the image cache, and the redraw\n * callback as durable state.\n */\nexport class CanvasSurfacePainter {\n private readonly imageCache = new Map<string, CachedWebHostImage>();\n private canvas?: HTMLCanvasElement;\n private requestRedraw: () => void = () => {};\n\n /**\n * Binds the canvas the painter draws into and the callback used to request a\n * full repaint after an asynchronous image decode completes.\n */\n attach(\n canvas: HTMLCanvasElement,\n requestRedraw: () => void\n ): void {\n this.canvas = canvas;\n this.requestRedraw = requestRedraw;\n }\n\n paint(\n metrics: CanvasSurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void {\n const canvas = this.canvas;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) {\n return;\n }\n\n const dirtyRegion = frame\n ? this.dirtyRegionForDamage(damage, frame, metrics)\n : undefined;\n if (dirtyRegion?.rects.length === 0) {\n return;\n }\n\n const scale = globalThis.window?.devicePixelRatio || 1;\n context.setTransform(scale, 0, 0, scale, 0, 0);\n context.textBaseline = \"alphabetic\";\n\n context.fillStyle = webTUITerminalBackgroundColor(metrics.style);\n if (dirtyRegion) {\n for (const rect of dirtyRegion.rects) {\n context.clearRect(rect.x, rect.y, rect.width, rect.height);\n context.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n } else {\n context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);\n context.fillRect(0, 0, metrics.columns * metrics.cellWidth, metrics.rows * metrics.cellHeight);\n }\n\n if (!frame) {\n return;\n }\n\n this.drawRows(context, frame, metrics, dirtyRegion);\n this.drawImages(context, frame.images ?? [], metrics, dirtyRegion);\n }\n\n private drawRows(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n if (dirtyRegion) {\n for (const [y, ranges] of dirtyRegion.rows) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y, ranges);\n }\n return;\n }\n\n for (let y = 0; y < frame.rows.length; y += 1) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y);\n }\n }\n\n private drawRow(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n row: WebHostSurfaceFrame[\"rows\"][number],\n y: number,\n ranges?: DirtyRowRanges\n ): void {\n for (const cell of row) {\n const [x, text, span, styleIndex] = cell;\n if (ranges !== undefined && !cellIntersectsRanges(x, span, ranges)) {\n continue;\n }\n const style = frame.styles[styleIndex] ?? undefined;\n this.drawCell(context, metrics, x, y, text, span, style);\n }\n }\n\n private drawImages(\n context: CanvasRenderingContext2D,\n images: WebHostSurfaceImage[],\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n for (const image of images) {\n this.drawImage(context, image, metrics, dirtyRegion);\n }\n }\n\n private drawImage(\n context: CanvasRenderingContext2D,\n image: WebHostSurfaceImage,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n const decodedImage = this.cachedImage(image);\n if (!decodedImage) {\n return;\n }\n\n const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;\n const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;\n if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) {\n return;\n }\n if (\n dirtyRegion\n && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)\n ) {\n return;\n }\n\n context.save();\n context.beginPath();\n context.rect(\n clipX * metrics.cellWidth,\n clipY * metrics.cellHeight,\n clipWidth * metrics.cellWidth,\n clipHeight * metrics.cellHeight\n );\n context.clip();\n context.drawImage(\n decodedImage,\n boundsX * metrics.cellWidth,\n boundsY * metrics.cellHeight,\n boundsWidth * metrics.cellWidth,\n boundsHeight * metrics.cellHeight\n );\n context.restore();\n }\n\n private cachedImage(\n image: WebHostSurfaceImage\n ): CanvasImageSource | undefined {\n const cached = this.imageCache.get(image.id);\n if (cached?.image) {\n return cached.image;\n }\n\n if (!cached?.promise && image.dataBase64) {\n const promise = decodeImage(image.dataBase64, image.format);\n this.imageCache.set(image.id, { promise });\n void promise.then((decodedImage) => {\n const latest = this.imageCache.get(image.id);\n if (latest?.promise !== promise) {\n return;\n }\n this.imageCache.set(image.id, { image: decodedImage });\n this.requestRedraw();\n }).catch(() => {\n this.imageCache.delete(image.id);\n });\n }\n\n return undefined;\n }\n\n private drawCell(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n text: string,\n span: number,\n style?: WebHostSurfaceStyle | null\n ): void {\n const rectX = x * metrics.cellWidth;\n const rectY = y * metrics.cellHeight;\n const width = Math.max(1, span) * metrics.cellWidth;\n const background = resolvedBackground(style, metrics.style);\n const foreground = resolvedForeground(style, metrics.style);\n const opacity = style?.opacity ?? 1;\n\n if (background) {\n context.globalAlpha = opacity;\n context.fillStyle = background;\n context.fillRect(rectX, rectY, width, metrics.cellHeight);\n }\n\n if (text !== \" \") {\n context.globalAlpha = opacity;\n context.fillStyle = foreground;\n context.strokeStyle = foreground;\n if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {\n x: rectX,\n y: rectY,\n width,\n height: metrics.cellHeight,\n })) {\n context.font = fontForStyle(metrics.style, style);\n context.fillText(\n text,\n rectX,\n rectY + Math.floor((metrics.cellHeight + metrics.style.fontSize) / 2) - 2\n );\n }\n }\n\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.underline, \"underline\", foreground);\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.strikethrough, \"strike\", foreground);\n context.globalAlpha = 1;\n }\n\n private dirtyRegionForDamage(\n damage: WebHostSurfaceDamage | undefined,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics\n ): DirtyRegion | undefined {\n if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) {\n return undefined;\n }\n\n const rects: DirtyRect[] = [];\n const rows = new Map<number, DirtyRowRanges>();\n for (const [row, ranges] of damage.textRows) {\n if (row < 0 || row >= frame.height) {\n continue;\n }\n if (ranges.length === 0) {\n rects.push(cellRect(metrics, 0, row, frame.width));\n rows.set(row, \"full\");\n continue;\n }\n const rowRanges: DirtyCellRange[] = rows.get(row) === \"full\"\n ? []\n : [...(rows.get(row) as DirtyCellRange[] | undefined ?? [])];\n for (const [start, end] of ranges) {\n const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));\n const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));\n if (lowerBound >= upperBound) {\n continue;\n }\n rects.push(cellRect(metrics, lowerBound, row, upperBound - lowerBound));\n rowRanges.push({ start: lowerBound, end: upperBound });\n }\n if (rows.get(row) !== \"full\" && rowRanges.length > 0) {\n rows.set(row, normalizeCellRanges(rowRanges));\n }\n }\n return { rects, rows };\n }\n\n private drawTextLine(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n width: number,\n line: WebHostSurfaceStyle[\"underline\"],\n placement: \"underline\" | \"strike\",\n fallbackColor: string\n ): void {\n if (!line) {\n return;\n }\n context.strokeStyle = line.color ?? fallbackColor;\n context.lineWidth = line.pattern === \"double\" ? 2 : 1;\n if (line.pattern === \"dot\") {\n context.setLineDash([1, 3]);\n } else if (line.pattern === \"dash\") {\n context.setLineDash([4, 3]);\n } else {\n context.setLineDash([]);\n }\n\n const lineY = placement === \"underline\"\n ? y + metrics.cellHeight - 2\n : y + Math.floor(metrics.cellHeight / 2);\n context.beginPath();\n context.moveTo(x, lineY);\n context.lineTo(x + width, lineY);\n context.stroke();\n context.setLineDash([]);\n }\n}\n\n/**\n * The CSS font string for a cell, folding the surface emphasis bits (bold,\n * italic) over the host terminal's font size/family. Exposed so the host can\n * reuse the exact same metric when measuring cell dimensions.\n */\nexport function fontForStyle(\n terminalStyle: ResolvedWebHostTerminalStyle,\n style?: WebHostSurfaceStyle | null\n): string {\n const emphasis = style?.em ?? 0;\n const italic = (emphasis & 2) !== 0 ? \"italic \" : \"\";\n const weight = (emphasis & 1) !== 0 ? \"700 \" : \"\";\n return `${italic}${weight}${terminalStyle.fontSize}px ${terminalStyle.fontFamily}`;\n}\n\nfunction cellRect(\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n span: number\n): DirtyRect {\n return {\n x: x * metrics.cellWidth,\n y: y * metrics.cellHeight,\n width: Math.max(1, span) * metrics.cellWidth,\n height: metrics.cellHeight,\n };\n}\n\nasync function decodeImage(\n dataBase64: string,\n format: WebHostSurfaceImageFormat\n): Promise<CanvasImageSource> {\n const bytes = decodeBase64Bytes(dataBase64);\n const blob = new Blob([bytes], { type: `image/${format}` });\n\n if (typeof createImageBitmap === \"function\") {\n // Animated GIFs collapse to their first frame in createImageBitmap\n // — that matches the Kitty path's first-frame composite. Phase 7\n // will replace this with a frame ticker.\n return createImageBitmap(blob);\n }\n\n return new Promise((resolve, reject) => {\n const image = new Image();\n const url = URL.createObjectURL(blob);\n image.onload = () => {\n URL.revokeObjectURL(url);\n resolve(image);\n };\n image.onerror = () => {\n URL.revokeObjectURL(url);\n reject(new Error(`Failed to decode ${format} image`));\n };\n image.src = url;\n });\n}\n\nfunction decodeBase64Bytes(\n value: string\n): Uint8Array {\n if (typeof atob === \"function\") {\n const binary = atob(value);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n }\n\n return new Uint8Array(Buffer.from(value, \"base64\"));\n}\n\nfunction normalizeCellRanges(\n ranges: DirtyCellRange[]\n): DirtyCellRange[] {\n const sorted = ranges\n .filter((range) => range.end > range.start)\n .sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);\n const normalized: DirtyCellRange[] = [];\n for (const range of sorted) {\n const previous = normalized[normalized.length - 1];\n if (previous && range.start <= previous.end) {\n previous.end = Math.max(previous.end, range.end);\n continue;\n }\n normalized.push({ ...range });\n }\n return normalized;\n}\n\nfunction cellIntersectsRanges(\n x: number,\n span: number,\n ranges: DirtyRowRanges\n): boolean {\n if (ranges === \"full\") {\n return true;\n }\n const start = Math.floor(x);\n const end = start + Math.max(1, Math.ceil(span));\n return ranges.some((range) => start < range.end && end > range.start);\n}\n\nfunction dirtyRegionIntersectsCellRect(\n region: DirtyRegion,\n x: number,\n y: number,\n width: number,\n height: number\n): boolean {\n const startRow = Math.max(0, Math.floor(y));\n const endRow = Math.max(startRow, Math.ceil(y + height));\n const rectRange = {\n start: Math.floor(x),\n end: Math.floor(x) + Math.max(1, Math.ceil(width)),\n };\n for (let row = startRow; row < endRow; row += 1) {\n const ranges = region.rows.get(row);\n if (!ranges) {\n continue;\n }\n if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) {\n return true;\n }\n }\n return false;\n}\n\nfunction resolvedForeground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string {\n if ((style?.em ?? 0) & 16) {\n return style?.bg ?? terminalStyle.theme.background;\n }\n return style?.fg ?? terminalStyle.theme.foreground;\n}\n\nfunction resolvedBackground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string | undefined {\n if ((style?.em ?? 0) & 16) {\n return style?.fg ?? terminalStyle.theme.foreground;\n }\n return style?.bg;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CanvasSurfacePainter.js","names":[],"sources":["../../src/CanvasSurfacePainter.ts"],"sourcesContent":["import {\n canRenderBoxDrawing,\n drawBoxDrawing,\n} from \"./BoxDrawingRenderer.ts\";\nimport {\n resolvedSurfaceBackground,\n resolvedSurfaceForeground,\n type SurfaceMetrics,\n type WebHostSurfacePainter,\n} from \"./SurfaceRenderer.ts\";\nimport {\n type ResolvedWebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\n WebHostSurfaceImageFormat,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/**\n * A read-only snapshot of the cell grid geometry and active style the painter\n * needs for a single paint pass — see {@link SurfaceMetrics}, shared with the\n * DOM painter. The alias keeps this painter's original public name stable.\n */\nexport type CanvasSurfaceMetrics = SurfaceMetrics;\n\ninterface CachedWebHostImage {\n image?: CanvasImageSource;\n promise?: Promise<CanvasImageSource>;\n}\n\ninterface DirtyRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface DirtyCellRange {\n start: number;\n end: number;\n}\n\ntype DirtyRowRanges = \"full\" | DirtyCellRange[];\n\ninterface DirtyRegion {\n rects: DirtyRect[];\n rows: Map<number, DirtyRowRanges>;\n}\n\n/**\n * Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell\n * text/box-drawing/decorations, surface images, and damage-scoped dirty-region\n * painting. The painter caches decoded images and asks the host to repaint once\n * an image finishes decoding (via the `requestRedraw` callback).\n *\n * Geometry and style are supplied per paint via {@link CanvasSurfaceMetrics};\n * the painter holds only the canvas handle, the image cache, and the redraw\n * callback as durable state.\n */\nexport class CanvasSurfacePainter implements WebHostSurfacePainter {\n private readonly imageCache = new Map<string, CachedWebHostImage>();\n private canvas?: HTMLCanvasElement;\n private requestRedraw: () => void = () => {};\n\n /**\n * Binds the canvas the painter draws into and the callback used to request a\n * full repaint after an asynchronous image decode completes.\n */\n attach(\n canvas: HTMLCanvasElement,\n requestRedraw: () => void\n ): void {\n this.canvas = canvas;\n this.requestRedraw = requestRedraw;\n }\n\n paint(\n metrics: CanvasSurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void {\n const canvas = this.canvas;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) {\n return;\n }\n\n const dirtyRegion = frame\n ? this.dirtyRegionForDamage(damage, frame, metrics)\n : undefined;\n if (dirtyRegion?.rects.length === 0) {\n return;\n }\n\n const scale = globalThis.window?.devicePixelRatio || 1;\n context.setTransform(scale, 0, 0, scale, 0, 0);\n context.textBaseline = \"alphabetic\";\n\n context.fillStyle = webTUITerminalBackgroundColor(metrics.style);\n if (dirtyRegion) {\n for (const rect of dirtyRegion.rects) {\n context.clearRect(rect.x, rect.y, rect.width, rect.height);\n context.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n } else {\n context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);\n context.fillRect(0, 0, metrics.columns * metrics.cellWidth, metrics.rows * metrics.cellHeight);\n }\n\n if (!frame) {\n return;\n }\n\n this.drawRows(context, frame, metrics, dirtyRegion);\n this.drawImages(context, frame.images ?? [], metrics, dirtyRegion);\n }\n\n private drawRows(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n if (dirtyRegion) {\n for (const [y, ranges] of dirtyRegion.rows) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y, ranges);\n }\n return;\n }\n\n for (let y = 0; y < frame.rows.length; y += 1) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y);\n }\n }\n\n private drawRow(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n row: WebHostSurfaceFrame[\"rows\"][number],\n y: number,\n ranges?: DirtyRowRanges\n ): void {\n for (const cell of row) {\n const [x, text, span, styleIndex] = cell;\n if (ranges !== undefined && !cellIntersectsRanges(x, span, ranges)) {\n continue;\n }\n const style = frame.styles[styleIndex] ?? undefined;\n this.drawCell(context, metrics, x, y, text, span, style);\n }\n }\n\n private drawImages(\n context: CanvasRenderingContext2D,\n images: WebHostSurfaceImage[],\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n for (const image of images) {\n this.drawImage(context, image, metrics, dirtyRegion);\n }\n }\n\n private drawImage(\n context: CanvasRenderingContext2D,\n image: WebHostSurfaceImage,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n const decodedImage = this.cachedImage(image);\n if (!decodedImage) {\n return;\n }\n\n const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;\n const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;\n if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) {\n return;\n }\n if (\n dirtyRegion\n && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)\n ) {\n return;\n }\n\n context.save();\n context.beginPath();\n context.rect(\n clipX * metrics.cellWidth,\n clipY * metrics.cellHeight,\n clipWidth * metrics.cellWidth,\n clipHeight * metrics.cellHeight\n );\n context.clip();\n context.drawImage(\n decodedImage,\n boundsX * metrics.cellWidth,\n boundsY * metrics.cellHeight,\n boundsWidth * metrics.cellWidth,\n boundsHeight * metrics.cellHeight\n );\n context.restore();\n }\n\n private cachedImage(\n image: WebHostSurfaceImage\n ): CanvasImageSource | undefined {\n const cached = this.imageCache.get(image.id);\n if (cached?.image) {\n return cached.image;\n }\n\n if (!cached?.promise && image.dataBase64) {\n const promise = decodeImage(image.dataBase64, image.format);\n this.imageCache.set(image.id, { promise });\n void promise.then((decodedImage) => {\n const latest = this.imageCache.get(image.id);\n if (latest?.promise !== promise) {\n return;\n }\n this.imageCache.set(image.id, { image: decodedImage });\n this.requestRedraw();\n }).catch(() => {\n this.imageCache.delete(image.id);\n });\n }\n\n return undefined;\n }\n\n private drawCell(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n text: string,\n span: number,\n style?: WebHostSurfaceStyle | null\n ): void {\n const rectX = x * metrics.cellWidth;\n const rectY = y * metrics.cellHeight;\n const width = Math.max(1, span) * metrics.cellWidth;\n const background = resolvedSurfaceBackground(style, metrics.style);\n const foreground = resolvedSurfaceForeground(style, metrics.style);\n const opacity = style?.opacity ?? 1;\n\n if (background) {\n context.globalAlpha = opacity;\n context.fillStyle = background;\n context.fillRect(rectX, rectY, width, metrics.cellHeight);\n }\n\n if (text !== \" \") {\n context.globalAlpha = opacity;\n context.fillStyle = foreground;\n context.strokeStyle = foreground;\n if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {\n x: rectX,\n y: rectY,\n width,\n height: metrics.cellHeight,\n })) {\n context.font = fontForStyle(metrics.style, style);\n context.fillText(\n text,\n rectX,\n rectY + Math.floor((metrics.cellHeight + metrics.style.fontSize) / 2) - 2\n );\n }\n }\n\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.underline, \"underline\", foreground);\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.strikethrough, \"strike\", foreground);\n context.globalAlpha = 1;\n }\n\n private dirtyRegionForDamage(\n damage: WebHostSurfaceDamage | undefined,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics\n ): DirtyRegion | undefined {\n if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) {\n return undefined;\n }\n\n const rects: DirtyRect[] = [];\n const rows = new Map<number, DirtyRowRanges>();\n for (const [row, ranges] of damage.textRows) {\n if (row < 0 || row >= frame.height) {\n continue;\n }\n if (ranges.length === 0) {\n rects.push(cellRect(metrics, 0, row, frame.width));\n rows.set(row, \"full\");\n continue;\n }\n const rowRanges: DirtyCellRange[] = rows.get(row) === \"full\"\n ? []\n : [...(rows.get(row) as DirtyCellRange[] | undefined ?? [])];\n for (const [start, end] of ranges) {\n const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));\n const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));\n if (lowerBound >= upperBound) {\n continue;\n }\n rects.push(cellRect(metrics, lowerBound, row, upperBound - lowerBound));\n rowRanges.push({ start: lowerBound, end: upperBound });\n }\n if (rows.get(row) !== \"full\" && rowRanges.length > 0) {\n rows.set(row, normalizeCellRanges(rowRanges));\n }\n }\n return { rects, rows };\n }\n\n private drawTextLine(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n width: number,\n line: WebHostSurfaceStyle[\"underline\"],\n placement: \"underline\" | \"strike\",\n fallbackColor: string\n ): void {\n if (!line) {\n return;\n }\n context.strokeStyle = line.color ?? fallbackColor;\n context.lineWidth = line.pattern === \"double\" ? 2 : 1;\n if (line.pattern === \"dot\") {\n context.setLineDash([1, 3]);\n } else if (line.pattern === \"dash\") {\n context.setLineDash([4, 3]);\n } else {\n context.setLineDash([]);\n }\n\n const lineY = placement === \"underline\"\n ? y + metrics.cellHeight - 2\n : y + Math.floor(metrics.cellHeight / 2);\n context.beginPath();\n context.moveTo(x, lineY);\n context.lineTo(x + width, lineY);\n context.stroke();\n context.setLineDash([]);\n }\n}\n\n/**\n * The CSS font string for a cell, folding the surface emphasis bits (bold,\n * italic) over the host terminal's font size/family. Exposed so the host can\n * reuse the exact same metric when measuring cell dimensions.\n */\nexport function fontForStyle(\n terminalStyle: ResolvedWebHostTerminalStyle,\n style?: WebHostSurfaceStyle | null\n): string {\n const emphasis = style?.em ?? 0;\n const italic = (emphasis & 2) !== 0 ? \"italic \" : \"\";\n const weight = (emphasis & 1) !== 0 ? \"700 \" : \"\";\n return `${italic}${weight}${terminalStyle.fontSize}px ${terminalStyle.fontFamily}`;\n}\n\nfunction cellRect(\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n span: number\n): DirtyRect {\n return {\n x: x * metrics.cellWidth,\n y: y * metrics.cellHeight,\n width: Math.max(1, span) * metrics.cellWidth,\n height: metrics.cellHeight,\n };\n}\n\nasync function decodeImage(\n dataBase64: string,\n format: WebHostSurfaceImageFormat\n): Promise<CanvasImageSource> {\n const bytes = decodeBase64Bytes(dataBase64);\n const blob = new Blob([bytes], { type: `image/${format}` });\n\n if (typeof createImageBitmap === \"function\") {\n // Animated GIFs collapse to their first frame in createImageBitmap\n // — that matches the Kitty path's first-frame composite. Phase 7\n // will replace this with a frame ticker.\n return createImageBitmap(blob);\n }\n\n return new Promise((resolve, reject) => {\n const image = new Image();\n const url = URL.createObjectURL(blob);\n image.onload = () => {\n URL.revokeObjectURL(url);\n resolve(image);\n };\n image.onerror = () => {\n URL.revokeObjectURL(url);\n reject(new Error(`Failed to decode ${format} image`));\n };\n image.src = url;\n });\n}\n\nfunction decodeBase64Bytes(\n value: string\n): Uint8Array {\n if (typeof atob === \"function\") {\n const binary = atob(value);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n }\n\n return new Uint8Array(Buffer.from(value, \"base64\"));\n}\n\nfunction normalizeCellRanges(\n ranges: DirtyCellRange[]\n): DirtyCellRange[] {\n const sorted = ranges\n .filter((range) => range.end > range.start)\n .sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);\n const normalized: DirtyCellRange[] = [];\n for (const range of sorted) {\n const previous = normalized[normalized.length - 1];\n if (previous && range.start <= previous.end) {\n previous.end = Math.max(previous.end, range.end);\n continue;\n }\n normalized.push({ ...range });\n }\n return normalized;\n}\n\nfunction cellIntersectsRanges(\n x: number,\n span: number,\n ranges: DirtyRowRanges\n): boolean {\n if (ranges === \"full\") {\n return true;\n }\n const start = Math.floor(x);\n const end = start + Math.max(1, Math.ceil(span));\n return ranges.some((range) => start < range.end && end > range.start);\n}\n\nfunction dirtyRegionIntersectsCellRect(\n region: DirtyRegion,\n x: number,\n y: number,\n width: number,\n height: number\n): boolean {\n const startRow = Math.max(0, Math.floor(y));\n const endRow = Math.max(startRow, Math.ceil(y + height));\n const rectRange = {\n start: Math.floor(x),\n end: Math.floor(x) + Math.max(1, Math.ceil(width)),\n };\n for (let row = startRow; row < endRow; row += 1) {\n const ranges = region.rows.get(row);\n if (!ranges) {\n continue;\n }\n if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) {\n return true;\n }\n }\n return false;\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { WebHostSurfaceDamage, WebHostSurfaceFrame } from "./WebHostSurfaceTransport.js";
|
|
2
|
+
import { SurfaceMetrics, WebHostSurfacePainter } from "./SurfaceRenderer.js";
|
|
3
|
+
//#region src/DomSurfacePainter.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Draws SwiftTUI surface frames as a DOM element tree instead of canvas
|
|
6
|
+
* pixels: one absolutely positioned row container per grid row, one `<span>`
|
|
7
|
+
* per styled cell run, and `<img>` elements for surface images.
|
|
8
|
+
*
|
|
9
|
+
* Rendering cells as real text buys what canvas cannot offer — the browser's
|
|
10
|
+
* own font shaping and fallback (emoji, CJK), crisp text at any page zoom, an
|
|
11
|
+
* inspectable tree, and native text selection — at the cost of pixel-exact
|
|
12
|
+
* box-drawing seams, which render as font glyphs here rather than the canvas
|
|
13
|
+
* painter's hand-drawn strokes.
|
|
14
|
+
*
|
|
15
|
+
* Damage handling mirrors the canvas painter at row granularity: a frame
|
|
16
|
+
* carrying scoped damage rebuilds only the touched rows' elements; geometry
|
|
17
|
+
* or style changes force a full rebuild. Grid alignment across a run is kept
|
|
18
|
+
* exact by stretching each glyph advance to the cell width via
|
|
19
|
+
* `letter-spacing`, measured once per font/size pair.
|
|
20
|
+
*/
|
|
21
|
+
declare class DomSurfacePainter implements WebHostSurfacePainter {
|
|
22
|
+
private root?;
|
|
23
|
+
private rowsLayer?;
|
|
24
|
+
private imagesLayer?;
|
|
25
|
+
private rowElements;
|
|
26
|
+
private renderedImages;
|
|
27
|
+
private appliedMetricsKey?;
|
|
28
|
+
private renderedGridKey?;
|
|
29
|
+
private hasRenderedFrame;
|
|
30
|
+
private letterSpacing?;
|
|
31
|
+
/**
|
|
32
|
+
* Binds the container the painter renders into. The runtime owns the
|
|
33
|
+
* container's size; the painter owns everything inside it.
|
|
34
|
+
*/
|
|
35
|
+
attach(root: HTMLElement): void;
|
|
36
|
+
paint(metrics: SurfaceMetrics, frame: WebHostSurfaceFrame | undefined, damage?: WebHostSurfaceDamage): void;
|
|
37
|
+
private rebuildRow;
|
|
38
|
+
private ensureRowElement;
|
|
39
|
+
private applyRootStyle;
|
|
40
|
+
/**
|
|
41
|
+
* The per-glyph advance correction that stretches the font's natural
|
|
42
|
+
* monospace advance to exactly `cellWidth`, so long runs stay on the cell
|
|
43
|
+
* grid instead of drifting by the sub-pixel remainder of the runtime's
|
|
44
|
+
* ceil'd cell measurement.
|
|
45
|
+
*/
|
|
46
|
+
private letterSpacingFor;
|
|
47
|
+
private reconcileImages;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { DomSurfacePainter };
|
|
51
|
+
//# sourceMappingURL=DomSurfacePainter.d.ts.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
|
|
2
|
+
import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./SurfaceRenderer.js";
|
|
3
|
+
import { fontForStyle } from "./CanvasSurfacePainter.js";
|
|
4
|
+
//#region src/DomSurfacePainter.ts
|
|
5
|
+
/**
|
|
6
|
+
* Draws SwiftTUI surface frames as a DOM element tree instead of canvas
|
|
7
|
+
* pixels: one absolutely positioned row container per grid row, one `<span>`
|
|
8
|
+
* per styled cell run, and `<img>` elements for surface images.
|
|
9
|
+
*
|
|
10
|
+
* Rendering cells as real text buys what canvas cannot offer — the browser's
|
|
11
|
+
* own font shaping and fallback (emoji, CJK), crisp text at any page zoom, an
|
|
12
|
+
* inspectable tree, and native text selection — at the cost of pixel-exact
|
|
13
|
+
* box-drawing seams, which render as font glyphs here rather than the canvas
|
|
14
|
+
* painter's hand-drawn strokes.
|
|
15
|
+
*
|
|
16
|
+
* Damage handling mirrors the canvas painter at row granularity: a frame
|
|
17
|
+
* carrying scoped damage rebuilds only the touched rows' elements; geometry
|
|
18
|
+
* or style changes force a full rebuild. Grid alignment across a run is kept
|
|
19
|
+
* exact by stretching each glyph advance to the cell width via
|
|
20
|
+
* `letter-spacing`, measured once per font/size pair.
|
|
21
|
+
*/
|
|
22
|
+
var DomSurfacePainter = class {
|
|
23
|
+
root;
|
|
24
|
+
rowsLayer;
|
|
25
|
+
imagesLayer;
|
|
26
|
+
rowElements = [];
|
|
27
|
+
renderedImages = /* @__PURE__ */ new Map();
|
|
28
|
+
appliedMetricsKey;
|
|
29
|
+
renderedGridKey;
|
|
30
|
+
hasRenderedFrame = false;
|
|
31
|
+
letterSpacing;
|
|
32
|
+
/**
|
|
33
|
+
* Binds the container the painter renders into. The runtime owns the
|
|
34
|
+
* container's size; the painter owns everything inside it.
|
|
35
|
+
*/
|
|
36
|
+
attach(root) {
|
|
37
|
+
this.root = root;
|
|
38
|
+
const rowsLayer = createElement("div");
|
|
39
|
+
rowsLayer.className = "webhost-scene__surface-rows";
|
|
40
|
+
fillContainer(rowsLayer.style);
|
|
41
|
+
const imagesLayer = createElement("div");
|
|
42
|
+
imagesLayer.className = "webhost-scene__surface-images";
|
|
43
|
+
fillContainer(imagesLayer.style);
|
|
44
|
+
imagesLayer.style.pointerEvents = "none";
|
|
45
|
+
this.rowsLayer = rowsLayer;
|
|
46
|
+
this.imagesLayer = imagesLayer;
|
|
47
|
+
root.replaceChildren(rowsLayer, imagesLayer);
|
|
48
|
+
this.rowElements = [];
|
|
49
|
+
this.renderedImages = /* @__PURE__ */ new Map();
|
|
50
|
+
this.appliedMetricsKey = void 0;
|
|
51
|
+
this.renderedGridKey = void 0;
|
|
52
|
+
this.hasRenderedFrame = false;
|
|
53
|
+
}
|
|
54
|
+
paint(metrics, frame, damage) {
|
|
55
|
+
const root = this.root;
|
|
56
|
+
const rowsLayer = this.rowsLayer;
|
|
57
|
+
if (!root || !rowsLayer) return;
|
|
58
|
+
const metricsKey = metricsKeyFor(metrics);
|
|
59
|
+
const metricsChanged = metricsKey !== this.appliedMetricsKey;
|
|
60
|
+
if (metricsChanged) {
|
|
61
|
+
this.applyRootStyle(root, metrics);
|
|
62
|
+
this.appliedMetricsKey = metricsKey;
|
|
63
|
+
}
|
|
64
|
+
if (!frame) {
|
|
65
|
+
this.rowElements = [];
|
|
66
|
+
rowsLayer.replaceChildren();
|
|
67
|
+
this.reconcileImages([], metrics);
|
|
68
|
+
this.renderedGridKey = void 0;
|
|
69
|
+
this.hasRenderedFrame = false;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const gridKey = `${frame.width}x${frame.height}x${frame.rows.length}`;
|
|
73
|
+
const fullRepaint = metricsChanged || !this.hasRenderedFrame || gridKey !== this.renderedGridKey || !damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay;
|
|
74
|
+
this.renderedGridKey = gridKey;
|
|
75
|
+
if (fullRepaint) {
|
|
76
|
+
for (let y = this.rowElements.length; y > frame.rows.length; y -= 1) this.rowElements[y - 1]?.remove();
|
|
77
|
+
this.rowElements.length = Math.min(this.rowElements.length, frame.rows.length);
|
|
78
|
+
for (let y = 0; y < frame.rows.length; y += 1) this.rebuildRow(y, frame, metrics);
|
|
79
|
+
} else for (const [row] of damage.textRows) {
|
|
80
|
+
if (row < 0 || row >= frame.rows.length) continue;
|
|
81
|
+
this.rebuildRow(row, frame, metrics);
|
|
82
|
+
}
|
|
83
|
+
this.reconcileImages(frame.images ?? [], metrics);
|
|
84
|
+
this.hasRenderedFrame = true;
|
|
85
|
+
}
|
|
86
|
+
rebuildRow(y, frame, metrics) {
|
|
87
|
+
const rowElement = this.ensureRowElement(y, metrics);
|
|
88
|
+
const children = [];
|
|
89
|
+
for (const [x, text, span, styleIndex] of frame.rows[y] ?? []) {
|
|
90
|
+
const cellElement = buildCellElement(x, text, span, frame.styles[styleIndex] ?? void 0, metrics);
|
|
91
|
+
if (cellElement) children.push(cellElement);
|
|
92
|
+
}
|
|
93
|
+
rowElement.replaceChildren(...children);
|
|
94
|
+
}
|
|
95
|
+
ensureRowElement(y, metrics) {
|
|
96
|
+
let rowElement = this.rowElements[y];
|
|
97
|
+
if (!rowElement) {
|
|
98
|
+
rowElement = createElement("div");
|
|
99
|
+
rowElement.className = "webhost-scene__surface-row";
|
|
100
|
+
rowElement.style.position = "absolute";
|
|
101
|
+
rowElement.style.left = "0";
|
|
102
|
+
this.rowElements[y] = rowElement;
|
|
103
|
+
this.rowsLayer?.appendChild(rowElement);
|
|
104
|
+
}
|
|
105
|
+
rowElement.style.top = `${y * metrics.cellHeight}px`;
|
|
106
|
+
rowElement.style.height = `${metrics.cellHeight}px`;
|
|
107
|
+
rowElement.style.width = `${metrics.columns * metrics.cellWidth}px`;
|
|
108
|
+
return rowElement;
|
|
109
|
+
}
|
|
110
|
+
applyRootStyle(root, metrics) {
|
|
111
|
+
const style = root.style;
|
|
112
|
+
style.position = "relative";
|
|
113
|
+
style.overflow = "hidden";
|
|
114
|
+
style.background = webTUITerminalBackgroundColor(metrics.style);
|
|
115
|
+
style.font = fontForStyle(metrics.style);
|
|
116
|
+
style.lineHeight = `${metrics.cellHeight}px`;
|
|
117
|
+
style.letterSpacing = this.letterSpacingFor(metrics);
|
|
118
|
+
style.fontVariantLigatures = "none";
|
|
119
|
+
style.userSelect = "text";
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The per-glyph advance correction that stretches the font's natural
|
|
123
|
+
* monospace advance to exactly `cellWidth`, so long runs stay on the cell
|
|
124
|
+
* grid instead of drifting by the sub-pixel remainder of the runtime's
|
|
125
|
+
* ceil'd cell measurement.
|
|
126
|
+
*/
|
|
127
|
+
letterSpacingFor(metrics) {
|
|
128
|
+
const font = fontForStyle(metrics.style);
|
|
129
|
+
const key = `${font}|${metrics.cellWidth}`;
|
|
130
|
+
if (this.letterSpacing?.key === key) return this.letterSpacing.value;
|
|
131
|
+
let value = "0px";
|
|
132
|
+
const context = createElement("canvas").getContext?.("2d");
|
|
133
|
+
if (context) {
|
|
134
|
+
context.font = font;
|
|
135
|
+
const advance = context.measureText("W").width;
|
|
136
|
+
const correction = metrics.cellWidth - advance;
|
|
137
|
+
if (advance > 0 && Math.abs(correction) >= .01) value = `${Math.round(correction * 1e3) / 1e3}px`;
|
|
138
|
+
}
|
|
139
|
+
this.letterSpacing = {
|
|
140
|
+
key,
|
|
141
|
+
value
|
|
142
|
+
};
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
reconcileImages(images, metrics) {
|
|
146
|
+
const layer = this.imagesLayer;
|
|
147
|
+
if (!layer) return;
|
|
148
|
+
const next = /* @__PURE__ */ new Map();
|
|
149
|
+
for (const image of images) {
|
|
150
|
+
const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
|
|
151
|
+
const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
|
|
152
|
+
if (!image.dataBase64 || boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) continue;
|
|
153
|
+
const existing = this.renderedImages.get(image.id);
|
|
154
|
+
const entry = existing ?? makeImageEntry();
|
|
155
|
+
entry.container.style.left = `${clipX * metrics.cellWidth}px`;
|
|
156
|
+
entry.container.style.top = `${clipY * metrics.cellHeight}px`;
|
|
157
|
+
entry.container.style.width = `${clipWidth * metrics.cellWidth}px`;
|
|
158
|
+
entry.container.style.height = `${clipHeight * metrics.cellHeight}px`;
|
|
159
|
+
entry.image.style.left = `${(boundsX - clipX) * metrics.cellWidth}px`;
|
|
160
|
+
entry.image.style.top = `${(boundsY - clipY) * metrics.cellHeight}px`;
|
|
161
|
+
entry.image.style.width = `${boundsWidth * metrics.cellWidth}px`;
|
|
162
|
+
entry.image.style.height = `${boundsHeight * metrics.cellHeight}px`;
|
|
163
|
+
const source = `data:image/${image.format};base64,${image.dataBase64}`;
|
|
164
|
+
if (entry.source !== source) {
|
|
165
|
+
entry.image.setAttribute("src", source);
|
|
166
|
+
entry.source = source;
|
|
167
|
+
}
|
|
168
|
+
if (!existing) layer.appendChild(entry.container);
|
|
169
|
+
next.set(image.id, entry);
|
|
170
|
+
}
|
|
171
|
+
for (const [id, entry] of this.renderedImages) if (!next.has(id)) entry.container.remove();
|
|
172
|
+
this.renderedImages = next;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/**
|
|
176
|
+
* A change key over everything the rendered tree bakes into element styles —
|
|
177
|
+
* grid geometry, font, and theme colors. A key change invalidates every
|
|
178
|
+
* rendered row, so the next paint restyles the root and rebuilds in full.
|
|
179
|
+
*/
|
|
180
|
+
function metricsKeyFor(metrics) {
|
|
181
|
+
return [
|
|
182
|
+
metrics.columns,
|
|
183
|
+
metrics.rows,
|
|
184
|
+
metrics.cellWidth,
|
|
185
|
+
metrics.cellHeight,
|
|
186
|
+
fontForStyle(metrics.style),
|
|
187
|
+
metrics.style.theme.foreground,
|
|
188
|
+
metrics.style.theme.background,
|
|
189
|
+
metrics.style.theme.windowBackground,
|
|
190
|
+
metrics.style.backgroundOpacity
|
|
191
|
+
].join("|");
|
|
192
|
+
}
|
|
193
|
+
function buildCellElement(x, text, span, style, metrics) {
|
|
194
|
+
const background = resolvedSurfaceBackground(style, metrics.style);
|
|
195
|
+
const hasDecoration = Boolean(style?.underline || style?.strikethrough);
|
|
196
|
+
if (!background && !hasDecoration && text.trim() === "") return;
|
|
197
|
+
const element = createElement("span");
|
|
198
|
+
element.textContent = text;
|
|
199
|
+
const elementStyle = element.style;
|
|
200
|
+
elementStyle.position = "absolute";
|
|
201
|
+
elementStyle.left = `${x * metrics.cellWidth}px`;
|
|
202
|
+
elementStyle.top = "0";
|
|
203
|
+
elementStyle.width = `${Math.max(1, span) * metrics.cellWidth}px`;
|
|
204
|
+
elementStyle.height = "100%";
|
|
205
|
+
elementStyle.whiteSpace = "pre";
|
|
206
|
+
elementStyle.color = resolvedSurfaceForeground(style, metrics.style);
|
|
207
|
+
if (background) elementStyle.backgroundColor = background;
|
|
208
|
+
const emphasis = style?.em ?? 0;
|
|
209
|
+
if (emphasis & 1) elementStyle.fontWeight = "700";
|
|
210
|
+
if (emphasis & 2) elementStyle.fontStyle = "italic";
|
|
211
|
+
const opacity = style?.opacity ?? 1;
|
|
212
|
+
if (opacity !== 1) elementStyle.opacity = String(opacity);
|
|
213
|
+
applyTextDecoration(elementStyle, style);
|
|
214
|
+
return element;
|
|
215
|
+
}
|
|
216
|
+
function applyTextDecoration(elementStyle, style) {
|
|
217
|
+
const lines = [];
|
|
218
|
+
if (style?.underline) lines.push("underline");
|
|
219
|
+
if (style?.strikethrough) lines.push("line-through");
|
|
220
|
+
if (lines.length === 0) return;
|
|
221
|
+
elementStyle.textDecorationLine = lines.join(" ");
|
|
222
|
+
elementStyle.textDecorationStyle = decorationStyleFor(style?.underline?.pattern ?? style?.strikethrough?.pattern);
|
|
223
|
+
const color = style?.underline?.color ?? style?.strikethrough?.color;
|
|
224
|
+
if (color) elementStyle.textDecorationColor = color;
|
|
225
|
+
}
|
|
226
|
+
function decorationStyleFor(pattern) {
|
|
227
|
+
switch (pattern) {
|
|
228
|
+
case "dot": return "dotted";
|
|
229
|
+
case "dash":
|
|
230
|
+
case "dashDot":
|
|
231
|
+
case "dashDotDot": return "dashed";
|
|
232
|
+
case "double": return "double";
|
|
233
|
+
case "curly": return "wavy";
|
|
234
|
+
default: return "solid";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function fillContainer(style) {
|
|
238
|
+
style.position = "absolute";
|
|
239
|
+
style.left = "0";
|
|
240
|
+
style.top = "0";
|
|
241
|
+
style.width = "100%";
|
|
242
|
+
style.height = "100%";
|
|
243
|
+
}
|
|
244
|
+
function makeImageEntry() {
|
|
245
|
+
const container = createElement("div");
|
|
246
|
+
container.className = "webhost-scene__surface-image";
|
|
247
|
+
container.style.position = "absolute";
|
|
248
|
+
container.style.overflow = "hidden";
|
|
249
|
+
const image = createElement("img");
|
|
250
|
+
image.style.position = "absolute";
|
|
251
|
+
image.setAttribute("alt", "");
|
|
252
|
+
image.setAttribute("draggable", "false");
|
|
253
|
+
container.appendChild(image);
|
|
254
|
+
return {
|
|
255
|
+
container,
|
|
256
|
+
image,
|
|
257
|
+
source: ""
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function createElement(tagName) {
|
|
261
|
+
if (typeof document === "undefined") throw new Error("document is not available");
|
|
262
|
+
return document.createElement(tagName);
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
export { DomSurfacePainter };
|
|
266
|
+
|
|
267
|
+
//# sourceMappingURL=DomSurfacePainter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DomSurfacePainter.js","names":[],"sources":["../../src/DomSurfacePainter.ts"],"sourcesContent":["import { fontForStyle } from \"./CanvasSurfacePainter.ts\";\nimport {\n resolvedSurfaceBackground,\n resolvedSurfaceForeground,\n type SurfaceMetrics,\n type WebHostSurfacePainter,\n} from \"./SurfaceRenderer.ts\";\nimport { webTUITerminalBackgroundColor } from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\n WebHostSurfaceLineStyle,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\ninterface RenderedImage {\n container: HTMLElement;\n image: HTMLElement;\n source: string;\n}\n\n/**\n * Draws SwiftTUI surface frames as a DOM element tree instead of canvas\n * pixels: one absolutely positioned row container per grid row, one `<span>`\n * per styled cell run, and `<img>` elements for surface images.\n *\n * Rendering cells as real text buys what canvas cannot offer — the browser's\n * own font shaping and fallback (emoji, CJK), crisp text at any page zoom, an\n * inspectable tree, and native text selection — at the cost of pixel-exact\n * box-drawing seams, which render as font glyphs here rather than the canvas\n * painter's hand-drawn strokes.\n *\n * Damage handling mirrors the canvas painter at row granularity: a frame\n * carrying scoped damage rebuilds only the touched rows' elements; geometry\n * or style changes force a full rebuild. Grid alignment across a run is kept\n * exact by stretching each glyph advance to the cell width via\n * `letter-spacing`, measured once per font/size pair.\n */\nexport class DomSurfacePainter implements WebHostSurfacePainter {\n private root?: HTMLElement;\n private rowsLayer?: HTMLElement;\n private imagesLayer?: HTMLElement;\n private rowElements: HTMLElement[] = [];\n private renderedImages = new Map<string, RenderedImage>();\n private appliedMetricsKey?: string;\n private renderedGridKey?: string;\n private hasRenderedFrame = false;\n private letterSpacing?: { key: string; value: string };\n\n /**\n * Binds the container the painter renders into. The runtime owns the\n * container's size; the painter owns everything inside it.\n */\n attach(\n root: HTMLElement\n ): void {\n this.root = root;\n\n const rowsLayer = createElement(\"div\");\n rowsLayer.className = \"webhost-scene__surface-rows\";\n fillContainer(rowsLayer.style);\n\n const imagesLayer = createElement(\"div\");\n imagesLayer.className = \"webhost-scene__surface-images\";\n fillContainer(imagesLayer.style);\n imagesLayer.style.pointerEvents = \"none\";\n\n this.rowsLayer = rowsLayer;\n this.imagesLayer = imagesLayer;\n root.replaceChildren(rowsLayer, imagesLayer);\n this.rowElements = [];\n this.renderedImages = new Map();\n this.appliedMetricsKey = undefined;\n this.renderedGridKey = undefined;\n this.hasRenderedFrame = false;\n }\n\n paint(\n metrics: SurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void {\n const root = this.root;\n const rowsLayer = this.rowsLayer;\n if (!root || !rowsLayer) {\n return;\n }\n\n const metricsKey = metricsKeyFor(metrics);\n const metricsChanged = metricsKey !== this.appliedMetricsKey;\n if (metricsChanged) {\n this.applyRootStyle(root, metrics);\n this.appliedMetricsKey = metricsKey;\n }\n\n if (!frame) {\n this.rowElements = [];\n rowsLayer.replaceChildren();\n this.reconcileImages([], metrics);\n this.renderedGridKey = undefined;\n this.hasRenderedFrame = false;\n return;\n }\n\n const gridKey = `${frame.width}x${frame.height}x${frame.rows.length}`;\n const fullRepaint = metricsChanged\n || !this.hasRenderedFrame\n || gridKey !== this.renderedGridKey\n || !damage\n || damage.requiresFullTextRepaint\n || damage.requiresFullGraphicsReplay;\n this.renderedGridKey = gridKey;\n\n if (fullRepaint) {\n for (let y = this.rowElements.length; y > frame.rows.length; y -= 1) {\n this.rowElements[y - 1]?.remove();\n }\n this.rowElements.length = Math.min(this.rowElements.length, frame.rows.length);\n for (let y = 0; y < frame.rows.length; y += 1) {\n this.rebuildRow(y, frame, metrics);\n }\n } else {\n for (const [row] of damage.textRows) {\n if (row < 0 || row >= frame.rows.length) {\n continue;\n }\n this.rebuildRow(row, frame, metrics);\n }\n }\n\n this.reconcileImages(frame.images ?? [], metrics);\n this.hasRenderedFrame = true;\n }\n\n private rebuildRow(\n y: number,\n frame: WebHostSurfaceFrame,\n metrics: SurfaceMetrics\n ): void {\n const rowElement = this.ensureRowElement(y, metrics);\n const children: HTMLElement[] = [];\n for (const [x, text, span, styleIndex] of frame.rows[y] ?? []) {\n const cellElement = buildCellElement(\n x,\n text,\n span,\n frame.styles[styleIndex] ?? undefined,\n metrics\n );\n if (cellElement) {\n children.push(cellElement);\n }\n }\n rowElement.replaceChildren(...children);\n }\n\n private ensureRowElement(\n y: number,\n metrics: SurfaceMetrics\n ): HTMLElement {\n let rowElement = this.rowElements[y];\n if (!rowElement) {\n rowElement = createElement(\"div\");\n rowElement.className = \"webhost-scene__surface-row\";\n rowElement.style.position = \"absolute\";\n rowElement.style.left = \"0\";\n this.rowElements[y] = rowElement;\n this.rowsLayer?.appendChild(rowElement);\n }\n rowElement.style.top = `${y * metrics.cellHeight}px`;\n rowElement.style.height = `${metrics.cellHeight}px`;\n rowElement.style.width = `${metrics.columns * metrics.cellWidth}px`;\n return rowElement;\n }\n\n private applyRootStyle(\n root: HTMLElement,\n metrics: SurfaceMetrics\n ): void {\n const style = root.style;\n style.position = \"relative\";\n style.overflow = \"hidden\";\n style.background = webTUITerminalBackgroundColor(metrics.style);\n style.font = fontForStyle(metrics.style);\n // Set after `font`: the shorthand resets line-height, and the grid needs\n // every row to be exactly one cell tall.\n style.lineHeight = `${metrics.cellHeight}px`;\n style.letterSpacing = this.letterSpacingFor(metrics);\n // Ligature-capable monospace fonts would merge runs like \"->\" into one\n // glyph and break the column grid.\n style.fontVariantLigatures = \"none\";\n style.userSelect = \"text\";\n }\n\n /**\n * The per-glyph advance correction that stretches the font's natural\n * monospace advance to exactly `cellWidth`, so long runs stay on the cell\n * grid instead of drifting by the sub-pixel remainder of the runtime's\n * ceil'd cell measurement.\n */\n private letterSpacingFor(\n metrics: SurfaceMetrics\n ): string {\n const font = fontForStyle(metrics.style);\n const key = `${font}|${metrics.cellWidth}`;\n if (this.letterSpacing?.key === key) {\n return this.letterSpacing.value;\n }\n\n let value = \"0px\";\n const canvas = createElement(\"canvas\") as HTMLCanvasElement;\n const context = canvas.getContext?.(\"2d\");\n if (context) {\n context.font = font;\n const advance = context.measureText(\"W\").width;\n const correction = metrics.cellWidth - advance;\n if (advance > 0 && Math.abs(correction) >= 0.01) {\n value = `${Math.round(correction * 1000) / 1000}px`;\n }\n }\n\n this.letterSpacing = { key, value };\n return value;\n }\n\n private reconcileImages(\n images: WebHostSurfaceImage[],\n metrics: SurfaceMetrics\n ): void {\n const layer = this.imagesLayer;\n if (!layer) {\n return;\n }\n\n const next = new Map<string, RenderedImage>();\n for (const image of images) {\n const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;\n const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;\n if (\n !image.dataBase64\n || boundsWidth <= 0\n || boundsHeight <= 0\n || clipWidth <= 0\n || clipHeight <= 0\n ) {\n continue;\n }\n\n const existing = this.renderedImages.get(image.id);\n const entry = existing ?? makeImageEntry();\n entry.container.style.left = `${clipX * metrics.cellWidth}px`;\n entry.container.style.top = `${clipY * metrics.cellHeight}px`;\n entry.container.style.width = `${clipWidth * metrics.cellWidth}px`;\n entry.container.style.height = `${clipHeight * metrics.cellHeight}px`;\n entry.image.style.left = `${(boundsX - clipX) * metrics.cellWidth}px`;\n entry.image.style.top = `${(boundsY - clipY) * metrics.cellHeight}px`;\n entry.image.style.width = `${boundsWidth * metrics.cellWidth}px`;\n entry.image.style.height = `${boundsHeight * metrics.cellHeight}px`;\n\n const source = `data:image/${image.format};base64,${image.dataBase64}`;\n if (entry.source !== source) {\n entry.image.setAttribute(\"src\", source);\n entry.source = source;\n }\n if (!existing) {\n layer.appendChild(entry.container);\n }\n next.set(image.id, entry);\n }\n\n for (const [id, entry] of this.renderedImages) {\n if (!next.has(id)) {\n entry.container.remove();\n }\n }\n this.renderedImages = next;\n }\n}\n\n/**\n * A change key over everything the rendered tree bakes into element styles —\n * grid geometry, font, and theme colors. A key change invalidates every\n * rendered row, so the next paint restyles the root and rebuilds in full.\n */\nfunction metricsKeyFor(\n metrics: SurfaceMetrics\n): string {\n return [\n metrics.columns,\n metrics.rows,\n metrics.cellWidth,\n metrics.cellHeight,\n fontForStyle(metrics.style),\n metrics.style.theme.foreground,\n metrics.style.theme.background,\n metrics.style.theme.windowBackground,\n metrics.style.backgroundOpacity,\n ].join(\"|\");\n}\n\nfunction buildCellElement(\n x: number,\n text: string,\n span: number,\n style: WebHostSurfaceStyle | undefined,\n metrics: SurfaceMetrics\n): HTMLElement | undefined {\n const background = resolvedSurfaceBackground(style, metrics.style);\n const hasDecoration = Boolean(style?.underline || style?.strikethrough);\n if (!background && !hasDecoration && text.trim() === \"\") {\n return undefined;\n }\n\n const element = createElement(\"span\");\n element.textContent = text;\n const elementStyle = element.style;\n elementStyle.position = \"absolute\";\n elementStyle.left = `${x * metrics.cellWidth}px`;\n elementStyle.top = \"0\";\n elementStyle.width = `${Math.max(1, span) * metrics.cellWidth}px`;\n elementStyle.height = \"100%\";\n elementStyle.whiteSpace = \"pre\";\n elementStyle.color = resolvedSurfaceForeground(style, metrics.style);\n if (background) {\n elementStyle.backgroundColor = background;\n }\n\n const emphasis = style?.em ?? 0;\n if (emphasis & 1) {\n elementStyle.fontWeight = \"700\";\n }\n if (emphasis & 2) {\n elementStyle.fontStyle = \"italic\";\n }\n\n const opacity = style?.opacity ?? 1;\n if (opacity !== 1) {\n elementStyle.opacity = String(opacity);\n }\n\n applyTextDecoration(elementStyle, style);\n return element;\n}\n\nfunction applyTextDecoration(\n elementStyle: CSSStyleDeclaration,\n style: WebHostSurfaceStyle | undefined\n): void {\n const lines: string[] = [];\n if (style?.underline) {\n lines.push(\"underline\");\n }\n if (style?.strikethrough) {\n lines.push(\"line-through\");\n }\n if (lines.length === 0) {\n return;\n }\n\n elementStyle.textDecorationLine = lines.join(\" \");\n const pattern = style?.underline?.pattern ?? style?.strikethrough?.pattern;\n elementStyle.textDecorationStyle = decorationStyleFor(pattern);\n // CSS shares one decoration color across both lines; the underline's color\n // wins when the app styles them differently.\n const color = style?.underline?.color ?? style?.strikethrough?.color;\n if (color) {\n elementStyle.textDecorationColor = color;\n }\n}\n\nfunction decorationStyleFor(\n pattern: WebHostSurfaceLineStyle[\"pattern\"] | undefined\n): string {\n switch (pattern) {\n case \"dot\":\n return \"dotted\";\n case \"dash\":\n case \"dashDot\":\n case \"dashDotDot\":\n return \"dashed\";\n case \"double\":\n return \"double\";\n case \"curly\":\n return \"wavy\";\n default:\n return \"solid\";\n }\n}\n\nfunction fillContainer(\n style: CSSStyleDeclaration\n): void {\n style.position = \"absolute\";\n style.left = \"0\";\n style.top = \"0\";\n style.width = \"100%\";\n style.height = \"100%\";\n}\n\nfunction makeImageEntry(): RenderedImage {\n const container = createElement(\"div\");\n container.className = \"webhost-scene__surface-image\";\n container.style.position = \"absolute\";\n container.style.overflow = \"hidden\";\n\n const image = createElement(\"img\");\n image.style.position = \"absolute\";\n image.setAttribute(\"alt\", \"\");\n image.setAttribute(\"draggable\", \"false\");\n container.appendChild(image);\n return { container, image, source: \"\" };\n}\n\nfunction createElement(\n tagName: string\n): HTMLElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"document is not available\");\n }\n return document.createElement(tagName);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ResolvedWebHostTerminalStyle } from "./WebHostTerminalStyle.js";
|
|
2
|
+
import { WebHostSurfaceDamage, WebHostSurfaceFrame, WebHostSurfaceStyle } from "./WebHostSurfaceTransport.js";
|
|
3
|
+
//#region src/SurfaceRenderer.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Which presenter draws surface frames into the scene mount.
|
|
6
|
+
*
|
|
7
|
+
* - `"canvas"` (default): paints cells onto a 2D `<canvas>` — pixel-exact
|
|
8
|
+
* box-drawing seams and decoration patterns, one DOM node total.
|
|
9
|
+
* - `"dom"`: renders cells as absolutely positioned text elements — native
|
|
10
|
+
* font rendering (fallback glyphs, subpixel AA, crisp zoom), an
|
|
11
|
+
* inspectable element tree, and real selectable text (hold Alt/Option and
|
|
12
|
+
* drag). Box drawing and decoration patterns render via font glyphs and
|
|
13
|
+
* CSS `text-decoration`, so hairline seams may differ from the canvas
|
|
14
|
+
* painter.
|
|
15
|
+
*/
|
|
16
|
+
type WebHostSurfaceRendererKind = "canvas" | "dom";
|
|
17
|
+
/**
|
|
18
|
+
* A read-only snapshot of the cell grid geometry and active style a surface
|
|
19
|
+
* painter needs for a single paint pass. The runtime owns this state and
|
|
20
|
+
* mutates it as the surface resizes or restyles; passing a fresh snapshot per
|
|
21
|
+
* `paint` keeps painters stateless about geometry and avoids stale reads.
|
|
22
|
+
*/
|
|
23
|
+
interface SurfaceMetrics {
|
|
24
|
+
columns: number;
|
|
25
|
+
rows: number;
|
|
26
|
+
cellWidth: number;
|
|
27
|
+
cellHeight: number;
|
|
28
|
+
style: ResolvedWebHostTerminalStyle;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The paint seam shared by the canvas and DOM painters. The runtime calls
|
|
32
|
+
* `paint` with the latest metrics snapshot, the current frame (or `undefined`
|
|
33
|
+
* before the first frame), and optional damage scoping the repaint.
|
|
34
|
+
*/
|
|
35
|
+
interface WebHostSurfacePainter {
|
|
36
|
+
paint(metrics: SurfaceMetrics, frame: WebHostSurfaceFrame | undefined, damage?: WebHostSurfaceDamage): void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The effective text color for a cell, folding the reverse-video emphasis bit
|
|
40
|
+
* (`em & 16`) over the host terminal theme's defaults.
|
|
41
|
+
*/
|
|
42
|
+
declare function resolvedSurfaceForeground(style: WebHostSurfaceStyle | null | undefined, terminalStyle: ResolvedWebHostTerminalStyle): string;
|
|
43
|
+
/**
|
|
44
|
+
* The effective background fill for a cell (or `undefined` for the terminal's
|
|
45
|
+
* base background), folding the reverse-video emphasis bit (`em & 16`).
|
|
46
|
+
*/
|
|
47
|
+
declare function resolvedSurfaceBackground(style: WebHostSurfaceStyle | null | undefined, terminalStyle: ResolvedWebHostTerminalStyle): string | undefined;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { SurfaceMetrics, WebHostSurfacePainter, WebHostSurfaceRendererKind, resolvedSurfaceBackground, resolvedSurfaceForeground };
|
|
50
|
+
//# sourceMappingURL=SurfaceRenderer.d.ts.map
|