bunite-core 0.0.1 → 0.0.3

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/package.json +2 -2
  2. package/src/bun/core/App.ts +155 -15
  3. package/src/bun/core/BrowserView.ts +124 -44
  4. package/src/bun/core/BrowserWindow.ts +94 -47
  5. package/src/bun/core/Socket.ts +2 -1
  6. package/src/bun/core/SurfaceBrowserIPC.ts +65 -0
  7. package/src/bun/core/SurfaceManager.ts +201 -0
  8. package/src/bun/core/SurfaceRegistry.ts +60 -0
  9. package/src/bun/core/Utils.ts +275 -46
  10. package/src/bun/events/appEvents.ts +2 -1
  11. package/src/bun/events/webviewEvents.ts +1 -3
  12. package/src/bun/events/windowEvents.ts +2 -0
  13. package/src/bun/index.ts +4 -3
  14. package/src/bun/preload/inline.ts +19 -25
  15. package/src/bun/proc/native.ts +158 -122
  16. package/src/native/shared/callbacks.h +6 -6
  17. package/src/native/shared/ffi_exports.h +123 -119
  18. package/src/native/shared/log.h +24 -0
  19. package/src/native/shared/webview_storage.h +5 -5
  20. package/src/native/win/native_host_appres.cpp +258 -0
  21. package/src/native/win/native_host_cef.cpp +834 -0
  22. package/src/native/win/native_host_ffi.cpp +935 -0
  23. package/src/native/win/native_host_internal.h +285 -0
  24. package/src/native/win/native_host_runtime.cpp +286 -0
  25. package/src/native/win/native_host_utils.cpp +314 -0
  26. package/src/native/win/process_helper_win.cpp +126 -26
  27. package/src/preload/runtime.built.js +1 -1
  28. package/src/preload/runtime.ts +65 -42
  29. package/src/preload/tsconfig.json +2 -1
  30. package/src/preload/tsconfig.tsbuildinfo +1 -0
  31. package/src/preload/webviewElement.ts +307 -0
  32. package/src/shared/cefVersion.ts +2 -0
  33. package/src/shared/log.ts +40 -0
  34. package/src/shared/paths.ts +122 -52
  35. package/src/shared/rpc.ts +7 -1
  36. package/src/view/index.ts +8 -5
  37. package/src/native/shared/cef_response_filter.h +0 -116
  38. package/src/native/win/native_host.cpp +0 -2453
  39. package/src/types/config.ts +0 -29
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "bunite-core",
3
3
  "description": "Uniting UI and Bun",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
6
  "scripts": {
7
+ "setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
7
8
  "build:native:win": "cmake -S . -B build/win -DBUNITE_TARGET_ARCH=x64 && cmake --build build/win --config Release",
8
9
  "build:preload": "bun build src/preload/runtime.ts --outfile src/preload/runtime.built.js --target browser --minify"
9
10
  },
@@ -11,7 +12,6 @@
11
12
  ".": "./src/bun/index.ts",
12
13
  "./bun": "./src/bun/index.ts",
13
14
  "./view": "./src/view/index.ts",
14
- "./config": "./src/types/config.ts",
15
15
  "./shared/rpc": "./src/shared/rpc.ts",
16
16
  "./package.json": "./package.json"
17
17
  },
@@ -1,18 +1,32 @@
1
- import { join } from "node:path";
1
+ import { isAbsolute, join, resolve } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { getBaseDir } from "../../shared/paths";
4
+ import { dlopen, FFIType } from "bun:ffi";
2
5
  import { BuniteEvent } from "../events/event";
3
6
  import { buniteEventEmitter } from "../events/eventEmitter";
7
+ import { handleMessageBoxResponse } from "./Utils";
4
8
  import {
5
9
  getNativeLibrary,
6
10
  initNativeRuntime,
7
11
  getNativeRuntimeState,
8
12
  setRouteRequestHandler,
13
+ setNativeLogLevel,
9
14
  toCString,
10
15
  type NativeBootstrapOptions
11
16
  } from "../proc/native";
12
17
  import { attachGlobalIPCResolver, ensureRPCServer } from "./Socket";
18
+ import { BrowserWindow } from "./BrowserWindow";
19
+ import { getSurfaceIPCHandlers } from "./SurfaceManager";
20
+ import { getWebviewIPCHandlers } from "./SurfaceBrowserIPC";
21
+ import { log, logLevelToInt } from "../../shared/log";
22
+
23
+ import type { LogLevel } from "../../shared/log";
13
24
 
14
25
  type AppInitOptions = NativeBootstrapOptions & {
15
26
  userDataDir?: string;
27
+ cefDir?: string;
28
+ exitOnLastWindowClosed?: boolean;
29
+ logLevel?: LogLevel;
16
30
  };
17
31
 
18
32
  export type GlobalIPCHandler = (params: unknown, ctx: { viewId: number }) => unknown | Promise<unknown>;
@@ -21,14 +35,50 @@ class AppRuntime {
21
35
  private initPromise: Promise<void> | null = null;
22
36
  private stubKeepAliveTimer: ReturnType<typeof setInterval> | null = null;
23
37
  private readonly globalIPCHandlers = new Map<string, GlobalIPCHandler>();
38
+ private exitOnLastWindowClosed = true;
39
+ private quitting = false;
24
40
 
25
41
  async init(options: AppInitOptions = {}) {
26
42
  if (!this.initPromise) {
27
43
  this.initPromise = (async () => {
44
+ if (options.exitOnLastWindowClosed !== undefined) {
45
+ this.exitOnLastWindowClosed = options.exitOnLastWindowClosed;
46
+ }
47
+
48
+ if (options.logLevel) {
49
+ log.setLevel(options.logLevel);
50
+ }
51
+
52
+ if (options.cefDir) {
53
+ process.env.BUNITE_CEF_DIR = options.cefDir;
54
+ }
55
+
28
56
  if (options.userDataDir) {
29
57
  process.env.BUNITE_USER_DATA_DIR = options.userDataDir;
30
58
  } else if (!process.env.BUNITE_USER_DATA_DIR) {
31
- process.env.BUNITE_USER_DATA_DIR = join(process.cwd(), ".bunite");
59
+ // XDG_DATA_HOME takes priority on any platform, then OS convention
60
+ const appDataDir = process.env.XDG_DATA_HOME
61
+ ?? (process.platform === "win32"
62
+ ? (process.env.APPDATA ?? join(process.env.USERPROFILE ?? "", "AppData", "Roaming"))
63
+ : process.platform === "darwin"
64
+ ? join(process.env.HOME ?? "", "Library", "Application Support")
65
+ : join(process.env.HOME ?? "", ".local", "share"));
66
+ let name = "bunite-app";
67
+ try {
68
+ // Walk up from entry script to find nearest package.json
69
+ let dir = getBaseDir();
70
+ while (dir) {
71
+ const pkgPath = join(dir, "package.json");
72
+ if (existsSync(pkgPath)) {
73
+ name = JSON.parse(require("node:fs").readFileSync(pkgPath, "utf8")).name ?? name;
74
+ break;
75
+ }
76
+ const parent = resolve(dir, "..");
77
+ if (parent === dir) break;
78
+ dir = parent;
79
+ }
80
+ } catch {}
81
+ process.env.BUNITE_USER_DATA_DIR = join(appDataDir, name);
32
82
  }
33
83
 
34
84
  const runtime = await initNativeRuntime({
@@ -38,12 +88,48 @@ class AppRuntime {
38
88
  chromiumFlags: options.chromiumFlags
39
89
  });
40
90
 
91
+ if (options.logLevel && runtime.nativeLoaded) {
92
+ setNativeLogLevel(logLevelToInt(options.logLevel));
93
+ }
94
+
41
95
  attachGlobalIPCResolver((channel) => this.getGlobalIPCHandler(channel));
96
+
97
+ for (const [channel, handler] of getSurfaceIPCHandlers()) {
98
+ this.globalIPCHandlers.set(channel, handler);
99
+ }
100
+ for (const [channel, handler] of getWebviewIPCHandlers()) {
101
+ this.globalIPCHandlers.set(channel, handler);
102
+ }
103
+
104
+ this.globalIPCHandlers.set("__bunite:messageBoxResponse", (params) => {
105
+ const { requestId, response } = params as { requestId: number; response: number };
106
+ handleMessageBoxResponse(requestId, response);
107
+ return {};
108
+ });
109
+
42
110
  setRouteRequestHandler((requestId, path) => this.handleRouteRequest(requestId, path));
43
111
 
44
- // Replay view routes registered before init
45
- for (const path of this.viewHandlers.keys()) {
46
- getNativeLibrary()?.symbols.bunite_register_view_route(toCString(path));
112
+ // Replay appres routes registered before init
113
+ for (const path of this.appresHandlers.keys()) {
114
+ getNativeLibrary()?.symbols.bunite_register_appres_route(toCString(path));
115
+ }
116
+
117
+
118
+ if (this.exitOnLastWindowClosed && runtime.nativeLoaded) {
119
+ buniteEventEmitter.on("all-windows-closed", () => {
120
+ if (this.quitting) {
121
+ return;
122
+ }
123
+ queueMicrotask(() => {
124
+ if (this.quitting) {
125
+ return;
126
+ }
127
+ // Recheck: a new window may have been created since the event
128
+ if (BrowserWindow.getAll().length === 0) {
129
+ this.quit();
130
+ }
131
+ });
132
+ });
47
133
  }
48
134
 
49
135
  ensureRPCServer();
@@ -60,6 +146,11 @@ class AppRuntime {
60
146
  }
61
147
 
62
148
  on(name: string, handler: (payload: unknown) => void) {
149
+ if (name === "before-quit") {
150
+ // before-quit listeners receive the BuniteEvent directly so they can set event.response
151
+ buniteEventEmitter.on(name, handler);
152
+ return () => buniteEventEmitter.off(name, handler);
153
+ }
63
154
  const wrapped = (event: { data: unknown }) => handler(event.data);
64
155
  buniteEventEmitter.on(name, wrapped);
65
156
  return () => buniteEventEmitter.off(name, wrapped);
@@ -76,18 +167,31 @@ class AppRuntime {
76
167
  }
77
168
 
78
169
  if (!this.stubKeepAliveTimer) {
79
- console.warn("[bunite] Running without a native event loop. Keeping the process alive in stub mode.");
170
+ log.warn("Running without a native event loop. Keeping the process alive in stub mode.");
80
171
  this.stubKeepAliveTimer = setInterval(() => {}, 60_000);
81
172
  }
82
173
  }
83
174
 
84
175
  quit(code = 0) {
176
+ if (this.quitting) {
177
+ return;
178
+ }
179
+ this.quitting = true;
180
+
181
+ const event = buniteEventEmitter.events.app.beforeQuit({});
182
+ buniteEventEmitter.emitEvent(event);
183
+ if (event.responseWasSet && event.response?.allow === false) {
184
+ this.quitting = false;
185
+ return;
186
+ }
85
187
  if (this.stubKeepAliveTimer) {
86
188
  clearInterval(this.stubKeepAliveTimer);
87
189
  this.stubKeepAliveTimer = null;
88
190
  }
191
+ // bunite_quit() blocks until native shutdown completes or times out
89
192
  getNativeLibrary()?.symbols.bunite_quit();
90
- setTimeout(() => process.exit(code), 0);
193
+ process.exitCode = code;
194
+ process.exit(code);
91
195
  }
92
196
 
93
197
  handle(channel: string, handler: GlobalIPCHandler) {
@@ -110,23 +214,23 @@ class AppRuntime {
110
214
  return this.globalIPCHandlers.get(channel);
111
215
  }
112
216
 
113
- private readonly viewHandlers = new Map<string, () => string>();
217
+ private readonly appresHandlers = new Map<string, () => string>();
114
218
 
115
- getView(path: string, handler: () => string) {
116
- this.viewHandlers.set(path, handler);
117
- getNativeLibrary()?.symbols.bunite_register_view_route(toCString(path));
219
+ getAppRes(path: string, handler: () => string) {
220
+ this.appresHandlers.set(path, handler);
221
+ getNativeLibrary()?.symbols.bunite_register_appres_route(toCString(path));
118
222
  }
119
223
 
120
- removeView(path: string) {
121
- this.viewHandlers.delete(path);
122
- getNativeLibrary()?.symbols.bunite_unregister_view_route(toCString(path));
224
+ removeAppRes(path: string) {
225
+ this.appresHandlers.delete(path);
226
+ getNativeLibrary()?.symbols.bunite_unregister_appres_route(toCString(path));
123
227
  }
124
228
 
125
229
  /** @internal */
126
230
  handleRouteRequest(requestId: number, path: string) {
127
231
  let html: string;
128
232
  try {
129
- const handler = this.viewHandlers.get(path);
233
+ const handler = this.appresHandlers.get(path);
130
234
  html = handler ? handler() : "<html><body>No handler for: " + path + "</body></html>";
131
235
  } catch (error) {
132
236
  html = "<html><body>Route handler error: " + (error instanceof Error ? error.message : String(error)) + "</body></html>";
@@ -134,9 +238,45 @@ class AppRuntime {
134
238
  getNativeLibrary()?.symbols.bunite_complete_route_request(requestId, toCString(html));
135
239
  }
136
240
 
241
+ /** Resolve a path relative to the entry script (dev) or executable (compiled). */
242
+ resolve(relativePath: string): string {
243
+ if (isAbsolute(relativePath)) return relativePath;
244
+ return resolve(getBaseDir(), relativePath);
245
+ }
246
+
137
247
  get runtime() {
138
248
  return getNativeRuntimeState();
139
249
  }
250
+
251
+ get version(): string {
252
+ try {
253
+ const { createRequire } = require("node:module");
254
+ const req = createRequire(import.meta.url);
255
+ const pkg = req("bunite-core/package.json");
256
+ return pkg.version ?? "unknown";
257
+ } catch {
258
+ return "unknown";
259
+ }
260
+ }
261
+
262
+ private cachedCefVersion: string | null | undefined;
263
+
264
+ get cefVersion(): string | null {
265
+ if (this.cachedCefVersion !== undefined) return this.cachedCefVersion;
266
+ this.cachedCefVersion = null;
267
+ const arts = getNativeRuntimeState()?.artifacts;
268
+ if (!arts?.cefDir) return null;
269
+ const libcefPath = join(arts.cefDir, "libcef.dll");
270
+ if (!existsSync(libcefPath)) return null;
271
+ try {
272
+ const lib = dlopen(libcefPath, {
273
+ cef_version_info: { returns: FFIType.i32, args: [FFIType.i32] },
274
+ });
275
+ const v = (entry: number) => lib.symbols.cef_version_info(entry);
276
+ this.cachedCefVersion = `${v(0)}.${v(1)}.${v(2)}+chromium-${v(4)}.${v(5)}.${v(6)}.${v(7)}`;
277
+ } catch { /* leave as null */ }
278
+ return this.cachedCefVersion;
279
+ }
140
280
  }
141
281
 
142
282
  export const app = new AppRuntime();
@@ -1,11 +1,14 @@
1
+ import { ptr } from "bun:ffi";
1
2
  import { buildViewPreloadScript } from "../preload/inline";
2
- import type { Pointer } from "bun:ffi";
3
+ import { log } from "../../shared/log";
3
4
  import { buniteEventEmitter } from "../events/eventEmitter";
4
5
  import { defineBuniteRPC, type BuniteRPCConfig, type BuniteRPCSchema, type RPCWithTransport } from "../../shared/rpc";
5
- import { ensureNativeRuntime, getNativeLibrary, toCString } from "../proc/native";
6
+ import { ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady } from "../proc/native";
6
7
  import { attachBrowserViewRegistry, getRPCPort, sendMessageToView } from "./Socket";
7
8
  import { randomBytes } from "node:crypto";
8
- import { resolveDefaultViewsRoot } from "../../shared/paths";
9
+ import { resolveDefaultAppResRoot } from "../../shared/paths";
10
+ import { removeSurfacesForHostView } from "./SurfaceRegistry";
11
+ import { cancelPendingMessageBoxesForView } from "./Utils";
9
12
 
10
13
  const BrowserViewMap: Record<number, BrowserView<any>> = {};
11
14
  let nextWebviewId = 1;
@@ -14,9 +17,9 @@ export type BrowserViewOptions<T = undefined> = {
14
17
  url: string | null;
15
18
  html: string | null;
16
19
  preload: string | null;
17
- viewsRoot: string | null;
20
+ appresRoot: string | null;
21
+ preloadOrigins?: string[];
18
22
  partition: string | null;
19
- windowPtr?: Pointer | null;
20
23
  frame: {
21
24
  x: number;
22
25
  y: number;
@@ -34,9 +37,9 @@ const defaultOptions: BrowserViewOptions = {
34
37
  url: null,
35
38
  html: null,
36
39
  preload: null,
37
- viewsRoot: null,
40
+ appresRoot: null,
41
+ preloadOrigins: undefined,
38
42
  partition: null,
39
- windowPtr: null,
40
43
  frame: {
41
44
  x: 0,
42
45
  y: 0,
@@ -51,12 +54,14 @@ const defaultOptions: BrowserViewOptions = {
51
54
 
52
55
  export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
53
56
  id = nextWebviewId++;
54
- ptr: Pointer | null = null;
57
+ private nativeAttached = false;
58
+ private _readyPromise: Promise<void>;
55
59
  windowId: number;
56
60
  url: string | null;
57
61
  html: string | null;
58
62
  preload: string | null;
59
- viewsRoot: string | null;
63
+ appresRoot: string | null;
64
+ preloadOrigins?: string[];
60
65
  partition: string | null;
61
66
  frame: BrowserViewOptions["frame"];
62
67
  rpc?: T;
@@ -73,7 +78,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
73
78
  this.url = options.url ?? defaultOptions.url;
74
79
  this.html = options.html ?? defaultOptions.html;
75
80
  this.preload = options.preload ?? defaultOptions.preload;
76
- this.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? resolveDefaultViewsRoot();
81
+ this.appresRoot = options.appresRoot ?? defaultOptions.appresRoot ?? resolveDefaultAppResRoot();
82
+ this.preloadOrigins = options.preloadOrigins ?? defaultOptions.preloadOrigins;
77
83
  this.partition = options.partition ?? defaultOptions.partition;
78
84
  this.frame = options.frame ?? defaultOptions.frame;
79
85
  this.rpc = options.rpc;
@@ -86,12 +92,12 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
86
92
  throw new Error("sandboxed BrowserView is not implemented in Bunite Windows Phase 1 yet.");
87
93
  }
88
94
  if (this.partition) {
89
- console.warn("[bunite] BrowserView.partition is not implemented in Bunite Windows Phase 1 yet.");
95
+ log.warn("BrowserView.partition is not implemented in Bunite Windows Phase 1 yet.");
90
96
  }
91
97
 
92
98
  const preloadScript = buildViewPreloadScript({
93
99
  preload: this.preload,
94
- viewsRoot: this.viewsRoot,
100
+ appresRoot: this.appresRoot,
95
101
  webviewId: this.id,
96
102
  rpcSocketPort: getRPCPort(),
97
103
  secretKey: this.secretKey
@@ -99,22 +105,51 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
99
105
 
100
106
  BrowserViewMap[this.id] = this;
101
107
  this.rpc?.setTransport(this.createTransport());
102
- this.ptr =
108
+ // Register ready waiter BEFORE native create — OnAfterCreated can fire
109
+ // on the CEF UI thread before bunite_view_create returns to JS.
110
+ this._readyPromise = waitForViewReady(this.id);
111
+ this.nativeAttached =
103
112
  getNativeLibrary()?.symbols.bunite_view_create(
104
113
  this.id,
105
- options.windowPtr ?? null,
114
+ this.windowId,
106
115
  toCString(this.url ?? ""),
107
116
  toCString(this.html ?? ""),
108
117
  toCString(preloadScript),
109
- toCString(this.viewsRoot ?? ""),
118
+ toCString(this.appresRoot ?? ""),
110
119
  toCString(this.navigationRules ? JSON.stringify(this.navigationRules) : ""),
111
120
  this.frame.x,
112
121
  this.frame.y,
113
122
  this.frame.width,
114
123
  this.frame.height,
115
124
  this.autoResize,
116
- this.sandbox
117
- ) ?? null;
125
+ this.sandbox,
126
+ toCString(this.preloadOrigins ? JSON.stringify(this.preloadOrigins) : "")
127
+ ) ?? false;
128
+
129
+ if (this.nativeAttached) {
130
+ // Clean up owned surfaces when this view navigates (page refresh/navigation
131
+ // destroys the JS context without firing disconnectedCallback).
132
+ // Uses did-navigate (not will-navigate) because will-navigate fires even
133
+ // when navigation is denied by navigationRules.
134
+ this.on("did-navigate", (event: any) => {
135
+ this.url = event.data?.detail ?? this.url;
136
+ cancelPendingMessageBoxesForView(this.id);
137
+ removeSurfacesForHostView(this.id);
138
+ });
139
+ } else {
140
+ cancelWaitForViewReady(this.id);
141
+ this._readyPromise = Promise.reject(new Error("Native view creation failed"));
142
+ this._readyPromise.catch(() => {}); // prevent unhandled rejection
143
+ }
144
+ }
145
+
146
+ whenReady(timeoutMs = 8000): Promise<void> {
147
+ return Promise.race([
148
+ this._readyPromise,
149
+ new Promise<never>((_, reject) =>
150
+ setTimeout(() => reject(new Error(`Browser creation timed out for view ${this.id}`)), timeoutMs)
151
+ )
152
+ ]);
118
153
  }
119
154
 
120
155
  static getById(id: number) {
@@ -155,84 +190,130 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
155
190
 
156
191
  setAnchor(mode: "none" | "fill" | "top" | "below-top", inset = 0) {
157
192
  const modeInt = { none: 0, fill: 1, top: 2, "below-top": 3 }[mode];
158
- if (this.ptr) {
159
- getNativeLibrary()?.symbols.bunite_view_set_anchor(this.ptr, modeInt, inset);
193
+ if (this.nativeAttached) {
194
+ getNativeLibrary()?.symbols.bunite_view_set_anchor(this.id, modeInt, inset);
195
+ }
196
+ }
197
+
198
+ executeJavaScript(script: string) {
199
+ if (this.nativeAttached) {
200
+ getNativeLibrary()?.symbols.bunite_view_execute_javascript(this.id, toCString(script));
160
201
  }
161
202
  }
162
203
 
163
204
  goBack() {
164
- if (this.ptr) {
165
- getNativeLibrary()?.symbols.bunite_view_go_back(this.ptr);
205
+ if (this.nativeAttached) {
206
+ getNativeLibrary()?.symbols.bunite_view_go_back(this.id);
166
207
  }
167
208
  }
168
209
 
169
210
  reload() {
170
- if (this.ptr) {
171
- getNativeLibrary()?.symbols.bunite_view_reload(this.ptr);
211
+ if (this.nativeAttached) {
212
+ getNativeLibrary()?.symbols.bunite_view_reload(this.id);
172
213
  }
173
214
  }
174
215
 
175
216
  setVisible(visible: boolean) {
176
- if (this.ptr) {
177
- getNativeLibrary()?.symbols.bunite_view_set_visible(this.ptr, visible);
217
+ if (this.nativeAttached) {
218
+ getNativeLibrary()?.symbols.bunite_view_set_visible(this.id, visible);
219
+ }
220
+ }
221
+
222
+ setInputPassthrough(passthrough: boolean) {
223
+ if (this.nativeAttached) {
224
+ getNativeLibrary()?.symbols.bunite_view_set_input_passthrough(this.id, passthrough);
225
+ }
226
+ }
227
+
228
+ setMaskRegion(rects: Array<{ x: number; y: number; w: number; h: number }>) {
229
+ if (!this.nativeAttached) return;
230
+ if (rects.length === 0) {
231
+ getNativeLibrary()?.symbols.bunite_view_set_mask_region(this.id, null as any, 0);
232
+ return;
233
+ }
234
+ const buf = new Float64Array(rects.length * 4);
235
+ for (let i = 0; i < rects.length; i++) {
236
+ buf[i * 4] = rects[i].x;
237
+ buf[i * 4 + 1] = rects[i].y;
238
+ buf[i * 4 + 2] = rects[i].w;
239
+ buf[i * 4 + 3] = rects[i].h;
240
+ }
241
+ getNativeLibrary()?.symbols.bunite_view_set_mask_region(
242
+ this.id, ptr(buf.buffer), rects.length
243
+ );
244
+ }
245
+
246
+ bringToFront() {
247
+ if (this.nativeAttached) {
248
+ getNativeLibrary()?.symbols.bunite_view_bring_to_front(this.id);
178
249
  }
179
250
  }
180
251
 
181
252
  setBounds(x: number, y: number, width: number, height: number) {
182
253
  this.frame = { x, y, width, height };
183
- if (this.ptr) {
184
- getNativeLibrary()?.symbols.bunite_view_set_bounds(this.ptr, x, y, width, height);
254
+ if (this.nativeAttached) {
255
+ getNativeLibrary()?.symbols.bunite_view_set_bounds(this.id, x, y, width, height);
256
+ }
257
+ }
258
+
259
+ /** Fire-and-forget setBounds — does not block on the UI thread. */
260
+ setBoundsAsync(x: number, y: number, width: number, height: number) {
261
+ this.frame = { x, y, width, height };
262
+ if (this.nativeAttached) {
263
+ getNativeLibrary()?.symbols.bunite_view_set_bounds_async(this.id, x, y, width, height);
185
264
  }
186
265
  }
187
266
 
188
267
  loadURL(url: string) {
189
268
  this.url = url;
190
- if (this.ptr) {
191
- getNativeLibrary()?.symbols.bunite_view_load_url(this.ptr, toCString(url));
269
+ if (this.nativeAttached) {
270
+ getNativeLibrary()?.symbols.bunite_view_load_url(this.id, toCString(url));
192
271
  }
193
272
  }
194
273
 
195
274
  loadHTML(html: string) {
196
275
  this.html = html;
197
- if (this.ptr) {
198
- getNativeLibrary()?.symbols.bunite_view_load_html(this.ptr, toCString(html));
276
+ if (this.nativeAttached) {
277
+ getNativeLibrary()?.symbols.bunite_view_load_html(this.id, toCString(html));
199
278
  }
200
279
  }
201
280
 
202
281
  remove() {
203
- if (this.ptr) {
204
- getNativeLibrary()?.symbols.bunite_view_remove(this.ptr);
282
+ if (this.nativeAttached) {
283
+ getNativeLibrary()?.symbols.bunite_view_remove(this.id);
205
284
  }
206
285
  this.detachFromNative();
207
286
  }
208
287
 
209
288
  openDevTools() {
210
- if (this.ptr) {
211
- getNativeLibrary()?.symbols.bunite_view_open_devtools(this.ptr);
289
+ if (this.nativeAttached) {
290
+ getNativeLibrary()?.symbols.bunite_view_open_devtools(this.id);
212
291
  }
213
292
  }
214
293
 
215
294
  closeDevTools() {
216
- if (this.ptr) {
217
- getNativeLibrary()?.symbols.bunite_view_close_devtools(this.ptr);
295
+ if (this.nativeAttached) {
296
+ getNativeLibrary()?.symbols.bunite_view_close_devtools(this.id);
218
297
  }
219
298
  }
220
299
 
221
300
  toggleDevTools() {
222
- if (this.ptr) {
223
- getNativeLibrary()?.symbols.bunite_view_toggle_devtools(this.ptr);
301
+ if (this.nativeAttached) {
302
+ getNativeLibrary()?.symbols.bunite_view_toggle_devtools(this.id);
224
303
  }
225
304
  }
226
305
 
227
306
  detachFromNative() {
228
- this.ptr = null;
307
+ cancelPendingMessageBoxesForView(this.id);
308
+ removeSurfacesForHostView(this.id);
309
+ cancelWaitForViewReady(this.id);
310
+ this.nativeAttached = false;
229
311
  for (const eventName of [
230
312
  "will-navigate",
231
313
  "did-navigate",
232
314
  "dom-ready",
233
315
  "new-window-open",
234
- "permission-requested",
235
- "message-box-response"
316
+ "permission-requested"
236
317
  ]) {
237
318
  buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
238
319
  }
@@ -245,8 +326,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
245
326
  | "did-navigate"
246
327
  | "dom-ready"
247
328
  | "new-window-open"
248
- | "permission-requested"
249
- | "message-box-response",
329
+ | "permission-requested",
250
330
  handler: (event: unknown) => void
251
331
  ) {
252
332
  const specificName = `${name}-${this.id}`;