@effindomv2/runtime 0.1.0

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 (92) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/bridge.js +4 -0
  3. package/dist/bridge.js.map +7 -0
  4. package/dist/effindom.v2.manifest.json +68 -0
  5. package/dist/fonts/NotoColorEmoji.ttf +0 -0
  6. package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
  7. package/dist/fonts/NotoSans-Bold.ttf +0 -0
  8. package/dist/fonts/NotoSans-BoldItalic.ttf +0 -0
  9. package/dist/fonts/NotoSans-Italic.ttf +0 -0
  10. package/dist/fonts/NotoSans-Regular.ttf +0 -0
  11. package/dist/fonts/NotoSansMono-Bold.ttf +0 -0
  12. package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
  13. package/dist/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  14. package/dist/harness.js +2 -0
  15. package/dist/harness.js.map +7 -0
  16. package/dist/index.html +53 -0
  17. package/dist/runtime/effindom-core-v2.wasm32-simd.JQXIaRaN0-JahfIVFiSLE49WzzCENvef_2EDEm09nJs.wasm +0 -0
  18. package/dist/runtime/effindom-core-v2.wasm32-simd.y7RzpkMARiFeRkpgiqKQsAfv4Hf17NYdpni-6aLNhMs.js.symbols +10079 -0
  19. package/dist/runtime/effindom-core-v2.wasm32-simd.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  20. package/dist/runtime/effindom-core-v2.wasm32.JSfMkp9ertJzSZxA-_xz3yacrJUhswxlwbqbJLRIuqw.wasm +0 -0
  21. package/dist/runtime/effindom-core-v2.wasm32.xNgsQv7dCwf8Uy-PfJSoRNyk9-q1OSogUwkk5g6ZBjk.js.symbols +10088 -0
  22. package/dist/runtime/effindom-core-v2.wasm32.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  23. package/dist/runtime/effindom-core-v2.wasm64-simd.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  24. package/dist/runtime/effindom-core-v2.wasm64-simd.p4P98oRu2wEWxtRRW8RHr27JhGeWvWlziZXDM_z3Nc4.js.symbols +10286 -0
  25. package/dist/runtime/effindom-core-v2.wasm64-simd.y75FYXRwhQrpaDGYbZWrohGDv0AmjTb-EjXwOjBIgnM.wasm +0 -0
  26. package/dist/runtime/effindom-core-v2.wasm64.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  27. package/dist/runtime/effindom-core-v2.wasm64.emhE1_CJs4_zXp8wiQS_5lYpUQ0OchmXgxksi0ykaBs.js.symbols +10298 -0
  28. package/dist/runtime/effindom-core-v2.wasm64.sO-Yu70cfN8Qs3a5iEp6cbFPaiOchqcMKUzryu4npNo.wasm +0 -0
  29. package/dist/runtime/effindom-ui-v2.wasm32-simd.0Mas1XD03eYvemryTioWaZOBuBA5ij7MFlTa8CgEZWs.wasm +0 -0
  30. package/dist/runtime/effindom-ui-v2.wasm32-simd.ThSDClMnSWdwf9d89JZfYor0G1Z6OxR4lOc75rNRuD4.js.symbols +1890 -0
  31. package/dist/runtime/effindom-ui-v2.wasm32-simd.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  32. package/dist/runtime/effindom-ui-v2.wasm32.H7kYg99bT9ADGh0uUvj6H9Dk1L058nVFLv_4R79IXW8.js.symbols +1900 -0
  33. package/dist/runtime/effindom-ui-v2.wasm32.tp53X7nHfG_EUq29naDyElfnqhMw2D1Tr1T-BJAYO7w.wasm +0 -0
  34. package/dist/runtime/effindom-ui-v2.wasm32.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  35. package/dist/runtime/effindom-ui-v2.wasm64-simd.86tk9Z3xIpgTOykET_8Nn9iUVJnp1AzOHW4fVQRGtQE.wasm +0 -0
  36. package/dist/runtime/effindom-ui-v2.wasm64-simd.RQaXil22Chu63-vxK9oOuX8wUY044kbo190oYIbBU4M.js.symbols +1918 -0
  37. package/dist/runtime/effindom-ui-v2.wasm64-simd.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  38. package/dist/runtime/effindom-ui-v2.wasm64.YSwpMFbr-Q1SBe0Ze8mub1u1PqsvSz3QIYuA3eaUMME.js.symbols +1924 -0
  39. package/dist/runtime/effindom-ui-v2.wasm64.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  40. package/dist/runtime/effindom-ui-v2.wasm64.ioQ9DuM6gR_EjlfRHdF8EvNPBcKCs0PQbbY9-cjTV6Y.wasm +0 -0
  41. package/dist/runtime/icudt_minimal.962CX1q0-Nbv-OqXPaub5piYTOLumUk-nEvemcvvnpw.dat +0 -0
  42. package/package.json +62 -0
  43. package/scripts/build.sh +279 -0
  44. package/scripts/build_assets.sh +51 -0
  45. package/scripts/font_assets.sh +52 -0
  46. package/scripts/generate_manifest.py +121 -0
  47. package/scripts/stage_package_assets.sh +42 -0
  48. package/src/bridge/commit-policy.ts +10 -0
  49. package/src/bridge/events/canvas-geometry.ts +78 -0
  50. package/src/bridge/events/key-router.ts +187 -0
  51. package/src/bridge/events/pointer-router.ts +619 -0
  52. package/src/bridge/events/semantic-hit-testing.ts +27 -0
  53. package/src/bridge/events.ts +54 -0
  54. package/src/bridge/find-dialog.ts +690 -0
  55. package/src/bridge/find-session.ts +158 -0
  56. package/src/bridge/font-catalog.ts +51 -0
  57. package/src/bridge/google-fonts.ts +63 -0
  58. package/src/bridge/incremental-font-packages.ts +216 -0
  59. package/src/bridge/init.ts +77 -0
  60. package/src/bridge/interaction/editor-model.ts +371 -0
  61. package/src/bridge/interaction/editor-mutations.ts +495 -0
  62. package/src/bridge/interaction/editor-session.ts +628 -0
  63. package/src/bridge/interaction/logs.ts +23 -0
  64. package/src/bridge/interaction/text-encoding.ts +51 -0
  65. package/src/bridge/interaction.ts +86 -0
  66. package/src/bridge/local-types.ts +105 -0
  67. package/src/bridge/platform.ts +68 -0
  68. package/src/bridge/pointer-move-coalescer.ts +41 -0
  69. package/src/bridge/pull-to-refresh.ts +124 -0
  70. package/src/bridge/render-loop.ts +268 -0
  71. package/src/bridge/runtime/asset-manager.ts +202 -0
  72. package/src/bridge/runtime/find-controller.ts +269 -0
  73. package/src/bridge/runtime/font-manager.ts +691 -0
  74. package/src/bridge/runtime/open-canvas-api.ts +72 -0
  75. package/src/bridge/runtime/semantic-controller.ts +133 -0
  76. package/src/bridge/runtime/text-documents.ts +234 -0
  77. package/src/bridge/runtime.ts +315 -0
  78. package/src/bridge/touch-gesture.ts +159 -0
  79. package/src/bridge/utils/assets.ts +572 -0
  80. package/src/bridge/utils/backends.ts +163 -0
  81. package/src/bridge/utils/encoding.ts +128 -0
  82. package/src/bridge/utils/fetch.ts +147 -0
  83. package/src/bridge/utils/heap.ts +118 -0
  84. package/src/bridge.ts +93 -0
  85. package/src/clipboard.ts +139 -0
  86. package/src/core-types.ts +595 -0
  87. package/src/find-on-page.ts +284 -0
  88. package/src/harness.ts +53 -0
  89. package/src/index.ts +40 -0
  90. package/src/open-canvas.ts +108 -0
  91. package/src/runtime-config.ts +96 -0
  92. package/src/semantic.ts +905 -0
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+
3
+ BRIDGE_FONT_SOURCE_DIR="${REPO_ROOT}/v2/fonts"
4
+ BRIDGE_FONT_ASSETS=(
5
+ "DejaVuSans.ttf"
6
+ "DejaVuSans-Bold.ttf"
7
+ "NotoSans-Regular.ttf"
8
+ "NotoSans-Bold.ttf"
9
+ "NotoSans-Italic.ttf"
10
+ "NotoSans-BoldItalic.ttf"
11
+ "NotoSansMono-Regular.ttf"
12
+ "NotoSansMono-Bold.ttf"
13
+ "NotoSansSymbols2-Regular.ttf"
14
+ "NotoEmoji-Regular.ttf"
15
+ "NotoColorEmoji.ttf"
16
+ "NotoSansThai-Regular.ttf"
17
+ "NotoNaskhArabic-Variable.ttf"
18
+ )
19
+
20
+ RUNTIME_PACKAGE_FONT_ASSETS=(
21
+ "NotoSans-Regular.ttf"
22
+ "NotoSans-Bold.ttf"
23
+ "NotoSans-Italic.ttf"
24
+ "NotoSans-BoldItalic.ttf"
25
+ "NotoSansMono-Regular.ttf"
26
+ "NotoSansMono-Bold.ttf"
27
+ "NotoSansSymbols2-Regular.ttf"
28
+ "NotoEmoji-Regular.ttf"
29
+ "NotoColorEmoji.ttf"
30
+ )
31
+
32
+ copy_bridge_font_assets() {
33
+ local out_dir="$1"
34
+ local font_asset=""
35
+ mkdir -p "${out_dir}"
36
+ for font_asset in "${BRIDGE_FONT_ASSETS[@]}"; do
37
+ if [ -f "${BRIDGE_FONT_SOURCE_DIR}/${font_asset}" ]; then
38
+ cp "${BRIDGE_FONT_SOURCE_DIR}/${font_asset}" "${out_dir}/${font_asset}"
39
+ fi
40
+ done
41
+ }
42
+
43
+ copy_runtime_package_font_assets() {
44
+ local out_dir="$1"
45
+ local font_asset=""
46
+ mkdir -p "${out_dir}"
47
+ for font_asset in "${RUNTIME_PACKAGE_FONT_ASSETS[@]}"; do
48
+ if [ -f "${BRIDGE_FONT_SOURCE_DIR}/${font_asset}" ]; then
49
+ cp "${BRIDGE_FONT_SOURCE_DIR}/${font_asset}" "${out_dir}/${font_asset}"
50
+ fi
51
+ done
52
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ REPO_ROOT = Path(__file__).resolve().parents[3]
11
+ sys.path.insert(0, str(REPO_ROOT / "scripts"))
12
+
13
+ from content_hash import short_content_hash, standard_content_hash # noqa: E402
14
+
15
+ MANIFEST_FILENAME = "effindom.v2.manifest.json"
16
+ MANIFEST_VERSION = "1.0"
17
+ ARCHITECTURES = ("wasm64-simd", "wasm64", "wasm32-simd", "wasm32")
18
+ BUNDLES = ("core", "ui")
19
+
20
+
21
+ def stable_json_bytes(value: object) -> bytes:
22
+ return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
23
+
24
+
25
+ def content_integrity(data: bytes) -> str:
26
+ return f"sha256-{standard_content_hash(data)}"
27
+
28
+
29
+ def stage_versioned_copy(source: Path, runtime_dir: Path, stem: str, suffix: str) -> dict[str, str]:
30
+ data = source.read_bytes()
31
+ hashed_name = f"{stem}.{short_content_hash(data)}{suffix}"
32
+ destination = runtime_dir / hashed_name
33
+ destination.write_bytes(data)
34
+ return {
35
+ "url": f"./runtime/{hashed_name}",
36
+ "integrity": content_integrity(data),
37
+ }
38
+
39
+
40
+ def stage_bundle(stage_dir: Path, runtime_dir: Path, architecture: str, bundle_name: str) -> dict[str, str]:
41
+ bundle_dir = stage_dir / architecture
42
+ js_asset = stage_versioned_copy(
43
+ bundle_dir / f"{bundle_name}.js",
44
+ runtime_dir,
45
+ f"effindom-{bundle_name}-v2.{architecture}",
46
+ ".js",
47
+ )
48
+ wasm_asset = stage_versioned_copy(
49
+ bundle_dir / f"{bundle_name}.wasm",
50
+ runtime_dir,
51
+ f"effindom-{bundle_name}-v2.{architecture}",
52
+ ".wasm",
53
+ )
54
+
55
+ symbols_source = bundle_dir / f"{bundle_name}.js.symbols"
56
+ if symbols_source.exists():
57
+ stage_versioned_copy(
58
+ symbols_source,
59
+ runtime_dir,
60
+ f"effindom-{bundle_name}-v2.{architecture}",
61
+ ".js.symbols",
62
+ )
63
+
64
+ return {
65
+ "js": js_asset["url"],
66
+ "js_integrity": js_asset["integrity"],
67
+ "wasm": wasm_asset["url"],
68
+ "wasm_integrity": wasm_asset["integrity"],
69
+ }
70
+
71
+
72
+ def main() -> int:
73
+ if len(sys.argv) != 4:
74
+ print(
75
+ f"Usage: {Path(sys.argv[0]).name} <out-dir> <stage-dir> <icu-source>",
76
+ file=sys.stderr,
77
+ )
78
+ return 1
79
+
80
+ out_dir = Path(sys.argv[1]).resolve()
81
+ stage_dir = Path(sys.argv[2]).resolve()
82
+ icu_source = Path(sys.argv[3]).resolve()
83
+ runtime_dir = out_dir / "runtime"
84
+
85
+ if runtime_dir.exists():
86
+ shutil.rmtree(runtime_dir)
87
+ runtime_dir.mkdir(parents=True, exist_ok=True)
88
+
89
+ architectures: dict[str, dict[str, dict[str, str]]] = {}
90
+ for architecture in ARCHITECTURES:
91
+ architectures[architecture] = {}
92
+ for bundle_name in BUNDLES:
93
+ architectures[architecture][bundle_name] = stage_bundle(stage_dir, runtime_dir, architecture, bundle_name)
94
+
95
+ icu_asset = stage_versioned_copy(icu_source, runtime_dir, "icudt_minimal", ".dat")
96
+ assets = {
97
+ "icu": {
98
+ "url": icu_asset["url"],
99
+ "integrity": icu_asset["integrity"],
100
+ }
101
+ }
102
+
103
+ manifest_payload = {
104
+ "architectures": architectures,
105
+ "assets": assets,
106
+ }
107
+ manifest = {
108
+ "version": MANIFEST_VERSION,
109
+ "manifest_hash": short_content_hash(stable_json_bytes(manifest_payload)),
110
+ **manifest_payload,
111
+ }
112
+
113
+ manifest_path = out_dir / MANIFEST_FILENAME
114
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
115
+ print(f"Generated: {manifest_path}")
116
+ print(json.dumps(manifest, indent=2))
117
+ return 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ raise SystemExit(main())
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
6
+ PACKAGE_DIR="${REPO_ROOT}/v2/browser-bridge"
7
+ BRIDGE_PUBLIC_DIR="${REPO_ROOT}/public/v2/browser-bridge"
8
+ PACKAGE_DIST_DIR="${PACKAGE_DIR}/dist"
9
+ source "${REPO_ROOT}/v2/browser-bridge/scripts/font_assets.sh"
10
+
11
+ require_path() {
12
+ local path="$1"
13
+ local label="$2"
14
+ if [ ! -e "${path}" ]; then
15
+ echo "Missing ${label}: ${path}" >&2
16
+ echo "Run 'npm run build:v2:browser-bridge' (or at least 'npm run build:v2:browser-bridge-assets') first." >&2
17
+ exit 1
18
+ fi
19
+ }
20
+
21
+ require_path "${BRIDGE_PUBLIC_DIR}/bridge.js" "bridge bundle"
22
+ require_path "${BRIDGE_PUBLIC_DIR}/harness.js" "harness bundle"
23
+ require_path "${BRIDGE_PUBLIC_DIR}/effindom.v2.manifest.json" "runtime manifest"
24
+ require_path "${BRIDGE_PUBLIC_DIR}/runtime" "runtime artifact directory"
25
+
26
+ rm -rf "${PACKAGE_DIST_DIR}"
27
+ mkdir -p "${PACKAGE_DIST_DIR}"
28
+
29
+ cp "${BRIDGE_PUBLIC_DIR}/bridge.js" "${PACKAGE_DIST_DIR}/bridge.js"
30
+ cp "${BRIDGE_PUBLIC_DIR}/bridge.js.map" "${PACKAGE_DIST_DIR}/bridge.js.map"
31
+ cp "${BRIDGE_PUBLIC_DIR}/harness.js" "${PACKAGE_DIST_DIR}/harness.js"
32
+ cp "${BRIDGE_PUBLIC_DIR}/harness.js.map" "${PACKAGE_DIST_DIR}/harness.js.map"
33
+ cp "${BRIDGE_PUBLIC_DIR}/effindom.v2.manifest.json" "${PACKAGE_DIST_DIR}/effindom.v2.manifest.json"
34
+ cp "${BRIDGE_PUBLIC_DIR}/index.html" "${PACKAGE_DIST_DIR}/index.html"
35
+ cp -R "${BRIDGE_PUBLIC_DIR}/runtime" "${PACKAGE_DIST_DIR}/runtime"
36
+
37
+ if [ -f "${BRIDGE_PUBLIC_DIR}/icu-asset.json" ]; then
38
+ cp "${BRIDGE_PUBLIC_DIR}/icu-asset.json" "${PACKAGE_DIST_DIR}/icu-asset.json"
39
+ fi
40
+
41
+ rm -rf "${PACKAGE_DIST_DIR}/fonts"
42
+ copy_runtime_package_font_assets "${PACKAGE_DIST_DIR}/fonts"
@@ -0,0 +1,10 @@
1
+ import type { BridgeRuntime } from '../core-types';
2
+
3
+ export function commitIfVisualWork(runtime: BridgeRuntime): boolean {
4
+ if (!runtime.uiHasPendingVisualWork()) {
5
+ return false;
6
+ }
7
+ runtime.commitFrame();
8
+ return true;
9
+ }
10
+
@@ -0,0 +1,78 @@
1
+ const DEFAULT_LOGICAL_WIDTH = 320;
2
+ const DEFAULT_LOGICAL_HEIGHT = 220;
3
+
4
+ export function ensureCanvasLogicalSize(canvas: HTMLCanvasElement): void {
5
+ const rect = canvas.getBoundingClientRect();
6
+ if (rect.width <= 0 || rect.height <= 0) {
7
+ canvas.style.width = `${String(DEFAULT_LOGICAL_WIDTH)}px`;
8
+ canvas.style.height = `${String(DEFAULT_LOGICAL_HEIGHT)}px`;
9
+ }
10
+ }
11
+
12
+ export function getCanvasSizeSource(canvas: HTMLCanvasElement): HTMLElement | HTMLCanvasElement {
13
+ const source = canvas.closest('[data-effindom-canvas-size-source]');
14
+ return source instanceof HTMLElement ? source : canvas;
15
+ }
16
+
17
+ export function readCanvasLogicalSize(canvas: HTMLCanvasElement): { readonly width: number; readonly height: number } {
18
+ const sizeSource = getCanvasSizeSource(canvas);
19
+ if (sizeSource.clientWidth > 0 && sizeSource.clientHeight > 0) {
20
+ return {
21
+ width: sizeSource.clientWidth,
22
+ height: sizeSource.clientHeight,
23
+ };
24
+ }
25
+ const styleWidth = Number.parseFloat(canvas.style.width);
26
+ const styleHeight = Number.parseFloat(canvas.style.height);
27
+ if (Number.isFinite(styleWidth) && styleWidth > 0 && Number.isFinite(styleHeight) && styleHeight > 0) {
28
+ return { width: styleWidth, height: styleHeight };
29
+ }
30
+ return {
31
+ width: canvas.clientWidth || DEFAULT_LOGICAL_WIDTH,
32
+ height: canvas.clientHeight || DEFAULT_LOGICAL_HEIGHT,
33
+ };
34
+ }
35
+
36
+ export function getPointerPosition(
37
+ canvas: HTMLCanvasElement,
38
+ event: { readonly clientX: number; readonly clientY: number },
39
+ ): { readonly x: number; readonly y: number } {
40
+ const sizeSource = getCanvasSizeSource(canvas);
41
+ const rect = sizeSource.getBoundingClientRect();
42
+ const logicalSize = readCanvasLogicalSize(canvas);
43
+ const contentLeft = rect.left + sizeSource.clientLeft;
44
+ const contentTop = rect.top + sizeSource.clientTop;
45
+ const displayWidth = sizeSource.clientWidth || (rect.width - (sizeSource.clientLeft + sizeSource.clientLeft)) || DEFAULT_LOGICAL_WIDTH;
46
+ const displayHeight = sizeSource.clientHeight || (rect.height - (sizeSource.clientTop + sizeSource.clientTop)) || DEFAULT_LOGICAL_HEIGHT;
47
+ const x = displayWidth > 0 ? ((event.clientX - contentLeft) / displayWidth) * logicalSize.width : 0;
48
+ const y = displayHeight > 0 ? ((event.clientY - contentTop) / displayHeight) * logicalSize.height : 0;
49
+ return { x, y };
50
+ }
51
+
52
+ export function isPointerInsideCanvas(
53
+ canvas: HTMLCanvasElement,
54
+ event: { readonly clientX: number; readonly clientY: number },
55
+ ): boolean {
56
+ const rect = getCanvasSizeSource(canvas).getBoundingClientRect();
57
+ return event.clientX >= rect.left &&
58
+ event.clientX <= rect.right &&
59
+ event.clientY >= rect.top &&
60
+ event.clientY <= rect.bottom;
61
+ }
62
+
63
+ export function normalizeWheelDelta(event: WheelEvent, canvas: HTMLCanvasElement): { readonly x: number; readonly y: number } {
64
+ let deltaX = event.deltaX;
65
+ let deltaY = event.deltaY;
66
+ if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
67
+ deltaX *= 16.0;
68
+ deltaY *= 16.0;
69
+ } else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
70
+ const logicalSize = readCanvasLogicalSize(canvas);
71
+ deltaX *= logicalSize.width;
72
+ deltaY *= logicalSize.height;
73
+ }
74
+ if (event.shiftKey && Math.abs(deltaX) < 0.001 && Math.abs(deltaY) > 0.0) {
75
+ return { x: deltaY, y: 0.0 };
76
+ }
77
+ return { x: deltaX, y: deltaY };
78
+ }
@@ -0,0 +1,187 @@
1
+ import type { BridgeRuntime } from '../../core-types';
2
+ import type { BridgeInteractionState } from '../local-types';
3
+ import { DesktopFindDialogController } from '../find-dialog';
4
+ import { computeModifiers } from '../utils/encoding';
5
+ import { writeUtf8ToHeap } from '../utils/heap';
6
+
7
+ const UI_KEY_EVENT_DOWN = 1;
8
+ const UI_KEY_EVENT_UP = 2;
9
+
10
+ function currentInteractionTimeMs(): bigint {
11
+ return BigInt(Math.floor(performance.now()));
12
+ }
13
+
14
+ function isVerticalCanvasNavigationKey(key: string): boolean {
15
+ return key === 'ArrowUp' ||
16
+ key === 'ArrowDown' ||
17
+ key === 'PageUp' ||
18
+ key === 'PageDown' ||
19
+ key === 'Home' ||
20
+ key === 'End';
21
+ }
22
+
23
+ function isTextNavigationKey(key: string): boolean {
24
+ return key === 'ArrowLeft' ||
25
+ key === 'ArrowRight' ||
26
+ key === 'ArrowUp' ||
27
+ key === 'ArrowDown' ||
28
+ key === 'PageUp' ||
29
+ key === 'PageDown' ||
30
+ key === 'Home' ||
31
+ key === 'End';
32
+ }
33
+
34
+ export function installKeyAndWindowHandlers(
35
+ runtime: BridgeRuntime,
36
+ interactionState: BridgeInteractionState,
37
+ desktopFindDialog: DesktopFindDialogController,
38
+ ): () => void {
39
+ const { ui } = runtime;
40
+
41
+ const forwardKeyEvent = (type: number) => (event: KeyboardEvent): void => {
42
+ if (desktopFindDialog.consumeGlobalKeyEvent(event, type === UI_KEY_EVENT_DOWN ? 'down' : 'up')) {
43
+ return;
44
+ }
45
+ const modifiers = computeModifiers(event);
46
+ const activeTextHandle = interactionState.getActiveTextHandle();
47
+ const activeTextEditable = interactionState.getActiveTextEditable();
48
+ const activeTextMultiline = interactionState.getActiveTextMultiline();
49
+ const activeTextInputFocused = interactionState.isActiveTextInputFocused();
50
+ const activeEditableTextOwnsNativeEditingKey =
51
+ activeTextHandle !== null &&
52
+ activeTextEditable &&
53
+ activeTextInputFocused &&
54
+ !event.ctrlKey &&
55
+ !event.metaKey &&
56
+ !event.altKey &&
57
+ (
58
+ event.key.length === 1 ||
59
+ (activeTextMultiline && event.key === 'Enter') ||
60
+ event.key === 'Backspace' ||
61
+ event.key === 'Delete'
62
+ );
63
+ const activeEditableTextNeedsRuntimeEditingFallback =
64
+ activeTextHandle !== null &&
65
+ activeTextEditable &&
66
+ !activeTextInputFocused &&
67
+ !event.ctrlKey &&
68
+ !event.metaKey &&
69
+ !event.altKey &&
70
+ (
71
+ event.key.length === 1 ||
72
+ (activeTextMultiline && event.key === 'Enter') ||
73
+ event.key === 'Backspace' ||
74
+ event.key === 'Delete'
75
+ );
76
+ if (activeEditableTextOwnsNativeEditingKey) {
77
+ if (
78
+ type === UI_KEY_EVENT_DOWN &&
79
+ (event.key === 'Backspace' || event.key === 'Delete') &&
80
+ interactionState.applyActiveTextDeletion(event.key === 'Delete')
81
+ ) {
82
+ event.preventDefault();
83
+ }
84
+ return;
85
+ }
86
+ const activeTextOwnsRuntimeNavigationKey =
87
+ activeTextHandle !== null &&
88
+ isTextNavigationKey(event.key);
89
+ const activeReadonlyTextOwnsRuntimeKey =
90
+ activeTextHandle !== null &&
91
+ !activeTextEditable &&
92
+ (
93
+ event.key.length === 1 ||
94
+ event.key === 'Backspace' ||
95
+ event.key === 'Delete' ||
96
+ isTextNavigationKey(event.key)
97
+ );
98
+ if (
99
+ activeEditableTextNeedsRuntimeEditingFallback ||
100
+ activeTextOwnsRuntimeNavigationKey ||
101
+ activeReadonlyTextOwnsRuntimeKey
102
+ ) {
103
+ event.preventDefault();
104
+ }
105
+ ui._ui_set_interaction_time(currentInteractionTimeMs());
106
+ const shouldAlwaysPreventDefault =
107
+ event.key === 'Tab' ||
108
+ ((event.ctrlKey || event.metaKey) &&
109
+ (
110
+ event.key === 'c' || event.key === 'C' ||
111
+ event.key === 'a' || event.key === 'A' ||
112
+ event.key === 'x' || event.key === 'X' ||
113
+ event.key === 'v' || event.key === 'V' ||
114
+ event.key === 'z' || event.key === 'Z' ||
115
+ event.key === 'y' || event.key === 'Y'
116
+ ));
117
+ if (shouldAlwaysPreventDefault) {
118
+ event.preventDefault();
119
+ }
120
+ const heapString = writeUtf8ToHeap(ui, event.key);
121
+ let callbackHandled = false;
122
+ try {
123
+ ui._ui_on_key_event(type, heapString.ptr, heapString.len, modifiers);
124
+ if (
125
+ !activeEditableTextNeedsRuntimeEditingFallback &&
126
+ !activeTextOwnsRuntimeNavigationKey &&
127
+ !activeReadonlyTextOwnsRuntimeKey
128
+ ) {
129
+ callbackHandled = window.__effindomCallbacks?.onKeyEventWithKey?.(type, event.key, modifiers) === true;
130
+ }
131
+ } finally {
132
+ heapString.dispose();
133
+ }
134
+ if (
135
+ type === UI_KEY_EVENT_DOWN &&
136
+ (
137
+ callbackHandled ||
138
+ (!event.ctrlKey && !event.altKey && !event.metaKey && isVerticalCanvasNavigationKey(event.key))
139
+ )
140
+ ) {
141
+ event.preventDefault();
142
+ }
143
+ runtime.commitFrame();
144
+ if (interactionState.getActiveTextHandle() !== null && !interactionState.isActiveTextInputFocused()) {
145
+ interactionState.refocusActiveTextInput();
146
+ }
147
+ };
148
+
149
+ const handleKeyDown = forwardKeyEvent(UI_KEY_EVENT_DOWN);
150
+ const handleKeyUp = forwardKeyEvent(UI_KEY_EVENT_UP);
151
+ const keyListenerCapture = true;
152
+ const reconcileFindSelection = (): void => {
153
+ requestAnimationFrame(() => {
154
+ runtime.syncFindSelection(true);
155
+ });
156
+ };
157
+ const handleWindowBlur = (): void => {
158
+ if (interactionState.getActiveTextHandle() === null) {
159
+ reconcileFindSelection();
160
+ return;
161
+ }
162
+ ui._ui_request_focus(0n);
163
+ runtime.commitFrame();
164
+ reconcileFindSelection();
165
+ };
166
+ const handleWindowFocus = (): void => {
167
+ reconcileFindSelection();
168
+ };
169
+ const handleResize = (): void => {
170
+ runtime.updateCanvasSize();
171
+ runtime.commitFrame();
172
+ };
173
+
174
+ window.addEventListener('keydown', handleKeyDown, keyListenerCapture);
175
+ window.addEventListener('keyup', handleKeyUp, keyListenerCapture);
176
+ window.addEventListener('blur', handleWindowBlur);
177
+ window.addEventListener('focus', handleWindowFocus);
178
+ window.addEventListener('resize', handleResize);
179
+
180
+ return () => {
181
+ window.removeEventListener('keydown', handleKeyDown, keyListenerCapture);
182
+ window.removeEventListener('keyup', handleKeyUp, keyListenerCapture);
183
+ window.removeEventListener('blur', handleWindowBlur);
184
+ window.removeEventListener('focus', handleWindowFocus);
185
+ window.removeEventListener('resize', handleResize);
186
+ };
187
+ }