@swifttui/web 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +31 -2
  2. package/dist/index.d.ts +4 -2
  3. package/dist/index.js +3 -1
  4. package/dist/src/CanvasSurfacePainter.js +3 -10
  5. package/dist/src/CanvasSurfacePainter.js.map +1 -1
  6. package/dist/src/DomSurfacePainter.d.ts +51 -0
  7. package/dist/src/DomSurfacePainter.js +267 -0
  8. package/dist/src/DomSurfacePainter.js.map +1 -0
  9. package/dist/src/SurfaceRenderer.d.ts +50 -0
  10. package/dist/src/SurfaceRenderer.js +21 -0
  11. package/dist/src/SurfaceRenderer.js.map +1 -0
  12. package/dist/src/WebHostApp.d.ts +28 -1
  13. package/dist/src/WebHostApp.js +34 -2
  14. package/dist/src/WebHostApp.js.map +1 -1
  15. package/dist/src/WebHostSceneRuntime.d.ts +48 -1
  16. package/dist/src/WebHostSceneRuntime.js +86 -14
  17. package/dist/src/WebHostSceneRuntime.js.map +1 -1
  18. package/dist/src/wasi/MainThreadWasmExecutor.d.ts +8 -0
  19. package/dist/src/wasi/MainThreadWasmExecutor.js +16 -1
  20. package/dist/src/wasi/MainThreadWasmExecutor.js.map +1 -1
  21. package/dist/src/wasi/WasiPollScheduler.js +6 -0
  22. package/dist/src/wasi/WasiPollScheduler.js.map +1 -1
  23. package/dist/src/wasi/WasmRuntimePause.d.ts +63 -0
  24. package/dist/src/wasi/WasmRuntimePause.js +130 -0
  25. package/dist/src/wasi/WasmRuntimePause.js.map +1 -0
  26. package/dist/src/wasi/WasmSceneRuntime.js +14 -1
  27. package/dist/src/wasi/WasmSceneRuntime.js.map +1 -1
  28. package/dist/src/wasi/WasmSceneWorker.d.ts +7 -0
  29. package/dist/src/wasi/WasmSceneWorker.js +11 -4
  30. package/dist/src/wasi/WasmSceneWorker.js.map +1 -1
  31. package/dist/wasi.d.ts +2 -1
  32. package/dist/wasi.js +2 -1
  33. package/package.json +1 -1
@@ -0,0 +1,21 @@
1
+ //#region src/SurfaceRenderer.ts
2
+ /**
3
+ * The effective text color for a cell, folding the reverse-video emphasis bit
4
+ * (`em & 16`) over the host terminal theme's defaults.
5
+ */
6
+ function resolvedSurfaceForeground(style, terminalStyle) {
7
+ if ((style?.em ?? 0) & 16) return style?.bg ?? terminalStyle.theme.background;
8
+ return style?.fg ?? terminalStyle.theme.foreground;
9
+ }
10
+ /**
11
+ * The effective background fill for a cell (or `undefined` for the terminal's
12
+ * base background), folding the reverse-video emphasis bit (`em & 16`).
13
+ */
14
+ function resolvedSurfaceBackground(style, terminalStyle) {
15
+ if ((style?.em ?? 0) & 16) return style?.fg ?? terminalStyle.theme.foreground;
16
+ return style?.bg;
17
+ }
18
+ //#endregion
19
+ export { resolvedSurfaceBackground, resolvedSurfaceForeground };
20
+
21
+ //# sourceMappingURL=SurfaceRenderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SurfaceRenderer.js","names":[],"sources":["../../src/SurfaceRenderer.ts"],"sourcesContent":["import type { ResolvedWebHostTerminalStyle } from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/**\n * Which presenter draws surface frames into the scene mount.\n *\n * - `\"canvas\"` (default): paints cells onto a 2D `<canvas>` — pixel-exact\n * box-drawing seams and decoration patterns, one DOM node total.\n * - `\"dom\"`: renders cells as absolutely positioned text elements — native\n * font rendering (fallback glyphs, subpixel AA, crisp zoom), an\n * inspectable element tree, and real selectable text (hold Alt/Option and\n * drag). Box drawing and decoration patterns render via font glyphs and\n * CSS `text-decoration`, so hairline seams may differ from the canvas\n * painter.\n */\nexport type WebHostSurfaceRendererKind = \"canvas\" | \"dom\";\n\n/**\n * A read-only snapshot of the cell grid geometry and active style a surface\n * painter needs for a single paint pass. The runtime owns this state and\n * mutates it as the surface resizes or restyles; passing a fresh snapshot per\n * `paint` keeps painters stateless about geometry and avoids stale reads.\n */\nexport interface SurfaceMetrics {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n style: ResolvedWebHostTerminalStyle;\n}\n\n/**\n * The paint seam shared by the canvas and DOM painters. The runtime calls\n * `paint` with the latest metrics snapshot, the current frame (or `undefined`\n * before the first frame), and optional damage scoping the repaint.\n */\nexport interface WebHostSurfacePainter {\n paint(\n metrics: SurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void;\n}\n\n/**\n * The effective text color for a cell, folding the reverse-video emphasis bit\n * (`em & 16`) over the host terminal theme's defaults.\n */\nexport function resolvedSurfaceForeground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string {\n if ((style?.em ?? 0) & 16) {\n return style?.bg ?? terminalStyle.theme.background;\n }\n return style?.fg ?? terminalStyle.theme.foreground;\n}\n\n/**\n * The effective background fill for a cell (or `undefined` for the terminal's\n * base background), folding the reverse-video emphasis bit (`em & 16`).\n */\nexport function resolvedSurfaceBackground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string | undefined {\n if ((style?.em ?? 0) & 16) {\n return style?.fg ?? terminalStyle.theme.foreground;\n }\n return style?.bg;\n}\n"],"mappings":";;;;;AAoDA,SAAgB,0BACd,OACA,eACQ;CACR,KAAK,OAAO,MAAM,KAAK,IACrB,OAAO,OAAO,MAAM,cAAc,MAAM;CAE1C,OAAO,OAAO,MAAM,cAAc,MAAM;AAC1C;;;;;AAMA,SAAgB,0BACd,OACA,eACoB;CACpB,KAAK,OAAO,MAAM,KAAK,IACrB,OAAO,OAAO,MAAM,cAAc,MAAM;CAE1C,OAAO,OAAO;AAChB"}
@@ -1,4 +1,5 @@
1
1
  import { WebHostTerminalStyle } from "./WebHostTerminalStyle.js";
2
+ import { WebHostSurfaceRendererKind } from "./SurfaceRenderer.js";
2
3
  import { WebHostSceneDescriptor, WebHostSceneManifestSource } from "./WebHostSceneManifest.js";
3
4
  import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions } from "./WebHostSceneRuntime.js";
4
5
  import { WebSocketSceneBridgeOptions } from "./WebSocketSceneBridge.js";
@@ -15,6 +16,16 @@ interface WebHostBridgeFactoryOptions {
15
16
  environment?: Record<string, string>;
16
17
  }
17
18
  type WebHostBridgeFactory = (options: WebHostBridgeFactoryOptions) => WebHostSceneBridge;
19
+ /**
20
+ * The slice of `Document` the app controller needs to track page visibility.
21
+ * Injectable for tests and non-browser hosts; defaults to the global
22
+ * `document` when one exists.
23
+ */
24
+ interface WebHostVisibilityDocument {
25
+ readonly hidden: boolean;
26
+ addEventListener(type: "visibilitychange", listener: () => void): void;
27
+ removeEventListener(type: "visibilitychange", listener: () => void): void;
28
+ }
18
29
  interface WebHostAppOptions {
19
30
  mount: HTMLElement;
20
31
  manifest?: WebHostSceneManifestSource;
@@ -26,6 +37,22 @@ interface WebHostAppOptions {
26
37
  bridgeFactory?: WebHostBridgeFactory;
27
38
  createElement?: (tagName: string) => HTMLElement;
28
39
  sceneRuntimeFactory?: (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;
40
+ /**
41
+ * Whether scenes that cannot be seen — background scenes after a switch,
42
+ * or every scene while the document is hidden — suspend their apps (run
43
+ * loop parked, monotonic clock frozen) instead of burning CPU. Forwarded to
44
+ * each scene runtime as `suspendWhenHidden`. Defaults to `true`.
45
+ */
46
+ suspendHiddenScenes?: boolean;
47
+ /** Visibility source override; defaults to the global `document`. */
48
+ visibilityDocument?: WebHostVisibilityDocument;
49
+ /**
50
+ * Which surface presenter every scene runtime uses: `"canvas"` (default)
51
+ * paints frames onto a 2D `<canvas>`; `"dom"` renders them as absolutely
52
+ * positioned text elements. Forwarded to each scene runtime as `renderer`.
53
+ * See {@link WebHostSurfaceRendererKind}.
54
+ */
55
+ renderer?: WebHostSurfaceRendererKind;
29
56
  }
30
57
  interface WebHostAppController {
31
58
  scenes: WebHostSceneDescriptor[];
@@ -36,5 +63,5 @@ interface WebHostAppController {
36
63
  }
37
64
  declare function createWebHostApp(options: WebHostAppOptions): Promise<WebHostAppController>;
38
65
  //#endregion
39
- export { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, createWebHostApp };
66
+ export { WebHostAppController, WebHostAppOptions, WebHostBridgeFactory, WebHostBridgeFactoryOptions, WebHostEmbeddedHostConfig, WebHostVisibilityDocument, createWebHostApp };
40
67
  //# sourceMappingURL=WebHostApp.d.ts.map
@@ -15,7 +15,10 @@ async function createWebHostApp(options) {
15
15
  bridgeFactory: options.bridgeFactory,
16
16
  initialSceneId: options.initialSceneId,
17
17
  createElement: options.createElement,
18
- sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions))
18
+ sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions)),
19
+ suspendHiddenScenes: options.suspendHiddenScenes,
20
+ visibilityDocument: options.visibilityDocument ?? defaultVisibilityDocument(),
21
+ renderer: options.renderer
19
22
  });
20
23
  await controller.initialize();
21
24
  return controller;
@@ -32,6 +35,10 @@ var InternalWebHostAppController = class {
32
35
  sceneRuntimeFactory;
33
36
  runtimes = /* @__PURE__ */ new Map();
34
37
  bridges = /* @__PURE__ */ new Map();
38
+ suspendHiddenScenes;
39
+ renderer;
40
+ visibilityDocument;
41
+ detachVisibilityListener;
35
42
  constructor(options) {
36
43
  this.mount = options.mount;
37
44
  this.style = normalizeWebHostTerminalStyle(options.style ?? {});
@@ -39,6 +46,9 @@ var InternalWebHostAppController = class {
39
46
  this.embeddedHost = options.embeddedHost;
40
47
  this.bridgeFactory = options.bridgeFactory;
41
48
  this.sceneRuntimeFactory = options.sceneRuntimeFactory;
49
+ this.suspendHiddenScenes = options.suspendHiddenScenes;
50
+ this.renderer = options.renderer;
51
+ this.visibilityDocument = options.visibilityDocument;
42
52
  this.scenes = options.manifest.scenes;
43
53
  this.selectedSceneId = options.initialSceneId && options.manifest.scenes.some((scene) => scene.id === options.initialSceneId) ? options.initialSceneId : options.manifest.scenes.find((scene) => scene.id === options.manifest.defaultSceneId)?.id ?? options.manifest.defaultSceneId;
44
54
  this.sceneRoot = (options.createElement ?? defaultCreateElement)("div");
@@ -47,6 +57,7 @@ var InternalWebHostAppController = class {
47
57
  this.applyHostFrameStyle();
48
58
  }
49
59
  async initialize() {
60
+ this.installVisibilityListener();
50
61
  await this.ensureRuntime(this.selectedSceneId);
51
62
  await this.switchScene(this.selectedSceneId);
52
63
  }
@@ -63,12 +74,26 @@ var InternalWebHostAppController = class {
63
74
  this.applyHostFrameStyle();
64
75
  }
65
76
  async dispose() {
77
+ this.detachVisibilityListener?.();
78
+ this.detachVisibilityListener = void 0;
66
79
  for (const runtime of this.runtimes.values()) runtime.dispose();
67
80
  for (const bridge of this.bridges.values()) bridge.dispose();
68
81
  this.runtimes.clear();
69
82
  this.bridges.clear();
70
83
  this.mount.replaceChildren();
71
84
  }
85
+ installVisibilityListener() {
86
+ const visibilityDocument = this.visibilityDocument;
87
+ if (!visibilityDocument) return;
88
+ const listener = () => {
89
+ const visible = !visibilityDocument.hidden;
90
+ for (const runtime of this.runtimes.values()) runtime.setDocumentVisible(visible);
91
+ };
92
+ visibilityDocument.addEventListener("visibilitychange", listener);
93
+ this.detachVisibilityListener = () => {
94
+ visibilityDocument.removeEventListener("visibilitychange", listener);
95
+ };
96
+ }
72
97
  async ensureRuntime(id) {
73
98
  const existing = this.runtimes.get(id);
74
99
  if (existing) return existing;
@@ -80,12 +105,15 @@ var InternalWebHostAppController = class {
80
105
  descriptor,
81
106
  style: this.style,
82
107
  bridge,
83
- onInput: (chunk) => bridge.sendInput(chunk)
108
+ onInput: (chunk) => bridge.sendInput(chunk),
109
+ suspendWhenHidden: this.suspendHiddenScenes,
110
+ renderer: this.renderer
84
111
  });
85
112
  this.bridges.set(id, bridge);
86
113
  this.runtimes.set(id, runtime);
87
114
  await runtime.mount();
88
115
  runtime.setVisible(id === this.selectedSceneId);
116
+ if (this.visibilityDocument) runtime.setDocumentVisible(!this.visibilityDocument.hidden);
89
117
  return runtime;
90
118
  }
91
119
  makeBridge(sceneId, descriptor) {
@@ -116,6 +144,10 @@ var InternalWebHostAppController = class {
116
144
  this.mount.style.padding = "1rem";
117
145
  }
118
146
  };
147
+ function defaultVisibilityDocument() {
148
+ if (typeof document === "undefined") return;
149
+ return document;
150
+ }
119
151
  function defaultCreateElement(tagName) {
120
152
  if (typeof document === "undefined") throw new Error("document is not available");
121
153
  return document.createElement(tagName);
@@ -1 +1 @@
1
- {"version":3,"file":"WebHostApp.js","names":[],"sources":["../../src/WebHostApp.ts"],"sourcesContent":["import { BrowserWASIBridge } from \"./wasi/BrowserWASIBridge.ts\";\nimport {\n WebSocketSceneBridge,\n type WebSocketSceneBridgeOptions,\n} from \"./WebSocketSceneBridge.ts\";\nimport {\n loadWebHostSceneManifest,\n normalizeWebHostSceneManifest,\n type WebHostSceneDescriptor,\n type WebHostSceneManifest,\n type WebHostSceneManifestSource,\n} from \"./WebHostSceneManifest.ts\";\nimport {\n mergeWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n WebHostSceneRuntime,\n type WebHostSceneBridge,\n type WebHostSceneRuntimeOptions,\n} from \"./WebHostSceneRuntime.ts\";\n\nexport interface WebHostEmbeddedHostConfig {\n token: string;\n webSocketBaseURL?: string | URL;\n webSocketFactory?: WebSocketSceneBridgeOptions[\"webSocketFactory\"];\n}\n\nexport interface WebHostBridgeFactoryOptions {\n sceneId: string;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n environment?: Record<string, string>;\n}\n\nexport type WebHostBridgeFactory = (options: WebHostBridgeFactoryOptions) => WebHostSceneBridge;\n\nexport interface WebHostAppOptions {\n mount: HTMLElement;\n manifest?: WebHostSceneManifestSource;\n manifestUrl?: string | URL;\n initialSceneId?: string;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory?: (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n}\n\nexport interface WebHostAppController {\n scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n switchScene(id: string): Promise<void>;\n setStyle(style: WebHostTerminalStyle): void;\n dispose(): Promise<void>;\n}\n\ntype RuntimeFactory = (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n\nexport async function createWebHostApp(\n options: WebHostAppOptions\n): Promise<WebHostAppController> {\n const manifest = await resolveManifest(options);\n const controller = new InternalWebHostAppController({\n mount: options.mount,\n manifest,\n style: options.style,\n environment: options.environment,\n embeddedHost: options.embeddedHost,\n bridgeFactory: options.bridgeFactory,\n initialSceneId: options.initialSceneId,\n createElement: options.createElement,\n sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions)),\n });\n await controller.initialize();\n return controller;\n}\n\nclass InternalWebHostAppController implements WebHostAppController {\n readonly scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n\n private readonly mount: HTMLElement;\n private readonly sceneRoot: HTMLElement;\n private style: ResolvedWebHostTerminalStyle;\n private readonly environment?: Record<string, string>;\n private readonly embeddedHost?: WebHostEmbeddedHostConfig;\n private readonly bridgeFactory?: WebHostBridgeFactory;\n private readonly sceneRuntimeFactory: RuntimeFactory;\n private readonly runtimes = new Map<string, WebHostSceneRuntime>();\n private readonly bridges = new Map<string, WebHostSceneBridge>();\n\n constructor(options: {\n mount: HTMLElement;\n manifest: WebHostSceneManifest;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n initialSceneId?: string;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory: RuntimeFactory;\n }) {\n this.mount = options.mount;\n this.style = normalizeWebHostTerminalStyle(options.style ?? {});\n this.environment = options.environment;\n this.embeddedHost = options.embeddedHost;\n this.bridgeFactory = options.bridgeFactory;\n this.sceneRuntimeFactory = options.sceneRuntimeFactory;\n this.scenes = options.manifest.scenes;\n this.selectedSceneId =\n options.initialSceneId &&\n options.manifest.scenes.some((scene) => scene.id === options.initialSceneId)\n ? options.initialSceneId\n : options.manifest.scenes.find((scene) => scene.id === options.manifest.defaultSceneId)?.id ??\n options.manifest.defaultSceneId;\n\n this.sceneRoot = (options.createElement ?? defaultCreateElement)(\"div\");\n this.sceneRoot.className = \"webhost-scene-root\";\n this.mount.replaceChildren(this.sceneRoot);\n this.applyHostFrameStyle();\n }\n\n async initialize(): Promise<void> {\n await this.ensureRuntime(this.selectedSceneId);\n await this.switchScene(this.selectedSceneId);\n }\n\n async switchScene(\n id: string\n ): Promise<void> {\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n for (const [sceneId, runtime] of this.runtimes) {\n runtime.setVisible(sceneId === id);\n }\n\n const runtime = await this.ensureRuntime(id);\n runtime.setVisible(true);\n this.selectedSceneId = id;\n }\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n const merged = mergeWebHostTerminalStyle(this.style, style);\n this.style = merged;\n\n for (const runtime of this.runtimes.values()) {\n runtime.setStyle(this.style);\n }\n this.applyHostFrameStyle();\n }\n\n async dispose(): Promise<void> {\n for (const runtime of this.runtimes.values()) {\n runtime.dispose();\n }\n for (const bridge of this.bridges.values()) {\n bridge.dispose();\n }\n this.runtimes.clear();\n this.bridges.clear();\n this.mount.replaceChildren();\n }\n\n private async ensureRuntime(\n id: string\n ): Promise<WebHostSceneRuntime> {\n const existing = this.runtimes.get(id);\n if (existing) {\n return existing;\n }\n\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n const bridge = this.makeBridge(id, descriptor);\n const runtime = this.sceneRuntimeFactory({\n mount: this.sceneRoot,\n descriptor,\n style: this.style,\n bridge,\n onInput: (chunk) => bridge.sendInput(chunk),\n });\n\n this.bridges.set(id, bridge);\n this.runtimes.set(id, runtime);\n await runtime.mount();\n runtime.setVisible(id === this.selectedSceneId);\n return runtime;\n }\n\n private makeBridge(\n sceneId: string,\n descriptor: WebHostSceneDescriptor\n ): WebHostSceneBridge {\n if (this.bridgeFactory) {\n return this.bridgeFactory({\n sceneId,\n descriptor,\n style: this.style,\n environment: this.environment,\n });\n }\n\n if (this.embeddedHost) {\n return new WebSocketSceneBridge({\n sceneId,\n token: this.embeddedHost.token,\n baseURL: this.embeddedHost.webSocketBaseURL,\n webSocketFactory: this.embeddedHost.webSocketFactory,\n });\n }\n\n return new BrowserWASIBridge({\n sceneId,\n columns: 80,\n rows: 24,\n environment: this.environment,\n renderStyle: this.style,\n });\n }\n\n private applyHostFrameStyle(): void {\n this.mount.style.background = \"linear-gradient(180deg, #0f172a 0%, #111827 100%)\";\n this.mount.style.minHeight = \"100%\";\n this.mount.style.display = \"block\";\n this.mount.style.padding = \"1rem\";\n }\n}\n\nfunction defaultCreateElement(\n tagName: string\n): HTMLElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"document is not available\");\n }\n\n return document.createElement(tagName);\n}\n\nasync function resolveManifest(\n options: WebHostAppOptions\n): Promise<WebHostSceneManifest> {\n if (options.manifest) {\n return loadWebHostSceneManifest(options.manifest);\n }\n\n if (options.manifestUrl) {\n return loadWebHostSceneManifest(options.manifestUrl);\n }\n\n return normalizeWebHostSceneManifest([\n {\n id: \"main\",\n title: \"Main\",\n isDefault: true,\n },\n ]);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WebHostApp.js","names":[],"sources":["../../src/WebHostApp.ts"],"sourcesContent":["import { BrowserWASIBridge } from \"./wasi/BrowserWASIBridge.ts\";\nimport {\n WebSocketSceneBridge,\n type WebSocketSceneBridgeOptions,\n} from \"./WebSocketSceneBridge.ts\";\nimport {\n loadWebHostSceneManifest,\n normalizeWebHostSceneManifest,\n type WebHostSceneDescriptor,\n type WebHostSceneManifest,\n type WebHostSceneManifestSource,\n} from \"./WebHostSceneManifest.ts\";\nimport {\n mergeWebHostTerminalStyle,\n normalizeWebHostTerminalStyle,\n type ResolvedWebHostTerminalStyle,\n type WebHostTerminalStyle,\n} from \"./WebHostTerminalStyle.ts\";\nimport {\n WebHostSceneRuntime,\n type WebHostSceneBridge,\n type WebHostSceneRuntimeOptions,\n} from \"./WebHostSceneRuntime.ts\";\nimport type { WebHostSurfaceRendererKind } from \"./SurfaceRenderer.ts\";\n\nexport interface WebHostEmbeddedHostConfig {\n token: string;\n webSocketBaseURL?: string | URL;\n webSocketFactory?: WebSocketSceneBridgeOptions[\"webSocketFactory\"];\n}\n\nexport interface WebHostBridgeFactoryOptions {\n sceneId: string;\n descriptor: WebHostSceneDescriptor;\n style: WebHostTerminalStyle;\n environment?: Record<string, string>;\n}\n\nexport type WebHostBridgeFactory = (options: WebHostBridgeFactoryOptions) => WebHostSceneBridge;\n\n/**\n * The slice of `Document` the app controller needs to track page visibility.\n * Injectable for tests and non-browser hosts; defaults to the global\n * `document` when one exists.\n */\nexport interface WebHostVisibilityDocument {\n readonly hidden: boolean;\n addEventListener(type: \"visibilitychange\", listener: () => void): void;\n removeEventListener(type: \"visibilitychange\", listener: () => void): void;\n}\n\nexport interface WebHostAppOptions {\n mount: HTMLElement;\n manifest?: WebHostSceneManifestSource;\n manifestUrl?: string | URL;\n initialSceneId?: string;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory?: (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n /**\n * Whether scenes that cannot be seen — background scenes after a switch,\n * or every scene while the document is hidden — suspend their apps (run\n * loop parked, monotonic clock frozen) instead of burning CPU. Forwarded to\n * each scene runtime as `suspendWhenHidden`. Defaults to `true`.\n */\n suspendHiddenScenes?: boolean;\n /** Visibility source override; defaults to the global `document`. */\n visibilityDocument?: WebHostVisibilityDocument;\n /**\n * Which surface presenter every scene runtime uses: `\"canvas\"` (default)\n * paints frames onto a 2D `<canvas>`; `\"dom\"` renders them as absolutely\n * positioned text elements. Forwarded to each scene runtime as `renderer`.\n * See {@link WebHostSurfaceRendererKind}.\n */\n renderer?: WebHostSurfaceRendererKind;\n}\n\nexport interface WebHostAppController {\n scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n switchScene(id: string): Promise<void>;\n setStyle(style: WebHostTerminalStyle): void;\n dispose(): Promise<void>;\n}\n\ntype RuntimeFactory = (options: WebHostSceneRuntimeOptions) => WebHostSceneRuntime;\n\nexport async function createWebHostApp(\n options: WebHostAppOptions\n): Promise<WebHostAppController> {\n const manifest = await resolveManifest(options);\n const controller = new InternalWebHostAppController({\n mount: options.mount,\n manifest,\n style: options.style,\n environment: options.environment,\n embeddedHost: options.embeddedHost,\n bridgeFactory: options.bridgeFactory,\n initialSceneId: options.initialSceneId,\n createElement: options.createElement,\n sceneRuntimeFactory: options.sceneRuntimeFactory ?? ((runtimeOptions) => new WebHostSceneRuntime(runtimeOptions)),\n suspendHiddenScenes: options.suspendHiddenScenes,\n visibilityDocument: options.visibilityDocument ?? defaultVisibilityDocument(),\n renderer: options.renderer,\n });\n await controller.initialize();\n return controller;\n}\n\nclass InternalWebHostAppController implements WebHostAppController {\n readonly scenes: WebHostSceneDescriptor[];\n selectedSceneId: string;\n\n private readonly mount: HTMLElement;\n private readonly sceneRoot: HTMLElement;\n private style: ResolvedWebHostTerminalStyle;\n private readonly environment?: Record<string, string>;\n private readonly embeddedHost?: WebHostEmbeddedHostConfig;\n private readonly bridgeFactory?: WebHostBridgeFactory;\n private readonly sceneRuntimeFactory: RuntimeFactory;\n private readonly runtimes = new Map<string, WebHostSceneRuntime>();\n private readonly bridges = new Map<string, WebHostSceneBridge>();\n private readonly suspendHiddenScenes?: boolean;\n private readonly renderer?: WebHostSurfaceRendererKind;\n private readonly visibilityDocument?: WebHostVisibilityDocument;\n private detachVisibilityListener?: () => void;\n\n constructor(options: {\n mount: HTMLElement;\n manifest: WebHostSceneManifest;\n style?: WebHostTerminalStyle;\n environment?: Record<string, string>;\n embeddedHost?: WebHostEmbeddedHostConfig;\n bridgeFactory?: WebHostBridgeFactory;\n initialSceneId?: string;\n createElement?: (tagName: string) => HTMLElement;\n sceneRuntimeFactory: RuntimeFactory;\n suspendHiddenScenes?: boolean;\n visibilityDocument?: WebHostVisibilityDocument;\n renderer?: WebHostSurfaceRendererKind;\n }) {\n this.mount = options.mount;\n this.style = normalizeWebHostTerminalStyle(options.style ?? {});\n this.environment = options.environment;\n this.embeddedHost = options.embeddedHost;\n this.bridgeFactory = options.bridgeFactory;\n this.sceneRuntimeFactory = options.sceneRuntimeFactory;\n this.suspendHiddenScenes = options.suspendHiddenScenes;\n this.renderer = options.renderer;\n this.visibilityDocument = options.visibilityDocument;\n this.scenes = options.manifest.scenes;\n this.selectedSceneId =\n options.initialSceneId &&\n options.manifest.scenes.some((scene) => scene.id === options.initialSceneId)\n ? options.initialSceneId\n : options.manifest.scenes.find((scene) => scene.id === options.manifest.defaultSceneId)?.id ??\n options.manifest.defaultSceneId;\n\n this.sceneRoot = (options.createElement ?? defaultCreateElement)(\"div\");\n this.sceneRoot.className = \"webhost-scene-root\";\n this.mount.replaceChildren(this.sceneRoot);\n this.applyHostFrameStyle();\n }\n\n async initialize(): Promise<void> {\n this.installVisibilityListener();\n await this.ensureRuntime(this.selectedSceneId);\n await this.switchScene(this.selectedSceneId);\n }\n\n async switchScene(\n id: string\n ): Promise<void> {\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n for (const [sceneId, runtime] of this.runtimes) {\n runtime.setVisible(sceneId === id);\n }\n\n const runtime = await this.ensureRuntime(id);\n runtime.setVisible(true);\n this.selectedSceneId = id;\n }\n\n setStyle(\n style: WebHostTerminalStyle\n ): void {\n const merged = mergeWebHostTerminalStyle(this.style, style);\n this.style = merged;\n\n for (const runtime of this.runtimes.values()) {\n runtime.setStyle(this.style);\n }\n this.applyHostFrameStyle();\n }\n\n async dispose(): Promise<void> {\n this.detachVisibilityListener?.();\n this.detachVisibilityListener = undefined;\n for (const runtime of this.runtimes.values()) {\n runtime.dispose();\n }\n for (const bridge of this.bridges.values()) {\n bridge.dispose();\n }\n this.runtimes.clear();\n this.bridges.clear();\n this.mount.replaceChildren();\n }\n\n private installVisibilityListener(): void {\n const visibilityDocument = this.visibilityDocument;\n if (!visibilityDocument) {\n return;\n }\n const listener = (): void => {\n const visible = !visibilityDocument.hidden;\n for (const runtime of this.runtimes.values()) {\n runtime.setDocumentVisible(visible);\n }\n };\n visibilityDocument.addEventListener(\"visibilitychange\", listener);\n this.detachVisibilityListener = () => {\n visibilityDocument.removeEventListener(\"visibilitychange\", listener);\n };\n }\n\n private async ensureRuntime(\n id: string\n ): Promise<WebHostSceneRuntime> {\n const existing = this.runtimes.get(id);\n if (existing) {\n return existing;\n }\n\n const descriptor = this.scenes.find((scene) => scene.id === id);\n if (!descriptor) {\n throw new Error(`Unknown scene: ${id}`);\n }\n\n const bridge = this.makeBridge(id, descriptor);\n const runtime = this.sceneRuntimeFactory({\n mount: this.sceneRoot,\n descriptor,\n style: this.style,\n bridge,\n onInput: (chunk) => bridge.sendInput(chunk),\n suspendWhenHidden: this.suspendHiddenScenes,\n renderer: this.renderer,\n });\n\n this.bridges.set(id, bridge);\n this.runtimes.set(id, runtime);\n await runtime.mount();\n runtime.setVisible(id === this.selectedSceneId);\n if (this.visibilityDocument) {\n runtime.setDocumentVisible(!this.visibilityDocument.hidden);\n }\n return runtime;\n }\n\n private makeBridge(\n sceneId: string,\n descriptor: WebHostSceneDescriptor\n ): WebHostSceneBridge {\n if (this.bridgeFactory) {\n return this.bridgeFactory({\n sceneId,\n descriptor,\n style: this.style,\n environment: this.environment,\n });\n }\n\n if (this.embeddedHost) {\n return new WebSocketSceneBridge({\n sceneId,\n token: this.embeddedHost.token,\n baseURL: this.embeddedHost.webSocketBaseURL,\n webSocketFactory: this.embeddedHost.webSocketFactory,\n });\n }\n\n return new BrowserWASIBridge({\n sceneId,\n columns: 80,\n rows: 24,\n environment: this.environment,\n renderStyle: this.style,\n });\n }\n\n private applyHostFrameStyle(): void {\n this.mount.style.background = \"linear-gradient(180deg, #0f172a 0%, #111827 100%)\";\n this.mount.style.minHeight = \"100%\";\n this.mount.style.display = \"block\";\n this.mount.style.padding = \"1rem\";\n }\n}\n\nfunction defaultVisibilityDocument(): WebHostVisibilityDocument | undefined {\n if (typeof document === \"undefined\") {\n return undefined;\n }\n return document;\n}\n\nfunction defaultCreateElement(\n tagName: string\n): HTMLElement {\n if (typeof document === \"undefined\") {\n throw new Error(\"document is not available\");\n }\n\n return document.createElement(tagName);\n}\n\nasync function resolveManifest(\n options: WebHostAppOptions\n): Promise<WebHostSceneManifest> {\n if (options.manifest) {\n return loadWebHostSceneManifest(options.manifest);\n }\n\n if (options.manifestUrl) {\n return loadWebHostSceneManifest(options.manifestUrl);\n }\n\n return normalizeWebHostSceneManifest([\n {\n id: \"main\",\n title: \"Main\",\n isDefault: true,\n },\n ]);\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { WebHostTerminalStyle } from "./WebHostTerminalStyle.js";
2
2
  import { WebHostFocusPresentation, WebHostFrameDiagnosticRecord, WebHostOutputSink, WebHostRuntimeIssue } from "./WebHostSurfaceTransport.js";
3
+ import { WebHostSurfaceRendererKind } from "./SurfaceRenderer.js";
3
4
  import { WebHostSceneDescriptor } from "./WebHostSceneManifest.js";
4
5
  //#region src/WebHostSceneRuntime.d.ts
5
6
  interface WebHostSceneBridge {
@@ -45,6 +46,23 @@ interface WebHostSceneRuntimeOptions {
45
46
  * and other schemes are ignored. Mirrors the Android host's tap-to-open.
46
47
  */
47
48
  onOpenHyperlink?: (url: string) => void;
49
+ /**
50
+ * Whether to suspend the scene's app while it cannot be seen — when the
51
+ * scene is switched to the background (`setVisible(false)`) or the whole
52
+ * document is hidden (`setDocumentVisible(false)`). Suspension parks the
53
+ * app's run loop and freezes its monotonic clock, so a hidden scene costs
54
+ * no CPU and resumes exactly where it left off. Defaults to `true`; set
55
+ * `false` to let background scenes keep running (pre-suspension behavior).
56
+ */
57
+ suspendWhenHidden?: boolean;
58
+ /**
59
+ * Which surface presenter draws the scene's frames. `"canvas"` (default)
60
+ * paints onto a 2D `<canvas>`; `"dom"` renders cells as absolutely
61
+ * positioned text elements — native font rendering and, uniquely, real
62
+ * text selection: hold Alt/Option and drag to select instead of sending
63
+ * pointer input to the app. See {@link WebHostSurfaceRendererKind}.
64
+ */
65
+ renderer?: WebHostSurfaceRendererKind;
48
66
  }
49
67
  type WheelMode = "capture" | "chain" | "passive";
50
68
  /**
@@ -63,10 +81,13 @@ declare class WebHostSceneRuntime {
63
81
  private readonly onFrameDiagnostic?;
64
82
  private readonly synchronizeAccessibilityFocus;
65
83
  private readonly wheelMode;
84
+ private readonly rendererKind;
66
85
  private readonly painter;
67
86
  private readonly inputEncoder;
68
87
  private currentStyle;
69
88
  private canvas?;
89
+ private domSurfaceRoot?;
90
+ private lastDomSurfaceSize?;
70
91
  private accessibilityTree?;
71
92
  private diagnosticText?;
72
93
  private resizeObserver?;
@@ -82,9 +103,26 @@ declare class WebHostSceneRuntime {
82
103
  private pointerDownLinkTarget?;
83
104
  private lastSentResize?;
84
105
  private isVisible;
106
+ private documentVisible;
107
+ private runtimeSuspended;
108
+ private readonly suspendWhenHidden;
85
109
  constructor(options: WebHostSceneRuntimeOptions);
86
110
  mount(): Promise<void>;
87
111
  setVisible(visible: boolean): void;
112
+ /**
113
+ * Reports whether the surrounding document can be seen at all (browser tab
114
+ * visible, iframe on-screen, …). Combined with the scene-level
115
+ * `setVisible`: the app is suspended while either says hidden, unless
116
+ * `suspendWhenHidden` is `false`.
117
+ */
118
+ setDocumentVisible(visible: boolean): void;
119
+ private updateRuntimeSuspension;
120
+ /**
121
+ * Suspension hook for subclasses that own an app execution vehicle (the
122
+ * WASI worker / JSPI executor). The base runtime only presents frames, so
123
+ * it has nothing to suspend.
124
+ */
125
+ protected onRuntimeSuspensionChange(_suspended: boolean): void;
88
126
  setStyle(style: WebHostTerminalStyle): void;
89
127
  resize(columns: number, rows: number): void;
90
128
  writeOutput(text: string): void;
@@ -112,17 +150,26 @@ declare class WebHostSceneRuntime {
112
150
  private linkTarget;
113
151
  private openHyperlink;
114
152
  private applyStyle;
153
+ /** The element the active painter presents frames into. */
154
+ private get surfaceElement();
115
155
  private applyVisibility;
116
156
  private installResizeObserver;
117
157
  private installInputHandlers;
118
158
  private resizeToMount;
119
159
  private sendResizeIfNeeded;
120
- private resizeCanvas;
160
+ private resizeSurface;
121
161
  private measureCells;
122
162
  private draw;
123
163
  private syncAccessibilityTree;
124
164
  private surfaceMetrics;
125
165
  private pointerMetrics;
166
+ /**
167
+ * Whether this pointer event should be left to the browser for native text
168
+ * selection instead of being forwarded to the app. Only the DOM renderer
169
+ * has real text nodes to select, and only while Alt/Option is held — plain
170
+ * pointer input still belongs to the app.
171
+ */
172
+ private allowsNativeTextSelection;
126
173
  private cellLocation;
127
174
  private rawCellLocation;
128
175
  }
@@ -1,5 +1,6 @@
1
1
  import { applyWebHostTerminalStyle, normalizeWebHostTerminalStyle, webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
2
2
  import { CanvasSurfacePainter, fontForStyle } from "./CanvasSurfacePainter.js";
3
+ import { DomSurfacePainter } from "./DomSurfacePainter.js";
3
4
  import { InputEventEncoder } from "./InputEventEncoder.js";
4
5
  import { cellLocationForEvent, linkTargetAt, rawCellLocationForEvent, wheelTargetCanScroll } from "./PointerGeometry.js";
5
6
  import { AccessibilityTreeMounter } from "./AccessibilityTree.js";
@@ -30,10 +31,13 @@ var WebHostSceneRuntime = class {
30
31
  onFrameDiagnostic;
31
32
  synchronizeAccessibilityFocus;
32
33
  wheelMode;
33
- painter = new CanvasSurfacePainter();
34
+ rendererKind;
35
+ painter;
34
36
  inputEncoder = new InputEventEncoder();
35
37
  currentStyle;
36
38
  canvas;
39
+ domSurfaceRoot;
40
+ lastDomSurfaceSize;
37
41
  accessibilityTree;
38
42
  diagnosticText;
39
43
  resizeObserver;
@@ -49,6 +53,9 @@ var WebHostSceneRuntime = class {
49
53
  pointerDownLinkTarget;
50
54
  lastSentResize;
51
55
  isVisible = false;
56
+ documentVisible = true;
57
+ runtimeSuspended = false;
58
+ suspendWhenHidden;
52
59
  constructor(options) {
53
60
  this.descriptor = options.descriptor;
54
61
  this.currentStyle = normalizeWebHostTerminalStyle(options.style);
@@ -57,7 +64,10 @@ var WebHostSceneRuntime = class {
57
64
  this.onFrameDiagnostic = options.onFrameDiagnostic;
58
65
  this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;
59
66
  this.wheelMode = options.wheelMode ?? legacyWheelMode(options.captureWheelInput);
67
+ this.rendererKind = options.renderer ?? "canvas";
68
+ this.painter = this.rendererKind === "dom" ? new DomSurfacePainter() : new CanvasSurfacePainter();
60
69
  this.onOpenHyperlink = options.onOpenHyperlink;
70
+ this.suspendWhenHidden = options.suspendWhenHidden ?? true;
61
71
  this.element = document.createElement("section");
62
72
  this.element.className = "webhost-scene";
63
73
  this.element.dataset.sceneId = options.descriptor.id;
@@ -73,14 +83,22 @@ var WebHostSceneRuntime = class {
73
83
  this.applyVisibility();
74
84
  }
75
85
  async mount() {
76
- if (this.canvas) return;
77
- const canvas = document.createElement("canvas");
78
- canvas.className = "webhost-scene__surface";
79
- canvas.setAttribute("aria-hidden", "true");
80
- this.canvas = canvas;
81
- this.painter.attach(canvas, () => this.draw());
86
+ if (this.surfaceElement) return;
87
+ if (this.painter instanceof DomSurfacePainter) {
88
+ const surfaceRoot = document.createElement("div");
89
+ surfaceRoot.className = "webhost-scene__surface webhost-scene__surface--dom";
90
+ surfaceRoot.setAttribute("aria-hidden", "true");
91
+ this.domSurfaceRoot = surfaceRoot;
92
+ this.painter.attach(surfaceRoot);
93
+ } else {
94
+ const canvas = document.createElement("canvas");
95
+ canvas.className = "webhost-scene__surface";
96
+ canvas.setAttribute("aria-hidden", "true");
97
+ this.canvas = canvas;
98
+ this.painter.attach(canvas, () => this.draw());
99
+ }
82
100
  this.accessibilityTree = new AccessibilityTreeMounter();
83
- this.terminalMount.replaceChildren(canvas, this.accessibilityTree.element, this.accessibilityTree.announcerElement);
101
+ this.terminalMount.replaceChildren(this.surfaceElement, this.accessibilityTree.element, this.accessibilityTree.announcerElement);
84
102
  this.installInputHandlers();
85
103
  this.installResizeObserver();
86
104
  this.bridge?.bindOutput({
@@ -104,7 +122,30 @@ var WebHostSceneRuntime = class {
104
122
  this.resizeToMount();
105
123
  if (this.synchronizeAccessibilityFocus) this.terminalMount.focus?.({ preventScroll: true });
106
124
  }
125
+ this.updateRuntimeSuspension();
126
+ }
127
+ /**
128
+ * Reports whether the surrounding document can be seen at all (browser tab
129
+ * visible, iframe on-screen, …). Combined with the scene-level
130
+ * `setVisible`: the app is suspended while either says hidden, unless
131
+ * `suspendWhenHidden` is `false`.
132
+ */
133
+ setDocumentVisible(visible) {
134
+ this.documentVisible = visible;
135
+ this.updateRuntimeSuspension();
107
136
  }
137
+ updateRuntimeSuspension() {
138
+ const suspended = this.suspendWhenHidden && (!this.isVisible || !this.documentVisible);
139
+ if (suspended === this.runtimeSuspended) return;
140
+ this.runtimeSuspended = suspended;
141
+ this.onRuntimeSuspensionChange(suspended);
142
+ }
143
+ /**
144
+ * Suspension hook for subclasses that own an app execution vehicle (the
145
+ * WASI worker / JSPI executor). The base runtime only presents frames, so
146
+ * it has nothing to suspend.
147
+ */
148
+ onRuntimeSuspensionChange(_suspended) {}
108
149
  setStyle(style) {
109
150
  this.currentStyle = normalizeWebHostTerminalStyle(style);
110
151
  this.applyStyle(this.currentStyle);
@@ -117,7 +158,7 @@ var WebHostSceneRuntime = class {
117
158
  resize(columns, rows) {
118
159
  this.columns = Math.max(1, Math.round(columns));
119
160
  this.rows = Math.max(1, Math.round(rows));
120
- this.resizeCanvas();
161
+ this.resizeSurface();
121
162
  this.draw();
122
163
  this.syncAccessibilityTree();
123
164
  }
@@ -156,7 +197,7 @@ var WebHostSceneRuntime = class {
156
197
  this.currentFrame = frame;
157
198
  this.columns = Math.max(1, Math.round(frame.width));
158
199
  this.rows = Math.max(1, Math.round(frame.height));
159
- const resized = this.resizeCanvas();
200
+ const resized = this.resizeSurface();
160
201
  this.draw(previousFrame && !resized ? frame.damage : void 0);
161
202
  this.syncAccessibilityTree();
162
203
  }
@@ -211,6 +252,14 @@ var WebHostSceneRuntime = class {
211
252
  this.canvas.style.width = "100%";
212
253
  this.canvas.style.height = "100%";
213
254
  }
255
+ if (this.domSurfaceRoot) {
256
+ this.domSurfaceRoot.style.display = "block";
257
+ this.domSurfaceRoot.style.position = "relative";
258
+ }
259
+ }
260
+ /** The element the active painter presents frames into. */
261
+ get surfaceElement() {
262
+ return this.canvas ?? this.domSurfaceRoot;
214
263
  }
215
264
  applyVisibility() {
216
265
  this.element.hidden = !this.isVisible;
@@ -238,6 +287,7 @@ var WebHostSceneRuntime = class {
238
287
  event.preventDefault();
239
288
  };
240
289
  const handlePointerDown = (event) => {
290
+ if (this.allowsNativeTextSelection(event)) return;
241
291
  const location = this.cellLocation(event);
242
292
  if (!location) return;
243
293
  const button = this.inputEncoder.pointerButton(event.button);
@@ -250,6 +300,7 @@ var WebHostSceneRuntime = class {
250
300
  event.preventDefault();
251
301
  };
252
302
  const handlePointerUp = (event) => {
303
+ if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) return;
253
304
  const location = this.hasCapturedPointer ? this.rawCellLocation(event) : this.cellLocation(event);
254
305
  this.terminalMount.releasePointerCapture?.(event.pointerId);
255
306
  this.hasCapturedPointer = false;
@@ -262,6 +313,7 @@ var WebHostSceneRuntime = class {
262
313
  event.preventDefault();
263
314
  };
264
315
  const handlePointerMove = (event) => {
316
+ if (!this.hasCapturedPointer && this.allowsNativeTextSelection(event)) return;
265
317
  const location = event.buttons && this.hasCapturedPointer ? this.rawCellLocation(event) : this.cellLocation(event);
266
318
  if (!location) return;
267
319
  if (!this.hasCapturedPointer) this.terminalMount.style.cursor = this.linkTarget(location) !== void 0 ? "pointer" : "";
@@ -300,7 +352,7 @@ var WebHostSceneRuntime = class {
300
352
  this.columns = nextColumns;
301
353
  this.rows = nextRows;
302
354
  this.sendResizeIfNeeded();
303
- this.resizeCanvas();
355
+ this.resizeSurface();
304
356
  }
305
357
  sendResizeIfNeeded() {
306
358
  const current = {
@@ -313,10 +365,21 @@ var WebHostSceneRuntime = class {
313
365
  this.lastSentResize = current;
314
366
  this.bridge?.resize(current.columns, current.rows, current.cellWidth, current.cellHeight);
315
367
  }
316
- resizeCanvas() {
317
- if (!this.canvas) return false;
368
+ resizeSurface() {
318
369
  const cssWidth = Math.max(1, this.columns * this.cellWidth);
319
370
  const cssHeight = Math.max(1, this.rows * this.cellHeight);
371
+ if (this.domSurfaceRoot) {
372
+ const last = this.lastDomSurfaceSize;
373
+ if (last && last.width === cssWidth && last.height === cssHeight) return false;
374
+ this.lastDomSurfaceSize = {
375
+ width: cssWidth,
376
+ height: cssHeight
377
+ };
378
+ this.domSurfaceRoot.style.width = `${cssWidth}px`;
379
+ this.domSurfaceRoot.style.height = `${cssHeight}px`;
380
+ return true;
381
+ }
382
+ if (!this.canvas) return false;
320
383
  const scale = globalThis.window?.devicePixelRatio || 1;
321
384
  const width = Math.ceil(cssWidth * scale);
322
385
  const height = Math.ceil(cssHeight * scale);
@@ -362,13 +425,22 @@ var WebHostSceneRuntime = class {
362
425
  }
363
426
  pointerMetrics() {
364
427
  return {
365
- rect: this.canvas?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),
428
+ rect: this.surfaceElement?.getBoundingClientRect?.() ?? this.terminalMount.getBoundingClientRect?.(),
366
429
  cellWidth: this.cellWidth,
367
430
  cellHeight: this.cellHeight,
368
431
  columns: this.columns,
369
432
  rows: this.rows
370
433
  };
371
434
  }
435
+ /**
436
+ * Whether this pointer event should be left to the browser for native text
437
+ * selection instead of being forwarded to the app. Only the DOM renderer
438
+ * has real text nodes to select, and only while Alt/Option is held — plain
439
+ * pointer input still belongs to the app.
440
+ */
441
+ allowsNativeTextSelection(event) {
442
+ return this.rendererKind === "dom" && event.altKey;
443
+ }
372
444
  cellLocation(event) {
373
445
  return cellLocationForEvent(event, this.pointerMetrics());
374
446
  }