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.
- package/package.json +3 -2
- package/src/bun/core/App.ts +155 -15
- package/src/bun/core/BrowserView.ts +124 -44
- package/src/bun/core/BrowserWindow.ts +94 -47
- package/src/bun/core/Socket.ts +2 -1
- package/src/bun/core/SurfaceBrowserIPC.ts +65 -0
- package/src/bun/core/SurfaceManager.ts +201 -0
- package/src/bun/core/SurfaceRegistry.ts +60 -0
- package/src/bun/core/Utils.ts +275 -46
- package/src/bun/events/appEvents.ts +2 -1
- package/src/bun/events/webviewEvents.ts +1 -3
- package/src/bun/events/windowEvents.ts +2 -0
- package/src/bun/index.ts +4 -3
- package/src/bun/preload/inline.ts +19 -25
- package/src/bun/proc/native.ts +158 -122
- package/src/native/shared/callbacks.h +6 -6
- package/src/native/shared/ffi_exports.h +123 -119
- package/src/native/shared/log.h +24 -0
- package/src/native/shared/webview_storage.h +5 -5
- package/src/native/win/native_host_appres.cpp +258 -0
- package/src/native/win/native_host_cef.cpp +834 -0
- package/src/native/win/native_host_ffi.cpp +935 -0
- package/src/native/win/native_host_internal.h +285 -0
- package/src/native/win/native_host_runtime.cpp +286 -0
- package/src/native/win/native_host_utils.cpp +314 -0
- package/src/native/win/process_helper_win.cpp +126 -26
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +65 -42
- package/src/preload/tsconfig.json +2 -1
- package/src/preload/tsconfig.tsbuildinfo +1 -0
- package/src/preload/webviewElement.ts +307 -0
- package/src/shared/cefVersion.ts +2 -0
- package/src/shared/log.ts +40 -0
- package/src/shared/paths.ts +122 -52
- package/src/shared/rpc.ts +7 -1
- package/src/shared/webviewPolyfill.ts +80 -0
- package/src/view/index.ts +8 -5
- package/src/native/shared/cef_response_filter.h +0 -116
- package/src/native/win/native_host.cpp +0 -2453
- package/src/types/config.ts +0 -29
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
) ??
|
|
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
|
-
|
|
158
|
-
|
|
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.
|
|
185
|
-
getNativeLibrary()?.symbols.bunite_window_show(this.
|
|
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.
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
223
|
-
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.
|
|
224
|
-
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.
|
|
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.
|
|
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.
|
|
239
|
-
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.
|
|
240
|
-
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
267
|
-
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.
|
|
268
|
-
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.
|
|
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.
|
|
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.
|
|
285
|
-
this.frame.minimized = native.symbols.bunite_window_is_minimized(this.
|
|
286
|
-
this.frame.maximized = native.symbols.bunite_window_is_maximized(this.
|
|
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.
|
|
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.
|
|
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.
|
|
302
|
-
getNativeLibrary()?.symbols.bunite_window_set_title(this.
|
|
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.
|
|
309
|
-
getNativeLibrary()?.symbols.bunite_window_set_frame(this.
|
|
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);
|
package/src/bun/core/Socket.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|