@swifttui/web 0.0.27 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,26 @@
1
1
  # `@swifttui/web`
2
2
 
3
- Browser runtime package for SwiftTUI apps.
3
+ **Browser runtime for [SwiftTUI](https://swifttui.sh) apps — draw a
4
+ Swift-authored UI into a `<canvas>`, no terminal emulator.**
4
5
 
5
- This package owns browser-safe runtime APIs: scene manifest loading, canvas
6
- rendering, ARIA mounting, WebSocket scene bridges, and WASI scene bridges. Build
7
- tooling lives in the sibling [`@swifttui/build`](../build) workspace package.
6
+ [![npm](https://img.shields.io/npm/v/@swifttui/web)](https://www.npmjs.com/package/@swifttui/web)
7
+ ![License](https://img.shields.io/badge/license-MIT-3DA639)
8
+
9
+ `@swifttui/web` is the browser host for SwiftTUI. A SwiftTUI app compiles to
10
+ `wasm32-wasi` and streams a structured raster surface on stdout; this package
11
+ loads the scene manifest, renders that surface into a canvas, mounts an ARIA
12
+ tree for accessibility, and bridges input back to the running app — so the same
13
+ view code you run in a terminal runs on a web page. It does not load a terminal
14
+ emulator.
15
+
16
+ The build side — compiling your Swift app to wasm and capturing its manifest —
17
+ lives in the sibling
18
+ [`@swifttui/build`](https://www.npmjs.com/package/@swifttui/build) package.
19
+
20
+ - **Live demo:** <https://swifttui.sh/webexample>
21
+ - **Reference template:** [`swift-tui-examples/WebExample`](https://github.com/SwiftTUI/swift-tui-examples/tree/main/WebExample)
22
+ (≈60 lines of embedding code)
23
+ - **The framework:** [`SwiftTUI/swift-tui`](https://github.com/SwiftTUI/swift-tui)
8
24
 
9
25
  ## Installation
10
26
 
@@ -20,36 +36,6 @@ does **not** require Bun or a TypeScript build step. Subpath entrypoints
20
36
  (`./wasi`, `./wasi-worker`, `./manifest`, `./websocket`, `./testing`) and the
21
37
  `./style.css` asset are declared in `package.json` `exports`.
22
38
 
23
- ## Toolchains
24
-
25
- Use Bun for repo-local development of this package, and use the repo-default
26
- `swiftly` Swift 6.3.1 toolchain for every Swift command that the build pipeline
27
- triggers.
28
-
29
- Quick check:
30
-
31
- ```bash
32
- swiftly run swift --version
33
- ```
34
-
35
- Native-only development should also work in Xcode, but the documented package
36
- and wasm build path uses `swiftly` plus Bun.
37
-
38
- For source development, run `bun install` from the repo root or from any
39
- workspace package directory, and Bun will maintain one root `bun.lock` plus
40
- stable relative workspace links.
41
-
42
- ## Surface Transport
43
-
44
- This package uses SwiftTUI's `web-surface` WASI transport. The Swift runner
45
- emits structured raster-surface records on stdout, and the browser host draws
46
- rectangles and text into a canvas. It does not load a terminal emulator and does
47
- not depend on `ghostty-web` or `ghostty-vt.wasm`.
48
-
49
- `web-surface` is the default `SwiftTUIWASI` browser transport. WebHost still
50
- sets `TUIGUI_TRANSPORT=surface` explicitly so generated app environments are
51
- self-describing.
52
-
53
39
  ## API
54
40
 
55
41
  ```ts
@@ -93,7 +79,42 @@ import { startWasmSceneWorker } from "@swifttui/web/wasi-worker";
93
79
  startWasmSceneWorker();
94
80
  ```
95
81
 
96
- ## Scripts
82
+ The page that hosts the WASI runtime must serve
83
+ `Cross-Origin-Opener-Policy: same-origin` and
84
+ `Cross-Origin-Embedder-Policy: require-corp` so the `SharedArrayBuffer`-backed
85
+ stdin works.
86
+
87
+ ## Surface transport
88
+
89
+ This package uses SwiftTUI's `web-surface` WASI transport. The Swift runner
90
+ emits structured raster-surface records on stdout, and the browser host draws
91
+ rectangles and text into a canvas. It does not load a terminal emulator and does
92
+ not depend on `ghostty-web` or `ghostty-vt.wasm`.
93
+
94
+ `web-surface` is the default `SwiftTUIWASI` browser transport. WebHost still
95
+ sets `TUIGUI_TRANSPORT=surface` explicitly so generated app environments are
96
+ self-describing.
97
+
98
+ ## Notes
99
+
100
+ - Scene switching is controller-managed and retains existing scene runtimes.
101
+ - Terminal styling is host-owned through `WebHostTerminalStyle`, which carries
102
+ one active palette/theme pair plus the runtime payload sent into SwiftTUI.
103
+ - Hosts that want multiple themes swap entire `WebHostTerminalStyle` objects;
104
+ the library does not provide a built-in mode switcher.
105
+ - `BrowserWASIBridge` sets `TUIGUI_TRANSPORT=surface` and decodes surface
106
+ frames before handing them to the canvas runtime.
107
+
108
+ ## Developing this package
109
+
110
+ > Only needed if you are working **on** `@swifttui/web` itself. Consuming the
111
+ > package from an app needs only `npm install` (above) — not Bun or the Swift
112
+ > toolchain.
113
+
114
+ Use Bun for repo-local development, and the repo-default `swiftly` Swift 6.3.1
115
+ toolchain for any Swift command the build pipeline triggers
116
+ (`swiftly run swift --version`). Run `bun install` from the repo root or any
117
+ workspace package directory; Bun maintains one root `bun.lock`.
97
118
 
98
119
  - `bun test`
99
120
  - `bun run build` — compile the publishable package to `dist/` with tsdown
@@ -119,12 +140,6 @@ The demo/app build flow is intentionally small:
119
140
  artifact only if stripping still produces browser-parseable wasm.
120
141
  3. `build:web` bundles `index.html` and the browser entrypoint with Bun.
121
142
 
122
- ## Notes
143
+ ## License
123
144
 
124
- - Scene switching is controller-managed and retains existing scene runtimes.
125
- - Terminal styling is host-owned through `WebHostTerminalStyle`, which carries
126
- one active palette/theme pair plus the runtime payload sent into SwiftTUI.
127
- - Hosts that want multiple themes swap entire `WebHostTerminalStyle` objects;
128
- the library does not provide a built-in mode switcher.
129
- - `BrowserWASIBridge` sets `TUIGUI_TRANSPORT=surface` and decodes surface
130
- frames before handing them to the canvas runtime.
145
+ MIT see [LICENSE](LICENSE).
@@ -15,40 +15,42 @@ var AccessibilityTreeMounter = class {
15
15
  applyScreenReaderOnlyStyle(this.announcerElement);
16
16
  }
17
17
  present(nodes, metrics, announcements = [], options = {}) {
18
- this.element.replaceChildren();
19
- this.nodesById.clear();
18
+ const previousById = this.nodesById;
19
+ const nextById = /* @__PURE__ */ new Map();
20
20
  for (const node of nodes) {
21
- const element = this.elementForNode(node, metrics);
22
- this.nodesById.set(node.id, element);
21
+ const element = previousById.get(node.id) ?? document.createElement("div");
22
+ this.applyNodeAttributes(element, node, metrics);
23
+ nextById.set(node.id, element);
23
24
  }
25
+ for (const id of previousById.keys()) if (!nextById.has(id)) previousById.get(id)?.remove();
26
+ this.nodesById = nextById;
24
27
  for (const node of nodes) {
25
- const element = this.nodesById.get(node.id);
28
+ const element = nextById.get(node.id);
26
29
  if (!element) continue;
27
- ((node.parentId ? this.nodesById.get(node.parentId) : void 0) ?? this.element).appendChild(element);
30
+ ((node.parentId ? nextById.get(node.parentId) : void 0) ?? this.element).appendChild(element);
28
31
  }
29
32
  this.announceLiveRegionChanges(nodes, announcements);
30
33
  const focused = nodes.find((node) => node.isFocused);
31
34
  if ((options.synchronizeFocus ?? true) && focused) this.nodesById.get(focused.id)?.focus?.({ preventScroll: true });
32
35
  }
33
- elementForNode(node, metrics) {
34
- const element = document.createElement("div");
36
+ applyNodeAttributes(element, node, metrics) {
35
37
  element.id = `swifttui-a11y-${stableDOMId(node.id)}`;
36
38
  element.dataset.accessibilityId = node.id;
37
39
  element.tabIndex = node.isFocused ? 0 : -1;
38
40
  const role = roleMapping(node.role);
39
- if (role.role) element.setAttribute("role", role.role);
40
- if (role.level !== void 0) element.setAttribute("aria-level", String(role.level));
41
- if (node.label) element.setAttribute("aria-label", node.label);
42
- if (node.hint) element.setAttribute("aria-description", node.hint);
43
- if (node.liveRegion) element.setAttribute("aria-live", node.liveRegion);
41
+ setOrRemoveAttribute(element, "role", role.role);
42
+ setOrRemoveAttribute(element, "aria-level", role.level !== void 0 ? String(role.level) : void 0);
43
+ setOrRemoveAttribute(element, "aria-label", node.label || void 0);
44
+ setOrRemoveAttribute(element, "aria-description", node.hint || void 0);
45
+ setOrRemoveAttribute(element, "aria-live", node.liveRegion || void 0);
44
46
  if (node.isFocused) element.dataset.focused = "true";
47
+ else delete element.dataset.focused;
45
48
  const [x, y, width, height] = node.rect;
46
49
  element.style.position = "absolute";
47
50
  element.style.left = `${x * metrics.cellWidth}px`;
48
51
  element.style.top = `${y * metrics.cellHeight}px`;
49
52
  element.style.width = `${Math.max(1, width) * metrics.cellWidth}px`;
50
53
  element.style.height = `${Math.max(1, height) * metrics.cellHeight}px`;
51
- return element;
52
54
  }
53
55
  announceLiveRegionChanges(nodes, announcements) {
54
56
  const candidates = nodes.filter((node) => node.liveRegion && node.liveRegion !== "off" && node.label);
@@ -86,6 +88,13 @@ var AccessibilityTreeMounter = class {
86
88
  }).join("\n");
87
89
  }
88
90
  };
91
+ function setOrRemoveAttribute(element, name, value) {
92
+ if (value === void 0) {
93
+ element.removeAttribute(name);
94
+ return;
95
+ }
96
+ element.setAttribute(name, value);
97
+ }
89
98
  function applyScreenReaderOnlyStyle(element) {
90
99
  element.style.position = "absolute";
91
100
  element.style.left = "0";
@@ -1 +1 @@
1
- {"version":3,"file":"AccessibilityTree.js","names":[],"sources":["../../src/AccessibilityTree.ts"],"sourcesContent":["import type {\n WebHostAccessibilityAnnouncement,\n WebHostAccessibilityNode,\n} from \"./WebHostSurfaceTransport.ts\";\n\ninterface AccessibilityTreeMetrics {\n cellWidth: number;\n cellHeight: number;\n}\n\ninterface AccessibilityTreePresentationOptions {\n synchronizeFocus?: boolean;\n}\n\ninterface RoleMapping {\n role?: string;\n level?: number;\n}\n\nexport class AccessibilityTreeMounter {\n readonly element: HTMLElement;\n readonly announcerElement: HTMLElement;\n\n private readonly nodesById = new Map<string, HTMLElement>();\n private previousLabelsById = new Map<string, string>();\n private hasLiveRegionBaseline = false;\n\n constructor() {\n this.element = document.createElement(\"div\");\n this.element.className = \"webhost-scene__accessibility-tree\";\n applyScreenReaderOnlyStyle(this.element);\n\n this.announcerElement = document.createElement(\"div\");\n this.announcerElement.className = \"webhost-scene__accessibility-announcer\";\n this.announcerElement.setAttribute(\"aria-atomic\", \"true\");\n applyScreenReaderOnlyStyle(this.announcerElement);\n }\n\n present(\n nodes: WebHostAccessibilityNode[],\n metrics: AccessibilityTreeMetrics,\n announcements: WebHostAccessibilityAnnouncement[] = [],\n options: AccessibilityTreePresentationOptions = {}\n ): void {\n this.element.replaceChildren();\n this.nodesById.clear();\n\n for (const node of nodes) {\n const element = this.elementForNode(node, metrics);\n this.nodesById.set(node.id, element);\n }\n\n for (const node of nodes) {\n const element = this.nodesById.get(node.id);\n if (!element) {\n continue;\n }\n\n const parent = node.parentId ? this.nodesById.get(node.parentId) : undefined;\n (parent ?? this.element).appendChild(element);\n }\n\n this.announceLiveRegionChanges(nodes, announcements);\n\n const focused = nodes.find((node) => node.isFocused);\n if ((options.synchronizeFocus ?? true) && focused) {\n this.nodesById.get(focused.id)?.focus?.({ preventScroll: true });\n }\n }\n\n private elementForNode(\n node: WebHostAccessibilityNode,\n metrics: AccessibilityTreeMetrics\n ): HTMLElement {\n const element = document.createElement(\"div\");\n element.id = `swifttui-a11y-${stableDOMId(node.id)}`;\n element.dataset.accessibilityId = node.id;\n element.tabIndex = node.isFocused ? 0 : -1;\n\n const role = roleMapping(node.role);\n if (role.role) {\n element.setAttribute(\"role\", role.role);\n }\n if (role.level !== undefined) {\n element.setAttribute(\"aria-level\", String(role.level));\n }\n if (node.label) {\n element.setAttribute(\"aria-label\", node.label);\n }\n if (node.hint) {\n element.setAttribute(\"aria-description\", node.hint);\n }\n if (node.liveRegion) {\n element.setAttribute(\"aria-live\", node.liveRegion);\n }\n if (node.isFocused) {\n element.dataset.focused = \"true\";\n }\n\n const [x, y, width, height] = node.rect;\n element.style.position = \"absolute\";\n element.style.left = `${x * metrics.cellWidth}px`;\n element.style.top = `${y * metrics.cellHeight}px`;\n element.style.width = `${Math.max(1, width) * metrics.cellWidth}px`;\n element.style.height = `${Math.max(1, height) * metrics.cellHeight}px`;\n\n return element;\n }\n\n private announceLiveRegionChanges(\n nodes: WebHostAccessibilityNode[],\n announcements: WebHostAccessibilityAnnouncement[]\n ): void {\n const candidates = nodes.filter(\n (node) => node.liveRegion && node.liveRegion !== \"off\" && node.label\n );\n const currentLabelsById = new Map(candidates.map((node) => [node.id, node.label ?? \"\"]));\n const imperativeAssertive = announcements.filter(\n (announcement) => announcement.politeness === \"assertive\"\n );\n const imperativePolite = announcements.filter(\n (announcement) => announcement.politeness === \"polite\"\n );\n\n if (!this.hasLiveRegionBaseline) {\n this.previousLabelsById = currentLabelsById;\n this.hasLiveRegionBaseline = true;\n this.publishAnnouncements([], imperativeAssertive, [], imperativePolite);\n return;\n }\n\n const changed = candidates.filter((node) => {\n const previous = this.previousLabelsById.get(node.id);\n return previous !== undefined && previous !== node.label;\n });\n this.previousLabelsById = currentLabelsById;\n\n const assertive = changed.filter((node) => node.liveRegion === \"assertive\");\n const polite = changed.filter((node) => node.liveRegion === \"polite\");\n this.publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite);\n }\n\n private publishAnnouncements(\n assertive: WebHostAccessibilityNode[],\n imperativeAssertive: WebHostAccessibilityAnnouncement[],\n polite: WebHostAccessibilityNode[],\n imperativePolite: WebHostAccessibilityAnnouncement[]\n ): void {\n const ordered = [...assertive, ...imperativeAssertive, ...polite, ...imperativePolite];\n if (ordered.length === 0) {\n return;\n }\n\n const politeness = assertive.length > 0 || imperativeAssertive.length > 0\n ? \"assertive\"\n : \"polite\";\n this.announcerElement.setAttribute(\"aria-live\", politeness);\n this.announcerElement.textContent = ordered.map((entry) => {\n if (\"message\" in entry) {\n return entry.message;\n }\n return entry.label ?? \"\";\n }).join(\"\\n\");\n }\n}\n\nfunction applyScreenReaderOnlyStyle(\n element: HTMLElement\n): void {\n element.style.position = \"absolute\";\n element.style.left = \"0\";\n element.style.top = \"0\";\n element.style.width = \"1px\";\n element.style.height = \"1px\";\n element.style.overflow = \"hidden\";\n element.style.clipPath = \"inset(50%)\";\n element.style.whiteSpace = \"nowrap\";\n}\n\nfunction roleMapping(\n role: string\n): RoleMapping {\n const heading = /^heading\\(level: ([0-9]+)\\)$/.exec(role);\n if (heading) {\n return {\n role: \"heading\",\n level: Math.max(1, Math.min(6, Number(heading[1]))),\n };\n }\n\n const custom = /^custom\\((.+)\\)$/.exec(role);\n if (custom) {\n return { role: custom[1] };\n }\n\n switch (role) {\n case \"alert\":\n case \"button\":\n case \"cell\":\n case \"checkbox\":\n case \"grid\":\n case \"group\":\n case \"link\":\n case \"list\":\n case \"menu\":\n case \"region\":\n case \"separator\":\n case \"slider\":\n case \"status\":\n case \"tab\":\n case \"table\":\n case \"timer\":\n return { role };\n case \"columnHeader\":\n return { role: \"columnheader\" };\n case \"confirmationDialog\":\n case \"sheet\":\n return { role: \"dialog\" };\n case \"disclosureGroup\":\n case \"scrollView\":\n case \"scrollViewWithIndicators\":\n case \"section\":\n return { role: \"region\" };\n case \"image\":\n return { role: \"img\" };\n case \"menuItem\":\n return { role: \"menuitem\" };\n case \"picker\":\n return { role: \"combobox\" };\n case \"progressBar\":\n return { role: \"progressbar\" };\n case \"rowHeader\":\n return { role: \"rowheader\" };\n case \"secureField\":\n case \"textEditor\":\n case \"textField\":\n return { role: \"textbox\" };\n case \"stepper\":\n return { role: \"spinbutton\" };\n case \"tabPanel\":\n return { role: \"tabpanel\" };\n case \"tableRow\":\n return { role: \"row\" };\n case \"tabView\":\n return { role: \"tablist\" };\n case \"toggle\":\n return { role: \"checkbox\" };\n default:\n return { role: \"group\" };\n }\n}\n\nfunction stableDOMId(\n id: string\n): string {\n return Array.from(id).map((character) => {\n if (/^[a-zA-Z0-9_-]$/.test(character)) {\n return character;\n }\n return `-${character.codePointAt(0)?.toString(16) ?? \"0\"}-`;\n }).join(\"\");\n}\n"],"mappings":";AAmBA,IAAa,2BAAb,MAAsC;CACpC;CACA;CAEA,4BAA6B,IAAI,IAAyB;CAC1D,qCAA6B,IAAI,IAAoB;CACrD,wBAAgC;CAEhC,cAAc;EACZ,KAAK,UAAU,SAAS,cAAc,KAAK;EAC3C,KAAK,QAAQ,YAAY;EACzB,2BAA2B,KAAK,OAAO;EAEvC,KAAK,mBAAmB,SAAS,cAAc,KAAK;EACpD,KAAK,iBAAiB,YAAY;EAClC,KAAK,iBAAiB,aAAa,eAAe,MAAM;EACxD,2BAA2B,KAAK,gBAAgB;CAClD;CAEA,QACE,OACA,SACA,gBAAoD,CAAC,GACrD,UAAgD,CAAC,GAC3C;EACN,KAAK,QAAQ,gBAAgB;EAC7B,KAAK,UAAU,MAAM;EAErB,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,UAAU,KAAK,eAAe,MAAM,OAAO;GACjD,KAAK,UAAU,IAAI,KAAK,IAAI,OAAO;EACrC;EAEA,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,UAAU,KAAK,UAAU,IAAI,KAAK,EAAE;GAC1C,IAAI,CAAC,SACH;GAIF,EADe,KAAK,WAAW,KAAK,UAAU,IAAI,KAAK,QAAQ,IAAI,KAAA,MACxD,KAAK,QAAA,CAAS,YAAY,OAAO;EAC9C;EAEA,KAAK,0BAA0B,OAAO,aAAa;EAEnD,MAAM,UAAU,MAAM,MAAM,SAAS,KAAK,SAAS;EACnD,KAAK,QAAQ,oBAAoB,SAAS,SACxC,KAAK,UAAU,IAAI,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,eAAe,KAAK,CAAC;CAEnE;CAEA,eACE,MACA,SACa;EACb,MAAM,UAAU,SAAS,cAAc,KAAK;EAC5C,QAAQ,KAAK,iBAAiB,YAAY,KAAK,EAAE;EACjD,QAAQ,QAAQ,kBAAkB,KAAK;EACvC,QAAQ,WAAW,KAAK,YAAY,IAAI;EAExC,MAAM,OAAO,YAAY,KAAK,IAAI;EAClC,IAAI,KAAK,MACP,QAAQ,aAAa,QAAQ,KAAK,IAAI;EAExC,IAAI,KAAK,UAAU,KAAA,GACjB,QAAQ,aAAa,cAAc,OAAO,KAAK,KAAK,CAAC;EAEvD,IAAI,KAAK,OACP,QAAQ,aAAa,cAAc,KAAK,KAAK;EAE/C,IAAI,KAAK,MACP,QAAQ,aAAa,oBAAoB,KAAK,IAAI;EAEpD,IAAI,KAAK,YACP,QAAQ,aAAa,aAAa,KAAK,UAAU;EAEnD,IAAI,KAAK,WACP,QAAQ,QAAQ,UAAU;EAG5B,MAAM,CAAC,GAAG,GAAG,OAAO,UAAU,KAAK;EACnC,QAAQ,MAAM,WAAW;EACzB,QAAQ,MAAM,OAAO,GAAG,IAAI,QAAQ,UAAU;EAC9C,QAAQ,MAAM,MAAM,GAAG,IAAI,QAAQ,WAAW;EAC9C,QAAQ,MAAM,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,UAAU;EAChE,QAAQ,MAAM,SAAS,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,QAAQ,WAAW;EAEnE,OAAO;CACT;CAEA,0BACE,OACA,eACM;EACN,MAAM,aAAa,MAAM,QACtB,SAAS,KAAK,cAAc,KAAK,eAAe,SAAS,KAAK,KACjE;EACA,MAAM,oBAAoB,IAAI,IAAI,WAAW,KAAK,SAAS,CAAC,KAAK,IAAI,KAAK,SAAS,EAAE,CAAC,CAAC;EACvF,MAAM,sBAAsB,cAAc,QACvC,iBAAiB,aAAa,eAAe,WAChD;EACA,MAAM,mBAAmB,cAAc,QACpC,iBAAiB,aAAa,eAAe,QAChD;EAEA,IAAI,CAAC,KAAK,uBAAuB;GAC/B,KAAK,qBAAqB;GAC1B,KAAK,wBAAwB;GAC7B,KAAK,qBAAqB,CAAC,GAAG,qBAAqB,CAAC,GAAG,gBAAgB;GACvE;EACF;EAEA,MAAM,UAAU,WAAW,QAAQ,SAAS;GAC1C,MAAM,WAAW,KAAK,mBAAmB,IAAI,KAAK,EAAE;GACpD,OAAO,aAAa,KAAA,KAAa,aAAa,KAAK;EACrD,CAAC;EACD,KAAK,qBAAqB;EAE1B,MAAM,YAAY,QAAQ,QAAQ,SAAS,KAAK,eAAe,WAAW;EAC1E,MAAM,SAAS,QAAQ,QAAQ,SAAS,KAAK,eAAe,QAAQ;EACpE,KAAK,qBAAqB,WAAW,qBAAqB,QAAQ,gBAAgB;CACpF;CAEA,qBACE,WACA,qBACA,QACA,kBACM;EACN,MAAM,UAAU;GAAC,GAAG;GAAW,GAAG;GAAqB,GAAG;GAAQ,GAAG;EAAgB;EACrF,IAAI,QAAQ,WAAW,GACrB;EAGF,MAAM,aAAa,UAAU,SAAS,KAAK,oBAAoB,SAAS,IACpE,cACA;EACJ,KAAK,iBAAiB,aAAa,aAAa,UAAU;EAC1D,KAAK,iBAAiB,cAAc,QAAQ,KAAK,UAAU;GACzD,IAAI,aAAa,OACf,OAAO,MAAM;GAEf,OAAO,MAAM,SAAS;EACxB,CAAC,CAAC,CAAC,KAAK,IAAI;CACd;AACF;AAEA,SAAS,2BACP,SACM;CACN,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,OAAO;CACrB,QAAQ,MAAM,MAAM;CACpB,QAAQ,MAAM,QAAQ;CACtB,QAAQ,MAAM,SAAS;CACvB,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,aAAa;AAC7B;AAEA,SAAS,YACP,MACa;CACb,MAAM,UAAU,+BAA+B,KAAK,IAAI;CACxD,IAAI,SACF,OAAO;EACL,MAAM;EACN,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,QAAQ,EAAE,CAAC,CAAC;CACpD;CAGF,MAAM,SAAS,mBAAmB,KAAK,IAAI;CAC3C,IAAI,QACF,OAAO,EAAE,MAAM,OAAO,GAAG;CAG3B,QAAQ,MAAR;EACA,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,SACH,OAAO,EAAE,KAAK;EAChB,KAAK,gBACH,OAAO,EAAE,MAAM,eAAe;EAChC,KAAK;EACL,KAAK,SACH,OAAO,EAAE,MAAM,SAAS;EAC1B,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,WACH,OAAO,EAAE,MAAM,SAAS;EAC1B,KAAK,SACH,OAAO,EAAE,MAAM,MAAM;EACvB,KAAK,YACH,OAAO,EAAE,MAAM,WAAW;EAC5B,KAAK,UACH,OAAO,EAAE,MAAM,WAAW;EAC5B,KAAK,eACH,OAAO,EAAE,MAAM,cAAc;EAC/B,KAAK,aACH,OAAO,EAAE,MAAM,YAAY;EAC7B,KAAK;EACL,KAAK;EACL,KAAK,aACH,OAAO,EAAE,MAAM,UAAU;EAC3B,KAAK,WACH,OAAO,EAAE,MAAM,aAAa;EAC9B,KAAK,YACH,OAAO,EAAE,MAAM,WAAW;EAC5B,KAAK,YACH,OAAO,EAAE,MAAM,MAAM;EACvB,KAAK,WACH,OAAO,EAAE,MAAM,UAAU;EAC3B,KAAK,UACH,OAAO,EAAE,MAAM,WAAW;EAC5B,SACE,OAAO,EAAE,MAAM,QAAQ;CACzB;AACF;AAEA,SAAS,YACP,IACQ;CACR,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,KAAK,cAAc;EACvC,IAAI,kBAAkB,KAAK,SAAS,GAClC,OAAO;EAET,OAAO,IAAI,UAAU,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,IAAI;CAC3D,CAAC,CAAC,CAAC,KAAK,EAAE;AACZ"}
1
+ {"version":3,"file":"AccessibilityTree.js","names":[],"sources":["../../src/AccessibilityTree.ts"],"sourcesContent":["import type {\n WebHostAccessibilityAnnouncement,\n WebHostAccessibilityNode,\n} from \"./WebHostSurfaceTransport.ts\";\n\ninterface AccessibilityTreeMetrics {\n cellWidth: number;\n cellHeight: number;\n}\n\ninterface AccessibilityTreePresentationOptions {\n synchronizeFocus?: boolean;\n}\n\ninterface RoleMapping {\n role?: string;\n level?: number;\n}\n\nexport class AccessibilityTreeMounter {\n readonly element: HTMLElement;\n readonly announcerElement: HTMLElement;\n\n private nodesById = new Map<string, HTMLElement>();\n private previousLabelsById = new Map<string, string>();\n private hasLiveRegionBaseline = false;\n\n constructor() {\n this.element = document.createElement(\"div\");\n this.element.className = \"webhost-scene__accessibility-tree\";\n applyScreenReaderOnlyStyle(this.element);\n\n this.announcerElement = document.createElement(\"div\");\n this.announcerElement.className = \"webhost-scene__accessibility-announcer\";\n this.announcerElement.setAttribute(\"aria-atomic\", \"true\");\n applyScreenReaderOnlyStyle(this.announcerElement);\n }\n\n present(\n nodes: WebHostAccessibilityNode[],\n metrics: AccessibilityTreeMetrics,\n announcements: WebHostAccessibilityAnnouncement[] = [],\n options: AccessibilityTreePresentationOptions = {}\n ): void {\n const previousById = this.nodesById;\n const nextById = new Map<string, HTMLElement>();\n\n for (const node of nodes) {\n const existing = previousById.get(node.id);\n const element = existing ?? document.createElement(\"div\");\n this.applyNodeAttributes(element, node, metrics);\n nextById.set(node.id, element);\n }\n\n for (const id of previousById.keys()) {\n if (!nextById.has(id)) {\n previousById.get(id)?.remove();\n }\n }\n\n this.nodesById = nextById;\n\n for (const node of nodes) {\n const element = nextById.get(node.id);\n if (!element) {\n continue;\n }\n\n const parent = node.parentId ? nextById.get(node.parentId) : undefined;\n (parent ?? this.element).appendChild(element);\n }\n\n this.announceLiveRegionChanges(nodes, announcements);\n\n const focused = nodes.find((node) => node.isFocused);\n if ((options.synchronizeFocus ?? true) && focused) {\n this.nodesById.get(focused.id)?.focus?.({ preventScroll: true });\n }\n }\n\n private applyNodeAttributes(\n element: HTMLElement,\n node: WebHostAccessibilityNode,\n metrics: AccessibilityTreeMetrics\n ): void {\n element.id = `swifttui-a11y-${stableDOMId(node.id)}`;\n element.dataset.accessibilityId = node.id;\n element.tabIndex = node.isFocused ? 0 : -1;\n\n const role = roleMapping(node.role);\n setOrRemoveAttribute(element, \"role\", role.role);\n setOrRemoveAttribute(\n element,\n \"aria-level\",\n role.level !== undefined ? String(role.level) : undefined\n );\n setOrRemoveAttribute(element, \"aria-label\", node.label || undefined);\n setOrRemoveAttribute(element, \"aria-description\", node.hint || undefined);\n setOrRemoveAttribute(element, \"aria-live\", node.liveRegion || undefined);\n if (node.isFocused) {\n element.dataset.focused = \"true\";\n } else {\n delete element.dataset.focused;\n }\n\n const [x, y, width, height] = node.rect;\n element.style.position = \"absolute\";\n element.style.left = `${x * metrics.cellWidth}px`;\n element.style.top = `${y * metrics.cellHeight}px`;\n element.style.width = `${Math.max(1, width) * metrics.cellWidth}px`;\n element.style.height = `${Math.max(1, height) * metrics.cellHeight}px`;\n }\n\n private announceLiveRegionChanges(\n nodes: WebHostAccessibilityNode[],\n announcements: WebHostAccessibilityAnnouncement[]\n ): void {\n const candidates = nodes.filter(\n (node) => node.liveRegion && node.liveRegion !== \"off\" && node.label\n );\n const currentLabelsById = new Map(candidates.map((node) => [node.id, node.label ?? \"\"]));\n const imperativeAssertive = announcements.filter(\n (announcement) => announcement.politeness === \"assertive\"\n );\n const imperativePolite = announcements.filter(\n (announcement) => announcement.politeness === \"polite\"\n );\n\n if (!this.hasLiveRegionBaseline) {\n this.previousLabelsById = currentLabelsById;\n this.hasLiveRegionBaseline = true;\n this.publishAnnouncements([], imperativeAssertive, [], imperativePolite);\n return;\n }\n\n const changed = candidates.filter((node) => {\n const previous = this.previousLabelsById.get(node.id);\n return previous !== undefined && previous !== node.label;\n });\n this.previousLabelsById = currentLabelsById;\n\n const assertive = changed.filter((node) => node.liveRegion === \"assertive\");\n const polite = changed.filter((node) => node.liveRegion === \"polite\");\n this.publishAnnouncements(assertive, imperativeAssertive, polite, imperativePolite);\n }\n\n private publishAnnouncements(\n assertive: WebHostAccessibilityNode[],\n imperativeAssertive: WebHostAccessibilityAnnouncement[],\n polite: WebHostAccessibilityNode[],\n imperativePolite: WebHostAccessibilityAnnouncement[]\n ): void {\n const ordered = [...assertive, ...imperativeAssertive, ...polite, ...imperativePolite];\n if (ordered.length === 0) {\n return;\n }\n\n const politeness = assertive.length > 0 || imperativeAssertive.length > 0\n ? \"assertive\"\n : \"polite\";\n this.announcerElement.setAttribute(\"aria-live\", politeness);\n this.announcerElement.textContent = ordered.map((entry) => {\n if (\"message\" in entry) {\n return entry.message;\n }\n return entry.label ?? \"\";\n }).join(\"\\n\");\n }\n}\n\nfunction setOrRemoveAttribute(\n element: HTMLElement,\n name: string,\n value: string | undefined\n): void {\n if (value === undefined) {\n element.removeAttribute(name);\n return;\n }\n element.setAttribute(name, value);\n}\n\nfunction applyScreenReaderOnlyStyle(\n element: HTMLElement\n): void {\n element.style.position = \"absolute\";\n element.style.left = \"0\";\n element.style.top = \"0\";\n element.style.width = \"1px\";\n element.style.height = \"1px\";\n element.style.overflow = \"hidden\";\n element.style.clipPath = \"inset(50%)\";\n element.style.whiteSpace = \"nowrap\";\n}\n\nfunction roleMapping(\n role: string\n): RoleMapping {\n const heading = /^heading\\(level: ([0-9]+)\\)$/.exec(role);\n if (heading) {\n return {\n role: \"heading\",\n level: Math.max(1, Math.min(6, Number(heading[1]))),\n };\n }\n\n const custom = /^custom\\((.+)\\)$/.exec(role);\n if (custom) {\n return { role: custom[1] };\n }\n\n switch (role) {\n case \"alert\":\n case \"button\":\n case \"cell\":\n case \"checkbox\":\n case \"grid\":\n case \"group\":\n case \"link\":\n case \"list\":\n case \"menu\":\n case \"region\":\n case \"separator\":\n case \"slider\":\n case \"status\":\n case \"tab\":\n case \"table\":\n case \"timer\":\n return { role };\n case \"columnHeader\":\n return { role: \"columnheader\" };\n case \"confirmationDialog\":\n case \"sheet\":\n return { role: \"dialog\" };\n case \"disclosureGroup\":\n case \"scrollView\":\n case \"scrollViewWithIndicators\":\n case \"section\":\n return { role: \"region\" };\n case \"image\":\n return { role: \"img\" };\n case \"menuItem\":\n return { role: \"menuitem\" };\n case \"picker\":\n return { role: \"combobox\" };\n case \"progressBar\":\n return { role: \"progressbar\" };\n case \"rowHeader\":\n return { role: \"rowheader\" };\n case \"secureField\":\n case \"textEditor\":\n case \"textField\":\n return { role: \"textbox\" };\n case \"stepper\":\n return { role: \"spinbutton\" };\n case \"tabPanel\":\n return { role: \"tabpanel\" };\n case \"tableRow\":\n return { role: \"row\" };\n case \"tabView\":\n return { role: \"tablist\" };\n case \"toggle\":\n return { role: \"checkbox\" };\n default:\n return { role: \"group\" };\n }\n}\n\nfunction stableDOMId(\n id: string\n): string {\n return Array.from(id).map((character) => {\n if (/^[a-zA-Z0-9_-]$/.test(character)) {\n return character;\n }\n return `-${character.codePointAt(0)?.toString(16) ?? \"0\"}-`;\n }).join(\"\");\n}\n"],"mappings":";AAmBA,IAAa,2BAAb,MAAsC;CACpC;CACA;CAEA,4BAAoB,IAAI,IAAyB;CACjD,qCAA6B,IAAI,IAAoB;CACrD,wBAAgC;CAEhC,cAAc;EACZ,KAAK,UAAU,SAAS,cAAc,KAAK;EAC3C,KAAK,QAAQ,YAAY;EACzB,2BAA2B,KAAK,OAAO;EAEvC,KAAK,mBAAmB,SAAS,cAAc,KAAK;EACpD,KAAK,iBAAiB,YAAY;EAClC,KAAK,iBAAiB,aAAa,eAAe,MAAM;EACxD,2BAA2B,KAAK,gBAAgB;CAClD;CAEA,QACE,OACA,SACA,gBAAoD,CAAC,GACrD,UAAgD,CAAC,GAC3C;EACN,MAAM,eAAe,KAAK;EAC1B,MAAM,2BAAW,IAAI,IAAyB;EAE9C,KAAK,MAAM,QAAQ,OAAO;GAExB,MAAM,UADW,aAAa,IAAI,KAAK,EAChB,KAAK,SAAS,cAAc,KAAK;GACxD,KAAK,oBAAoB,SAAS,MAAM,OAAO;GAC/C,SAAS,IAAI,KAAK,IAAI,OAAO;EAC/B;EAEA,KAAK,MAAM,MAAM,aAAa,KAAK,GACjC,IAAI,CAAC,SAAS,IAAI,EAAE,GAClB,aAAa,IAAI,EAAE,CAAC,EAAE,OAAO;EAIjC,KAAK,YAAY;EAEjB,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,UAAU,SAAS,IAAI,KAAK,EAAE;GACpC,IAAI,CAAC,SACH;GAIF,EADe,KAAK,WAAW,SAAS,IAAI,KAAK,QAAQ,IAAI,KAAA,MAClD,KAAK,QAAA,CAAS,YAAY,OAAO;EAC9C;EAEA,KAAK,0BAA0B,OAAO,aAAa;EAEnD,MAAM,UAAU,MAAM,MAAM,SAAS,KAAK,SAAS;EACnD,KAAK,QAAQ,oBAAoB,SAAS,SACxC,KAAK,UAAU,IAAI,QAAQ,EAAE,CAAC,EAAE,QAAQ,EAAE,eAAe,KAAK,CAAC;CAEnE;CAEA,oBACE,SACA,MACA,SACM;EACN,QAAQ,KAAK,iBAAiB,YAAY,KAAK,EAAE;EACjD,QAAQ,QAAQ,kBAAkB,KAAK;EACvC,QAAQ,WAAW,KAAK,YAAY,IAAI;EAExC,MAAM,OAAO,YAAY,KAAK,IAAI;EAClC,qBAAqB,SAAS,QAAQ,KAAK,IAAI;EAC/C,qBACE,SACA,cACA,KAAK,UAAU,KAAA,IAAY,OAAO,KAAK,KAAK,IAAI,KAAA,CAClD;EACA,qBAAqB,SAAS,cAAc,KAAK,SAAS,KAAA,CAAS;EACnE,qBAAqB,SAAS,oBAAoB,KAAK,QAAQ,KAAA,CAAS;EACxE,qBAAqB,SAAS,aAAa,KAAK,cAAc,KAAA,CAAS;EACvE,IAAI,KAAK,WACP,QAAQ,QAAQ,UAAU;OAE1B,OAAO,QAAQ,QAAQ;EAGzB,MAAM,CAAC,GAAG,GAAG,OAAO,UAAU,KAAK;EACnC,QAAQ,MAAM,WAAW;EACzB,QAAQ,MAAM,OAAO,GAAG,IAAI,QAAQ,UAAU;EAC9C,QAAQ,MAAM,MAAM,GAAG,IAAI,QAAQ,WAAW;EAC9C,QAAQ,MAAM,QAAQ,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,UAAU;EAChE,QAAQ,MAAM,SAAS,GAAG,KAAK,IAAI,GAAG,MAAM,IAAI,QAAQ,WAAW;CACrE;CAEA,0BACE,OACA,eACM;EACN,MAAM,aAAa,MAAM,QACtB,SAAS,KAAK,cAAc,KAAK,eAAe,SAAS,KAAK,KACjE;EACA,MAAM,oBAAoB,IAAI,IAAI,WAAW,KAAK,SAAS,CAAC,KAAK,IAAI,KAAK,SAAS,EAAE,CAAC,CAAC;EACvF,MAAM,sBAAsB,cAAc,QACvC,iBAAiB,aAAa,eAAe,WAChD;EACA,MAAM,mBAAmB,cAAc,QACpC,iBAAiB,aAAa,eAAe,QAChD;EAEA,IAAI,CAAC,KAAK,uBAAuB;GAC/B,KAAK,qBAAqB;GAC1B,KAAK,wBAAwB;GAC7B,KAAK,qBAAqB,CAAC,GAAG,qBAAqB,CAAC,GAAG,gBAAgB;GACvE;EACF;EAEA,MAAM,UAAU,WAAW,QAAQ,SAAS;GAC1C,MAAM,WAAW,KAAK,mBAAmB,IAAI,KAAK,EAAE;GACpD,OAAO,aAAa,KAAA,KAAa,aAAa,KAAK;EACrD,CAAC;EACD,KAAK,qBAAqB;EAE1B,MAAM,YAAY,QAAQ,QAAQ,SAAS,KAAK,eAAe,WAAW;EAC1E,MAAM,SAAS,QAAQ,QAAQ,SAAS,KAAK,eAAe,QAAQ;EACpE,KAAK,qBAAqB,WAAW,qBAAqB,QAAQ,gBAAgB;CACpF;CAEA,qBACE,WACA,qBACA,QACA,kBACM;EACN,MAAM,UAAU;GAAC,GAAG;GAAW,GAAG;GAAqB,GAAG;GAAQ,GAAG;EAAgB;EACrF,IAAI,QAAQ,WAAW,GACrB;EAGF,MAAM,aAAa,UAAU,SAAS,KAAK,oBAAoB,SAAS,IACpE,cACA;EACJ,KAAK,iBAAiB,aAAa,aAAa,UAAU;EAC1D,KAAK,iBAAiB,cAAc,QAAQ,KAAK,UAAU;GACzD,IAAI,aAAa,OACf,OAAO,MAAM;GAEf,OAAO,MAAM,SAAS;EACxB,CAAC,CAAC,CAAC,KAAK,IAAI;CACd;AACF;AAEA,SAAS,qBACP,SACA,MACA,OACM;CACN,IAAI,UAAU,KAAA,GAAW;EACvB,QAAQ,gBAAgB,IAAI;EAC5B;CACF;CACA,QAAQ,aAAa,MAAM,KAAK;AAClC;AAEA,SAAS,2BACP,SACM;CACN,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,OAAO;CACrB,QAAQ,MAAM,MAAM;CACpB,QAAQ,MAAM,QAAQ;CACtB,QAAQ,MAAM,SAAS;CACvB,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,WAAW;CACzB,QAAQ,MAAM,aAAa;AAC7B;AAEA,SAAS,YACP,MACa;CACb,MAAM,UAAU,+BAA+B,KAAK,IAAI;CACxD,IAAI,SACF,OAAO;EACL,MAAM;EACN,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,QAAQ,EAAE,CAAC,CAAC;CACpD;CAGF,MAAM,SAAS,mBAAmB,KAAK,IAAI;CAC3C,IAAI,QACF,OAAO,EAAE,MAAM,OAAO,GAAG;CAG3B,QAAQ,MAAR;EACA,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,SACH,OAAO,EAAE,KAAK;EAChB,KAAK,gBACH,OAAO,EAAE,MAAM,eAAe;EAChC,KAAK;EACL,KAAK,SACH,OAAO,EAAE,MAAM,SAAS;EAC1B,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,WACH,OAAO,EAAE,MAAM,SAAS;EAC1B,KAAK,SACH,OAAO,EAAE,MAAM,MAAM;EACvB,KAAK,YACH,OAAO,EAAE,MAAM,WAAW;EAC5B,KAAK,UACH,OAAO,EAAE,MAAM,WAAW;EAC5B,KAAK,eACH,OAAO,EAAE,MAAM,cAAc;EAC/B,KAAK,aACH,OAAO,EAAE,MAAM,YAAY;EAC7B,KAAK;EACL,KAAK;EACL,KAAK,aACH,OAAO,EAAE,MAAM,UAAU;EAC3B,KAAK,WACH,OAAO,EAAE,MAAM,aAAa;EAC9B,KAAK,YACH,OAAO,EAAE,MAAM,WAAW;EAC5B,KAAK,YACH,OAAO,EAAE,MAAM,MAAM;EACvB,KAAK,WACH,OAAO,EAAE,MAAM,UAAU;EAC3B,KAAK,UACH,OAAO,EAAE,MAAM,WAAW;EAC5B,SACE,OAAO,EAAE,MAAM,QAAQ;CACzB;AACF;AAEA,SAAS,YACP,IACQ;CACR,OAAO,MAAM,KAAK,EAAE,CAAC,CAAC,KAAK,cAAc;EACvC,IAAI,kBAAkB,KAAK,SAAS,GAClC,OAAO;EAET,OAAO,IAAI,UAAU,YAAY,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,IAAI;CAC3D,CAAC,CAAC,CAAC,KAAK,EAAE;AACZ"}
@@ -0,0 +1,263 @@
1
+ import { webTUITerminalBackgroundColor } from "./WebHostTerminalStyle.js";
2
+ import { canRenderBoxDrawing, drawBoxDrawing } from "./BoxDrawingRenderer.js";
3
+ //#region src/CanvasSurfacePainter.ts
4
+ /**
5
+ * Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell
6
+ * text/box-drawing/decorations, surface images, and damage-scoped dirty-region
7
+ * painting. The painter caches decoded images and asks the host to repaint once
8
+ * an image finishes decoding (via the `requestRedraw` callback).
9
+ *
10
+ * Geometry and style are supplied per paint via {@link CanvasSurfaceMetrics};
11
+ * the painter holds only the canvas handle, the image cache, and the redraw
12
+ * callback as durable state.
13
+ */
14
+ var CanvasSurfacePainter = class {
15
+ imageCache = /* @__PURE__ */ new Map();
16
+ canvas;
17
+ requestRedraw = () => {};
18
+ /**
19
+ * Binds the canvas the painter draws into and the callback used to request a
20
+ * full repaint after an asynchronous image decode completes.
21
+ */
22
+ attach(canvas, requestRedraw) {
23
+ this.canvas = canvas;
24
+ this.requestRedraw = requestRedraw;
25
+ }
26
+ paint(metrics, frame, damage) {
27
+ const canvas = this.canvas;
28
+ const context = canvas?.getContext("2d");
29
+ if (!canvas || !context) return;
30
+ const dirtyRegion = frame ? this.dirtyRegionForDamage(damage, frame, metrics) : void 0;
31
+ if (dirtyRegion?.rects.length === 0) return;
32
+ const scale = globalThis.window?.devicePixelRatio || 1;
33
+ context.setTransform(scale, 0, 0, scale, 0, 0);
34
+ context.textBaseline = "alphabetic";
35
+ context.fillStyle = webTUITerminalBackgroundColor(metrics.style);
36
+ if (dirtyRegion) for (const rect of dirtyRegion.rects) {
37
+ context.clearRect(rect.x, rect.y, rect.width, rect.height);
38
+ context.fillRect(rect.x, rect.y, rect.width, rect.height);
39
+ }
40
+ else {
41
+ context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);
42
+ context.fillRect(0, 0, metrics.columns * metrics.cellWidth, metrics.rows * metrics.cellHeight);
43
+ }
44
+ if (!frame) return;
45
+ this.drawRows(context, frame, metrics, dirtyRegion);
46
+ this.drawImages(context, frame.images ?? [], metrics, dirtyRegion);
47
+ }
48
+ drawRows(context, frame, metrics, dirtyRegion) {
49
+ if (dirtyRegion) {
50
+ for (const [y, ranges] of dirtyRegion.rows) {
51
+ const row = frame.rows[y] ?? [];
52
+ this.drawRow(context, frame, metrics, row, y, ranges);
53
+ }
54
+ return;
55
+ }
56
+ for (let y = 0; y < frame.rows.length; y += 1) {
57
+ const row = frame.rows[y] ?? [];
58
+ this.drawRow(context, frame, metrics, row, y);
59
+ }
60
+ }
61
+ drawRow(context, frame, metrics, row, y, ranges) {
62
+ for (const cell of row) {
63
+ const [x, text, span, styleIndex] = cell;
64
+ if (ranges !== void 0 && !cellIntersectsRanges(x, span, ranges)) continue;
65
+ const style = frame.styles[styleIndex] ?? void 0;
66
+ this.drawCell(context, metrics, x, y, text, span, style);
67
+ }
68
+ }
69
+ drawImages(context, images, metrics, dirtyRegion) {
70
+ for (const image of images) this.drawImage(context, image, metrics, dirtyRegion);
71
+ }
72
+ drawImage(context, image, metrics, dirtyRegion) {
73
+ const decodedImage = this.cachedImage(image);
74
+ if (!decodedImage) return;
75
+ const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;
76
+ const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;
77
+ if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) return;
78
+ if (dirtyRegion && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)) return;
79
+ context.save();
80
+ context.beginPath();
81
+ context.rect(clipX * metrics.cellWidth, clipY * metrics.cellHeight, clipWidth * metrics.cellWidth, clipHeight * metrics.cellHeight);
82
+ context.clip();
83
+ context.drawImage(decodedImage, boundsX * metrics.cellWidth, boundsY * metrics.cellHeight, boundsWidth * metrics.cellWidth, boundsHeight * metrics.cellHeight);
84
+ context.restore();
85
+ }
86
+ cachedImage(image) {
87
+ const cached = this.imageCache.get(image.id);
88
+ if (cached?.image) return cached.image;
89
+ if (!cached?.promise && image.dataBase64) {
90
+ const promise = decodeImage(image.dataBase64, image.format);
91
+ this.imageCache.set(image.id, { promise });
92
+ promise.then((decodedImage) => {
93
+ if (this.imageCache.get(image.id)?.promise !== promise) return;
94
+ this.imageCache.set(image.id, { image: decodedImage });
95
+ this.requestRedraw();
96
+ }).catch(() => {
97
+ this.imageCache.delete(image.id);
98
+ });
99
+ }
100
+ }
101
+ drawCell(context, metrics, x, y, text, span, style) {
102
+ const rectX = x * metrics.cellWidth;
103
+ const rectY = y * metrics.cellHeight;
104
+ const width = Math.max(1, span) * metrics.cellWidth;
105
+ const background = resolvedBackground(style, metrics.style);
106
+ const foreground = resolvedForeground(style, metrics.style);
107
+ const opacity = style?.opacity ?? 1;
108
+ if (background) {
109
+ context.globalAlpha = opacity;
110
+ context.fillStyle = background;
111
+ context.fillRect(rectX, rectY, width, metrics.cellHeight);
112
+ }
113
+ if (text !== " ") {
114
+ context.globalAlpha = opacity;
115
+ context.fillStyle = foreground;
116
+ context.strokeStyle = foreground;
117
+ if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {
118
+ x: rectX,
119
+ y: rectY,
120
+ width,
121
+ height: metrics.cellHeight
122
+ })) {
123
+ context.font = fontForStyle(metrics.style, style);
124
+ context.fillText(text, rectX, rectY + Math.floor((metrics.cellHeight + metrics.style.fontSize) / 2) - 2);
125
+ }
126
+ }
127
+ this.drawTextLine(context, metrics, rectX, rectY, width, style?.underline, "underline", foreground);
128
+ this.drawTextLine(context, metrics, rectX, rectY, width, style?.strikethrough, "strike", foreground);
129
+ context.globalAlpha = 1;
130
+ }
131
+ dirtyRegionForDamage(damage, frame, metrics) {
132
+ if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) return;
133
+ const rects = [];
134
+ const rows = /* @__PURE__ */ new Map();
135
+ for (const [row, ranges] of damage.textRows) {
136
+ if (row < 0 || row >= frame.height) continue;
137
+ if (ranges.length === 0) {
138
+ rects.push(cellRect(metrics, 0, row, frame.width));
139
+ rows.set(row, "full");
140
+ continue;
141
+ }
142
+ const rowRanges = rows.get(row) === "full" ? [] : [...rows.get(row) ?? []];
143
+ for (const [start, end] of ranges) {
144
+ const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));
145
+ const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));
146
+ if (lowerBound >= upperBound) continue;
147
+ rects.push(cellRect(metrics, lowerBound, row, upperBound - lowerBound));
148
+ rowRanges.push({
149
+ start: lowerBound,
150
+ end: upperBound
151
+ });
152
+ }
153
+ if (rows.get(row) !== "full" && rowRanges.length > 0) rows.set(row, normalizeCellRanges(rowRanges));
154
+ }
155
+ return {
156
+ rects,
157
+ rows
158
+ };
159
+ }
160
+ drawTextLine(context, metrics, x, y, width, line, placement, fallbackColor) {
161
+ if (!line) return;
162
+ context.strokeStyle = line.color ?? fallbackColor;
163
+ context.lineWidth = line.pattern === "double" ? 2 : 1;
164
+ if (line.pattern === "dot") context.setLineDash([1, 3]);
165
+ else if (line.pattern === "dash") context.setLineDash([4, 3]);
166
+ else context.setLineDash([]);
167
+ const lineY = placement === "underline" ? y + metrics.cellHeight - 2 : y + Math.floor(metrics.cellHeight / 2);
168
+ context.beginPath();
169
+ context.moveTo(x, lineY);
170
+ context.lineTo(x + width, lineY);
171
+ context.stroke();
172
+ context.setLineDash([]);
173
+ }
174
+ };
175
+ /**
176
+ * The CSS font string for a cell, folding the surface emphasis bits (bold,
177
+ * italic) over the host terminal's font size/family. Exposed so the host can
178
+ * reuse the exact same metric when measuring cell dimensions.
179
+ */
180
+ function fontForStyle(terminalStyle, style) {
181
+ const emphasis = style?.em ?? 0;
182
+ return `${(emphasis & 2) !== 0 ? "italic " : ""}${(emphasis & 1) !== 0 ? "700 " : ""}${terminalStyle.fontSize}px ${terminalStyle.fontFamily}`;
183
+ }
184
+ function cellRect(metrics, x, y, span) {
185
+ return {
186
+ x: x * metrics.cellWidth,
187
+ y: y * metrics.cellHeight,
188
+ width: Math.max(1, span) * metrics.cellWidth,
189
+ height: metrics.cellHeight
190
+ };
191
+ }
192
+ async function decodeImage(dataBase64, format) {
193
+ const bytes = decodeBase64Bytes(dataBase64);
194
+ const blob = new Blob([bytes], { type: `image/${format}` });
195
+ if (typeof createImageBitmap === "function") return createImageBitmap(blob);
196
+ return new Promise((resolve, reject) => {
197
+ const image = new Image();
198
+ const url = URL.createObjectURL(blob);
199
+ image.onload = () => {
200
+ URL.revokeObjectURL(url);
201
+ resolve(image);
202
+ };
203
+ image.onerror = () => {
204
+ URL.revokeObjectURL(url);
205
+ reject(/* @__PURE__ */ new Error(`Failed to decode ${format} image`));
206
+ };
207
+ image.src = url;
208
+ });
209
+ }
210
+ function decodeBase64Bytes(value) {
211
+ if (typeof atob === "function") {
212
+ const binary = atob(value);
213
+ const bytes = new Uint8Array(binary.length);
214
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
215
+ return bytes;
216
+ }
217
+ return new Uint8Array(Buffer.from(value, "base64"));
218
+ }
219
+ function normalizeCellRanges(ranges) {
220
+ const sorted = ranges.filter((range) => range.end > range.start).sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);
221
+ const normalized = [];
222
+ for (const range of sorted) {
223
+ const previous = normalized[normalized.length - 1];
224
+ if (previous && range.start <= previous.end) {
225
+ previous.end = Math.max(previous.end, range.end);
226
+ continue;
227
+ }
228
+ normalized.push({ ...range });
229
+ }
230
+ return normalized;
231
+ }
232
+ function cellIntersectsRanges(x, span, ranges) {
233
+ if (ranges === "full") return true;
234
+ const start = Math.floor(x);
235
+ const end = start + Math.max(1, Math.ceil(span));
236
+ return ranges.some((range) => start < range.end && end > range.start);
237
+ }
238
+ function dirtyRegionIntersectsCellRect(region, x, y, width, height) {
239
+ const startRow = Math.max(0, Math.floor(y));
240
+ const endRow = Math.max(startRow, Math.ceil(y + height));
241
+ const rectRange = {
242
+ start: Math.floor(x),
243
+ end: Math.floor(x) + Math.max(1, Math.ceil(width))
244
+ };
245
+ for (let row = startRow; row < endRow; row += 1) {
246
+ const ranges = region.rows.get(row);
247
+ if (!ranges) continue;
248
+ if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) return true;
249
+ }
250
+ return false;
251
+ }
252
+ function resolvedForeground(style, terminalStyle) {
253
+ if ((style?.em ?? 0) & 16) return style?.bg ?? terminalStyle.theme.background;
254
+ return style?.fg ?? terminalStyle.theme.foreground;
255
+ }
256
+ function resolvedBackground(style, terminalStyle) {
257
+ if ((style?.em ?? 0) & 16) return style?.fg ?? terminalStyle.theme.foreground;
258
+ return style?.bg;
259
+ }
260
+ //#endregion
261
+ export { CanvasSurfacePainter, fontForStyle };
262
+
263
+ //# sourceMappingURL=CanvasSurfacePainter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CanvasSurfacePainter.js","names":[],"sources":["../../src/CanvasSurfacePainter.ts"],"sourcesContent":["import {\n canRenderBoxDrawing,\n drawBoxDrawing,\n} from \"./BoxDrawingRenderer.ts\";\nimport {\n type ResolvedWebHostTerminalStyle,\n webTUITerminalBackgroundColor,\n} from \"./WebHostTerminalStyle.ts\";\nimport type {\n WebHostSurfaceDamage,\n WebHostSurfaceFrame,\n WebHostSurfaceImage,\n WebHostSurfaceImageFormat,\n WebHostSurfaceStyle,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/**\n * A read-only snapshot of the cell grid geometry and active style the painter\n * needs for a single paint pass. The runtime owns this state and mutates it as\n * the surface resizes or restyles; passing a fresh snapshot per `paint` keeps\n * the painter stateless about geometry and avoids stale reads.\n */\nexport interface CanvasSurfaceMetrics {\n columns: number;\n rows: number;\n cellWidth: number;\n cellHeight: number;\n style: ResolvedWebHostTerminalStyle;\n}\n\ninterface CachedWebHostImage {\n image?: CanvasImageSource;\n promise?: Promise<CanvasImageSource>;\n}\n\ninterface DirtyRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ninterface DirtyCellRange {\n start: number;\n end: number;\n}\n\ntype DirtyRowRanges = \"full\" | DirtyCellRange[];\n\ninterface DirtyRegion {\n rects: DirtyRect[];\n rows: Map<number, DirtyRowRanges>;\n}\n\n/**\n * Draws SwiftTUI surface frames onto a 2D canvas: background fills, per-cell\n * text/box-drawing/decorations, surface images, and damage-scoped dirty-region\n * painting. The painter caches decoded images and asks the host to repaint once\n * an image finishes decoding (via the `requestRedraw` callback).\n *\n * Geometry and style are supplied per paint via {@link CanvasSurfaceMetrics};\n * the painter holds only the canvas handle, the image cache, and the redraw\n * callback as durable state.\n */\nexport class CanvasSurfacePainter {\n private readonly imageCache = new Map<string, CachedWebHostImage>();\n private canvas?: HTMLCanvasElement;\n private requestRedraw: () => void = () => {};\n\n /**\n * Binds the canvas the painter draws into and the callback used to request a\n * full repaint after an asynchronous image decode completes.\n */\n attach(\n canvas: HTMLCanvasElement,\n requestRedraw: () => void\n ): void {\n this.canvas = canvas;\n this.requestRedraw = requestRedraw;\n }\n\n paint(\n metrics: CanvasSurfaceMetrics,\n frame: WebHostSurfaceFrame | undefined,\n damage?: WebHostSurfaceDamage\n ): void {\n const canvas = this.canvas;\n const context = canvas?.getContext(\"2d\");\n if (!canvas || !context) {\n return;\n }\n\n const dirtyRegion = frame\n ? this.dirtyRegionForDamage(damage, frame, metrics)\n : undefined;\n if (dirtyRegion?.rects.length === 0) {\n return;\n }\n\n const scale = globalThis.window?.devicePixelRatio || 1;\n context.setTransform(scale, 0, 0, scale, 0, 0);\n context.textBaseline = \"alphabetic\";\n\n context.fillStyle = webTUITerminalBackgroundColor(metrics.style);\n if (dirtyRegion) {\n for (const rect of dirtyRegion.rects) {\n context.clearRect(rect.x, rect.y, rect.width, rect.height);\n context.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n } else {\n context.clearRect(0, 0, canvas.width / scale, canvas.height / scale);\n context.fillRect(0, 0, metrics.columns * metrics.cellWidth, metrics.rows * metrics.cellHeight);\n }\n\n if (!frame) {\n return;\n }\n\n this.drawRows(context, frame, metrics, dirtyRegion);\n this.drawImages(context, frame.images ?? [], metrics, dirtyRegion);\n }\n\n private drawRows(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n if (dirtyRegion) {\n for (const [y, ranges] of dirtyRegion.rows) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y, ranges);\n }\n return;\n }\n\n for (let y = 0; y < frame.rows.length; y += 1) {\n const row = frame.rows[y] ?? [];\n this.drawRow(context, frame, metrics, row, y);\n }\n }\n\n private drawRow(\n context: CanvasRenderingContext2D,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics,\n row: WebHostSurfaceFrame[\"rows\"][number],\n y: number,\n ranges?: DirtyRowRanges\n ): void {\n for (const cell of row) {\n const [x, text, span, styleIndex] = cell;\n if (ranges !== undefined && !cellIntersectsRanges(x, span, ranges)) {\n continue;\n }\n const style = frame.styles[styleIndex] ?? undefined;\n this.drawCell(context, metrics, x, y, text, span, style);\n }\n }\n\n private drawImages(\n context: CanvasRenderingContext2D,\n images: WebHostSurfaceImage[],\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n for (const image of images) {\n this.drawImage(context, image, metrics, dirtyRegion);\n }\n }\n\n private drawImage(\n context: CanvasRenderingContext2D,\n image: WebHostSurfaceImage,\n metrics: CanvasSurfaceMetrics,\n dirtyRegion?: DirtyRegion\n ): void {\n const decodedImage = this.cachedImage(image);\n if (!decodedImage) {\n return;\n }\n\n const [boundsX, boundsY, boundsWidth, boundsHeight] = image.bounds;\n const [clipX, clipY, clipWidth, clipHeight] = image.visibleBounds;\n if (boundsWidth <= 0 || boundsHeight <= 0 || clipWidth <= 0 || clipHeight <= 0) {\n return;\n }\n if (\n dirtyRegion\n && !dirtyRegionIntersectsCellRect(dirtyRegion, clipX, clipY, clipWidth, clipHeight)\n ) {\n return;\n }\n\n context.save();\n context.beginPath();\n context.rect(\n clipX * metrics.cellWidth,\n clipY * metrics.cellHeight,\n clipWidth * metrics.cellWidth,\n clipHeight * metrics.cellHeight\n );\n context.clip();\n context.drawImage(\n decodedImage,\n boundsX * metrics.cellWidth,\n boundsY * metrics.cellHeight,\n boundsWidth * metrics.cellWidth,\n boundsHeight * metrics.cellHeight\n );\n context.restore();\n }\n\n private cachedImage(\n image: WebHostSurfaceImage\n ): CanvasImageSource | undefined {\n const cached = this.imageCache.get(image.id);\n if (cached?.image) {\n return cached.image;\n }\n\n if (!cached?.promise && image.dataBase64) {\n const promise = decodeImage(image.dataBase64, image.format);\n this.imageCache.set(image.id, { promise });\n void promise.then((decodedImage) => {\n const latest = this.imageCache.get(image.id);\n if (latest?.promise !== promise) {\n return;\n }\n this.imageCache.set(image.id, { image: decodedImage });\n this.requestRedraw();\n }).catch(() => {\n this.imageCache.delete(image.id);\n });\n }\n\n return undefined;\n }\n\n private drawCell(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n text: string,\n span: number,\n style?: WebHostSurfaceStyle | null\n ): void {\n const rectX = x * metrics.cellWidth;\n const rectY = y * metrics.cellHeight;\n const width = Math.max(1, span) * metrics.cellWidth;\n const background = resolvedBackground(style, metrics.style);\n const foreground = resolvedForeground(style, metrics.style);\n const opacity = style?.opacity ?? 1;\n\n if (background) {\n context.globalAlpha = opacity;\n context.fillStyle = background;\n context.fillRect(rectX, rectY, width, metrics.cellHeight);\n }\n\n if (text !== \" \") {\n context.globalAlpha = opacity;\n context.fillStyle = foreground;\n context.strokeStyle = foreground;\n if (!canRenderBoxDrawing(text) || !drawBoxDrawing(context, text, {\n x: rectX,\n y: rectY,\n width,\n height: metrics.cellHeight,\n })) {\n context.font = fontForStyle(metrics.style, style);\n context.fillText(\n text,\n rectX,\n rectY + Math.floor((metrics.cellHeight + metrics.style.fontSize) / 2) - 2\n );\n }\n }\n\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.underline, \"underline\", foreground);\n this.drawTextLine(context, metrics, rectX, rectY, width, style?.strikethrough, \"strike\", foreground);\n context.globalAlpha = 1;\n }\n\n private dirtyRegionForDamage(\n damage: WebHostSurfaceDamage | undefined,\n frame: WebHostSurfaceFrame,\n metrics: CanvasSurfaceMetrics\n ): DirtyRegion | undefined {\n if (!damage || damage.requiresFullTextRepaint || damage.requiresFullGraphicsReplay) {\n return undefined;\n }\n\n const rects: DirtyRect[] = [];\n const rows = new Map<number, DirtyRowRanges>();\n for (const [row, ranges] of damage.textRows) {\n if (row < 0 || row >= frame.height) {\n continue;\n }\n if (ranges.length === 0) {\n rects.push(cellRect(metrics, 0, row, frame.width));\n rows.set(row, \"full\");\n continue;\n }\n const rowRanges: DirtyCellRange[] = rows.get(row) === \"full\"\n ? []\n : [...(rows.get(row) as DirtyCellRange[] | undefined ?? [])];\n for (const [start, end] of ranges) {\n const lowerBound = Math.max(0, Math.min(frame.width, Math.floor(start)));\n const upperBound = Math.max(lowerBound, Math.min(frame.width, Math.ceil(end)));\n if (lowerBound >= upperBound) {\n continue;\n }\n rects.push(cellRect(metrics, lowerBound, row, upperBound - lowerBound));\n rowRanges.push({ start: lowerBound, end: upperBound });\n }\n if (rows.get(row) !== \"full\" && rowRanges.length > 0) {\n rows.set(row, normalizeCellRanges(rowRanges));\n }\n }\n return { rects, rows };\n }\n\n private drawTextLine(\n context: CanvasRenderingContext2D,\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n width: number,\n line: WebHostSurfaceStyle[\"underline\"],\n placement: \"underline\" | \"strike\",\n fallbackColor: string\n ): void {\n if (!line) {\n return;\n }\n context.strokeStyle = line.color ?? fallbackColor;\n context.lineWidth = line.pattern === \"double\" ? 2 : 1;\n if (line.pattern === \"dot\") {\n context.setLineDash([1, 3]);\n } else if (line.pattern === \"dash\") {\n context.setLineDash([4, 3]);\n } else {\n context.setLineDash([]);\n }\n\n const lineY = placement === \"underline\"\n ? y + metrics.cellHeight - 2\n : y + Math.floor(metrics.cellHeight / 2);\n context.beginPath();\n context.moveTo(x, lineY);\n context.lineTo(x + width, lineY);\n context.stroke();\n context.setLineDash([]);\n }\n}\n\n/**\n * The CSS font string for a cell, folding the surface emphasis bits (bold,\n * italic) over the host terminal's font size/family. Exposed so the host can\n * reuse the exact same metric when measuring cell dimensions.\n */\nexport function fontForStyle(\n terminalStyle: ResolvedWebHostTerminalStyle,\n style?: WebHostSurfaceStyle | null\n): string {\n const emphasis = style?.em ?? 0;\n const italic = (emphasis & 2) !== 0 ? \"italic \" : \"\";\n const weight = (emphasis & 1) !== 0 ? \"700 \" : \"\";\n return `${italic}${weight}${terminalStyle.fontSize}px ${terminalStyle.fontFamily}`;\n}\n\nfunction cellRect(\n metrics: CanvasSurfaceMetrics,\n x: number,\n y: number,\n span: number\n): DirtyRect {\n return {\n x: x * metrics.cellWidth,\n y: y * metrics.cellHeight,\n width: Math.max(1, span) * metrics.cellWidth,\n height: metrics.cellHeight,\n };\n}\n\nasync function decodeImage(\n dataBase64: string,\n format: WebHostSurfaceImageFormat\n): Promise<CanvasImageSource> {\n const bytes = decodeBase64Bytes(dataBase64);\n const blob = new Blob([bytes], { type: `image/${format}` });\n\n if (typeof createImageBitmap === \"function\") {\n // Animated GIFs collapse to their first frame in createImageBitmap\n // — that matches the Kitty path's first-frame composite. Phase 7\n // will replace this with a frame ticker.\n return createImageBitmap(blob);\n }\n\n return new Promise((resolve, reject) => {\n const image = new Image();\n const url = URL.createObjectURL(blob);\n image.onload = () => {\n URL.revokeObjectURL(url);\n resolve(image);\n };\n image.onerror = () => {\n URL.revokeObjectURL(url);\n reject(new Error(`Failed to decode ${format} image`));\n };\n image.src = url;\n });\n}\n\nfunction decodeBase64Bytes(\n value: string\n): Uint8Array {\n if (typeof atob === \"function\") {\n const binary = atob(value);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n }\n\n return new Uint8Array(Buffer.from(value, \"base64\"));\n}\n\nfunction normalizeCellRanges(\n ranges: DirtyCellRange[]\n): DirtyCellRange[] {\n const sorted = ranges\n .filter((range) => range.end > range.start)\n .sort((lhs, rhs) => lhs.start - rhs.start || lhs.end - rhs.end);\n const normalized: DirtyCellRange[] = [];\n for (const range of sorted) {\n const previous = normalized[normalized.length - 1];\n if (previous && range.start <= previous.end) {\n previous.end = Math.max(previous.end, range.end);\n continue;\n }\n normalized.push({ ...range });\n }\n return normalized;\n}\n\nfunction cellIntersectsRanges(\n x: number,\n span: number,\n ranges: DirtyRowRanges\n): boolean {\n if (ranges === \"full\") {\n return true;\n }\n const start = Math.floor(x);\n const end = start + Math.max(1, Math.ceil(span));\n return ranges.some((range) => start < range.end && end > range.start);\n}\n\nfunction dirtyRegionIntersectsCellRect(\n region: DirtyRegion,\n x: number,\n y: number,\n width: number,\n height: number\n): boolean {\n const startRow = Math.max(0, Math.floor(y));\n const endRow = Math.max(startRow, Math.ceil(y + height));\n const rectRange = {\n start: Math.floor(x),\n end: Math.floor(x) + Math.max(1, Math.ceil(width)),\n };\n for (let row = startRow; row < endRow; row += 1) {\n const ranges = region.rows.get(row);\n if (!ranges) {\n continue;\n }\n if (cellIntersectsRanges(rectRange.start, rectRange.end - rectRange.start, ranges)) {\n return true;\n }\n }\n return false;\n}\n\nfunction resolvedForeground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string {\n if ((style?.em ?? 0) & 16) {\n return style?.bg ?? terminalStyle.theme.background;\n }\n return style?.fg ?? terminalStyle.theme.foreground;\n}\n\nfunction resolvedBackground(\n style: WebHostSurfaceStyle | null | undefined,\n terminalStyle: ResolvedWebHostTerminalStyle\n): string | undefined {\n if ((style?.em ?? 0) & 16) {\n return style?.fg ?? terminalStyle.theme.foreground;\n }\n return style?.bg;\n}\n"],"mappings":"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"}