bunite-core 0.0.1 → 0.0.4

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 (40) hide show
  1. package/package.json +3 -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/shared/webviewPolyfill.ts +80 -0
  37. package/src/view/index.ts +8 -5
  38. package/src/native/shared/cef_response_filter.h +0 -116
  39. package/src/native/win/native_host.cpp +0 -2453
  40. package/src/types/config.ts +0 -29
@@ -1,11 +1,11 @@
1
- import type { Pointer } from "bun:ffi";
1
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
2
2
  import { BuniteEvent } from "../events/event";
3
3
  import { buniteEventEmitter } from "../events/eventEmitter";
4
4
  import { ensureNativeRuntime, getNativeLibrary, toCString } from "../proc/native";
5
5
  import { BrowserView, type BrowserViewOptions } from "./BrowserView";
6
6
  import type { RPCWithTransport } from "../../shared/rpc";
7
7
  import { getNextWindowId } from "./windowIds";
8
- import { resolveDefaultViewsRoot } from "../../shared/paths";
8
+ import { getBaseDir, resolveDefaultAppResRoot } from "../../shared/paths";
9
9
 
10
10
  export type WindowOptionsType<T = undefined> = {
11
11
  title: string;
@@ -20,7 +20,8 @@ export type WindowOptionsType<T = undefined> = {
20
20
  url: string | null;
21
21
  html: string | null;
22
22
  preload: string | null;
23
- viewsRoot: string | null;
23
+ appresRoot: string | null;
24
+ preloadOrigins?: string[];
24
25
  rpc?: T;
25
26
  titleBarStyle: "hidden" | "hiddenInset" | "default";
26
27
  transparent: boolean;
@@ -40,7 +41,8 @@ const defaultOptions: WindowOptionsType = {
40
41
  url: null,
41
42
  html: null,
42
43
  preload: null,
43
- viewsRoot: null,
44
+ appresRoot: null,
45
+ preloadOrigins: undefined,
44
46
  titleBarStyle: "default",
45
47
  transparent: false,
46
48
  hidden: false,
@@ -50,15 +52,22 @@ const defaultOptions: WindowOptionsType = {
50
52
 
51
53
  const BrowserWindowMap: Record<number, BrowserWindow<any>> = {};
52
54
 
55
+ let lastFocusedWindowId: number | null = null;
56
+
57
+ export function getLastFocusedWindowId(): number | null {
58
+ return lastFocusedWindowId;
59
+ }
60
+
53
61
  export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
54
62
  id = getNextWindowId();
55
- ptr: Pointer | null = null;
63
+ private nativeAttached = false;
56
64
  title: string;
57
65
  frame: WindowOptionsType["frame"];
58
66
  url: string | null;
59
67
  html: string | null;
60
68
  preload: string | null;
61
- viewsRoot: string | null;
69
+ appresRoot: string | null;
70
+ preloadOrigins?: string[];
62
71
  titleBarStyle: WindowOptionsType["titleBarStyle"];
63
72
  transparent: boolean;
64
73
  hidden: boolean;
@@ -106,12 +115,16 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
106
115
  return;
107
116
  }
108
117
  this.closed = true;
109
- this.ptr = null;
118
+ this.nativeAttached = false;
119
+ if (lastFocusedWindowId === this.id) {
120
+ lastFocusedWindowId = null;
121
+ }
110
122
  BrowserView.getById(this.webviewId)?.detachFromNative();
111
123
  delete BrowserWindowMap[this.id];
112
124
  buniteEventEmitter.off(`move-${this.id}`, this.handleNativeMove);
113
125
  buniteEventEmitter.off(`resize-${this.id}`, this.handleNativeResize);
114
126
  buniteEventEmitter.off(`close-${this.id}`, this.handleNativeClose);
127
+ buniteEventEmitter.removeAllListeners(`close-requested-${this.id}`);
115
128
  };
116
129
 
117
130
  constructor(options: Partial<WindowOptionsType<T>> = {}) {
@@ -119,10 +132,27 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
119
132
 
120
133
  this.title = options.title ?? defaultOptions.title;
121
134
  this.frame = { ...defaultOptions.frame, ...options.frame };
122
- this.url = options.url ?? defaultOptions.url;
123
135
  this.html = options.html ?? defaultOptions.html;
124
136
  this.preload = options.preload ?? defaultOptions.preload;
125
- this.viewsRoot = options.viewsRoot ?? defaultOptions.viewsRoot ?? resolveDefaultViewsRoot();
137
+ this.preloadOrigins = options.preloadOrigins ?? defaultOptions.preloadOrigins;
138
+
139
+ const baseDir = getBaseDir();
140
+
141
+ let url = options.url ?? defaultOptions.url;
142
+ let appresRoot = options.appresRoot ?? defaultOptions.appresRoot;
143
+ if (appresRoot && !isAbsolute(appresRoot)) {
144
+ appresRoot = resolve(baseDir, appresRoot);
145
+ }
146
+ if (url && !url.includes("://")) {
147
+ const resolved = isAbsolute(url) ? url : resolve(baseDir, url);
148
+ if (!appresRoot) {
149
+ appresRoot = dirname(resolved);
150
+ }
151
+ const rel = relative(appresRoot!, resolved).replaceAll(sep, "/");
152
+ url = `appres://app.internal/${rel}`;
153
+ }
154
+ this.url = url;
155
+ this.appresRoot = appresRoot ?? resolveDefaultAppResRoot();
126
156
  this.titleBarStyle = options.titleBarStyle ?? defaultOptions.titleBarStyle;
127
157
  this.transparent = options.transparent ?? defaultOptions.transparent;
128
158
  this.hidden = options.hidden ?? defaultOptions.hidden!;
@@ -130,7 +160,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
130
160
  this.sandbox = options.sandbox ?? defaultOptions.sandbox;
131
161
 
132
162
  const native = getNativeLibrary();
133
- this.ptr =
163
+ this.nativeAttached =
134
164
  native?.symbols.bunite_window_create(
135
165
  this.id,
136
166
  this.frame.x,
@@ -143,9 +173,10 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
143
173
  this.hidden,
144
174
  Boolean(this.frame.minimized),
145
175
  Boolean(this.frame.maximized)
146
- ) ?? null;
176
+ ) ?? false;
147
177
 
148
178
  BrowserWindowMap[this.id] = this;
179
+ buniteEventEmitter.on(`focus-${this.id}`, () => { lastFocusedWindowId = this.id; });
149
180
  buniteEventEmitter.on(`move-${this.id}`, this.handleNativeMove);
150
181
  buniteEventEmitter.on(`resize-${this.id}`, this.handleNativeResize);
151
182
  buniteEventEmitter.on(`close-${this.id}`, this.handleNativeClose);
@@ -154,8 +185,8 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
154
185
  url: this.url,
155
186
  html: this.html,
156
187
  preload: this.preload,
157
- viewsRoot: this.viewsRoot,
158
- windowPtr: this.ptr,
188
+ appresRoot: this.appresRoot,
189
+ preloadOrigins: this.preloadOrigins,
159
190
  frame: {
160
191
  x: 0,
161
192
  y: 0,
@@ -175,14 +206,18 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
175
206
  return BrowserWindowMap[id];
176
207
  }
177
208
 
209
+ static getAll() {
210
+ return Object.values(BrowserWindowMap);
211
+ }
212
+
178
213
  get webview() {
179
214
  return BrowserView.getById(this.webviewId) as BrowserView<T>;
180
215
  }
181
216
 
182
217
  show() {
183
218
  this.hidden = false;
184
- if (this.ptr) {
185
- getNativeLibrary()?.symbols.bunite_window_show(this.ptr);
219
+ if (this.nativeAttached) {
220
+ getNativeLibrary()?.symbols.bunite_window_show(this.id);
186
221
  }
187
222
  }
188
223
 
@@ -190,25 +225,37 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
190
225
  if (this.closed) {
191
226
  return;
192
227
  }
193
- this.closed = true;
194
- const hadNativePtr = Boolean(this.ptr);
195
- if (this.ptr) {
196
- getNativeLibrary()?.symbols.bunite_window_close(this.ptr);
197
- this.ptr = null;
228
+ if (this.nativeAttached) {
229
+ // Triggers WM_CLOSE → "close-requested" event → vetoable
230
+ getNativeLibrary()?.symbols.bunite_window_close(this.id);
198
231
  } else {
199
- BrowserView.getById(this.webviewId)?.remove();
232
+ this.destroy();
233
+ }
234
+ }
235
+
236
+ destroy() {
237
+ if (this.closed) {
238
+ return;
239
+ }
240
+ this.closed = true;
241
+ BrowserView.getById(this.webviewId)?.detachFromNative();
242
+ const hadNative = this.nativeAttached;
243
+ if (this.nativeAttached) {
244
+ getNativeLibrary()?.symbols.bunite_window_destroy(this.id);
245
+ this.nativeAttached = false;
200
246
  }
201
247
  delete BrowserWindowMap[this.id];
202
248
  buniteEventEmitter.off(`move-${this.id}`, this.handleNativeMove);
203
249
  buniteEventEmitter.off(`resize-${this.id}`, this.handleNativeResize);
204
250
  buniteEventEmitter.off(`close-${this.id}`, this.handleNativeClose);
205
- if (!hadNativePtr) {
251
+ buniteEventEmitter.removeAllListeners(`close-requested-${this.id}`);
252
+ if (!hadNative) {
206
253
  buniteEventEmitter.emitEvent(buniteEventEmitter.events.window.close({ id: this.id }), this.id);
207
254
  }
208
255
  }
209
256
 
210
257
  maximize() {
211
- if (!this.ptr) {
258
+ if (!this.nativeAttached) {
212
259
  this.frame.maximized = true;
213
260
  this.frame.minimized = false;
214
261
  return;
@@ -219,13 +266,13 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
219
266
  return;
220
267
  }
221
268
 
222
- native.symbols.bunite_window_maximize(this.ptr);
223
- this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
224
- this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
269
+ native.symbols.bunite_window_maximize(this.id);
270
+ this.frame.minimized = native.symbols.bunite_window_is_minimized(this.id);
271
+ this.frame.maximized = native.symbols.bunite_window_is_maximized(this.id);
225
272
  }
226
273
 
227
274
  unmaximize() {
228
- if (!this.ptr) {
275
+ if (!this.nativeAttached) {
229
276
  this.frame.maximized = false;
230
277
  return;
231
278
  }
@@ -235,23 +282,23 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
235
282
  return;
236
283
  }
237
284
 
238
- native.symbols.bunite_window_unmaximize(this.ptr);
239
- this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
240
- this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
285
+ native.symbols.bunite_window_unmaximize(this.id);
286
+ this.frame.minimized = native.symbols.bunite_window_is_minimized(this.id);
287
+ this.frame.maximized = native.symbols.bunite_window_is_maximized(this.id);
241
288
  }
242
289
 
243
290
  isMaximized() {
244
- if (!this.ptr) {
291
+ if (!this.nativeAttached) {
245
292
  return Boolean(this.frame.maximized);
246
293
  }
247
294
 
248
- const maximized = getNativeLibrary()?.symbols.bunite_window_is_maximized(this.ptr) ?? false;
295
+ const maximized = getNativeLibrary()?.symbols.bunite_window_is_maximized(this.id) ?? false;
249
296
  this.frame.maximized = maximized;
250
297
  return maximized;
251
298
  }
252
299
 
253
300
  minimize() {
254
- if (!this.ptr) {
301
+ if (!this.nativeAttached) {
255
302
  this.restoreMaximizedAfterMinimize = Boolean(this.frame.maximized);
256
303
  this.frame.minimized = true;
257
304
  this.frame.maximized = false;
@@ -263,13 +310,13 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
263
310
  return;
264
311
  }
265
312
 
266
- native.symbols.bunite_window_minimize(this.ptr);
267
- this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
268
- this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
313
+ native.symbols.bunite_window_minimize(this.id);
314
+ this.frame.minimized = native.symbols.bunite_window_is_minimized(this.id);
315
+ this.frame.maximized = native.symbols.bunite_window_is_maximized(this.id);
269
316
  }
270
317
 
271
318
  unminimize() {
272
- if (!this.ptr) {
319
+ if (!this.nativeAttached) {
273
320
  this.frame.minimized = false;
274
321
  this.frame.maximized = this.restoreMaximizedAfterMinimize;
275
322
  this.restoreMaximizedAfterMinimize = false;
@@ -281,32 +328,32 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
281
328
  return;
282
329
  }
283
330
 
284
- native.symbols.bunite_window_unminimize(this.ptr);
285
- this.frame.minimized = native.symbols.bunite_window_is_minimized(this.ptr);
286
- this.frame.maximized = native.symbols.bunite_window_is_maximized(this.ptr);
331
+ native.symbols.bunite_window_unminimize(this.id);
332
+ this.frame.minimized = native.symbols.bunite_window_is_minimized(this.id);
333
+ this.frame.maximized = native.symbols.bunite_window_is_maximized(this.id);
287
334
  }
288
335
 
289
336
  isMinimized() {
290
- if (!this.ptr) {
337
+ if (!this.nativeAttached) {
291
338
  return Boolean(this.frame.minimized);
292
339
  }
293
340
 
294
- const minimized = getNativeLibrary()?.symbols.bunite_window_is_minimized(this.ptr) ?? false;
341
+ const minimized = getNativeLibrary()?.symbols.bunite_window_is_minimized(this.id) ?? false;
295
342
  this.frame.minimized = minimized;
296
343
  return minimized;
297
344
  }
298
345
 
299
346
  setTitle(title: string) {
300
347
  this.title = title;
301
- if (this.ptr) {
302
- getNativeLibrary()?.symbols.bunite_window_set_title(this.ptr, toCString(title));
348
+ if (this.nativeAttached) {
349
+ getNativeLibrary()?.symbols.bunite_window_set_title(this.id, toCString(title));
303
350
  }
304
351
  }
305
352
 
306
353
  setFrame(x: number, y: number, width: number, height: number) {
307
354
  this.frame = { ...this.frame, x, y, width, height };
308
- if (this.ptr) {
309
- getNativeLibrary()?.symbols.bunite_window_set_frame(this.ptr, x, y, width, height);
355
+ if (this.nativeAttached) {
356
+ getNativeLibrary()?.symbols.bunite_window_set_frame(this.id, x, y, width, height);
310
357
  }
311
358
  }
312
359
 
@@ -314,7 +361,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
314
361
  return this.frame;
315
362
  }
316
363
 
317
- on(name: "close" | "focus" | "blur" | "move" | "resize", handler: (event: unknown) => void) {
364
+ on(name: "close-requested" | "close" | "focus" | "blur" | "move" | "resize", handler: (event: unknown) => void) {
318
365
  const specificName = `${name}-${this.id}`;
319
366
  buniteEventEmitter.on(specificName, handler);
320
367
  return () => buniteEventEmitter.off(specificName, handler);
@@ -3,6 +3,7 @@ import type { BrowserView } from "./BrowserView";
3
3
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
4
4
  import type { RPCPacket, RPCRequestPacket } from "../../shared/rpc";
5
5
  import type { GlobalIPCHandler } from "./App";
6
+ import { log } from "../../shared/log";
6
7
  import {
7
8
  asUint8Array,
8
9
  createEncryptedRPCFrame,
@@ -114,7 +115,7 @@ export function ensureRPCServer() {
114
115
 
115
116
  view.handleIncomingRPC(packet);
116
117
  } catch (error) {
117
- console.error("[bunite] Failed to parse RPC payload", error);
118
+ log.error("Failed to parse RPC payload", error);
118
119
  }
119
120
  }
120
121
  }
@@ -0,0 +1,65 @@
1
+ import { getOwnedSurface } from "./SurfaceRegistry";
2
+ import { sendMessageToView } from "./Socket";
3
+ import { onSurfaceInit } from "./SurfaceManager";
4
+
5
+ import type { GlobalIPCHandler } from "./App";
6
+
7
+ // --- did-navigate forwarding ---
8
+
9
+ onSurfaceInit((surfaceId, hostViewId, view) => {
10
+ view.on("did-navigate", (event: any) => {
11
+ sendMessageToView(hostViewId, {
12
+ type: "event",
13
+ channel: "__bunite:webview.didNavigate",
14
+ data: { surfaceId, url: event.data.detail }
15
+ });
16
+ });
17
+ });
18
+
19
+ // --- Helpers ---
20
+
21
+ function assertNum(v: unknown, label: string): number {
22
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`Invalid ${label}`);
23
+ return v;
24
+ }
25
+
26
+ function assertStr(v: unknown, label: string): string {
27
+ if (typeof v !== "string") throw new Error(`Invalid ${label}`);
28
+ return v;
29
+ }
30
+
31
+ function assertObj(v: unknown, label: string): Record<string, unknown> {
32
+ if (!v || typeof v !== "object") throw new Error(`Invalid ${label}`);
33
+ return v as Record<string, unknown>;
34
+ }
35
+
36
+ // --- Handlers ---
37
+
38
+ const handleGoBack: GlobalIPCHandler = async (params, ctx) => {
39
+ const record = getOwnedSurface(assertNum(assertObj(params, "p").surfaceId, "surfaceId"), ctx);
40
+ if (record) record.view.goBack();
41
+ return {};
42
+ };
43
+
44
+ const handleReload: GlobalIPCHandler = async (params, ctx) => {
45
+ const record = getOwnedSurface(assertNum(assertObj(params, "p").surfaceId, "surfaceId"), ctx);
46
+ if (record) record.view.reload();
47
+ return {};
48
+ };
49
+
50
+ const handleNavigate: GlobalIPCHandler = async (params, ctx) => {
51
+ const p = assertObj(params, "webview.navigate params");
52
+ const surfaceId = assertNum(p.surfaceId, "surfaceId");
53
+ const url = assertStr(p.url, "url");
54
+ const record = getOwnedSurface(surfaceId, ctx);
55
+ if (record) record.view.loadURL(url);
56
+ return {};
57
+ };
58
+
59
+ export function getWebviewIPCHandlers(): Map<string, GlobalIPCHandler> {
60
+ return new Map([
61
+ ["__bunite:webview.goBack", handleGoBack],
62
+ ["__bunite:webview.reload", handleReload],
63
+ ["__bunite:webview.navigate", handleNavigate]
64
+ ]);
65
+ }
@@ -0,0 +1,201 @@
1
+ import { BrowserView } from "./BrowserView";
2
+ import {
3
+ trackSurface, untrackSurface, getOwnedSurface,
4
+ getHostSurfaceIds, getSurfaceRecord,
5
+ MAX_SURFACES_PER_HOST
6
+ } from "./SurfaceRegistry";
7
+
8
+ import type { GlobalIPCHandler } from "./App";
9
+
10
+ // --- Helpers ---
11
+
12
+ function applyHostOffset(hostView: BrowserView, x: number, y: number) {
13
+ return { x: x + hostView.frame.x, y: y + hostView.frame.y };
14
+ }
15
+
16
+ function assertNum(v: unknown, label: string): number {
17
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`Invalid ${label}`);
18
+ return v;
19
+ }
20
+
21
+ function assertBool(v: unknown, label: string): boolean {
22
+ if (typeof v !== "boolean") throw new Error(`Invalid ${label}`);
23
+ return v;
24
+ }
25
+
26
+ function assertStr(v: unknown, label: string): string {
27
+ if (typeof v !== "string") throw new Error(`Invalid ${label}`);
28
+ return v;
29
+ }
30
+
31
+ function assertObj(v: unknown, label: string): Record<string, unknown> {
32
+ if (!v || typeof v !== "object") throw new Error(`Invalid ${label}`);
33
+ return v as Record<string, unknown>;
34
+ }
35
+
36
+ // --- Surface lifecycle callbacks (called by SurfaceBrowserIPC after init) ---
37
+
38
+ type SurfaceInitCallback = (surfaceId: number, hostViewId: number, view: BrowserView) => void;
39
+ const initCallbacks: SurfaceInitCallback[] = [];
40
+
41
+ export function onSurfaceInit(cb: SurfaceInitCallback) {
42
+ initCallbacks.push(cb);
43
+ }
44
+
45
+ // --- Handlers ---
46
+
47
+ const handleSurfaceInit: GlobalIPCHandler = async (params, ctx) => {
48
+ const p = assertObj(params, "surface.init params");
49
+ const src = assertStr(p.src, "src");
50
+ const x = assertNum(p.x, "x");
51
+ const y = assertNum(p.y, "y");
52
+ const width = assertNum(p.width, "width");
53
+ const height = assertNum(p.height, "height");
54
+ const hidden = typeof p.hidden === "boolean" ? p.hidden : false;
55
+
56
+ const hostView = BrowserView.getById(ctx.viewId);
57
+ if (!hostView) throw new Error(`Host view not found: ${ctx.viewId}`);
58
+ if (!hostView.windowId) throw new Error(`Host window not found for view: ${ctx.viewId}`);
59
+
60
+ const hostIds = getHostSurfaceIds(ctx.viewId);
61
+ if (hostIds && hostIds.size >= MAX_SURFACES_PER_HOST) {
62
+ throw new Error(`Surface limit reached (${MAX_SURFACES_PER_HOST}) for host view ${ctx.viewId}`);
63
+ }
64
+
65
+ const offset = applyHostOffset(hostView, x, y);
66
+ const view = new BrowserView({
67
+ url: src,
68
+ windowId: hostView.windowId,
69
+ appresRoot: hostView.appresRoot,
70
+ frame: { x: offset.x, y: offset.y, width, height },
71
+ autoResize: false
72
+ });
73
+ trackSurface(view.id, { view, hostViewId: ctx.viewId, hidden });
74
+ try {
75
+ await view.whenReady();
76
+ } catch {
77
+ untrackSurface(view.id);
78
+ view.remove();
79
+ throw new Error("Surface browser creation failed or timed out");
80
+ }
81
+
82
+ for (const cb of initCallbacks) cb(view.id, ctx.viewId, view);
83
+
84
+ if (hidden) {
85
+ view.setVisible(false);
86
+ } else {
87
+ view.bringToFront();
88
+ }
89
+ return { surfaceId: view.id };
90
+ };
91
+
92
+ const handleSurfaceResize: GlobalIPCHandler = async (params, ctx) => {
93
+ const p = assertObj(params, "surface.resize params");
94
+ const surfaceId = assertNum(p.surfaceId, "surfaceId");
95
+ const x = assertNum(p.x, "x");
96
+ const y = assertNum(p.y, "y");
97
+ const w = assertNum(p.w, "w");
98
+ const h = assertNum(p.h, "h");
99
+
100
+ const record = getOwnedSurface(surfaceId, ctx);
101
+ if (!record) return {};
102
+
103
+ const hostView = BrowserView.getById(ctx.viewId);
104
+ if (hostView) {
105
+ const offset = applyHostOffset(hostView, x, y);
106
+ record.view.setBoundsAsync(offset.x, offset.y, w, h);
107
+ }
108
+ return {};
109
+ };
110
+
111
+ const handleSurfaceRemove: GlobalIPCHandler = async (params, ctx) => {
112
+ const p = assertObj(params, "surface.remove params");
113
+ const surfaceId = assertNum(p.surfaceId, "surfaceId");
114
+
115
+ const record = getOwnedSurface(surfaceId, ctx);
116
+ if (!record) return {};
117
+
118
+ untrackSurface(surfaceId);
119
+ record.view.remove();
120
+ return {};
121
+ };
122
+
123
+ const handleSurfaceSetHidden: GlobalIPCHandler = async (params, ctx) => {
124
+ const p = assertObj(params, "surface.setHidden params");
125
+ const surfaceId = assertNum(p.surfaceId, "surfaceId");
126
+ const hidden = assertBool(p.hidden, "hidden");
127
+
128
+ const record = getOwnedSurface(surfaceId, ctx);
129
+ if (!record) return {};
130
+
131
+ record.hidden = hidden;
132
+ record.view.setVisible(!hidden);
133
+ if (!hidden) {
134
+ record.view.bringToFront();
135
+ }
136
+ return {};
137
+ };
138
+
139
+ const handleSurfaceSetMasks: GlobalIPCHandler = async (params, ctx) => {
140
+ const p = assertObj(params, "surface.setMasks params");
141
+ const surfaceId = assertNum(p.surfaceId, "surfaceId");
142
+ const masksRaw = Array.isArray(p.masks) ? p.masks : [];
143
+
144
+ const record = getOwnedSurface(surfaceId, ctx);
145
+ if (!record) return {};
146
+
147
+ const hostView = BrowserView.getById(ctx.viewId);
148
+ if (!hostView) return {};
149
+
150
+ const offset = applyHostOffset(hostView, 0, 0);
151
+ const rects = masksRaw.map((r: any) => ({
152
+ x: assertNum(r.x, "mask.x") + offset.x,
153
+ y: assertNum(r.y, "mask.y") + offset.y,
154
+ w: assertNum(r.w, "mask.w"),
155
+ h: assertNum(r.h, "mask.h")
156
+ }));
157
+
158
+ record.view.setMaskRegion(rects);
159
+ return {};
160
+ };
161
+
162
+ const handleSurfaceSetAllPassthrough: GlobalIPCHandler = async (params, ctx) => {
163
+ const p = assertObj(params, "surface.setAllPassthrough params");
164
+ const passthrough = assertBool(p.passthrough, "passthrough");
165
+
166
+ const ids = getHostSurfaceIds(ctx.viewId);
167
+ if (!ids) return {};
168
+
169
+ for (const surfaceId of ids) {
170
+ const record = getSurfaceRecord(surfaceId);
171
+ if (record) {
172
+ record.view.setInputPassthrough(passthrough);
173
+ }
174
+ }
175
+ return {};
176
+ };
177
+
178
+ const handleSurfaceBringAllVisiblesToFront: GlobalIPCHandler = async (_params, ctx) => {
179
+ const ids = getHostSurfaceIds(ctx.viewId);
180
+ if (!ids) return {};
181
+
182
+ for (const surfaceId of ids) {
183
+ const record = getSurfaceRecord(surfaceId);
184
+ if (record && !record.hidden) {
185
+ record.view.bringToFront();
186
+ }
187
+ }
188
+ return {};
189
+ };
190
+
191
+ export function getSurfaceIPCHandlers(): Map<string, GlobalIPCHandler> {
192
+ return new Map([
193
+ ["__bunite:surface.init", handleSurfaceInit],
194
+ ["__bunite:surface.resize", handleSurfaceResize],
195
+ ["__bunite:surface.remove", handleSurfaceRemove],
196
+ ["__bunite:surface.setHidden", handleSurfaceSetHidden],
197
+ ["__bunite:surface.setMasks", handleSurfaceSetMasks],
198
+ ["__bunite:surface.setAllPassthrough", handleSurfaceSetAllPassthrough],
199
+ ["__bunite:surface.bringAllVisiblesToFront", handleSurfaceBringAllVisiblesToFront]
200
+ ]);
201
+ }
@@ -0,0 +1,60 @@
1
+ import type { BrowserView } from "./BrowserView";
2
+
3
+ export type SurfaceRecord = {
4
+ view: BrowserView;
5
+ hostViewId: number;
6
+ hidden: boolean;
7
+ };
8
+
9
+ export const MAX_SURFACES_PER_HOST = 32;
10
+
11
+ const surfaces = new Map<number, SurfaceRecord>();
12
+ const hostSurfaceIds = new Map<number, Set<number>>();
13
+
14
+ export function trackSurface(surfaceId: number, record: SurfaceRecord) {
15
+ surfaces.set(surfaceId, record);
16
+ let ids = hostSurfaceIds.get(record.hostViewId);
17
+ if (!ids) {
18
+ ids = new Set();
19
+ hostSurfaceIds.set(record.hostViewId, ids);
20
+ }
21
+ ids.add(surfaceId);
22
+ }
23
+
24
+ export function untrackSurface(surfaceId: number) {
25
+ const record = surfaces.get(surfaceId);
26
+ if (!record) return;
27
+ surfaces.delete(surfaceId);
28
+ const ids = hostSurfaceIds.get(record.hostViewId);
29
+ if (ids) {
30
+ ids.delete(surfaceId);
31
+ if (ids.size === 0) hostSurfaceIds.delete(record.hostViewId);
32
+ }
33
+ }
34
+
35
+ export function getOwnedSurface(surfaceId: number, ctx: { viewId: number }): SurfaceRecord | null {
36
+ const record = surfaces.get(surfaceId);
37
+ if (!record) return null;
38
+ if (record.hostViewId !== ctx.viewId) throw new Error("Surface access denied.");
39
+ return record;
40
+ }
41
+
42
+ export function getHostSurfaceIds(hostViewId: number): Set<number> | undefined {
43
+ return hostSurfaceIds.get(hostViewId);
44
+ }
45
+
46
+ export function getSurfaceRecord(surfaceId: number): SurfaceRecord | undefined {
47
+ return surfaces.get(surfaceId);
48
+ }
49
+
50
+ export function removeSurfacesForHostView(hostViewId: number) {
51
+ const ids = hostSurfaceIds.get(hostViewId);
52
+ if (!ids || ids.size === 0) return;
53
+
54
+ for (const surfaceId of Array.from(ids)) {
55
+ const record = surfaces.get(surfaceId);
56
+ if (!record) continue;
57
+ untrackSurface(surfaceId);
58
+ record.view.remove();
59
+ }
60
+ }