@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.
- package/README.md +31 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/src/CanvasSurfacePainter.js +3 -10
- package/dist/src/CanvasSurfacePainter.js.map +1 -1
- package/dist/src/DomSurfacePainter.d.ts +51 -0
- package/dist/src/DomSurfacePainter.js +267 -0
- package/dist/src/DomSurfacePainter.js.map +1 -0
- package/dist/src/SurfaceRenderer.d.ts +50 -0
- package/dist/src/SurfaceRenderer.js +21 -0
- package/dist/src/SurfaceRenderer.js.map +1 -0
- package/dist/src/WebHostApp.d.ts +28 -1
- package/dist/src/WebHostApp.js +34 -2
- package/dist/src/WebHostApp.js.map +1 -1
- package/dist/src/WebHostSceneRuntime.d.ts +48 -1
- package/dist/src/WebHostSceneRuntime.js +86 -14
- package/dist/src/WebHostSceneRuntime.js.map +1 -1
- package/dist/src/wasi/MainThreadWasmExecutor.d.ts +8 -0
- package/dist/src/wasi/MainThreadWasmExecutor.js +16 -1
- package/dist/src/wasi/MainThreadWasmExecutor.js.map +1 -1
- package/dist/src/wasi/WasiPollScheduler.js +6 -0
- package/dist/src/wasi/WasiPollScheduler.js.map +1 -1
- package/dist/src/wasi/WasmRuntimePause.d.ts +63 -0
- package/dist/src/wasi/WasmRuntimePause.js +130 -0
- package/dist/src/wasi/WasmRuntimePause.js.map +1 -0
- package/dist/src/wasi/WasmSceneRuntime.js +14 -1
- package/dist/src/wasi/WasmSceneRuntime.js.map +1 -1
- package/dist/src/wasi/WasmSceneWorker.d.ts +7 -0
- package/dist/src/wasi/WasmSceneWorker.js +11 -4
- package/dist/src/wasi/WasmSceneWorker.js.map +1 -1
- package/dist/wasi.d.ts +2 -1
- package/dist/wasi.js +2 -1
- 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"}
|
package/dist/src/WebHostApp.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WebHostTerminalStyle } from "./WebHostTerminalStyle.js";
|
|
2
|
+
import { WebHostSurfaceRendererKind } from "./SurfaceRenderer.js";
|
|
2
3
|
import { WebHostSceneDescriptor, WebHostSceneManifestSource } from "./WebHostSceneManifest.js";
|
|
3
4
|
import { WebHostSceneBridge, WebHostSceneRuntime, WebHostSceneRuntimeOptions } from "./WebHostSceneRuntime.js";
|
|
4
5
|
import { WebSocketSceneBridgeOptions } from "./WebSocketSceneBridge.js";
|
|
@@ -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
|
package/dist/src/WebHostApp.js
CHANGED
|
@@ -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":";;;;;;AA0FA,eAAsB,iBACpB,SAC+B;CAC/B,MAAM,WAAW,MAAM,gBAAgB,OAAO;CAC9C,MAAM,aAAa,IAAI,6BAA6B;EAClD,OAAO,QAAQ;EACf;EACA,OAAO,QAAQ;EACf,aAAa,QAAQ;EACrB,cAAc,QAAQ;EACtB,eAAe,QAAQ;EACvB,gBAAgB,QAAQ;EACxB,eAAe,QAAQ;EACvB,qBAAqB,QAAQ,yBAAyB,mBAAmB,IAAI,oBAAoB,cAAc;EAC/G,qBAAqB,QAAQ;EAC7B,oBAAoB,QAAQ,sBAAsB,0BAA0B;EAC5E,UAAU,QAAQ;CACpB,CAAC;CACD,MAAM,WAAW,WAAW;CAC5B,OAAO;AACT;AAEA,IAAM,+BAAN,MAAmE;CACjE;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,2BAA4B,IAAI,IAAiC;CACjE,0BAA2B,IAAI,IAAgC;CAC/D;CACA;CACA;CACA;CAEA,YAAY,SAaT;EACD,KAAK,QAAQ,QAAQ;EACrB,KAAK,QAAQ,8BAA8B,QAAQ,SAAS,CAAC,CAAC;EAC9D,KAAK,cAAc,QAAQ;EAC3B,KAAK,eAAe,QAAQ;EAC5B,KAAK,gBAAgB,QAAQ;EAC7B,KAAK,sBAAsB,QAAQ;EACnC,KAAK,sBAAsB,QAAQ;EACnC,KAAK,WAAW,QAAQ;EACxB,KAAK,qBAAqB,QAAQ;EAClC,KAAK,SAAS,QAAQ,SAAS;EAC/B,KAAK,kBACH,QAAQ,kBACR,QAAQ,SAAS,OAAO,MAAM,UAAU,MAAM,OAAO,QAAQ,cAAc,IACvE,QAAQ,iBACR,QAAQ,SAAS,OAAO,MAAM,UAAU,MAAM,OAAO,QAAQ,SAAS,cAAc,CAAC,EAAE,MACvF,QAAQ,SAAS;EAEvB,KAAK,aAAa,QAAQ,iBAAiB,qBAAA,CAAsB,KAAK;EACtE,KAAK,UAAU,YAAY;EAC3B,KAAK,MAAM,gBAAgB,KAAK,SAAS;EACzC,KAAK,oBAAoB;CAC3B;CAEA,MAAM,aAA4B;EAChC,KAAK,0BAA0B;EAC/B,MAAM,KAAK,cAAc,KAAK,eAAe;EAC7C,MAAM,KAAK,YAAY,KAAK,eAAe;CAC7C;CAEA,MAAM,YACJ,IACe;EAEf,IAAI,CADe,KAAK,OAAO,MAAM,UAAU,MAAM,OAAO,EAC9C,GACZ,MAAM,IAAI,MAAM,kBAAkB,IAAI;EAGxC,KAAK,MAAM,CAAC,SAAS,YAAY,KAAK,UACpC,QAAQ,WAAW,YAAY,EAAE;EAInC,CAAA,MADsB,KAAK,cAAc,EAAE,EAAA,CACnC,WAAW,IAAI;EACvB,KAAK,kBAAkB;CACzB;CAEA,SACE,OACM;EACN,MAAM,SAAS,0BAA0B,KAAK,OAAO,KAAK;EAC1D,KAAK,QAAQ;EAEb,KAAK,MAAM,WAAW,KAAK,SAAS,OAAO,GACzC,QAAQ,SAAS,KAAK,KAAK;EAE7B,KAAK,oBAAoB;CAC3B;CAEA,MAAM,UAAyB;EAC7B,KAAK,2BAA2B;EAChC,KAAK,2BAA2B,KAAA;EAChC,KAAK,MAAM,WAAW,KAAK,SAAS,OAAO,GACzC,QAAQ,QAAQ;EAElB,KAAK,MAAM,UAAU,KAAK,QAAQ,OAAO,GACvC,OAAO,QAAQ;EAEjB,KAAK,SAAS,MAAM;EACpB,KAAK,QAAQ,MAAM;EACnB,KAAK,MAAM,gBAAgB;CAC7B;CAEA,4BAA0C;EACxC,MAAM,qBAAqB,KAAK;EAChC,IAAI,CAAC,oBACH;EAEF,MAAM,iBAAuB;GAC3B,MAAM,UAAU,CAAC,mBAAmB;GACpC,KAAK,MAAM,WAAW,KAAK,SAAS,OAAO,GACzC,QAAQ,mBAAmB,OAAO;EAEtC;EACA,mBAAmB,iBAAiB,oBAAoB,QAAQ;EAChE,KAAK,iCAAiC;GACpC,mBAAmB,oBAAoB,oBAAoB,QAAQ;EACrE;CACF;CAEA,MAAc,cACZ,IAC8B;EAC9B,MAAM,WAAW,KAAK,SAAS,IAAI,EAAE;EACrC,IAAI,UACF,OAAO;EAGT,MAAM,aAAa,KAAK,OAAO,MAAM,UAAU,MAAM,OAAO,EAAE;EAC9D,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,kBAAkB,IAAI;EAGxC,MAAM,SAAS,KAAK,WAAW,IAAI,UAAU;EAC7C,MAAM,UAAU,KAAK,oBAAoB;GACvC,OAAO,KAAK;GACZ;GACA,OAAO,KAAK;GACZ;GACA,UAAU,UAAU,OAAO,UAAU,KAAK;GAC1C,mBAAmB,KAAK;GACxB,UAAU,KAAK;EACjB,CAAC;EAED,KAAK,QAAQ,IAAI,IAAI,MAAM;EAC3B,KAAK,SAAS,IAAI,IAAI,OAAO;EAC7B,MAAM,QAAQ,MAAM;EACpB,QAAQ,WAAW,OAAO,KAAK,eAAe;EAC9C,IAAI,KAAK,oBACP,QAAQ,mBAAmB,CAAC,KAAK,mBAAmB,MAAM;EAE5D,OAAO;CACT;CAEA,WACE,SACA,YACoB;EACpB,IAAI,KAAK,eACP,OAAO,KAAK,cAAc;GACxB;GACA;GACA,OAAO,KAAK;GACZ,aAAa,KAAK;EACpB,CAAC;EAGH,IAAI,KAAK,cACP,OAAO,IAAI,qBAAqB;GAC9B;GACA,OAAO,KAAK,aAAa;GACzB,SAAS,KAAK,aAAa;GAC3B,kBAAkB,KAAK,aAAa;EACtC,CAAC;EAGH,OAAO,IAAI,kBAAkB;GAC3B;GACA,SAAS;GACT,MAAM;GACN,aAAa,KAAK;GAClB,aAAa,KAAK;EACpB,CAAC;CACH;CAEA,sBAAoC;EAClC,KAAK,MAAM,MAAM,aAAa;EAC9B,KAAK,MAAM,MAAM,YAAY;EAC7B,KAAK,MAAM,MAAM,UAAU;EAC3B,KAAK,MAAM,MAAM,UAAU;CAC7B;AACF;AAEA,SAAS,4BAAmE;CAC1E,IAAI,OAAO,aAAa,aACtB;CAEF,OAAO;AACT;AAEA,SAAS,qBACP,SACa;CACb,IAAI,OAAO,aAAa,aACtB,MAAM,IAAI,MAAM,2BAA2B;CAG7C,OAAO,SAAS,cAAc,OAAO;AACvC;AAEA,eAAe,gBACb,SAC+B;CAC/B,IAAI,QAAQ,UACV,OAAO,yBAAyB,QAAQ,QAAQ;CAGlD,IAAI,QAAQ,aACV,OAAO,yBAAyB,QAAQ,WAAW;CAGrD,OAAO,8BAA8B,CACnC;EACE,IAAI;EACJ,OAAO;EACP,WAAW;CACb,CACF,CAAC;AACH"}
|
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
}
|