@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 +58 -43
- package/dist/src/AccessibilityTree.js +23 -14
- package/dist/src/AccessibilityTree.js.map +1 -1
- package/dist/src/CanvasSurfacePainter.js +263 -0
- package/dist/src/CanvasSurfacePainter.js.map +1 -0
- package/dist/src/InputEventEncoder.js +117 -0
- package/dist/src/InputEventEncoder.js.map +1 -0
- package/dist/src/PointerGeometry.js +72 -0
- package/dist/src/PointerGeometry.js.map +1 -0
- package/dist/src/WebHostSceneRuntime.d.ts +11 -20
- package/dist/src/WebHostSceneRuntime.js +40 -359
- package/dist/src/WebHostSceneRuntime.js.map +1 -1
- package/dist/src/wasi/SharedInputQueue.js +14 -8
- package/dist/src/wasi/SharedInputQueue.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { encodeKeyInputMessage, encodeMouseInputMessage, encodePasteInputMessage } from "./WebHostSurfaceTransport.js";
|
|
2
|
+
//#region src/InputEventEncoder.ts
|
|
3
|
+
/**
|
|
4
|
+
* Encodes DOM keyboard/pointer/wheel/paste events into the `web-surface` wire
|
|
5
|
+
* messages the SwiftTUI host consumes. This collaborator owns the translation
|
|
6
|
+
* from browser event vocabulary (key names, button indices, wheel deltas,
|
|
7
|
+
* modifier flags) to the transport's message vocabulary; it produces raw byte
|
|
8
|
+
* chunks and holds no DOM or geometry state.
|
|
9
|
+
*
|
|
10
|
+
* Hit-testing (pixel → cell) lives in the host, which passes already-resolved
|
|
11
|
+
* {@link CellLocation}s here so the encoder stays free of layout concerns.
|
|
12
|
+
*/
|
|
13
|
+
var InputEventEncoder = class {
|
|
14
|
+
/**
|
|
15
|
+
* Builds the key message for a keyboard event, or `undefined` when the event
|
|
16
|
+
* does not map to a forwarded key (e.g. a multi-codepoint composed string).
|
|
17
|
+
* Returning `undefined` lets the host leave the event unhandled.
|
|
18
|
+
*/
|
|
19
|
+
encodeKey(event) {
|
|
20
|
+
const key = keyInputFromKeyboardEvent(event);
|
|
21
|
+
if (!key) return;
|
|
22
|
+
return encodeKeyInputMessage({
|
|
23
|
+
...key,
|
|
24
|
+
modifiers: modifierMask(event)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
encodePaste(text) {
|
|
28
|
+
return encodePasteInputMessage(text);
|
|
29
|
+
}
|
|
30
|
+
encodePointerDown(location, button, event) {
|
|
31
|
+
return encodeMouseInputMessage({
|
|
32
|
+
kind: "down",
|
|
33
|
+
x: location.x,
|
|
34
|
+
y: location.y,
|
|
35
|
+
button,
|
|
36
|
+
modifiers: modifierMask(event)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
encodePointerUp(location, button, event) {
|
|
40
|
+
return encodeMouseInputMessage({
|
|
41
|
+
kind: "up",
|
|
42
|
+
x: location.x,
|
|
43
|
+
y: location.y,
|
|
44
|
+
button,
|
|
45
|
+
modifiers: modifierMask(event)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
encodePointerMove(location, button, event) {
|
|
49
|
+
return encodeMouseInputMessage({
|
|
50
|
+
kind: event.buttons ? "dragged" : "moved",
|
|
51
|
+
x: location.x,
|
|
52
|
+
y: location.y,
|
|
53
|
+
button,
|
|
54
|
+
modifiers: modifierMask(event)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
encodeWheel(location, event) {
|
|
58
|
+
return encodeMouseInputMessage({
|
|
59
|
+
kind: "scrolled",
|
|
60
|
+
x: location.x,
|
|
61
|
+
y: location.y,
|
|
62
|
+
deltaX: normalizedWheelDelta(event.deltaX),
|
|
63
|
+
deltaY: normalizedWheelDelta(event.deltaY),
|
|
64
|
+
modifiers: modifierMask(event)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Translates a DOM `MouseEvent.button` index into the wire button identity. */
|
|
68
|
+
pointerButton(button) {
|
|
69
|
+
return pointerButton(button);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
function keyInputFromKeyboardEvent(event) {
|
|
73
|
+
switch (event.key) {
|
|
74
|
+
case "Enter": return { key: "return" };
|
|
75
|
+
case " ": return { key: "space" };
|
|
76
|
+
case "Tab": return { key: "tab" };
|
|
77
|
+
case "ArrowLeft": return { key: "arrowLeft" };
|
|
78
|
+
case "ArrowRight": return { key: "arrowRight" };
|
|
79
|
+
case "ArrowUp": return { key: "arrowUp" };
|
|
80
|
+
case "ArrowDown": return { key: "arrowDown" };
|
|
81
|
+
case "Backspace": return { key: "backspace" };
|
|
82
|
+
case "Escape": return { key: "escape" };
|
|
83
|
+
case "Home": return { key: "home" };
|
|
84
|
+
case "End": return { key: "end" };
|
|
85
|
+
default: {
|
|
86
|
+
const characters = Array.from(event.key);
|
|
87
|
+
if (characters.length !== 1) return;
|
|
88
|
+
return {
|
|
89
|
+
key: "character",
|
|
90
|
+
character: characters[0]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function pointerButton(button) {
|
|
96
|
+
switch (button) {
|
|
97
|
+
case 1: return "middle";
|
|
98
|
+
case 2: return "secondary";
|
|
99
|
+
default: return "primary";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function modifierMask(event) {
|
|
103
|
+
let mask = 0;
|
|
104
|
+
if (event.shiftKey) mask |= 1;
|
|
105
|
+
if (event.altKey) mask |= 2;
|
|
106
|
+
if (event.ctrlKey) mask |= 4;
|
|
107
|
+
return mask;
|
|
108
|
+
}
|
|
109
|
+
function normalizedWheelDelta(delta) {
|
|
110
|
+
if (delta > 0) return 1;
|
|
111
|
+
if (delta < 0) return -1;
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
export { InputEventEncoder };
|
|
116
|
+
|
|
117
|
+
//# sourceMappingURL=InputEventEncoder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InputEventEncoder.js","names":[],"sources":["../../src/InputEventEncoder.ts"],"sourcesContent":["import {\n encodeKeyInputMessage,\n encodeMouseInputMessage,\n encodePasteInputMessage,\n type WebHostKeyInput,\n} from \"./WebHostSurfaceTransport.ts\";\n\n/** A resolved cell-grid location (fractional cell coordinates). */\nexport interface CellLocation {\n x: number;\n y: number;\n}\n\n/**\n * Pointer button identity in the wire vocabulary. Mirrors the values the Swift\n * host expects on `mouse:` records.\n */\nexport type PointerButton = \"primary\" | \"middle\" | \"secondary\";\n\n/**\n * Encodes DOM keyboard/pointer/wheel/paste events into the `web-surface` wire\n * messages the SwiftTUI host consumes. This collaborator owns the translation\n * from browser event vocabulary (key names, button indices, wheel deltas,\n * modifier flags) to the transport's message vocabulary; it produces raw byte\n * chunks and holds no DOM or geometry state.\n *\n * Hit-testing (pixel → cell) lives in the host, which passes already-resolved\n * {@link CellLocation}s here so the encoder stays free of layout concerns.\n */\nexport class InputEventEncoder {\n /**\n * Builds the key message for a keyboard event, or `undefined` when the event\n * does not map to a forwarded key (e.g. a multi-codepoint composed string).\n * Returning `undefined` lets the host leave the event unhandled.\n */\n encodeKey(\n event: KeyboardEvent\n ): Uint8Array | undefined {\n const key = keyInputFromKeyboardEvent(event);\n if (!key) {\n return undefined;\n }\n return encodeKeyInputMessage({\n ...key,\n modifiers: modifierMask(event),\n });\n }\n\n encodePaste(\n text: string\n ): Uint8Array {\n return encodePasteInputMessage(text);\n }\n\n encodePointerDown(\n location: CellLocation,\n button: PointerButton,\n event: PointerEvent\n ): Uint8Array {\n return encodeMouseInputMessage({\n kind: \"down\",\n x: location.x,\n y: location.y,\n button,\n modifiers: modifierMask(event),\n });\n }\n\n encodePointerUp(\n location: CellLocation,\n button: PointerButton,\n event: PointerEvent\n ): Uint8Array {\n return encodeMouseInputMessage({\n kind: \"up\",\n x: location.x,\n y: location.y,\n button,\n modifiers: modifierMask(event),\n });\n }\n\n encodePointerMove(\n location: CellLocation,\n button: PointerButton,\n event: PointerEvent\n ): Uint8Array {\n return encodeMouseInputMessage({\n kind: event.buttons ? \"dragged\" : \"moved\",\n x: location.x,\n y: location.y,\n button,\n modifiers: modifierMask(event),\n });\n }\n\n encodeWheel(\n location: CellLocation,\n event: WheelEvent\n ): Uint8Array {\n return encodeMouseInputMessage({\n kind: \"scrolled\",\n x: location.x,\n y: location.y,\n deltaX: normalizedWheelDelta(event.deltaX),\n deltaY: normalizedWheelDelta(event.deltaY),\n modifiers: modifierMask(event),\n });\n }\n\n /** Translates a DOM `MouseEvent.button` index into the wire button identity. */\n pointerButton(\n button: number\n ): PointerButton {\n return pointerButton(button);\n }\n}\n\nfunction keyInputFromKeyboardEvent(\n event: KeyboardEvent\n): Pick<WebHostKeyInput, \"key\" | \"character\"> | undefined {\n switch (event.key) {\n case \"Enter\":\n return { key: \"return\" };\n case \" \":\n return { key: \"space\" };\n case \"Tab\":\n return { key: \"tab\" };\n case \"ArrowLeft\":\n return { key: \"arrowLeft\" };\n case \"ArrowRight\":\n return { key: \"arrowRight\" };\n case \"ArrowUp\":\n return { key: \"arrowUp\" };\n case \"ArrowDown\":\n return { key: \"arrowDown\" };\n case \"Backspace\":\n return { key: \"backspace\" };\n case \"Escape\":\n return { key: \"escape\" };\n case \"Home\":\n return { key: \"home\" };\n case \"End\":\n return { key: \"end\" };\n default:\n {\n const characters = Array.from(event.key);\n if (characters.length !== 1) {\n return undefined;\n }\n return {\n key: \"character\",\n character: characters[0],\n };\n }\n }\n}\n\nfunction pointerButton(\n button: number\n): PointerButton {\n switch (button) {\n case 1:\n return \"middle\";\n case 2:\n return \"secondary\";\n default:\n return \"primary\";\n }\n}\n\nfunction modifierMask(\n event: MouseEvent | KeyboardEvent\n): number {\n let mask = 0;\n if (event.shiftKey) {\n mask |= 1;\n }\n if (event.altKey) {\n mask |= 2;\n }\n if (event.ctrlKey) {\n mask |= 4;\n }\n return mask;\n}\n\nfunction normalizedWheelDelta(\n delta: number\n): number {\n if (delta > 0) {\n return 1;\n }\n if (delta < 0) {\n return -1;\n }\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;AA6BA,IAAa,oBAAb,MAA+B;;;;;;CAM7B,UACE,OACwB;EACxB,MAAM,MAAM,0BAA0B,KAAK;EAC3C,IAAI,CAAC,KACH;EAEF,OAAO,sBAAsB;GAC3B,GAAG;GACH,WAAW,aAAa,KAAK;EAC/B,CAAC;CACH;CAEA,YACE,MACY;EACZ,OAAO,wBAAwB,IAAI;CACrC;CAEA,kBACE,UACA,QACA,OACY;EACZ,OAAO,wBAAwB;GAC7B,MAAM;GACN,GAAG,SAAS;GACZ,GAAG,SAAS;GACZ;GACA,WAAW,aAAa,KAAK;EAC/B,CAAC;CACH;CAEA,gBACE,UACA,QACA,OACY;EACZ,OAAO,wBAAwB;GAC7B,MAAM;GACN,GAAG,SAAS;GACZ,GAAG,SAAS;GACZ;GACA,WAAW,aAAa,KAAK;EAC/B,CAAC;CACH;CAEA,kBACE,UACA,QACA,OACY;EACZ,OAAO,wBAAwB;GAC7B,MAAM,MAAM,UAAU,YAAY;GAClC,GAAG,SAAS;GACZ,GAAG,SAAS;GACZ;GACA,WAAW,aAAa,KAAK;EAC/B,CAAC;CACH;CAEA,YACE,UACA,OACY;EACZ,OAAO,wBAAwB;GAC7B,MAAM;GACN,GAAG,SAAS;GACZ,GAAG,SAAS;GACZ,QAAQ,qBAAqB,MAAM,MAAM;GACzC,QAAQ,qBAAqB,MAAM,MAAM;GACzC,WAAW,aAAa,KAAK;EAC/B,CAAC;CACH;;CAGA,cACE,QACe;EACf,OAAO,cAAc,MAAM;CAC7B;AACF;AAEA,SAAS,0BACP,OACwD;CACxD,QAAQ,MAAM,KAAd;EACA,KAAK,SACH,OAAO,EAAE,KAAK,SAAS;EACzB,KAAK,KACH,OAAO,EAAE,KAAK,QAAQ;EACxB,KAAK,OACH,OAAO,EAAE,KAAK,MAAM;EACtB,KAAK,aACH,OAAO,EAAE,KAAK,YAAY;EAC5B,KAAK,cACH,OAAO,EAAE,KAAK,aAAa;EAC7B,KAAK,WACH,OAAO,EAAE,KAAK,UAAU;EAC1B,KAAK,aACH,OAAO,EAAE,KAAK,YAAY;EAC5B,KAAK,aACH,OAAO,EAAE,KAAK,YAAY;EAC5B,KAAK,UACH,OAAO,EAAE,KAAK,SAAS;EACzB,KAAK,QACH,OAAO,EAAE,KAAK,OAAO;EACvB,KAAK,OACH,OAAO,EAAE,KAAK,MAAM;EACtB,SACE;GACE,MAAM,aAAa,MAAM,KAAK,MAAM,GAAG;GACvC,IAAI,WAAW,WAAW,GACxB;GAEF,OAAO;IACL,KAAK;IACL,WAAW,WAAW;GACxB;EACF;CACF;AACF;AAEA,SAAS,cACP,QACe;CACf,QAAQ,QAAR;EACA,KAAK,GACH,OAAO;EACT,KAAK,GACH,OAAO;EACT,SACE,OAAO;CACT;AACF;AAEA,SAAS,aACP,OACQ;CACR,IAAI,OAAO;CACX,IAAI,MAAM,UACR,QAAQ;CAEV,IAAI,MAAM,QACR,QAAQ;CAEV,IAAI,MAAM,SACR,QAAQ;CAEV,OAAO;AACT;AAEA,SAAS,qBACP,OACQ;CACR,IAAI,QAAQ,GACV,OAAO;CAET,IAAI,QAAQ,GACV,OAAO;CAET,OAAO;AACT"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/PointerGeometry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Converts a pointer event to fractional cell coordinates, clamped to the
|
|
4
|
+
* grid. Returns `undefined` when the pointer is outside the cell grid (in the
|
|
5
|
+
* sub-cell margin/gutter), so the host can leave the event unhandled.
|
|
6
|
+
*/
|
|
7
|
+
function cellLocationForEvent(event, metrics) {
|
|
8
|
+
const location = rawCellLocationForEvent(event, metrics);
|
|
9
|
+
if (!location) return;
|
|
10
|
+
const cellX = Math.floor(location.x);
|
|
11
|
+
const cellY = Math.floor(location.y);
|
|
12
|
+
if (cellX < 0 || cellY < 0 || cellX >= metrics.columns || cellY >= metrics.rows) return;
|
|
13
|
+
return location;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Converts a pointer event to fractional cell coordinates *without* clamping to
|
|
17
|
+
* the grid. Used while a pointer is captured (drag) so the host keeps tracking
|
|
18
|
+
* the gesture even when it strays outside the surface bounds.
|
|
19
|
+
*/
|
|
20
|
+
function rawCellLocationForEvent(event, metrics) {
|
|
21
|
+
const rect = metrics.rect;
|
|
22
|
+
if (!rect) return;
|
|
23
|
+
return {
|
|
24
|
+
x: (event.clientX - rect.left) / metrics.cellWidth,
|
|
25
|
+
y: (event.clientY - rect.top) / metrics.cellHeight
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Whether any scrollable region under `location` can still scroll in the
|
|
30
|
+
* wheel's direction. Mirrors the Swift host's scroll hit-test: a region
|
|
31
|
+
* qualifies when its viewport contains the cell AND it has remaining headroom
|
|
32
|
+
* in the delta's direction. Used by "chain" wheel mode to decide capture vs.
|
|
33
|
+
* fall-through. With no published `scrollRegions`, nothing can scroll, so the
|
|
34
|
+
* wheel chains to the page (a scene with no ScrollView stays fully passive).
|
|
35
|
+
*/
|
|
36
|
+
function wheelTargetCanScroll(regions, location, deltaX, deltaY) {
|
|
37
|
+
if (!regions || regions.length === 0) return false;
|
|
38
|
+
const cellX = Math.floor(location.x);
|
|
39
|
+
const cellY = Math.floor(location.y);
|
|
40
|
+
for (const region of regions) {
|
|
41
|
+
const [rx, ry, rw, rh] = region.rect;
|
|
42
|
+
if (cellX < rx || cellY < ry || cellX >= rx + rw || cellY >= ry + rh) continue;
|
|
43
|
+
if (regionCanScrollInDirection(region, deltaX, deltaY)) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a published scroll region has remaining headroom in the wheel's
|
|
49
|
+
* direction, recomputing the per-direction extent from offset/content/viewport.
|
|
50
|
+
* Mirrors SwiftTUI's clamp (`min(max(0, offset), max(0, content - viewport))`)
|
|
51
|
+
* so the host and the app agree on "at edge". Wheel sign convention matches the
|
|
52
|
+
* app: `deltaY > 0` scrolls down (offset grows toward the content bottom).
|
|
53
|
+
* Diagonal wheels qualify if either axis has headroom.
|
|
54
|
+
*/
|
|
55
|
+
function regionCanScrollInDirection(region, deltaX, deltaY) {
|
|
56
|
+
const [, , viewportWidth, viewportHeight] = region.rect;
|
|
57
|
+
const [offsetX, offsetY] = region.offset;
|
|
58
|
+
const [contentWidth, contentHeight] = region.content;
|
|
59
|
+
const maxX = Math.max(0, contentWidth - viewportWidth);
|
|
60
|
+
const maxY = Math.max(0, contentHeight - viewportHeight);
|
|
61
|
+
const clampedX = Math.min(Math.max(0, offsetX), maxX);
|
|
62
|
+
const clampedY = Math.min(Math.max(0, offsetY), maxY);
|
|
63
|
+
if (deltaY > 0 && clampedY < maxY) return true;
|
|
64
|
+
if (deltaY < 0 && clampedY > 0) return true;
|
|
65
|
+
if (deltaX > 0 && clampedX < maxX) return true;
|
|
66
|
+
if (deltaX < 0 && clampedX > 0) return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { cellLocationForEvent, rawCellLocationForEvent, wheelTargetCanScroll };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=PointerGeometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PointerGeometry.js","names":[],"sources":["../../src/PointerGeometry.ts"],"sourcesContent":["import type {\n WebHostScrollRegion,\n} from \"./WebHostSurfaceTransport.ts\";\nimport type { CellLocation } from \"./InputEventEncoder.ts\";\n\n/**\n * The cell-grid geometry a pointer hit-test needs: the surface's bounding\n * client rect (the canvas's, falling back to the terminal mount's), the cell\n * dimensions, and the grid extent in cells. The host snapshots this per event.\n */\nexport interface PointerGeometryMetrics {\n rect?: { left: number; top: number } | null;\n cellWidth: number;\n cellHeight: number;\n columns: number;\n rows: number;\n}\n\n/**\n * Converts a pointer event to fractional cell coordinates, clamped to the\n * grid. Returns `undefined` when the pointer is outside the cell grid (in the\n * sub-cell margin/gutter), so the host can leave the event unhandled.\n */\nexport function cellLocationForEvent(\n event: MouseEvent,\n metrics: PointerGeometryMetrics\n): CellLocation | undefined {\n const location = rawCellLocationForEvent(event, metrics);\n if (!location) {\n return undefined;\n }\n\n const cellX = Math.floor(location.x);\n const cellY = Math.floor(location.y);\n if (cellX < 0 || cellY < 0 || cellX >= metrics.columns || cellY >= metrics.rows) {\n return undefined;\n }\n return location;\n}\n\n/**\n * Converts a pointer event to fractional cell coordinates *without* clamping to\n * the grid. Used while a pointer is captured (drag) so the host keeps tracking\n * the gesture even when it strays outside the surface bounds.\n */\nexport function rawCellLocationForEvent(\n event: MouseEvent,\n metrics: PointerGeometryMetrics\n): CellLocation | undefined {\n const rect = metrics.rect;\n if (!rect) {\n return undefined;\n }\n\n const x = (event.clientX - rect.left) / metrics.cellWidth;\n const y = (event.clientY - rect.top) / metrics.cellHeight;\n return { x, y };\n}\n\n/**\n * Whether any scrollable region under `location` can still scroll in the\n * wheel's direction. Mirrors the Swift host's scroll hit-test: a region\n * qualifies when its viewport contains the cell AND it has remaining headroom\n * in the delta's direction. Used by \"chain\" wheel mode to decide capture vs.\n * fall-through. With no published `scrollRegions`, nothing can scroll, so the\n * wheel chains to the page (a scene with no ScrollView stays fully passive).\n */\nexport function wheelTargetCanScroll(\n regions: readonly WebHostScrollRegion[] | undefined,\n location: CellLocation,\n deltaX: number,\n deltaY: number\n): boolean {\n if (!regions || regions.length === 0) {\n return false;\n }\n\n const cellX = Math.floor(location.x);\n const cellY = Math.floor(location.y);\n // Any region under the pointer that can move in this direction qualifies —\n // this is what lets an outer ScrollView take over when an inner one is at\n // its edge (nested scroll), and chains to the page only when none can.\n for (const region of regions) {\n const [rx, ry, rw, rh] = region.rect;\n if (cellX < rx || cellY < ry || cellX >= rx + rw || cellY >= ry + rh) {\n continue;\n }\n if (regionCanScrollInDirection(region, deltaX, deltaY)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Whether a published scroll region has remaining headroom in the wheel's\n * direction, recomputing the per-direction extent from offset/content/viewport.\n * Mirrors SwiftTUI's clamp (`min(max(0, offset), max(0, content - viewport))`)\n * so the host and the app agree on \"at edge\". Wheel sign convention matches the\n * app: `deltaY > 0` scrolls down (offset grows toward the content bottom).\n * Diagonal wheels qualify if either axis has headroom.\n */\nfunction regionCanScrollInDirection(\n region: WebHostScrollRegion,\n deltaX: number,\n deltaY: number\n): boolean {\n const [, , viewportWidth, viewportHeight] = region.rect;\n const [offsetX, offsetY] = region.offset;\n const [contentWidth, contentHeight] = region.content;\n const maxX = Math.max(0, contentWidth - viewportWidth);\n const maxY = Math.max(0, contentHeight - viewportHeight);\n const clampedX = Math.min(Math.max(0, offsetX), maxX);\n const clampedY = Math.min(Math.max(0, offsetY), maxY);\n\n if (deltaY > 0 && clampedY < maxY) {\n return true;\n }\n if (deltaY < 0 && clampedY > 0) {\n return true;\n }\n if (deltaX > 0 && clampedX < maxX) {\n return true;\n }\n if (deltaX < 0 && clampedX > 0) {\n return true;\n }\n return false;\n}\n"],"mappings":";;;;;;AAuBA,SAAgB,qBACd,OACA,SAC0B;CAC1B,MAAM,WAAW,wBAAwB,OAAO,OAAO;CACvD,IAAI,CAAC,UACH;CAGF,MAAM,QAAQ,KAAK,MAAM,SAAS,CAAC;CACnC,MAAM,QAAQ,KAAK,MAAM,SAAS,CAAC;CACnC,IAAI,QAAQ,KAAK,QAAQ,KAAK,SAAS,QAAQ,WAAW,SAAS,QAAQ,MACzE;CAEF,OAAO;AACT;;;;;;AAOA,SAAgB,wBACd,OACA,SAC0B;CAC1B,MAAM,OAAO,QAAQ;CACrB,IAAI,CAAC,MACH;CAKF,OAAO;EAAE,IAFE,MAAM,UAAU,KAAK,QAAQ,QAAQ;EAEpC,IADD,MAAM,UAAU,KAAK,OAAO,QAAQ;CACjC;AAChB;;;;;;;;;AAUA,SAAgB,qBACd,SACA,UACA,QACA,QACS;CACT,IAAI,CAAC,WAAW,QAAQ,WAAW,GACjC,OAAO;CAGT,MAAM,QAAQ,KAAK,MAAM,SAAS,CAAC;CACnC,MAAM,QAAQ,KAAK,MAAM,SAAS,CAAC;CAInC,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,CAAC,IAAI,IAAI,IAAI,MAAM,OAAO;EAChC,IAAI,QAAQ,MAAM,QAAQ,MAAM,SAAS,KAAK,MAAM,SAAS,KAAK,IAChE;EAEF,IAAI,2BAA2B,QAAQ,QAAQ,MAAM,GACnD,OAAO;CAEX;CACA,OAAO;AACT;;;;;;;;;AAUA,SAAS,2BACP,QACA,QACA,QACS;CACT,MAAM,KAAK,eAAe,kBAAkB,OAAO;CACnD,MAAM,CAAC,SAAS,WAAW,OAAO;CAClC,MAAM,CAAC,cAAc,iBAAiB,OAAO;CAC7C,MAAM,OAAO,KAAK,IAAI,GAAG,eAAe,aAAa;CACrD,MAAM,OAAO,KAAK,IAAI,GAAG,gBAAgB,cAAc;CACvD,MAAM,WAAW,KAAK,IAAI,KAAK,IAAI,GAAG,OAAO,GAAG,IAAI;CACpD,MAAM,WAAW,KAAK,IAAI,KAAK,IAAI,GAAG,OAAO,GAAG,IAAI;CAEpD,IAAI,SAAS,KAAK,WAAW,MAC3B,OAAO;CAET,IAAI,SAAS,KAAK,WAAW,GAC3B,OAAO;CAET,IAAI,SAAS,KAAK,WAAW,MAC3B,OAAO;CAET,IAAI,SAAS,KAAK,WAAW,GAC3B,OAAO;CAET,OAAO;AACT"}
|
|
@@ -41,6 +41,13 @@ interface WebHostSceneRuntimeOptions {
|
|
|
41
41
|
captureWheelInput?: boolean;
|
|
42
42
|
}
|
|
43
43
|
type WheelMode = "capture" | "chain" | "passive";
|
|
44
|
+
/**
|
|
45
|
+
* Coordinates a single SwiftTUI scene's browser presentation: it owns the DOM
|
|
46
|
+
* mount, canvas, accessibility tree, and bridge wiring, and delegates the heavy
|
|
47
|
+
* responsibilities to focused collaborators — {@link CanvasSurfacePainter} for
|
|
48
|
+
* canvas drawing, {@link InputEventEncoder} for wire-message encoding, and the
|
|
49
|
+
* {@link PointerGeometry} helpers for pixel→cell hit-testing and wheel chaining.
|
|
50
|
+
*/
|
|
44
51
|
declare class WebHostSceneRuntime {
|
|
45
52
|
readonly descriptor: WebHostSceneDescriptor;
|
|
46
53
|
readonly element: HTMLElement;
|
|
@@ -50,7 +57,8 @@ declare class WebHostSceneRuntime {
|
|
|
50
57
|
private readonly onFrameDiagnostic?;
|
|
51
58
|
private readonly synchronizeAccessibilityFocus;
|
|
52
59
|
private readonly wheelMode;
|
|
53
|
-
private readonly
|
|
60
|
+
private readonly painter;
|
|
61
|
+
private readonly inputEncoder;
|
|
54
62
|
private currentStyle;
|
|
55
63
|
private canvas?;
|
|
56
64
|
private accessibilityTree?;
|
|
@@ -87,26 +95,9 @@ declare class WebHostSceneRuntime {
|
|
|
87
95
|
private resizeCanvas;
|
|
88
96
|
private measureCells;
|
|
89
97
|
private draw;
|
|
90
|
-
private drawRows;
|
|
91
|
-
private drawRow;
|
|
92
98
|
private syncAccessibilityTree;
|
|
93
|
-
private
|
|
94
|
-
private
|
|
95
|
-
private cachedImage;
|
|
96
|
-
private drawCell;
|
|
97
|
-
private dirtyRegionForDamage;
|
|
98
|
-
private cellRect;
|
|
99
|
-
private drawTextLine;
|
|
100
|
-
private fontForStyle;
|
|
101
|
-
/**
|
|
102
|
-
* Whether any scrollable region under `location` can still scroll in the
|
|
103
|
-
* wheel's direction. Mirrors the Swift host's scroll hit-test: a region
|
|
104
|
-
* qualifies when its viewport contains the cell AND it has remaining headroom
|
|
105
|
-
* in the delta's direction. Used by "chain" wheel mode to decide capture vs.
|
|
106
|
-
* fall-through. With no published `scrollRegions`, nothing can scroll, so the
|
|
107
|
-
* wheel chains to the page (a scene with no ScrollView stays fully passive).
|
|
108
|
-
*/
|
|
109
|
-
private wheelTargetCanScroll;
|
|
99
|
+
private surfaceMetrics;
|
|
100
|
+
private pointerMetrics;
|
|
110
101
|
private cellLocation;
|
|
111
102
|
private rawCellLocation;
|
|
112
103
|
}
|