@swifttui/web 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.js +2 -2
  3. package/dist/src/CanvasSurfacePainter.d.ts +16 -0
  4. package/dist/src/CanvasSurfacePainter.js +160 -21
  5. package/dist/src/CanvasSurfacePainter.js.map +1 -1
  6. package/dist/src/DomSurfacePainter.d.ts +17 -3
  7. package/dist/src/DomSurfacePainter.js +50 -5
  8. package/dist/src/DomSurfacePainter.js.map +1 -1
  9. package/dist/src/SurfacePainterConformanceControl.js +13 -0
  10. package/dist/src/SurfacePainterConformanceControl.js.map +1 -0
  11. package/dist/src/SurfaceRenderer.d.ts +3 -2
  12. package/dist/src/SurfaceRenderer.js.map +1 -1
  13. package/dist/src/WebHostSceneRuntime.d.ts +3 -1
  14. package/dist/src/WebHostSceneRuntime.js +10 -7
  15. package/dist/src/WebHostSceneRuntime.js.map +1 -1
  16. package/dist/src/WebHostSurfaceTransport.d.ts +61 -4
  17. package/dist/src/WebHostSurfaceTransport.js +150 -12
  18. package/dist/src/WebHostSurfaceTransport.js.map +1 -1
  19. package/dist/src/WebSocketSceneBridge.d.ts +2 -1
  20. package/dist/src/WebSocketSceneBridge.js +26 -11
  21. package/dist/src/WebSocketSceneBridge.js.map +1 -1
  22. package/dist/src/normalizeWireTokens.d.ts +5 -0
  23. package/dist/src/wasi/BrowserWASIBridge.d.ts +8 -0
  24. package/dist/src/wasi/BrowserWASIBridge.js +29 -7
  25. package/dist/src/wasi/BrowserWASIBridge.js.map +1 -1
  26. package/dist/src/wasi/SharedInputQueue.d.ts +61 -1
  27. package/dist/src/wasi/SharedInputQueue.js +108 -0
  28. package/dist/src/wasi/SharedInputQueue.js.map +1 -1
  29. package/dist/src/wasi/WasmSceneRuntime.js +38 -8
  30. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -1
  31. package/dist/src/wasi/WasmSceneWorker.js +1 -1
  32. package/dist/testing.d.ts +2 -1
  33. package/dist/wasi.d.ts +2 -2
  34. package/dist/wasi.js +1 -1
  35. package/package.json +2 -1
package/dist/index.d.ts CHANGED
@@ -1,12 +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
- import { SUPPORTED_SURFACE_VERSION, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostResyncRequest, WebHostRuntimeIssue, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage } from "./src/WebHostSurfaceTransport.js";
2
+ import { MAX_IMAGE_RECOVERY_ID_BYTES, MAX_OUTSTANDING_IMAGE_RECOVERY_IDS, SUPPORTED_SURFACE_VERSION, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostImagePayloadRequestHandler, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostResyncRequest, WebHostRuntimeIssue, WebHostScrollRegion, WebHostSurfaceCell, WebHostSurfaceDamage, WebHostSurfaceDamageRange, WebHostSurfaceDamageTextRow, WebHostSurfaceDeltaFrame, WebHostSurfaceDeltaRow, WebHostSurfaceFrame, WebHostSurfaceImage, WebHostSurfaceImageFormat, WebHostSurfaceLineStyle, WebHostSurfaceLinkRow, WebHostSurfaceLinkRun, WebHostSurfaceRect, WebHostSurfaceSize, WebHostSurfaceStyle, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage, isWebHostImageRecoveryId } from "./src/WebHostSurfaceTransport.js";
3
3
  import { SurfaceMetrics, WebHostSurfacePainter, WebHostSurfaceRendererKind, resolvedSurfaceBackground, resolvedSurfaceForeground } from "./src/SurfaceRenderer.js";
4
4
  import { WebHostSceneDescriptor, WebHostSceneManifest, WebHostSceneManifestSource, loadWebHostSceneManifest, normalizeWebHostSceneManifest, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON } from "./src/WebHostSceneManifest.js";
5
5
  import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions, WheelMode } from "./src/WebHostSceneRuntime.js";
6
6
  import { WebSocketSceneBridge, WebSocketSceneBridgeFactory, WebSocketSceneBridgeOptions, WebSocketSceneSocket, webSocketSceneURL } from "./src/WebSocketSceneBridge.js";
7
7
  import { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostVisibilityDocument, createWebHostApp } from "./src/WebHostApp.js";
8
- import { DomSurfacePainter } from "./src/DomSurfacePainter.js";
8
+ import { DomSurfacePainter, DomSurfacePainterOptions } from "./src/DomSurfacePainter.js";
9
9
  import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
10
10
  import { JSPIConstructors, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
11
11
  import { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink } from "./src/wasi/BrowserWASIBridge.js";
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, WebHostResyncRequest, 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, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
12
+ export { BrowserWASIBridge, BrowserWASIBridgeOptions, BrowserWASIOutputSink, DomSurfacePainter, DomSurfacePainterOptions, JSPIConstructors, MAX_IMAGE_RECOVERY_ID_BYTES, MAX_OUTSTANDING_IMAGE_RECOVERY_IDS, ResolvedWebHostTerminalPalette, ResolvedWebHostTerminalStyle, SUPPORTED_SURFACE_VERSION, StdIOPipe, SurfaceMetrics, WasmEngineCapabilities, WasmEngineFamily, WasmEngineProbeSignals, WebHostANSIColors, WebHostAccessibilityAnnouncement, WebHostAccessibilityLiveRegion, WebHostAccessibilityNode, WebHostAccessibilityPoint, WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostFocusPresentation, WebHostFocusSemantics, WebHostFrameDiagnosticRecord, WebHostImagePayloadRequestHandler, WebHostKeyInput, WebHostMouseInput, WebHostOutputDecoder, WebHostOutputRecord, WebHostOutputSink, WebHostResyncRequest, 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, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage, encodeWebHostTerminalRenderStyleBase64, isWebHostImageRecoveryId, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { StdIOPipe } from "./src/wasi/StdIOPipe.js";
2
2
  import { classifyWasmEngineFamily, collectWasmEngineProbeSignals, jspiConstructors, mainThreadStackProfileEnvironmentDefaults, resolveWasmEngineCapabilities, stackProfileEnvironmentDefaults } from "./src/wasi/WasmEngineCapabilities.js";
3
3
  import { applyWebHostTerminalStyle, decodeWebHostTerminalRenderStyleBase64, encodeWebHostTerminalRenderStyleBase64, mergeWebHostTerminalStyle, normalizeWebHostTerminalStyle, resolveWebHostTerminalRenderStyle, webTUITerminalBackgroundColor } from "./src/WebHostTerminalStyle.js";
4
- import { SUPPORTED_SURFACE_VERSION, WebHostOutputDecoder, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage } from "./src/WebHostSurfaceTransport.js";
4
+ import { MAX_IMAGE_RECOVERY_ID_BYTES, MAX_OUTSTANDING_IMAGE_RECOVERY_IDS, SUPPORTED_SURFACE_VERSION, WebHostOutputDecoder, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage, isWebHostImageRecoveryId } from "./src/WebHostSurfaceTransport.js";
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";
@@ -9,4 +9,4 @@ import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./src/Surf
9
9
  import { DomSurfacePainter } from "./src/DomSurfacePainter.js";
10
10
  import { WebHostSceneRuntime } from "./src/WebHostSceneRuntime.js";
11
11
  import { createWebHostApp } from "./src/WebHostApp.js";
12
- export { BrowserWASIBridge, DomSurfacePainter, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage, encodeWebHostTerminalRenderStyleBase64, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
12
+ export { BrowserWASIBridge, DomSurfacePainter, MAX_IMAGE_RECOVERY_ID_BYTES, MAX_OUTSTANDING_IMAGE_RECOVERY_IDS, SUPPORTED_SURFACE_VERSION, StdIOPipe, WebHostOutputDecoder, WebHostSceneRuntime, WebSocketSceneBridge, applyWebHostTerminalStyle, classifyWasmEngineFamily, collectWasmEngineProbeSignals, createWebHostApp, decodeWebHostTerminalRenderStyleBase64, encodeCapabilitiesControlMessage, encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage, encodeRenderStyleControlMessage, encodeResizeControlMessage, encodeResyncControlMessage, encodeWebHostTerminalRenderStyleBase64, isWebHostImageRecoveryId, jspiConstructors, loadWebHostSceneManifest, mainThreadStackProfileEnvironmentDefaults, mergeWebHostTerminalStyle, normalizeWebHostSceneManifest, normalizeWebHostTerminalStyle, resolveWasmEngineCapabilities, resolveWebHostTerminalRenderStyle, resolvedSurfaceBackground, resolvedSurfaceForeground, stackProfileEnvironmentDefaults, webSocketSceneURL, webTUISceneManifestFromDescriptors, webTUISceneManifestToJSON, webTUITerminalBackgroundColor };
@@ -0,0 +1,16 @@
1
+ import { WebHostImagePayloadRequestHandler } from "./WebHostSurfaceTransport.js";
2
+ import { NormalizedSurfaceImageFormat } from "./normalizeWireTokens.js";
3
+ //#region src/CanvasSurfacePainter.d.ts
4
+ interface CanvasSurfacePainterOptions {
5
+ /** Injectable decode seam for browser hosts and deterministic failure tests. */
6
+ decodeImage?: (dataBase64: string, format: NormalizedSurfaceImageFormat, imageID: string) => Promise<CanvasImageSource>;
7
+ /**
8
+ * Reports supported, positive-area images whose payload cannot be resolved
9
+ * locally. Returning the admitted subset keeps rejected IDs eligible for a
10
+ * later paint; `void` preserves legacy all-accepted behavior.
11
+ */
12
+ onImagePayloadMiss?: WebHostImagePayloadRequestHandler;
13
+ }
14
+ //#endregion
15
+ export { CanvasSurfacePainterOptions };
16
+ //# sourceMappingURL=CanvasSurfacePainter.d.ts.map
@@ -1,8 +1,10 @@
1
1
  import { webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
2
+ import { isWebHostImageRecoveryId } from "./WebHostSurfaceTransport.js";
2
3
  import { canRenderBoxDrawing, drawBoxDrawing } from "./BoxDrawingRenderer.js";
3
4
  import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./SurfaceRenderer.js";
4
- import { isSupportedImageFormat, normalizeScalingMode } from "./normalizeWireTokens.js";
5
- //#region src/CanvasSurfacePainter.ts
5
+ import { isSupportedImageFormat } from "./normalizeWireTokens.js";
6
+ import { registerCanvasSurfacePainterConformanceControl } from "./SurfacePainterConformanceControl.js";
7
+ const MAX_UNRESOLVED_IMAGE_PAYLOAD_CHARACTERS = 64 * 1024 * 1024;
6
8
  /**
7
9
  * Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell
8
10
  * text/box-drawing/decorations, surface images, and damage-scoped dirty-region
@@ -15,8 +17,29 @@ import { isSupportedImageFormat, normalizeScalingMode } from "./normalizeWireTok
15
17
  */
16
18
  var CanvasSurfacePainter = class {
17
19
  imageCache = /* @__PURE__ */ new Map();
20
+ unresolvedImageIds = /* @__PURE__ */ new Set();
21
+ unresolvedImagePayloadCharacters = 0;
22
+ imageDecoder;
23
+ onImagePayloadMiss;
18
24
  canvas;
19
25
  requestRedraw = () => {};
26
+ lastEpoch;
27
+ pendingImagePayloadMissIds = /* @__PURE__ */ new Set();
28
+ imagePayloadMissScheduled = false;
29
+ constructor(options = {}) {
30
+ this.imageDecoder = options.decodeImage ?? decodeImage;
31
+ this.onImagePayloadMiss = options.onImagePayloadMiss ?? (() => {});
32
+ registerCanvasSurfacePainterConformanceControl(this, {
33
+ evictImages: (ids) => {
34
+ for (const id of ids) {
35
+ this.removeUnresolvedImage(id);
36
+ this.imageCache.delete(id);
37
+ this.pendingImagePayloadMissIds.delete(id);
38
+ }
39
+ },
40
+ visibleImageIDs: (images) => [...new Set(images.filter(isPaintableSurfaceImage).filter((image) => this.imageCache.get(image.id)?.image !== void 0).map((image) => image.id))].sort()
41
+ });
42
+ }
20
43
  /**
21
44
  * Binds the canvas the painter draws into and the callback used to request a
22
45
  * full repaint after an asynchronous image decode completes.
@@ -25,12 +48,21 @@ var CanvasSurfacePainter = class {
25
48
  this.canvas = canvas;
26
49
  this.requestRedraw = requestRedraw;
27
50
  }
28
- paint(metrics, frame, damage) {
51
+ paint(metrics, frame, damage, recoveredImagePayloadIds = []) {
29
52
  const canvas = this.canvas;
30
53
  const context = canvas?.getContext("2d");
31
54
  if (!canvas || !context) return;
55
+ if (frame?.epoch !== void 0 && frame.epoch !== this.lastEpoch) {
56
+ this.lastEpoch = frame.epoch;
57
+ for (const cached of this.imageCache.values()) if (!cached.image) cached.missReported = false;
58
+ }
59
+ this.sweepUnresolvedImages(frame?.images);
32
60
  const dirtyRegion = frame ? this.dirtyRegionForDamage(damage, frame, metrics) : void 0;
33
- if (dirtyRegion?.rects.length === 0) return;
61
+ const recoveredPayloadIds = new Set(recoveredImagePayloadIds);
62
+ if (dirtyRegion?.rects.length === 0) {
63
+ this.prepareImages(frame?.images ?? [], recoveredPayloadIds);
64
+ return;
65
+ }
34
66
  const scale = globalThis.window?.devicePixelRatio || 1;
35
67
  context.setTransform(scale, 0, 0, scale, 0, 0);
36
68
  context.textBaseline = "alphabetic";
@@ -45,7 +77,7 @@ var CanvasSurfacePainter = class {
45
77
  }
46
78
  if (!frame) return;
47
79
  this.drawRows(context, frame, metrics, dirtyRegion);
48
- this.drawImages(context, frame.images ?? [], metrics, dirtyRegion);
80
+ this.drawImages(context, frame.images ?? [], metrics, dirtyRegion, recoveredPayloadIds);
49
81
  }
50
82
  drawRows(context, frame, metrics, dirtyRegion) {
51
83
  if (dirtyRegion) {
@@ -68,21 +100,28 @@ var CanvasSurfacePainter = class {
68
100
  this.drawCell(context, metrics, x, y, text, span, style);
69
101
  }
70
102
  }
71
- drawImages(context, images, metrics, dirtyRegion) {
103
+ drawImages(context, images, metrics, dirtyRegion, recoveredPayloadIds) {
104
+ const missingPayloadIds = /* @__PURE__ */ new Set();
72
105
  for (const image of images) {
73
- if (!isSupportedImageFormat(image.format)) continue;
74
- this.drawImage(context, {
75
- ...image,
76
- scalingMode: normalizeScalingMode(image.scalingMode)
77
- }, metrics, dirtyRegion);
106
+ if (!isPaintableSurfaceImage(image)) continue;
107
+ this.drawImage(context, image, metrics, dirtyRegion, missingPayloadIds, recoveredPayloadIds);
78
108
  }
109
+ this.reportImagePayloadMisses(missingPayloadIds);
79
110
  }
80
- drawImage(context, image, metrics, dirtyRegion) {
81
- const decodedImage = this.cachedImage(image);
82
- if (!decodedImage) return;
111
+ prepareImages(images, recoveredPayloadIds) {
112
+ const missingPayloadIds = /* @__PURE__ */ new Set();
113
+ for (const image of images) {
114
+ if (!isPaintableSurfaceImage(image)) continue;
115
+ this.cachedImage(image, missingPayloadIds, recoveredPayloadIds);
116
+ }
117
+ this.reportImagePayloadMisses(missingPayloadIds);
118
+ }
119
+ drawImage(context, image, metrics, dirtyRegion, missingPayloadIds, recoveredPayloadIds) {
83
120
  const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
84
121
  const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
85
122
  if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) return;
123
+ const decodedImage = this.cachedImage(image, missingPayloadIds, recoveredPayloadIds);
124
+ if (!decodedImage) return;
86
125
  if (dirtyRegion && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)) return;
87
126
  context.save();
88
127
  context.beginPath();
@@ -91,21 +130,116 @@ var CanvasSurfacePainter = class {
91
130
  context.drawImage(decodedImage, boundsX * metrics.cellWidth, boundsY * metrics.cellHeight, boundsWidth * metrics.cellWidth, boundsHeight * metrics.cellHeight);
92
131
  context.restore();
93
132
  }
94
- cachedImage(image) {
95
- const cached = this.imageCache.get(image.id);
133
+ cachedImage(image, missingPayloadIds, recoveredPayloadIds) {
134
+ let cached = this.imageCache.get(image.id);
96
135
  if (cached?.image) return cached.image;
97
- if (!cached?.promise && image.dataBase64) {
98
- const promise = decodeImage(image.dataBase64, image.format);
99
- this.imageCache.set(image.id, { promise });
136
+ const beginsRecoveredGeneration = image.dataBase64 !== void 0 && recoveredPayloadIds.delete(image.id);
137
+ if (image.dataBase64 !== void 0 && (cached?.payload === void 0 || beginsRecoveredGeneration)) {
138
+ if (!this.canTrackUnresolvedImage(image.id, image.dataBase64)) return;
139
+ cached = {
140
+ payload: image.dataBase64,
141
+ retries: 0
142
+ };
143
+ this.setUnresolvedImage(image.id, cached);
144
+ } else if (!cached) {
145
+ if (!isWebHostImageRecoveryId(image.id)) return;
146
+ if (!this.canTrackUnresolvedImage(image.id)) return;
147
+ cached = { missReported: false };
148
+ this.setUnresolvedImage(image.id, cached);
149
+ missingPayloadIds.add(image.id);
150
+ return;
151
+ }
152
+ const attempts = cached.retries ?? 0;
153
+ if (cached.payload === void 0) {
154
+ if (!cached.missReported) {
155
+ cached.missReported = true;
156
+ missingPayloadIds.add(image.id);
157
+ }
158
+ return;
159
+ }
160
+ if (attempts >= 3) {
161
+ if (!cached.missReported) {
162
+ cached.missReported = true;
163
+ missingPayloadIds.add(image.id);
164
+ }
165
+ return;
166
+ }
167
+ if (!cached.promise) {
168
+ const nextAttempts = attempts + 1;
169
+ const promise = this.imageDecoder(cached.payload, image.format, image.id);
170
+ cached.promise = promise;
171
+ cached.retries = nextAttempts;
100
172
  promise.then((decodedImage) => {
101
173
  if (this.imageCache.get(image.id)?.promise !== promise) return;
174
+ this.removeUnresolvedImage(image.id);
102
175
  this.imageCache.set(image.id, { image: decodedImage });
103
176
  this.requestRedraw();
104
177
  }).catch(() => {
105
- this.imageCache.delete(image.id);
178
+ const latest = this.imageCache.get(image.id);
179
+ if (latest?.promise !== promise) return;
180
+ latest.promise = void 0;
181
+ if ((latest.retries ?? 0) >= 3 && !latest.missReported) {
182
+ latest.missReported = true;
183
+ if (isWebHostImageRecoveryId(image.id)) this.scheduleImagePayloadMiss(image.id);
184
+ }
185
+ this.requestRedraw();
106
186
  });
107
187
  }
108
188
  }
189
+ canTrackUnresolvedImage(id, payload) {
190
+ const existing = this.imageCache.get(id);
191
+ const existingPayloadCharacters = existing?.image ? 0 : existing?.payload?.length ?? 0;
192
+ const nextPayloadCharacters = this.unresolvedImagePayloadCharacters - existingPayloadCharacters + (payload?.length ?? 0);
193
+ return (this.unresolvedImageIds.has(id) || this.unresolvedImageIds.size < 256) && nextPayloadCharacters <= 67108864;
194
+ }
195
+ setUnresolvedImage(id, cached) {
196
+ const existing = this.imageCache.get(id);
197
+ if (this.unresolvedImageIds.has(id)) this.unresolvedImagePayloadCharacters -= existing?.payload?.length ?? 0;
198
+ this.imageCache.set(id, cached);
199
+ this.unresolvedImageIds.add(id);
200
+ this.unresolvedImagePayloadCharacters += cached.payload?.length ?? 0;
201
+ this.pendingImagePayloadMissIds.delete(id);
202
+ }
203
+ removeUnresolvedImage(id) {
204
+ if (!this.unresolvedImageIds.delete(id)) return;
205
+ this.unresolvedImagePayloadCharacters -= this.imageCache.get(id)?.payload?.length ?? 0;
206
+ this.pendingImagePayloadMissIds.delete(id);
207
+ }
208
+ sweepUnresolvedImages(images) {
209
+ const presentedIds = /* @__PURE__ */ new Set();
210
+ for (const image of images ?? []) {
211
+ if (!this.unresolvedImageIds.has(image.id)) continue;
212
+ if (isPaintableSurfaceImage(image)) presentedIds.add(image.id);
213
+ }
214
+ for (const id of this.unresolvedImageIds) {
215
+ if (presentedIds.has(id)) continue;
216
+ this.removeUnresolvedImage(id);
217
+ this.imageCache.delete(id);
218
+ }
219
+ }
220
+ reportImagePayloadMisses(ids) {
221
+ if (ids.size === 0) return;
222
+ const candidateIds = [...ids].sort();
223
+ this.applyImagePayloadMissAdmission(candidateIds, this.onImagePayloadMiss(candidateIds));
224
+ }
225
+ scheduleImagePayloadMiss(id) {
226
+ this.pendingImagePayloadMissIds.add(id);
227
+ if (this.imagePayloadMissScheduled) return;
228
+ this.imagePayloadMissScheduled = true;
229
+ queueMicrotask(() => {
230
+ this.imagePayloadMissScheduled = false;
231
+ const ids = [...this.pendingImagePayloadMissIds].sort();
232
+ this.pendingImagePayloadMissIds.clear();
233
+ if (ids.length > 0) this.applyImagePayloadMissAdmission(ids, this.onImagePayloadMiss(ids));
234
+ });
235
+ }
236
+ applyImagePayloadMissAdmission(candidateIds, admittedIds) {
237
+ const acceptedIds = new Set(Array.isArray(admittedIds) ? admittedIds : candidateIds);
238
+ for (const id of candidateIds) {
239
+ const cached = this.imageCache.get(id);
240
+ if (cached && !cached.image) cached.missReported = acceptedIds.has(id);
241
+ }
242
+ }
109
243
  drawCell(context, metrics, x, y, text, span, style) {
110
244
  const rectX = x * metrics.cellWidth;
111
245
  const rectY = y * metrics.cellHeight;
@@ -180,6 +314,11 @@ var CanvasSurfacePainter = class {
180
314
  context.setLineDash([]);
181
315
  }
182
316
  };
317
+ function isPaintableSurfaceImage(image) {
318
+ const [, , boundsWidth, boundsHeight] = image.bounds;
319
+ const [, , clipWidth, clipHeight] = image.visibleBounds;
320
+ return isSupportedImageFormat(image.format) && boundsWidth > 0 && boundsHeight > 0 && clipWidth > 0 && clipHeight > 0;
321
+ }
183
322
  /**
184
323
  * The CSS font string for a cell, folding the surface emphasis bits (bold,
185
324
  * italic) over the host terminal's font size/family. Exposed so the host can
@@ -258,6 +397,6 @@ function dirtyRegionIntersectsCellRect(region, x, y, width, height) {
258
397
  return false;
259
398
  }
260
399
  //#endregion
261
- export { CanvasSurfacePainter, fontForStyle };
400
+ export { CanvasSurfacePainter, MAX_UNRESOLVED_IMAGE_PAYLOAD_CHARACTERS, fontForStyle };
262
401
 
263
402
  //# sourceMappingURL=CanvasSurfacePainter.js.map
@@ -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 resolvedSurfaceBackground,\n resolvedSurfaceForeground,\n type SurfaceMetrics,\n type WebHostSurfacePainter,\n} from \"./SurfaceRenderer.ts\";\nimport {\n isSupportedImageFormat,\n normalizeScalingMode,\n type NormalizedSurfaceImageFormat,\n} from \"./normalizeWireTokens.ts\";\nimport {\n type ResolvedWebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\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 if (!isSupportedImageFormat(image.format)) {\n continue;\n }\n this.drawImage(\n context,\n {\n ...image,\n scalingMode: normalizeScalingMode(image.scalingMode),\n },\n metrics,\n dirtyRegion\n );\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: NormalizedSurfaceImageFormat\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"],"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 isSupportedImageFormat,\n type NormalizedSurfaceImageFormat,\n} from \"./normalizeWireTokens.ts\";\nimport {\n type ResolvedWebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n isWebHostImageRecoveryId,\n type WebHostImagePayloadRequestHandler,\n type WebHostSurfaceDamage,\n type WebHostSurfaceFrame,\n type WebHostSurfaceImage,\n type WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\nimport {\n registerCanvasSurfacePainterConformanceControl,\n} from \"./SurfacePainterConformanceControl.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 payload?: string;\n /** Total decode attempts already started; bounded by MAX_IMAGE_DECODE_ATTEMPTS. */\n retries?: number;\n missReported?: boolean;\n}\n\nexport const MAX_IMAGE_DECODE_ATTEMPTS = 3;\nexport const MAX_UNRESOLVED_IMAGE_CACHE_ENTRIES = 256;\nexport const MAX_UNRESOLVED_IMAGE_PAYLOAD_CHARACTERS = 64 * 1024 * 1024;\n\nexport interface CanvasSurfacePainterOptions {\n /** Injectable decode seam for browser hosts and deterministic failure tests. */\n decodeImage?: (\n dataBase64: string,\n format: NormalizedSurfaceImageFormat,\n imageID: string\n ) => Promise<CanvasImageSource>;\n /**\n * Reports supported, positive-area images whose payload cannot be resolved\n * locally. Returning the admitted subset keeps rejected IDs eligible for a\n * later paint; `void` preserves legacy all-accepted behavior.\n */\n onImagePayloadMiss?: WebHostImagePayloadRequestHandler;\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 readonly unresolvedImageIds = new Set<string>();\n private unresolvedImagePayloadCharacters = 0;\n private readonly imageDecoder: NonNullable<CanvasSurfacePainterOptions[\"decodeImage\"]>;\n private readonly onImagePayloadMiss: WebHostImagePayloadRequestHandler;\n private canvas?: HTMLCanvasElement;\n private requestRedraw: () => void = () => {};\n private lastEpoch?: number;\n private readonly pendingImagePayloadMissIds = new Set<string>();\n private imagePayloadMissScheduled = false;\n\n constructor(options: CanvasSurfacePainterOptions = {}) {\n this.imageDecoder = options.decodeImage ?? decodeImage;\n this.onImagePayloadMiss = options.onImagePayloadMiss ?? (() => {});\n registerCanvasSurfacePainterConformanceControl(this, {\n evictImages: (ids) => {\n for (const id of ids) {\n this.removeUnresolvedImage(id);\n this.imageCache.delete(id);\n this.pendingImagePayloadMissIds.delete(id);\n }\n },\n visibleImageIDs: (images) => [...new Set(\n images\n .filter(isPaintableSurfaceImage)\n .filter((image) => this.imageCache.get(image.id)?.image !== undefined)\n .map((image) => image.id)\n )].sort(),\n });\n }\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 recoveredImagePayloadIds: readonly string[] = []\n ): void {\n const canvas = this.canvas;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) {\n return;\n }\n\n if (frame?.epoch !== undefined && frame.epoch !== this.lastEpoch) {\n this.lastEpoch = frame.epoch;\n for (const cached of this.imageCache.values()) {\n if (!cached.image) {\n cached.missReported = false;\n }\n }\n }\n this.sweepUnresolvedImages(frame?.images);\n\n const dirtyRegion = frame\n ? this.dirtyRegionForDamage(damage, frame, metrics)\n : undefined;\n const recoveredPayloadIds = new Set(recoveredImagePayloadIds);\n if (dirtyRegion?.rects.length === 0) {\n this.prepareImages(frame?.images ?? [], recoveredPayloadIds);\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(\n context,\n frame.images ?? [],\n metrics,\n dirtyRegion,\n recoveredPayloadIds\n );\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 | undefined,\n recoveredPayloadIds: Set<string>\n ): void {\n const missingPayloadIds = new Set<string>();\n for (const image of images) {\n if (!isPaintableSurfaceImage(image)) {\n continue;\n }\n this.drawImage(\n context,\n image,\n metrics,\n dirtyRegion,\n missingPayloadIds,\n recoveredPayloadIds\n );\n }\n this.reportImagePayloadMisses(missingPayloadIds);\n }\n\n private prepareImages(\n images: WebHostSurfaceImage[],\n recoveredPayloadIds: Set<string>\n ): void {\n const missingPayloadIds = new Set<string>();\n for (const image of images) {\n if (!isPaintableSurfaceImage(image)) {\n continue;\n }\n this.cachedImage(image, missingPayloadIds, recoveredPayloadIds);\n }\n this.reportImagePayloadMisses(missingPayloadIds);\n }\n\n private drawImage(\n context: CanvasRenderingContext2D,\n image: WebHostSurfaceImage,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion: DirtyRegion | undefined,\n missingPayloadIds: Set<string>,\n recoveredPayloadIds: Set<string>\n ): void {\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\n const decodedImage = this.cachedImage(\n image,\n missingPayloadIds,\n recoveredPayloadIds\n );\n if (!decodedImage) {\n return;\n }\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 missingPayloadIds: Set<string>,\n recoveredPayloadIds: Set<string>\n ): CanvasImageSource | undefined {\n let cached = this.imageCache.get(image.id);\n if (cached?.image) {\n return cached.image;\n }\n\n const beginsRecoveredGeneration = image.dataBase64 !== undefined\n && recoveredPayloadIds.delete(image.id);\n if (\n image.dataBase64 !== undefined\n && (cached?.payload === undefined || beginsRecoveredGeneration)\n ) {\n if (!this.canTrackUnresolvedImage(image.id, image.dataBase64)) {\n return undefined;\n }\n cached = {\n payload: image.dataBase64,\n retries: 0,\n };\n this.setUnresolvedImage(image.id, cached);\n } else if (!cached) {\n if (!isWebHostImageRecoveryId(image.id)) {\n return undefined;\n }\n if (!this.canTrackUnresolvedImage(image.id)) {\n return undefined;\n }\n cached = { missReported: false };\n this.setUnresolvedImage(image.id, cached);\n missingPayloadIds.add(image.id);\n return undefined;\n }\n\n const attempts = cached.retries ?? 0;\n if (cached.payload === undefined) {\n if (!cached.missReported) {\n cached.missReported = true;\n missingPayloadIds.add(image.id);\n }\n return undefined;\n }\n if (attempts >= MAX_IMAGE_DECODE_ATTEMPTS) {\n if (!cached.missReported) {\n cached.missReported = true;\n missingPayloadIds.add(image.id);\n }\n return undefined;\n }\n\n if (!cached.promise) {\n const nextAttempts = attempts + 1;\n const promise = this.imageDecoder(cached.payload, image.format, image.id);\n cached.promise = promise;\n cached.retries = nextAttempts;\n void promise.then((decodedImage) => {\n const latest = this.imageCache.get(image.id);\n if (latest?.promise !== promise) {\n return;\n }\n this.removeUnresolvedImage(image.id);\n this.imageCache.set(image.id, { image: decodedImage });\n this.requestRedraw();\n }).catch(() => {\n const latest = this.imageCache.get(image.id);\n if (latest?.promise !== promise) {\n return;\n }\n latest.promise = undefined;\n if (\n (latest.retries ?? 0) >= MAX_IMAGE_DECODE_ATTEMPTS\n && !latest.missReported\n ) {\n latest.missReported = true;\n if (isWebHostImageRecoveryId(image.id)) {\n this.scheduleImagePayloadMiss(image.id);\n }\n }\n this.requestRedraw();\n });\n }\n\n return undefined;\n }\n\n private canTrackUnresolvedImage(\n id: string,\n payload?: string\n ): boolean {\n const existing = this.imageCache.get(id);\n const existingPayloadCharacters = existing?.image\n ? 0\n : existing?.payload?.length ?? 0;\n const nextPayloadCharacters = this.unresolvedImagePayloadCharacters\n - existingPayloadCharacters\n + (payload?.length ?? 0);\n return (\n (this.unresolvedImageIds.has(id)\n || this.unresolvedImageIds.size < MAX_UNRESOLVED_IMAGE_CACHE_ENTRIES)\n && nextPayloadCharacters <= MAX_UNRESOLVED_IMAGE_PAYLOAD_CHARACTERS\n );\n }\n\n private setUnresolvedImage(\n id: string,\n cached: CachedWebHostImage\n ): void {\n const existing = this.imageCache.get(id);\n if (this.unresolvedImageIds.has(id)) {\n this.unresolvedImagePayloadCharacters -= existing?.payload?.length ?? 0;\n }\n this.imageCache.set(id, cached);\n this.unresolvedImageIds.add(id);\n this.unresolvedImagePayloadCharacters += cached.payload?.length ?? 0;\n this.pendingImagePayloadMissIds.delete(id);\n }\n\n private removeUnresolvedImage(\n id: string\n ): void {\n if (!this.unresolvedImageIds.delete(id)) {\n return;\n }\n this.unresolvedImagePayloadCharacters -=\n this.imageCache.get(id)?.payload?.length ?? 0;\n this.pendingImagePayloadMissIds.delete(id);\n }\n\n private sweepUnresolvedImages(\n images: WebHostSurfaceImage[] | undefined\n ): void {\n const presentedIds = new Set<string>();\n for (const image of images ?? []) {\n if (!this.unresolvedImageIds.has(image.id)) {\n continue;\n }\n if (isPaintableSurfaceImage(image)) {\n presentedIds.add(image.id);\n }\n }\n for (const id of this.unresolvedImageIds) {\n if (presentedIds.has(id)) {\n continue;\n }\n this.removeUnresolvedImage(id);\n this.imageCache.delete(id);\n }\n }\n\n private reportImagePayloadMisses(\n ids: Set<string>\n ): void {\n if (ids.size === 0) {\n return;\n }\n const candidateIds = [...ids].sort();\n this.applyImagePayloadMissAdmission(\n candidateIds,\n this.onImagePayloadMiss(candidateIds)\n );\n }\n\n private scheduleImagePayloadMiss(\n id: string\n ): void {\n this.pendingImagePayloadMissIds.add(id);\n if (this.imagePayloadMissScheduled) {\n return;\n }\n this.imagePayloadMissScheduled = true;\n queueMicrotask(() => {\n this.imagePayloadMissScheduled = false;\n const ids = [...this.pendingImagePayloadMissIds].sort();\n this.pendingImagePayloadMissIds.clear();\n if (ids.length > 0) {\n this.applyImagePayloadMissAdmission(\n ids,\n this.onImagePayloadMiss(ids)\n );\n }\n });\n }\n\n private applyImagePayloadMissAdmission(\n candidateIds: readonly string[],\n admittedIds: readonly string[] | void\n ): void {\n // Callbacks authored against the former void contract commonly use a\n // concise `array.push(...)` body, whose runtime result is a number.\n const acceptedIds = new Set(\n Array.isArray(admittedIds) ? admittedIds : candidateIds\n );\n for (const id of candidateIds) {\n const cached = this.imageCache.get(id);\n if (cached && !cached.image) {\n cached.missReported = acceptedIds.has(id);\n }\n }\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\nfunction isPaintableSurfaceImage(\n image: WebHostSurfaceImage\n): boolean {\n const [, , boundsWidth, boundsHeight] = image.bounds;\n const [, , clipWidth, clipHeight] = image.visibleBounds;\n return isSupportedImageFormat(image.format)\n && boundsWidth > 0\n && boundsHeight > 0\n && clipWidth > 0\n && clipHeight > 0;\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: NormalizedSurfaceImageFormat\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"],"mappings":"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"}
@@ -1,6 +1,15 @@
1
- import { WebHostSurfaceDamage, WebHostSurfaceFrame } from "./WebHostSurfaceTransport.js";
1
+ import { WebHostImagePayloadRequestHandler, WebHostSurfaceDamage, WebHostSurfaceFrame } from "./WebHostSurfaceTransport.js";
2
2
  import { SurfaceMetrics, WebHostSurfacePainter } from "./SurfaceRenderer.js";
3
3
  //#region src/DomSurfacePainter.d.ts
4
+ interface DomSurfacePainterOptions {
5
+ /**
6
+ * Reports supported, positive-area image IDs absent from the retained DOM
7
+ * cache. Returning the admitted subset lets bounded transports keep overflow
8
+ * IDs eligible on the next presentation frame; `void` preserves legacy
9
+ * all-accepted behavior for custom hosts.
10
+ */
11
+ onImagePayloadMiss?: WebHostImagePayloadRequestHandler;
12
+ }
4
13
  /**
5
14
  * Draws SwiftTUI surface frames as a DOM element tree instead of canvas
6
15
  * pixels: one absolutely positioned row container per grid row, one `<span>`
@@ -19,6 +28,7 @@ import { SurfaceMetrics, WebHostSurfacePainter } from "./SurfaceRenderer.js";
19
28
  * `letter-spacing`, measured once per font/size pair.
20
29
  */
21
30
  declare class DomSurfacePainter implements WebHostSurfacePainter {
31
+ private readonly onImagePayloadMiss;
22
32
  private root?;
23
33
  private rowsLayer?;
24
34
  private imagesLayer?;
@@ -28,12 +38,16 @@ declare class DomSurfacePainter implements WebHostSurfacePainter {
28
38
  private renderedGridKey?;
29
39
  private hasRenderedFrame;
30
40
  private letterSpacing?;
41
+ private reportedMissingImageIds;
42
+ private lastImageRecoveryFrame?;
43
+ private lastEpoch?;
44
+ constructor(options?: DomSurfacePainterOptions);
31
45
  /**
32
46
  * Binds the container the painter renders into. The runtime owns the
33
47
  * container's size; the painter owns everything inside it.
34
48
  */
35
49
  attach(root: HTMLElement): void;
36
- paint(metrics: SurfaceMetrics, frame: WebHostSurfaceFrame | undefined, damage?: WebHostSurfaceDamage): void;
50
+ paint(metrics: SurfaceMetrics, frame: WebHostSurfaceFrame | undefined, damage?: WebHostSurfaceDamage, _recoveredImagePayloadIds?: readonly string[]): void;
37
51
  private rebuildRow;
38
52
  private ensureRowElement;
39
53
  private applyRootStyle;
@@ -47,5 +61,5 @@ declare class DomSurfacePainter implements WebHostSurfacePainter {
47
61
  private reconcileImages;
48
62
  }
49
63
  //#endregion
50
- export { DomSurfacePainter };
64
+ export { DomSurfacePainter, DomSurfacePainterOptions };
51
65
  //# sourceMappingURL=DomSurfacePainter.d.ts.map
@@ -1,6 +1,8 @@
1
1
  import { webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
2
+ import { isWebHostImageRecoveryId } from "./WebHostSurfaceTransport.js";
2
3
  import { resolvedSurfaceBackground, resolvedSurfaceForeground } from "./SurfaceRenderer.js";
3
4
  import { isSupportedImageFormat, normalizeScalingMode } from "./normalizeWireTokens.js";
5
+ import { registerDomSurfacePainterConformanceControl } from "./SurfacePainterConformanceControl.js";
4
6
  import { fontForStyle } from "./CanvasSurfacePainter.js";
5
7
  //#region src/DomSurfacePainter.ts
6
8
  /**
@@ -21,6 +23,7 @@ import { fontForStyle } from "./CanvasSurfacePainter.js";
21
23
  * `letter-spacing`, measured once per font/size pair.
22
24
  */
23
25
  var DomSurfacePainter = class {
26
+ onImagePayloadMiss;
24
27
  root;
25
28
  rowsLayer;
26
29
  imagesLayer;
@@ -30,6 +33,22 @@ var DomSurfacePainter = class {
30
33
  renderedGridKey;
31
34
  hasRenderedFrame = false;
32
35
  letterSpacing;
36
+ reportedMissingImageIds = /* @__PURE__ */ new Set();
37
+ lastImageRecoveryFrame;
38
+ lastEpoch;
39
+ constructor(options = {}) {
40
+ this.onImagePayloadMiss = options.onImagePayloadMiss ?? (() => {});
41
+ registerDomSurfacePainterConformanceControl(this, {
42
+ evictImages: (ids) => {
43
+ for (const id of ids) {
44
+ this.renderedImages.get(id)?.container.remove();
45
+ this.renderedImages.delete(id);
46
+ this.reportedMissingImageIds.delete(id);
47
+ }
48
+ },
49
+ visibleImageIDs: () => [...this.renderedImages.keys()].sort()
50
+ });
51
+ }
33
52
  /**
34
53
  * Binds the container the painter renders into. The runtime owns the
35
54
  * container's size; the painter owns everything inside it.
@@ -51,11 +70,18 @@ var DomSurfacePainter = class {
51
70
  this.appliedMetricsKey = void 0;
52
71
  this.renderedGridKey = void 0;
53
72
  this.hasRenderedFrame = false;
73
+ this.reportedMissingImageIds.clear();
74
+ this.lastImageRecoveryFrame = void 0;
75
+ this.lastEpoch = void 0;
54
76
  }
55
- paint(metrics, frame, damage) {
77
+ paint(metrics, frame, damage, _recoveredImagePayloadIds) {
56
78
  const root = this.root;
57
79
  const rowsLayer = this.rowsLayer;
58
80
  if (!root || !rowsLayer) return;
81
+ if (frame?.epoch !== void 0 && frame.epoch !== this.lastEpoch) {
82
+ this.lastEpoch = frame.epoch;
83
+ this.reportedMissingImageIds.clear();
84
+ }
59
85
  const metricsKey = metricsKeyFor(metrics);
60
86
  const metricsChanged = metricsKey !== this.appliedMetricsKey;
61
87
  if (metricsChanged) {
@@ -65,7 +91,8 @@ var DomSurfacePainter = class {
65
91
  if (!frame) {
66
92
  this.rowElements = [];
67
93
  rowsLayer.replaceChildren();
68
- this.reconcileImages([], metrics);
94
+ this.lastImageRecoveryFrame = void 0;
95
+ this.reconcileImages([], metrics, false);
69
96
  this.renderedGridKey = void 0;
70
97
  this.hasRenderedFrame = false;
71
98
  return;
@@ -81,7 +108,9 @@ var DomSurfacePainter = class {
81
108
  if (row < 0 || row >= frame.rows.length) continue;
82
109
  this.rebuildRow(row, frame, metrics);
83
110
  }
84
- this.reconcileImages(frame.images ?? [], metrics);
111
+ const allowRecoveryRequests = frame !== this.lastImageRecoveryFrame;
112
+ this.lastImageRecoveryFrame = frame;
113
+ this.reconcileImages(frame.images ?? [], metrics, allowRecoveryRequests);
85
114
  this.hasRenderedFrame = true;
86
115
  }
87
116
  rebuildRow(y, frame, metrics) {
@@ -143,10 +172,12 @@ var DomSurfacePainter = class {
143
172
  };
144
173
  return value;
145
174
  }
146
- reconcileImages(images, metrics) {
175
+ reconcileImages(images, metrics, allowRecoveryRequests) {
147
176
  const layer = this.imagesLayer;
148
177
  if (!layer) return;
149
178
  const next = /* @__PURE__ */ new Map();
179
+ const currentMissingImageIds = /* @__PURE__ */ new Set();
180
+ const newlyMissingImageIds = /* @__PURE__ */ new Set();
150
181
  for (const rawImage of images) {
151
182
  if (!isSupportedImageFormat(rawImage.format)) continue;
152
183
  const image = {
@@ -156,7 +187,13 @@ var DomSurfacePainter = class {
156
187
  const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
157
188
  const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
158
189
  const existing = this.renderedImages.get(image.id);
159
- if (!existing && !image.dataBase64 || boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) continue;
190
+ if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) continue;
191
+ if (!existing && image.dataBase64 === void 0) {
192
+ if (!isWebHostImageRecoveryId(image.id)) continue;
193
+ currentMissingImageIds.add(image.id);
194
+ if (!this.reportedMissingImageIds.has(image.id)) newlyMissingImageIds.add(image.id);
195
+ continue;
196
+ }
160
197
  const entry = existing ?? makeImageEntry();
161
198
  entry.container.style.left = `${clipX * metrics.cellWidth}px`;
162
199
  entry.container.style.top = `${clipY * metrics.cellHeight}px`;
@@ -178,6 +215,14 @@ var DomSurfacePainter = class {
178
215
  }
179
216
  for (const [id, entry] of this.renderedImages) if (!next.has(id)) entry.container.remove();
180
217
  this.renderedImages = next;
218
+ for (const id of this.reportedMissingImageIds) if (!currentMissingImageIds.has(id)) this.reportedMissingImageIds.delete(id);
219
+ if (allowRecoveryRequests && newlyMissingImageIds.size > 0) {
220
+ const candidateIds = [...newlyMissingImageIds].sort();
221
+ const admittedIds = this.onImagePayloadMiss(candidateIds);
222
+ const acceptedIds = Array.isArray(admittedIds) ? admittedIds : candidateIds;
223
+ const candidates = new Set(candidateIds);
224
+ for (const id of acceptedIds) if (candidates.has(id)) this.reportedMissingImageIds.add(id);
225
+ }
181
226
  }
182
227
  };
183
228
  /**