@swifttui/web 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/AGENTS.md +52 -0
  2. package/README.md +116 -0
  3. package/cli.ts +168 -0
  4. package/index.html +50 -0
  5. package/index.ts +8 -0
  6. package/manifest.ts +1 -0
  7. package/package.json +33 -0
  8. package/src/AccessibilityTree.ts +262 -0
  9. package/src/BoxDrawingRenderer.ts +585 -0
  10. package/src/PublicEntrypointBoundary.test.ts +20 -0
  11. package/src/WebHostApp.test.ts +222 -0
  12. package/src/WebHostApp.ts +269 -0
  13. package/src/WebHostSceneManifest.test.ts +38 -0
  14. package/src/WebHostSceneManifest.ts +156 -0
  15. package/src/WebHostSceneRuntime.test.ts +1752 -0
  16. package/src/WebHostSceneRuntime.ts +955 -0
  17. package/src/WebHostSurfaceTransport.test.ts +362 -0
  18. package/src/WebHostSurfaceTransport.ts +648 -0
  19. package/src/WebHostTerminalStyle.test.ts +123 -0
  20. package/src/WebHostTerminalStyle.ts +471 -0
  21. package/src/WebHostTestFixtures.ts +10 -0
  22. package/src/WebSocketSceneBridge.test.ts +198 -0
  23. package/src/WebSocketSceneBridge.ts +233 -0
  24. package/src/browser.ts +59 -0
  25. package/src/wasi/BrowserWASIBridge.test.ts +168 -0
  26. package/src/wasi/BrowserWASIBridge.ts +167 -0
  27. package/src/wasi/SharedInputQueue.test.ts +146 -0
  28. package/src/wasi/SharedInputQueue.ts +199 -0
  29. package/src/wasi/StdIOPipe.ts +72 -0
  30. package/src/wasi/WasiPollScheduler.test.ts +176 -0
  31. package/src/wasi/WasiPollScheduler.ts +305 -0
  32. package/src/wasi/WasmSceneRuntime.ts +205 -0
  33. package/src/wasi/WasmSceneWorker.ts +182 -0
  34. package/style.css +15 -0
  35. package/testing.ts +1 -0
  36. package/tsconfig.json +29 -0
  37. package/wasi-worker.ts +1 -0
  38. package/wasi.ts +4 -0
  39. package/websocket.ts +1 -0
@@ -0,0 +1,648 @@
1
+ import {
2
+ encodeWebHostTerminalRenderStyleBase64,
3
+ type WebHostTerminalStyle,
4
+ } from "./WebHostTerminalStyle.ts";
5
+
6
+ export interface WebHostSurfaceStyle {
7
+ fg?: string;
8
+ bg?: string;
9
+ em?: number;
10
+ underline?: WebHostSurfaceLineStyle;
11
+ strikethrough?: WebHostSurfaceLineStyle;
12
+ opacity?: number;
13
+ }
14
+
15
+ export interface WebHostSurfaceLineStyle {
16
+ pattern: "solid" | "dot" | "dash" | "dashDot" | "dashDotDot" | "double" | "curly";
17
+ color?: string;
18
+ }
19
+
20
+ export type WebHostSurfaceCell = [
21
+ x: number,
22
+ text: string,
23
+ span: number,
24
+ styleIndex: number,
25
+ ];
26
+
27
+ export type WebHostSurfaceRect = [
28
+ x: number,
29
+ y: number,
30
+ width: number,
31
+ height: number,
32
+ ];
33
+
34
+ export type WebHostSurfaceSize = [
35
+ width: number,
36
+ height: number,
37
+ ];
38
+
39
+ export type WebHostAccessibilityPoint = [
40
+ x: number,
41
+ y: number,
42
+ ];
43
+
44
+ export type WebHostAccessibilityLiveRegion = "off" | "polite" | "assertive";
45
+
46
+ export interface WebHostAccessibilityNode {
47
+ id: string;
48
+ parentId?: string;
49
+ rect: WebHostSurfaceRect;
50
+ role: string;
51
+ label?: string;
52
+ hint?: string;
53
+ liveRegion?: WebHostAccessibilityLiveRegion;
54
+ cursorAnchor?: WebHostAccessibilityPoint;
55
+ isFocused?: boolean;
56
+ }
57
+
58
+ export interface WebHostAccessibilityAnnouncement {
59
+ message: string;
60
+ politeness: WebHostAccessibilityLiveRegion;
61
+ }
62
+
63
+ export type WebHostSurfaceImageFormat = "png" | "jpeg" | "gif";
64
+
65
+ export interface WebHostSurfaceImage {
66
+ id: string;
67
+ format: WebHostSurfaceImageFormat;
68
+ bounds: WebHostSurfaceRect;
69
+ visibleBounds: WebHostSurfaceRect;
70
+ scalingMode: "stretch" | "fit" | "fill";
71
+ pixelSize?: WebHostSurfaceSize;
72
+ dataBase64?: string;
73
+ }
74
+
75
+ export type WebHostSurfaceDamageRange = [
76
+ start: number,
77
+ end: number,
78
+ ];
79
+
80
+ export type WebHostSurfaceDamageTextRow = [
81
+ row: number,
82
+ ranges: WebHostSurfaceDamageRange[],
83
+ ];
84
+
85
+ export interface WebHostSurfaceDamage {
86
+ textRows: WebHostSurfaceDamageTextRow[];
87
+ requiresFullTextRepaint: boolean;
88
+ requiresFullGraphicsReplay: boolean;
89
+ }
90
+
91
+ export interface WebHostSurfaceFrame {
92
+ version: 1 | 2;
93
+ sequence?: number;
94
+ width: number;
95
+ height: number;
96
+ styles: Array<WebHostSurfaceStyle | null>;
97
+ rows: WebHostSurfaceCell[][];
98
+ images?: WebHostSurfaceImage[];
99
+ damage?: WebHostSurfaceDamage;
100
+ accessibilityTree?: WebHostAccessibilityNode[];
101
+ accessibilityAnnouncements?: WebHostAccessibilityAnnouncement[];
102
+ }
103
+
104
+ export type WebHostSurfaceDeltaRow = [
105
+ row: number,
106
+ cells: WebHostSurfaceCell[],
107
+ ];
108
+
109
+ export interface WebHostSurfaceDeltaFrame {
110
+ version: 3;
111
+ encoding: "delta";
112
+ sequence?: number;
113
+ width: number;
114
+ height: number;
115
+ styles: Array<WebHostSurfaceStyle | null>;
116
+ deltaRows: WebHostSurfaceDeltaRow[];
117
+ images?: WebHostSurfaceImage[];
118
+ damage?: WebHostSurfaceDamage;
119
+ accessibilityTree?: WebHostAccessibilityNode[];
120
+ accessibilityAnnouncements?: WebHostAccessibilityAnnouncement[];
121
+ }
122
+
123
+ export interface WebHostRuntimeIssue {
124
+ severity: "warning" | "error";
125
+ code: string;
126
+ message: string;
127
+ description: string;
128
+ identity?: string;
129
+ source?: string;
130
+ }
131
+
132
+ export interface WebHostFrameDiagnosticRecord {
133
+ format: "swift-tui-frame-diagnostics-v1";
134
+ header: string[];
135
+ fields: string[];
136
+ }
137
+
138
+ export type WebHostOutputRecord =
139
+ | { type: "surface"; frame: WebHostSurfaceFrame }
140
+ | { type: "clipboard"; text: string }
141
+ | { type: "runtimeIssue"; issue: WebHostRuntimeIssue }
142
+ | { type: "frameDiagnostic"; diagnostic: WebHostFrameDiagnosticRecord }
143
+ | { type: "text"; text: string };
144
+
145
+ export interface WebHostOutputSink {
146
+ presentSurface(frame: WebHostSurfaceFrame): void;
147
+ writeClipboard?(text: string): void | Promise<void>;
148
+ notifyRuntimeIssue?(issue: WebHostRuntimeIssue): void;
149
+ recordFrameDiagnostic?(diagnostic: WebHostFrameDiagnosticRecord): void;
150
+ writeOutput?(text: string): void;
151
+ writeError?(text: string): void;
152
+ }
153
+
154
+ export interface WebHostKeyInput {
155
+ key:
156
+ | "return"
157
+ | "space"
158
+ | "tab"
159
+ | "arrowLeft"
160
+ | "arrowRight"
161
+ | "arrowUp"
162
+ | "arrowDown"
163
+ | "backspace"
164
+ | "escape"
165
+ | "home"
166
+ | "end"
167
+ | "character";
168
+ character?: string;
169
+ modifiers?: number;
170
+ }
171
+
172
+ export interface WebHostMouseInput {
173
+ kind: "down" | "up" | "moved" | "dragged" | "scrolled";
174
+ x: number;
175
+ y: number;
176
+ button?: "primary" | "middle" | "secondary";
177
+ deltaX?: number;
178
+ deltaY?: number;
179
+ modifiers?: number;
180
+ }
181
+
182
+ const recordPrefix = "\u001E";
183
+ const textEncoder = new TextEncoder();
184
+
185
+ export class WebHostOutputDecoder {
186
+ private readonly textDecoder = new TextDecoder();
187
+ private bufferedText = "";
188
+ private lastSurfaceFrame?: WebHostSurfaceFrame;
189
+
190
+ feed(
191
+ chunk: Uint8Array
192
+ ): WebHostOutputRecord[] {
193
+ this.bufferedText += this.textDecoder.decode(chunk, { stream: true });
194
+ const records: WebHostOutputRecord[] = [];
195
+
196
+ while (true) {
197
+ const newlineIndex = this.bufferedText.indexOf("\n");
198
+ if (newlineIndex < 0) {
199
+ break;
200
+ }
201
+
202
+ const line = this.bufferedText.slice(0, newlineIndex);
203
+ this.bufferedText = this.bufferedText.slice(newlineIndex + 1);
204
+ records.push(this.decodeLine(line));
205
+ }
206
+
207
+ if (this.bufferedText.length > 4096 && !this.bufferedText.startsWith(recordPrefix)) {
208
+ records.push({ type: "text", text: this.bufferedText });
209
+ this.bufferedText = "";
210
+ }
211
+
212
+ return records;
213
+ }
214
+
215
+ flush(): WebHostOutputRecord[] {
216
+ if (!this.bufferedText) {
217
+ return [];
218
+ }
219
+ const text = this.bufferedText;
220
+ this.bufferedText = "";
221
+ return [this.decodeLine(text)];
222
+ }
223
+
224
+ private decodeLine(
225
+ line: string
226
+ ): WebHostOutputRecord {
227
+ if (line.startsWith(`${recordPrefix}clipboard:`)) {
228
+ try {
229
+ const record = JSON.parse(line.slice(`${recordPrefix}clipboard:`.length));
230
+ if (isWebHostClipboardRecord(record)) {
231
+ return { type: "clipboard", text: record.text };
232
+ }
233
+ } catch {
234
+ // Fall through to the text path below so malformed output remains visible.
235
+ }
236
+
237
+ return { type: "text", text: `${line}\n` };
238
+ }
239
+
240
+ if (line.startsWith(`${recordPrefix}runtimeIssue:`)) {
241
+ try {
242
+ const record = JSON.parse(line.slice(`${recordPrefix}runtimeIssue:`.length));
243
+ if (isWebHostRuntimeIssue(record)) {
244
+ return { type: "runtimeIssue", issue: record };
245
+ }
246
+ } catch {
247
+ // Fall through to the text path below so malformed output remains visible.
248
+ }
249
+
250
+ return { type: "text", text: `${line}\n` };
251
+ }
252
+
253
+ if (line.startsWith(`${recordPrefix}frameDiagnostic:`)) {
254
+ try {
255
+ const record = JSON.parse(line.slice(`${recordPrefix}frameDiagnostic:`.length));
256
+ if (isWebHostFrameDiagnosticRecord(record)) {
257
+ return { type: "frameDiagnostic", diagnostic: record };
258
+ }
259
+ } catch {
260
+ // Fall through to the text path below so malformed output remains visible.
261
+ }
262
+
263
+ return { type: "text", text: `${line}\n` };
264
+ }
265
+
266
+ if (!line.startsWith(`${recordPrefix}surface:`)) {
267
+ return { type: "text", text: `${line}\n` };
268
+ }
269
+
270
+ try {
271
+ const frame = JSON.parse(line.slice(`${recordPrefix}surface:`.length));
272
+ if (isWebHostSurfaceFrame(frame)) {
273
+ this.lastSurfaceFrame = frame;
274
+ return { type: "surface", frame };
275
+ }
276
+ if (isWebHostSurfaceDeltaFrame(frame)) {
277
+ const materialized = this.materializeDeltaFrame(frame);
278
+ if (materialized) {
279
+ this.lastSurfaceFrame = materialized;
280
+ return { type: "surface", frame: materialized };
281
+ }
282
+ }
283
+ } catch {
284
+ // Fall through to the text path below so malformed output remains visible.
285
+ }
286
+
287
+ return { type: "text", text: `${line}\n` };
288
+ }
289
+
290
+ private materializeDeltaFrame(
291
+ frame: WebHostSurfaceDeltaFrame
292
+ ): WebHostSurfaceFrame | undefined {
293
+ const baseline = this.lastSurfaceFrame;
294
+ if (!baseline || baseline.width !== frame.width || baseline.height !== frame.height) {
295
+ return undefined;
296
+ }
297
+
298
+ const rows = baseline.rows.slice();
299
+ for (const [row, cells] of frame.deltaRows) {
300
+ if (!Number.isSafeInteger(row) || row < 0 || row >= frame.height) {
301
+ return undefined;
302
+ }
303
+ rows[row] = cells;
304
+ }
305
+
306
+ return {
307
+ version: baseline.version,
308
+ sequence: frame.sequence,
309
+ width: frame.width,
310
+ height: frame.height,
311
+ styles: frame.styles,
312
+ rows,
313
+ images: frame.images,
314
+ damage: frame.damage,
315
+ accessibilityTree: frame.accessibilityTree,
316
+ accessibilityAnnouncements: frame.accessibilityAnnouncements,
317
+ };
318
+ }
319
+ }
320
+
321
+ function isWebHostClipboardRecord(
322
+ value: unknown
323
+ ): value is { text: string } {
324
+ return !!value && typeof value === "object" && typeof (value as { text?: unknown }).text === "string";
325
+ }
326
+
327
+ function isWebHostRuntimeIssue(
328
+ value: unknown
329
+ ): value is WebHostRuntimeIssue {
330
+ if (!value || typeof value !== "object") {
331
+ return false;
332
+ }
333
+ const record = value as Partial<WebHostRuntimeIssue>;
334
+ return (record.severity === "warning" || record.severity === "error")
335
+ && typeof record.code === "string"
336
+ && typeof record.message === "string"
337
+ && typeof record.description === "string"
338
+ && (record.identity === undefined || typeof record.identity === "string")
339
+ && (record.source === undefined || typeof record.source === "string");
340
+ }
341
+
342
+ function isWebHostFrameDiagnosticRecord(
343
+ value: unknown
344
+ ): value is WebHostFrameDiagnosticRecord {
345
+ if (!value || typeof value !== "object") {
346
+ return false;
347
+ }
348
+ const record = value as Partial<WebHostFrameDiagnosticRecord>;
349
+ return record.format === "swift-tui-frame-diagnostics-v1"
350
+ && Array.isArray(record.header)
351
+ && record.header.every((field) => typeof field === "string")
352
+ && Array.isArray(record.fields)
353
+ && record.fields.every((field) => typeof field === "string");
354
+ }
355
+
356
+ export function encodeResizeControlMessage(
357
+ columns: number,
358
+ rows: number,
359
+ cellWidth?: number,
360
+ cellHeight?: number
361
+ ): Uint8Array {
362
+ const normalizedColumns = Math.max(1, Math.round(columns));
363
+ const normalizedRows = Math.max(1, Math.round(rows));
364
+ if (cellWidth && cellHeight) {
365
+ return textEncoder.encode(
366
+ `${recordPrefix}resize:${normalizedColumns}:${normalizedRows}:${Math.max(1, Math.round(cellWidth))}:${Math.max(1, Math.round(cellHeight))}\n`
367
+ );
368
+ }
369
+
370
+ return textEncoder.encode(`${recordPrefix}resize:${normalizedColumns}:${normalizedRows}\n`);
371
+ }
372
+
373
+ export function encodeRenderStyleControlMessage(
374
+ style: WebHostTerminalStyle
375
+ ): Uint8Array {
376
+ const encoded = encodeWebHostTerminalRenderStyleBase64(style);
377
+ return textEncoder.encode(`${recordPrefix}style:${encoded}\n`);
378
+ }
379
+
380
+ export function encodeKeyInputMessage(
381
+ input: WebHostKeyInput
382
+ ): Uint8Array {
383
+ const modifiers = Math.max(0, Math.round(input.modifiers ?? 0));
384
+ if (input.key === "character") {
385
+ return textEncoder.encode(
386
+ `${recordPrefix}key:character:${encodeURIComponent(input.character ?? "")}:${modifiers}\n`
387
+ );
388
+ }
389
+ return textEncoder.encode(`${recordPrefix}key:${input.key}:${modifiers}\n`);
390
+ }
391
+
392
+ export function encodePasteInputMessage(
393
+ text: string
394
+ ): Uint8Array {
395
+ return textEncoder.encode(`${recordPrefix}paste:${encodeURIComponent(text)}\n`);
396
+ }
397
+
398
+ export function encodeMouseInputMessage(
399
+ input: WebHostMouseInput
400
+ ): Uint8Array {
401
+ return textEncoder.encode(
402
+ recordPrefix + [
403
+ "mouse",
404
+ input.kind,
405
+ formatCellCoordinate(input.x),
406
+ formatCellCoordinate(input.y),
407
+ input.button ?? "none",
408
+ Math.round(input.deltaX ?? 0),
409
+ Math.round(input.deltaY ?? 0),
410
+ Math.max(0, Math.round(input.modifiers ?? 0)),
411
+ ].join(":") + "\n"
412
+ );
413
+ }
414
+
415
+ function formatCellCoordinate(
416
+ value: number
417
+ ): string {
418
+ return Number.isFinite(value) ? String(value) : "0";
419
+ }
420
+
421
+ function isWebHostSurfaceFrame(
422
+ value: unknown
423
+ ): value is WebHostSurfaceFrame {
424
+ if (!value || typeof value !== "object") {
425
+ return false;
426
+ }
427
+ const frame = value as Partial<WebHostSurfaceFrame>;
428
+ return (frame.version === 1 || frame.version === 2)
429
+ && (
430
+ frame.sequence === undefined
431
+ || (Number.isSafeInteger(frame.sequence) && frame.sequence >= 0)
432
+ )
433
+ && typeof frame.width === "number"
434
+ && typeof frame.height === "number"
435
+ && Array.isArray(frame.styles)
436
+ && Array.isArray(frame.rows)
437
+ && frame.rows.every(isWebHostSurfaceRow)
438
+ && (frame.images === undefined || isWebHostSurfaceImages(frame.images))
439
+ && (frame.damage === undefined || isWebHostSurfaceDamage(frame.damage))
440
+ && (
441
+ frame.accessibilityTree === undefined
442
+ || isWebHostAccessibilityNodes(frame.accessibilityTree)
443
+ )
444
+ && (
445
+ frame.accessibilityAnnouncements === undefined
446
+ || isWebHostAccessibilityAnnouncements(frame.accessibilityAnnouncements)
447
+ );
448
+ }
449
+
450
+ function isWebHostSurfaceDeltaFrame(
451
+ value: unknown
452
+ ): value is WebHostSurfaceDeltaFrame {
453
+ if (!value || typeof value !== "object") {
454
+ return false;
455
+ }
456
+ const frame = value as Partial<WebHostSurfaceDeltaFrame>;
457
+ return frame.version === 3
458
+ && frame.encoding === "delta"
459
+ && (
460
+ frame.sequence === undefined
461
+ || (Number.isSafeInteger(frame.sequence) && frame.sequence >= 0)
462
+ )
463
+ && typeof frame.width === "number"
464
+ && typeof frame.height === "number"
465
+ && Array.isArray(frame.styles)
466
+ && Array.isArray(frame.deltaRows)
467
+ && frame.deltaRows.every(isWebHostSurfaceDeltaRow)
468
+ && (frame.images === undefined || isWebHostSurfaceImages(frame.images))
469
+ && (frame.damage === undefined || isWebHostSurfaceDamage(frame.damage))
470
+ && (
471
+ frame.accessibilityTree === undefined
472
+ || isWebHostAccessibilityNodes(frame.accessibilityTree)
473
+ )
474
+ && (
475
+ frame.accessibilityAnnouncements === undefined
476
+ || isWebHostAccessibilityAnnouncements(frame.accessibilityAnnouncements)
477
+ );
478
+ }
479
+
480
+ function isWebHostSurfaceDeltaRow(
481
+ value: unknown
482
+ ): value is WebHostSurfaceDeltaRow {
483
+ return Array.isArray(value)
484
+ && value.length === 2
485
+ && Number.isSafeInteger(value[0])
486
+ && value[0] >= 0
487
+ && isWebHostSurfaceRow(value[1]);
488
+ }
489
+
490
+ function isWebHostSurfaceRow(
491
+ value: unknown
492
+ ): value is WebHostSurfaceCell[] {
493
+ return Array.isArray(value) && value.every(isWebHostSurfaceCell);
494
+ }
495
+
496
+ function isWebHostSurfaceCell(
497
+ value: unknown
498
+ ): value is WebHostSurfaceCell {
499
+ return Array.isArray(value)
500
+ && value.length === 4
501
+ && Number.isSafeInteger(value[0])
502
+ && value[0] >= 0
503
+ && typeof value[1] === "string"
504
+ && Number.isSafeInteger(value[2])
505
+ && value[2] >= 1
506
+ && Number.isSafeInteger(value[3])
507
+ && value[3] >= 0;
508
+ }
509
+
510
+ function isWebHostAccessibilityNodes(
511
+ value: unknown
512
+ ): value is WebHostAccessibilityNode[] {
513
+ return Array.isArray(value) && value.every(isWebHostAccessibilityNode);
514
+ }
515
+
516
+ function isWebHostAccessibilityNode(
517
+ value: unknown
518
+ ): value is WebHostAccessibilityNode {
519
+ if (!value || typeof value !== "object") {
520
+ return false;
521
+ }
522
+ const node = value as Partial<WebHostAccessibilityNode>;
523
+ return typeof node.id === "string"
524
+ && (node.parentId === undefined || typeof node.parentId === "string")
525
+ && isWebHostSurfaceRect(node.rect)
526
+ && typeof node.role === "string"
527
+ && (node.label === undefined || typeof node.label === "string")
528
+ && (node.hint === undefined || typeof node.hint === "string")
529
+ && (
530
+ node.liveRegion === undefined
531
+ || node.liveRegion === "off"
532
+ || node.liveRegion === "polite"
533
+ || node.liveRegion === "assertive"
534
+ )
535
+ && (node.cursorAnchor === undefined || isWebHostAccessibilityPoint(node.cursorAnchor))
536
+ && (node.isFocused === undefined || typeof node.isFocused === "boolean");
537
+ }
538
+
539
+ function isWebHostAccessibilityPoint(
540
+ value: unknown
541
+ ): value is WebHostAccessibilityPoint {
542
+ return Array.isArray(value)
543
+ && value.length === 2
544
+ && value.every((entry) => typeof entry === "number");
545
+ }
546
+
547
+ function isWebHostAccessibilityAnnouncements(
548
+ value: unknown
549
+ ): value is WebHostAccessibilityAnnouncement[] {
550
+ return Array.isArray(value) && value.every(isWebHostAccessibilityAnnouncement);
551
+ }
552
+
553
+ function isWebHostAccessibilityAnnouncement(
554
+ value: unknown
555
+ ): value is WebHostAccessibilityAnnouncement {
556
+ if (!value || typeof value !== "object") {
557
+ return false;
558
+ }
559
+ const announcement = value as Partial<WebHostAccessibilityAnnouncement>;
560
+ return typeof announcement.message === "string"
561
+ && (
562
+ announcement.politeness === "off"
563
+ || announcement.politeness === "polite"
564
+ || announcement.politeness === "assertive"
565
+ );
566
+ }
567
+
568
+ function isWebHostSurfaceImages(
569
+ value: unknown
570
+ ): value is WebHostSurfaceImage[] {
571
+ return Array.isArray(value) && value.every(isWebHostSurfaceImage);
572
+ }
573
+
574
+ function isWebHostSurfaceImage(
575
+ value: unknown
576
+ ): value is WebHostSurfaceImage {
577
+ if (!value || typeof value !== "object") {
578
+ return false;
579
+ }
580
+ const image = value as Partial<WebHostSurfaceImage>;
581
+ return typeof image.id === "string"
582
+ && isWebHostSurfaceImageFormat(image.format)
583
+ && isWebHostSurfaceRect(image.bounds)
584
+ && isWebHostSurfaceRect(image.visibleBounds)
585
+ && isWebHostSurfaceScalingMode(image.scalingMode)
586
+ && (image.pixelSize === undefined || isWebHostSurfaceSize(image.pixelSize))
587
+ && (image.dataBase64 === undefined || typeof image.dataBase64 === "string");
588
+ }
589
+
590
+ function isWebHostSurfaceDamage(
591
+ value: unknown
592
+ ): value is WebHostSurfaceDamage {
593
+ if (!value || typeof value !== "object") {
594
+ return false;
595
+ }
596
+ const damage = value as Partial<WebHostSurfaceDamage>;
597
+ return Array.isArray(damage.textRows)
598
+ && damage.textRows.every(isWebHostSurfaceDamageTextRow)
599
+ && typeof damage.requiresFullTextRepaint === "boolean"
600
+ && typeof damage.requiresFullGraphicsReplay === "boolean";
601
+ }
602
+
603
+ function isWebHostSurfaceDamageTextRow(
604
+ value: unknown
605
+ ): value is WebHostSurfaceDamageTextRow {
606
+ return Array.isArray(value)
607
+ && value.length === 2
608
+ && typeof value[0] === "number"
609
+ && Array.isArray(value[1])
610
+ && value[1].every(isWebHostSurfaceDamageRange);
611
+ }
612
+
613
+ function isWebHostSurfaceDamageRange(
614
+ value: unknown
615
+ ): value is WebHostSurfaceDamageRange {
616
+ return Array.isArray(value)
617
+ && value.length === 2
618
+ && typeof value[0] === "number"
619
+ && typeof value[1] === "number";
620
+ }
621
+
622
+ function isWebHostSurfaceImageFormat(
623
+ value: unknown
624
+ ): value is WebHostSurfaceImageFormat {
625
+ return value === "png" || value === "jpeg" || value === "gif";
626
+ }
627
+
628
+ function isWebHostSurfaceRect(
629
+ value: unknown
630
+ ): value is WebHostSurfaceRect {
631
+ return Array.isArray(value)
632
+ && value.length === 4
633
+ && value.every((entry) => typeof entry === "number");
634
+ }
635
+
636
+ function isWebHostSurfaceSize(
637
+ value: unknown
638
+ ): value is WebHostSurfaceSize {
639
+ return Array.isArray(value)
640
+ && value.length === 2
641
+ && value.every((entry) => typeof entry === "number");
642
+ }
643
+
644
+ function isWebHostSurfaceScalingMode(
645
+ value: unknown
646
+ ): value is WebHostSurfaceImage["scalingMode"] {
647
+ return value === "stretch" || value === "fit" || value === "fill";
648
+ }