@swifttui/web 0.0.10 → 0.0.13
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/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "./wasi/BrowserWASIBridge.ts";
|
|
8
8
|
import { SharedInputQueueReader } from "./wasi/SharedInputQueue.ts";
|
|
9
9
|
import { createWasmSceneRuntimeFactory } from "./wasi/WasmSceneRuntime.ts";
|
|
10
|
-
import { WebHostSceneRuntime } from "./WebHostSceneRuntime.ts";
|
|
10
|
+
import { WebHostSceneRuntime, type WheelMode } from "./WebHostSceneRuntime.ts";
|
|
11
11
|
import { transportFixture } from "./WebHostTestFixtures.ts";
|
|
12
12
|
|
|
13
13
|
const encoder = new TextEncoder();
|
|
@@ -1211,6 +1211,114 @@ test("runtime can run as a passive embed without stealing focus or wheel scroll"
|
|
|
1211
1211
|
}
|
|
1212
1212
|
});
|
|
1213
1213
|
|
|
1214
|
+
test("chain mode captures the wheel when a region under the pointer can scroll", async () => {
|
|
1215
|
+
const dom = installFakeDOM();
|
|
1216
|
+
try {
|
|
1217
|
+
// Region covers the whole 4x2 surface; content is taller than the viewport
|
|
1218
|
+
// and scrolled to the top, so a downward wheel has headroom.
|
|
1219
|
+
const result = await wheelScenario({
|
|
1220
|
+
wheelMode: "chain",
|
|
1221
|
+
scrollRegions: [{ id: "list", rect: [0, 0, 4, 2], offset: [0, 0], content: [4, 10] }],
|
|
1222
|
+
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1223
|
+
});
|
|
1224
|
+
expect(result.captured).toBe(true);
|
|
1225
|
+
expect(result.wheelPrevented).toBe(true);
|
|
1226
|
+
} finally {
|
|
1227
|
+
dom.restore();
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
test("chain mode lets the wheel fall through at the region's scroll edge", async () => {
|
|
1232
|
+
const dom = installFakeDOM();
|
|
1233
|
+
try {
|
|
1234
|
+
// Same region, but scrolled to the bottom (offset.y == maxY == 10 - 2),
|
|
1235
|
+
// so a further downward wheel has no headroom and must chain to the page.
|
|
1236
|
+
const result = await wheelScenario({
|
|
1237
|
+
wheelMode: "chain",
|
|
1238
|
+
scrollRegions: [{ id: "list", rect: [0, 0, 4, 2], offset: [0, 8], content: [4, 10] }],
|
|
1239
|
+
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1240
|
+
});
|
|
1241
|
+
expect(result.captured).toBe(false);
|
|
1242
|
+
expect(result.wheelPrevented).toBe(false);
|
|
1243
|
+
} finally {
|
|
1244
|
+
dom.restore();
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
test("chain mode captures an upward wheel when scrolled away from the top", async () => {
|
|
1249
|
+
const dom = installFakeDOM();
|
|
1250
|
+
try {
|
|
1251
|
+
// At the bottom edge, downward chains but upward still has headroom.
|
|
1252
|
+
const result = await wheelScenario({
|
|
1253
|
+
wheelMode: "chain",
|
|
1254
|
+
scrollRegions: [{ id: "list", rect: [0, 0, 4, 2], offset: [0, 8], content: [4, 10] }],
|
|
1255
|
+
wheel: { clientX: 5, clientY: 5, deltaY: -20 },
|
|
1256
|
+
});
|
|
1257
|
+
expect(result.captured).toBe(true);
|
|
1258
|
+
expect(result.wheelPrevented).toBe(true);
|
|
1259
|
+
} finally {
|
|
1260
|
+
dom.restore();
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
test("chain mode falls through when the pointer is outside every scroll region", async () => {
|
|
1265
|
+
const dom = installFakeDOM();
|
|
1266
|
+
try {
|
|
1267
|
+
// Region only covers the right half (cells x>=2); wheel at cell (0,0).
|
|
1268
|
+
const result = await wheelScenario({
|
|
1269
|
+
wheelMode: "chain",
|
|
1270
|
+
scrollRegions: [{ id: "list", rect: [2, 0, 2, 2], offset: [0, 0], content: [2, 10] }],
|
|
1271
|
+
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1272
|
+
});
|
|
1273
|
+
expect(result.captured).toBe(false);
|
|
1274
|
+
expect(result.wheelPrevented).toBe(false);
|
|
1275
|
+
} finally {
|
|
1276
|
+
dom.restore();
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
test("chain mode falls through when the scene publishes no scroll regions", async () => {
|
|
1281
|
+
const dom = installFakeDOM();
|
|
1282
|
+
try {
|
|
1283
|
+
const result = await wheelScenario({
|
|
1284
|
+
wheelMode: "chain",
|
|
1285
|
+
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1286
|
+
});
|
|
1287
|
+
expect(result.captured).toBe(false);
|
|
1288
|
+
expect(result.wheelPrevented).toBe(false);
|
|
1289
|
+
} finally {
|
|
1290
|
+
dom.restore();
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
|
|
1294
|
+
test("capture mode always eats the wheel even without scroll regions", async () => {
|
|
1295
|
+
const dom = installFakeDOM();
|
|
1296
|
+
try {
|
|
1297
|
+
const result = await wheelScenario({
|
|
1298
|
+
wheelMode: "capture",
|
|
1299
|
+
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1300
|
+
});
|
|
1301
|
+
expect(result.captured).toBe(true);
|
|
1302
|
+
expect(result.wheelPrevented).toBe(true);
|
|
1303
|
+
} finally {
|
|
1304
|
+
dom.restore();
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
test("legacy captureWheelInput:true maps to capture mode", async () => {
|
|
1309
|
+
const dom = installFakeDOM();
|
|
1310
|
+
try {
|
|
1311
|
+
const result = await wheelScenario({
|
|
1312
|
+
captureWheelInput: true,
|
|
1313
|
+
wheel: { clientX: 5, clientY: 5, deltaY: 20 },
|
|
1314
|
+
});
|
|
1315
|
+
expect(result.captured).toBe(true);
|
|
1316
|
+
expect(result.wheelPrevented).toBe(true);
|
|
1317
|
+
} finally {
|
|
1318
|
+
dom.restore();
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1214
1322
|
test("runtime preserves pointer movement within one cell", async () => {
|
|
1215
1323
|
const dom = installFakeDOM();
|
|
1216
1324
|
try {
|
|
@@ -1443,6 +1551,62 @@ async function flushPromises(): Promise<void> {
|
|
|
1443
1551
|
await Promise.resolve();
|
|
1444
1552
|
}
|
|
1445
1553
|
|
|
1554
|
+
// Drives a single wheel event over a 4x2 surface (cellWidth 10, cellHeight 27
|
|
1555
|
+
// under the fake DOM) with the given wheel mode and published scroll regions,
|
|
1556
|
+
// and reports whether the wheel was forwarded to the app and/or preventDefault'd.
|
|
1557
|
+
// Assumes a fake DOM is already installed by the caller.
|
|
1558
|
+
async function wheelScenario(options: {
|
|
1559
|
+
wheelMode?: WheelMode;
|
|
1560
|
+
captureWheelInput?: boolean;
|
|
1561
|
+
scrollRegions?: Array<Record<string, unknown>>;
|
|
1562
|
+
wheel: { clientX: number; clientY: number; deltaX?: number; deltaY?: number };
|
|
1563
|
+
}): Promise<{ captured: boolean; wheelPrevented: boolean }> {
|
|
1564
|
+
const inputs: string[] = [];
|
|
1565
|
+
const bridge = new BrowserWASIBridge({ sceneId: "main", columns: 4, rows: 2 });
|
|
1566
|
+
const mount = new FakeElement("div");
|
|
1567
|
+
const runtime = new WebHostSceneRuntime({
|
|
1568
|
+
mount: mount as unknown as HTMLElement,
|
|
1569
|
+
descriptor: { id: "main", title: "Main", isDefault: true },
|
|
1570
|
+
style: { fontSize: 20 },
|
|
1571
|
+
bridge,
|
|
1572
|
+
onInput: (chunk) => {
|
|
1573
|
+
inputs.push(decoder.decode(chunk));
|
|
1574
|
+
},
|
|
1575
|
+
synchronizeAccessibilityFocus: false,
|
|
1576
|
+
wheelMode: options.wheelMode,
|
|
1577
|
+
captureWheelInput: options.captureWheelInput,
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
await runtime.mount();
|
|
1581
|
+
const frame: Record<string, unknown> = {
|
|
1582
|
+
version: 2,
|
|
1583
|
+
width: 4,
|
|
1584
|
+
height: 2,
|
|
1585
|
+
styles: [null],
|
|
1586
|
+
rows: [[], []],
|
|
1587
|
+
};
|
|
1588
|
+
if (options.scrollRegions) {
|
|
1589
|
+
frame.scrollRegions = options.scrollRegions;
|
|
1590
|
+
}
|
|
1591
|
+
bridge.stdout.write(encoder.encode(surfaceRecord(frame)));
|
|
1592
|
+
|
|
1593
|
+
let wheelPrevented = false;
|
|
1594
|
+
runtime.terminalMount.dispatch("wheel", {
|
|
1595
|
+
clientX: options.wheel.clientX,
|
|
1596
|
+
clientY: options.wheel.clientY,
|
|
1597
|
+
deltaX: options.wheel.deltaX ?? 0,
|
|
1598
|
+
deltaY: options.wheel.deltaY ?? 0,
|
|
1599
|
+
shiftKey: false,
|
|
1600
|
+
altKey: false,
|
|
1601
|
+
ctrlKey: false,
|
|
1602
|
+
preventDefault() {
|
|
1603
|
+
wheelPrevented = true;
|
|
1604
|
+
},
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
return { captured: inputs.some((i) => i.includes("scrolled")), wheelPrevented };
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1446
1610
|
function surfaceRecord(
|
|
1447
1611
|
frame: Record<string, unknown>
|
|
1448
1612
|
): string {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type WebHostOutputSink,
|
|
19
19
|
type WebHostKeyInput,
|
|
20
20
|
type WebHostRuntimeIssue,
|
|
21
|
+
type WebHostScrollRegion,
|
|
21
22
|
type WebHostSurfaceDamage,
|
|
22
23
|
type WebHostSurfaceFrame,
|
|
23
24
|
type WebHostSurfaceImage,
|
|
@@ -42,9 +43,28 @@ export interface WebHostSceneRuntimeOptions {
|
|
|
42
43
|
onInput(chunk: Uint8Array): void;
|
|
43
44
|
onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;
|
|
44
45
|
synchronizeAccessibilityFocus?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* How the embedded view treats mouse-wheel input.
|
|
48
|
+
* - `"capture"`: always forward the wheel to the app while the pointer is over
|
|
49
|
+
* the surface (and `preventDefault` page scroll). Legacy default.
|
|
50
|
+
* - `"chain"`: forward the wheel only while a scrollable region under the
|
|
51
|
+
* pointer can still scroll in that direction; otherwise let it fall through
|
|
52
|
+
* so the page (or parent iframe) scrolls. Requires the app to publish
|
|
53
|
+
* `scrollRegions` in its frames.
|
|
54
|
+
* - `"passive"`: never capture; the page always scrolls.
|
|
55
|
+
*
|
|
56
|
+
* Takes precedence over the legacy `captureWheelInput` flag.
|
|
57
|
+
*/
|
|
58
|
+
wheelMode?: WheelMode;
|
|
59
|
+
/**
|
|
60
|
+
* Legacy boolean wheel gate. `true` → `"capture"`, `false` → `"passive"`.
|
|
61
|
+
* Prefer `wheelMode`. Ignored when `wheelMode` is set.
|
|
62
|
+
*/
|
|
45
63
|
captureWheelInput?: boolean;
|
|
46
64
|
}
|
|
47
65
|
|
|
66
|
+
export type WheelMode = "capture" | "chain" | "passive";
|
|
67
|
+
|
|
48
68
|
interface CachedWebHostImage {
|
|
49
69
|
image?: CanvasImageSource;
|
|
50
70
|
promise?: Promise<CanvasImageSource>;
|
|
@@ -78,7 +98,7 @@ export class WebHostSceneRuntime {
|
|
|
78
98
|
private readonly onInput: (chunk: Uint8Array) => void;
|
|
79
99
|
private readonly onFrameDiagnostic?: (diagnostic: WebHostFrameDiagnosticRecord) => void;
|
|
80
100
|
private readonly synchronizeAccessibilityFocus: boolean;
|
|
81
|
-
private readonly
|
|
101
|
+
private readonly wheelMode: WheelMode;
|
|
82
102
|
private readonly imageCache = new Map<string, CachedWebHostImage>();
|
|
83
103
|
private currentStyle: ResolvedWebHostTerminalStyle;
|
|
84
104
|
private canvas?: HTMLCanvasElement;
|
|
@@ -108,7 +128,8 @@ export class WebHostSceneRuntime {
|
|
|
108
128
|
this.onInput = options.onInput;
|
|
109
129
|
this.onFrameDiagnostic = options.onFrameDiagnostic;
|
|
110
130
|
this.synchronizeAccessibilityFocus = options.synchronizeAccessibilityFocus ?? true;
|
|
111
|
-
this.
|
|
131
|
+
this.wheelMode = options.wheelMode
|
|
132
|
+
?? (options.captureWheelInput === false ? "passive" : "capture");
|
|
112
133
|
this.element = document.createElement("section");
|
|
113
134
|
this.element.className = "webhost-scene";
|
|
114
135
|
this.element.dataset.sceneId = options.descriptor.id;
|
|
@@ -274,6 +295,9 @@ export class WebHostSceneRuntime {
|
|
|
274
295
|
|
|
275
296
|
this.terminalMount.style.position = "relative";
|
|
276
297
|
this.terminalMount.style.overflow = "hidden";
|
|
298
|
+
// Keep a captured wheel from rubber-banding/chaining the page; the wheel
|
|
299
|
+
// capture vs. fall-through decision lives in handleWheel.
|
|
300
|
+
this.terminalMount.style.overscrollBehavior = "contain";
|
|
277
301
|
this.terminalMount.style.outline = "none";
|
|
278
302
|
this.terminalMount.style.background = webTUITerminalBackgroundColor(this.currentStyle);
|
|
279
303
|
this.terminalMount.style.minHeight = `${this.cellHeight * 8}px`;
|
|
@@ -390,12 +414,23 @@ export class WebHostSceneRuntime {
|
|
|
390
414
|
};
|
|
391
415
|
|
|
392
416
|
const handleWheel = (event: WheelEvent) => {
|
|
393
|
-
if (
|
|
417
|
+
if (this.wheelMode === "passive") {
|
|
394
418
|
return;
|
|
395
419
|
}
|
|
396
420
|
|
|
397
421
|
const location = this.cellLocation(event);
|
|
398
422
|
if (!location) {
|
|
423
|
+
// Pointer is outside the cell grid (sub-cell margin / gutter). Don't
|
|
424
|
+
// capture — let the wheel fall through to the page.
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// In "chain" mode, capture only while a scrollable region under the
|
|
429
|
+
// pointer can still move in this direction; otherwise let the wheel fall
|
|
430
|
+
// through so the page (or parent iframe) scrolls — iframe-like behavior.
|
|
431
|
+
// "capture" mode always forwards while over the surface (legacy).
|
|
432
|
+
if (this.wheelMode === "chain"
|
|
433
|
+
&& !this.wheelTargetCanScroll(location, event.deltaX, event.deltaY)) {
|
|
399
434
|
return;
|
|
400
435
|
}
|
|
401
436
|
|
|
@@ -804,6 +839,41 @@ export class WebHostSceneRuntime {
|
|
|
804
839
|
return `${italic}${weight}${this.currentStyle.fontSize}px ${this.currentStyle.fontFamily}`;
|
|
805
840
|
}
|
|
806
841
|
|
|
842
|
+
/**
|
|
843
|
+
* Whether any scrollable region under `location` can still scroll in the
|
|
844
|
+
* wheel's direction. Mirrors the Swift host's scroll hit-test: a region
|
|
845
|
+
* qualifies when its viewport contains the cell AND it has remaining headroom
|
|
846
|
+
* in the delta's direction. Used by "chain" wheel mode to decide capture vs.
|
|
847
|
+
* fall-through. With no published `scrollRegions`, nothing can scroll, so the
|
|
848
|
+
* wheel chains to the page (a scene with no ScrollView stays fully passive).
|
|
849
|
+
*/
|
|
850
|
+
private wheelTargetCanScroll(
|
|
851
|
+
location: { x: number; y: number },
|
|
852
|
+
deltaX: number,
|
|
853
|
+
deltaY: number
|
|
854
|
+
): boolean {
|
|
855
|
+
const regions = this.currentFrame?.scrollRegions;
|
|
856
|
+
if (!regions || regions.length === 0) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const cellX = Math.floor(location.x);
|
|
861
|
+
const cellY = Math.floor(location.y);
|
|
862
|
+
// Any region under the pointer that can move in this direction qualifies —
|
|
863
|
+
// this is what lets an outer ScrollView take over when an inner one is at
|
|
864
|
+
// its edge (nested scroll), and chains to the page only when none can.
|
|
865
|
+
for (const region of regions) {
|
|
866
|
+
const [rx, ry, rw, rh] = region.rect;
|
|
867
|
+
if (cellX < rx || cellY < ry || cellX >= rx + rw || cellY >= ry + rh) {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (regionCanScrollInDirection(region, deltaX, deltaY)) {
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
|
|
807
877
|
private cellLocation(
|
|
808
878
|
event: MouseEvent
|
|
809
879
|
): { x: number; y: number } | undefined {
|
|
@@ -959,6 +1029,42 @@ function normalizedWheelDelta(
|
|
|
959
1029
|
return 0;
|
|
960
1030
|
}
|
|
961
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* Whether a published scroll region has remaining headroom in the wheel's
|
|
1034
|
+
* direction, recomputing the per-direction extent from offset/content/viewport.
|
|
1035
|
+
* Mirrors SwiftTUI's clamp (`min(max(0, offset), max(0, content - viewport))`)
|
|
1036
|
+
* so the host and the app agree on "at edge". Wheel sign convention matches the
|
|
1037
|
+
* app: `deltaY > 0` scrolls down (offset grows toward the content bottom).
|
|
1038
|
+
* Diagonal wheels qualify if either axis has headroom.
|
|
1039
|
+
*/
|
|
1040
|
+
function regionCanScrollInDirection(
|
|
1041
|
+
region: WebHostScrollRegion,
|
|
1042
|
+
deltaX: number,
|
|
1043
|
+
deltaY: number
|
|
1044
|
+
): boolean {
|
|
1045
|
+
const [, , viewportWidth, viewportHeight] = region.rect;
|
|
1046
|
+
const [offsetX, offsetY] = region.offset;
|
|
1047
|
+
const [contentWidth, contentHeight] = region.content;
|
|
1048
|
+
const maxX = Math.max(0, contentWidth - viewportWidth);
|
|
1049
|
+
const maxY = Math.max(0, contentHeight - viewportHeight);
|
|
1050
|
+
const clampedX = Math.min(Math.max(0, offsetX), maxX);
|
|
1051
|
+
const clampedY = Math.min(Math.max(0, offsetY), maxY);
|
|
1052
|
+
|
|
1053
|
+
if (deltaY > 0 && clampedY < maxY) {
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
if (deltaY < 0 && clampedY > 0) {
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
if (deltaX > 0 && clampedX < maxX) {
|
|
1060
|
+
return true;
|
|
1061
|
+
}
|
|
1062
|
+
if (deltaX < 0 && clampedX > 0) {
|
|
1063
|
+
return true;
|
|
1064
|
+
}
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
962
1068
|
function normalizeCellRanges(
|
|
963
1069
|
ranges: DirtyCellRange[]
|
|
964
1070
|
): DirtyCellRange[] {
|
|
@@ -88,6 +88,25 @@ export interface WebHostSurfaceDamage {
|
|
|
88
88
|
requiresFullGraphicsReplay: boolean;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Per-region scroll extent published with each frame so the host can implement
|
|
93
|
+
* scroll-chaining: capture the wheel only while the region under the pointer can
|
|
94
|
+
* still scroll in the wheel's direction, otherwise let it fall through to the
|
|
95
|
+
* page. The host recomputes the per-direction headroom from `offset`/`content`/
|
|
96
|
+
* the viewport `rect`, mirroring SwiftTUI's
|
|
97
|
+
* `min(max(0, offset), max(0, content - viewport))` clamp.
|
|
98
|
+
*/
|
|
99
|
+
export interface WebHostScrollRegion {
|
|
100
|
+
/** identity path — same key space as accessibility node ids */
|
|
101
|
+
id: string;
|
|
102
|
+
/** viewport rect in cells: [x, y, width, height] */
|
|
103
|
+
rect: WebHostSurfaceRect;
|
|
104
|
+
/** current clamped scroll offset in cells: [x, y] */
|
|
105
|
+
offset: WebHostAccessibilityPoint;
|
|
106
|
+
/** total content size in cells: [width, height] */
|
|
107
|
+
content: WebHostSurfaceSize;
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
export interface WebHostSurfaceFrame {
|
|
92
111
|
version: 1 | 2;
|
|
93
112
|
sequence?: number;
|
|
@@ -99,6 +118,7 @@ export interface WebHostSurfaceFrame {
|
|
|
99
118
|
damage?: WebHostSurfaceDamage;
|
|
100
119
|
accessibilityTree?: WebHostAccessibilityNode[];
|
|
101
120
|
accessibilityAnnouncements?: WebHostAccessibilityAnnouncement[];
|
|
121
|
+
scrollRegions?: WebHostScrollRegion[];
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
export type WebHostSurfaceDeltaRow = [
|
|
@@ -118,6 +138,7 @@ export interface WebHostSurfaceDeltaFrame {
|
|
|
118
138
|
damage?: WebHostSurfaceDamage;
|
|
119
139
|
accessibilityTree?: WebHostAccessibilityNode[];
|
|
120
140
|
accessibilityAnnouncements?: WebHostAccessibilityAnnouncement[];
|
|
141
|
+
scrollRegions?: WebHostScrollRegion[];
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
export interface WebHostRuntimeIssue {
|
|
@@ -314,6 +335,7 @@ export class WebHostOutputDecoder {
|
|
|
314
335
|
damage: frame.damage,
|
|
315
336
|
accessibilityTree: frame.accessibilityTree,
|
|
316
337
|
accessibilityAnnouncements: frame.accessibilityAnnouncements,
|
|
338
|
+
scrollRegions: frame.scrollRegions,
|
|
317
339
|
};
|
|
318
340
|
}
|
|
319
341
|
}
|
|
@@ -444,7 +466,8 @@ function isWebHostSurfaceFrame(
|
|
|
444
466
|
&& (
|
|
445
467
|
frame.accessibilityAnnouncements === undefined
|
|
446
468
|
|| isWebHostAccessibilityAnnouncements(frame.accessibilityAnnouncements)
|
|
447
|
-
)
|
|
469
|
+
)
|
|
470
|
+
&& (frame.scrollRegions === undefined || isWebHostScrollRegions(frame.scrollRegions));
|
|
448
471
|
}
|
|
449
472
|
|
|
450
473
|
function isWebHostSurfaceDeltaFrame(
|
|
@@ -474,7 +497,8 @@ function isWebHostSurfaceDeltaFrame(
|
|
|
474
497
|
&& (
|
|
475
498
|
frame.accessibilityAnnouncements === undefined
|
|
476
499
|
|| isWebHostAccessibilityAnnouncements(frame.accessibilityAnnouncements)
|
|
477
|
-
)
|
|
500
|
+
)
|
|
501
|
+
&& (frame.scrollRegions === undefined || isWebHostScrollRegions(frame.scrollRegions));
|
|
478
502
|
}
|
|
479
503
|
|
|
480
504
|
function isWebHostSurfaceDeltaRow(
|
|
@@ -625,6 +649,25 @@ function isWebHostSurfaceImageFormat(
|
|
|
625
649
|
return value === "png" || value === "jpeg" || value === "gif";
|
|
626
650
|
}
|
|
627
651
|
|
|
652
|
+
function isWebHostScrollRegions(
|
|
653
|
+
value: unknown
|
|
654
|
+
): value is WebHostScrollRegion[] {
|
|
655
|
+
return Array.isArray(value) && value.every(isWebHostScrollRegion);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function isWebHostScrollRegion(
|
|
659
|
+
value: unknown
|
|
660
|
+
): value is WebHostScrollRegion {
|
|
661
|
+
if (!value || typeof value !== "object") {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
const region = value as Partial<WebHostScrollRegion>;
|
|
665
|
+
return typeof region.id === "string"
|
|
666
|
+
&& isWebHostSurfaceRect(region.rect)
|
|
667
|
+
&& isWebHostSurfaceSize(region.offset)
|
|
668
|
+
&& isWebHostSurfaceSize(region.content);
|
|
669
|
+
}
|
|
670
|
+
|
|
628
671
|
function isWebHostSurfaceRect(
|
|
629
672
|
value: unknown
|
|
630
673
|
): value is WebHostSurfaceRect {
|