bunite-core 0.14.0 → 0.17.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.
- package/package.json +4 -4
- package/src/host/core/App.ts +6 -3
- package/src/host/core/BrowserView.ts +345 -24
- package/src/host/core/BrowserWindow.ts +52 -6
- package/src/host/core/SurfaceBrowserIPC.ts +10 -1
- package/src/host/core/SurfaceManager.ts +357 -16
- package/src/host/core/windowCap.ts +69 -0
- package/src/host/events/webviewEvents.ts +18 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +145 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +225 -1
- package/src/native/linux/bunite_linux_internal.h +12 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_view.cpp +211 -5
- package/src/native/mac/bunite_mac_ffi.mm +293 -4
- package/src/native/mac/bunite_mac_internal.h +13 -0
- package/src/native/mac/bunite_mac_view.mm +227 -7
- package/src/native/shared/ffi_exports.h +97 -30
- package/src/native/win/native_host_cef.cpp +107 -13
- package/src/native/win/native_host_ffi.cpp +831 -2
- package/src/native/win/native_host_internal.h +22 -0
- package/src/native/win/native_host_runtime.cpp +34 -0
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +827 -5
- package/src/native/win-webview2/webview2_internal.h +19 -0
- package/src/native/win-webview2/webview2_runtime.cpp +383 -31
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +39 -0
- package/src/rpc/framework.ts +194 -12
- package/src/rpc/index.ts +12 -0
- package/src/rpc/peer.ts +1 -1
- package/src/webview/native.ts +142 -32
- package/src/webview/polyfill.ts +91 -14
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunite-core",
|
|
3
3
|
"description": "Uniting UI and Bun",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"setup:cef": "bun ../tools/bunite-dev/scripts/setup-cef.ts",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"msgpackr": "^1.11.9"
|
|
25
25
|
},
|
|
26
26
|
"optionalDependencies": {
|
|
27
|
-
"bunite-native-win-x64": "0.0.
|
|
28
|
-
"bunite-native-mac-arm64": "0.0.
|
|
29
|
-
"bunite-native-linux-x64": "0.0.
|
|
27
|
+
"bunite-native-win-x64": "0.0.15",
|
|
28
|
+
"bunite-native-mac-arm64": "0.0.5",
|
|
29
|
+
"bunite-native-linux-x64": "0.0.5"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/src/host/core/App.ts
CHANGED
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
} from "../native";
|
|
16
16
|
import { ensureRpcServer } from "./Socket";
|
|
17
17
|
import { BrowserWindow } from "./BrowserWindow";
|
|
18
|
-
import { createSurfaceCapImpl } from "./SurfaceManager";
|
|
18
|
+
import { createSurfaceCapImpl, getPopupMetricsSnapshot } from "./SurfaceManager";
|
|
19
|
+
import { createWindowCapImpl } from "./windowCap";
|
|
19
20
|
import "./SurfaceBrowserIPC";
|
|
20
21
|
import { log, logLevelToInt } from "../log";
|
|
21
|
-
import { RuntimeCap, SurfaceCap, PageReportingCap, IpcError, type ImplOf } from "../../rpc/index";
|
|
22
|
+
import { RuntimeCap, WindowCap, SurfaceCap, PageReportingCap, IpcError, type ImplOf } from "../../rpc/index";
|
|
22
23
|
|
|
23
24
|
import type { LogLevel } from "../log";
|
|
24
25
|
|
|
@@ -179,7 +180,8 @@ export class AppRuntime {
|
|
|
179
180
|
throw new IpcError({ code: "not_found", message: `Runtime.${name}` });
|
|
180
181
|
};
|
|
181
182
|
const impl = {
|
|
182
|
-
window: (
|
|
183
|
+
window: (_: void, ctx: Parameters<ImplOf<typeof RuntimeCap>["window"]>[1]) =>
|
|
184
|
+
ctx.exportCap(WindowCap, createWindowCapImpl(viewId)),
|
|
183
185
|
dialogs: () => notImpl("dialogs"),
|
|
184
186
|
clipboard: () => notImpl("clipboard"),
|
|
185
187
|
shell: () => notImpl("shell"),
|
|
@@ -205,6 +207,7 @@ export class AppRuntime {
|
|
|
205
207
|
});
|
|
206
208
|
},
|
|
207
209
|
}),
|
|
210
|
+
popupMetrics: () => getPopupMetricsSnapshot(),
|
|
208
211
|
} satisfies ImplOf<typeof RuntimeCap>;
|
|
209
212
|
return impl;
|
|
210
213
|
}
|
|
@@ -8,13 +8,20 @@ import {
|
|
|
8
8
|
type Connection,
|
|
9
9
|
type BytesPipe,
|
|
10
10
|
} from "../../rpc/index";
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
EvaluateResult, SurfaceCapabilities, ScreenshotResult, Modifier,
|
|
13
|
+
AccessibilitySnapshotResult, AxNode, ListFramesResult,
|
|
14
|
+
ResolveAndClickArgs, ResolveAndClickResult,
|
|
15
|
+
} from "../../rpc/framework";
|
|
12
16
|
import { encodeModifiers, resolveKey } from "./inputDispatch";
|
|
13
17
|
import { createEncryptedPipe } from "../encryptedPipe";
|
|
14
18
|
import {
|
|
15
19
|
ensureNativeRuntime, getNativeLibrary, toCString, waitForViewReady, cancelWaitForViewReady,
|
|
16
20
|
setEvaluateResultHandler, type NativeEvaluateResult,
|
|
17
21
|
setScreenshotResultHandler, type NativeScreenshotResult,
|
|
22
|
+
setAccessibilityResultHandler, type NativeAccessibilityResult,
|
|
23
|
+
setListFramesResultHandler, type NativeListFramesResult,
|
|
24
|
+
setResolveAndClickResultHandler, type NativeResolveAndClickResult,
|
|
18
25
|
} from "../native";
|
|
19
26
|
import { attachBrowserViewRegistry, getRpcPort } from "./Socket";
|
|
20
27
|
import { getAppRuntimeOrThrow } from "./App";
|
|
@@ -69,6 +76,175 @@ function rejectScreenshotsForView(viewId: number) {
|
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
type AxPending = { viewId: number; resolve: (result: AccessibilitySnapshotResult) => void; interestingOnly: boolean };
|
|
80
|
+
let nextAxRequestId = 1;
|
|
81
|
+
const axResolvers = new Map<number, AxPending>();
|
|
82
|
+
|
|
83
|
+
function registerAxRequest(viewId: number, resolve: (result: AccessibilitySnapshotResult) => void, interestingOnly: boolean): number {
|
|
84
|
+
const id = nextAxRequestId++;
|
|
85
|
+
axResolvers.set(id, { viewId, resolve, interestingOnly });
|
|
86
|
+
return id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rejectAxForView(viewId: number) {
|
|
90
|
+
for (const [reqId, entry] of axResolvers) {
|
|
91
|
+
if (entry.viewId === viewId) {
|
|
92
|
+
axResolvers.delete(reqId);
|
|
93
|
+
entry.resolve({ ok: false, code: "not_supported", message: "view destroyed" });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// CDP `Accessibility.getFullAXTree` returns `{nodes: [flat]}` with `childIds`
|
|
99
|
+
// references — build a nested tree from the first node. When `interestingOnly`
|
|
100
|
+
// is true, ignored nodes are dropped and their children reparent up.
|
|
101
|
+
function convertAxTree(cdpResult: { nodes?: any[] } | undefined, interestingOnly: boolean): AxNode {
|
|
102
|
+
const flat = cdpResult?.nodes ?? [];
|
|
103
|
+
if (flat.length === 0) return { nodeId: "", role: "", name: "" };
|
|
104
|
+
const byId = new Map<string, any>();
|
|
105
|
+
for (const n of flat) if (n?.nodeId != null) byId.set(String(n.nodeId), n);
|
|
106
|
+
// Root = node without a parentId (rather than relying on flat[0] ordering).
|
|
107
|
+
const root = flat.find((n) => n?.parentId == null) ?? flat[0];
|
|
108
|
+
// Walk childIds skipping ignored nodes (when filtering) and produce a flat list
|
|
109
|
+
// of "interesting" descendants for the given node.
|
|
110
|
+
const seen = new Set<string>();
|
|
111
|
+
const interestingDescendants = (cdpNode: any): any[] => {
|
|
112
|
+
const out: any[] = [];
|
|
113
|
+
if (!Array.isArray(cdpNode?.childIds)) return out;
|
|
114
|
+
for (const cid of cdpNode.childIds) {
|
|
115
|
+
const child = byId.get(String(cid));
|
|
116
|
+
if (!child) continue;
|
|
117
|
+
if (interestingOnly && child.ignored === true) {
|
|
118
|
+
out.push(...interestingDescendants(child));
|
|
119
|
+
} else {
|
|
120
|
+
out.push(child);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
const build = (n: any): AxNode => {
|
|
126
|
+
const id = String(n?.nodeId ?? "");
|
|
127
|
+
if (id && seen.has(id)) return { nodeId: id, role: "", name: "" }; // cycle guard
|
|
128
|
+
if (id) seen.add(id);
|
|
129
|
+
const props = new Map<string, unknown>();
|
|
130
|
+
if (Array.isArray(n?.properties)) {
|
|
131
|
+
for (const p of n.properties) {
|
|
132
|
+
if (p?.name && p.value && "value" in p.value) props.set(p.name, p.value.value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const out: AxNode = {
|
|
136
|
+
nodeId: id,
|
|
137
|
+
role: String(n?.role?.value ?? ""),
|
|
138
|
+
name: String(n?.name?.value ?? ""),
|
|
139
|
+
};
|
|
140
|
+
if (n?.value?.value !== undefined) out.value = String(n.value.value);
|
|
141
|
+
if (n?.description?.value !== undefined) out.description = String(n.description.value);
|
|
142
|
+
const level = props.get("level"); if (typeof level === "number") out.level = level;
|
|
143
|
+
const checked = props.get("checked"); if (checked === true || checked === false || checked === "mixed") out.checked = checked;
|
|
144
|
+
const pressed = props.get("pressed"); if (pressed === true || pressed === false || pressed === "mixed") out.pressed = pressed;
|
|
145
|
+
const expanded = props.get("expanded"); if (typeof expanded === "boolean") out.expanded = expanded;
|
|
146
|
+
const disabled = props.get("disabled"); if (typeof disabled === "boolean") out.disabled = disabled;
|
|
147
|
+
const focused = props.get("focused"); if (typeof focused === "boolean") out.focused = focused;
|
|
148
|
+
const invalid = props.get("invalid"); if (typeof invalid === "boolean") out.invalid = invalid;
|
|
149
|
+
const required = props.get("required"); if (typeof required === "boolean") out.required = required;
|
|
150
|
+
const selected = props.get("selected"); if (typeof selected === "boolean") out.selected = selected;
|
|
151
|
+
const kids = interestingDescendants(n).map(build);
|
|
152
|
+
if (kids.length > 0) out.children = kids;
|
|
153
|
+
return out;
|
|
154
|
+
};
|
|
155
|
+
return build(root);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type FramesPending = { viewId: number; resolve: (result: ListFramesResult) => void };
|
|
159
|
+
let nextFramesRequestId = 1;
|
|
160
|
+
const framesResolvers = new Map<number, FramesPending>();
|
|
161
|
+
function registerFramesRequest(viewId: number, resolve: (result: ListFramesResult) => void): number {
|
|
162
|
+
const id = nextFramesRequestId++;
|
|
163
|
+
framesResolvers.set(id, { viewId, resolve });
|
|
164
|
+
return id;
|
|
165
|
+
}
|
|
166
|
+
function rejectFramesForView(viewId: number) {
|
|
167
|
+
for (const [reqId, entry] of framesResolvers) {
|
|
168
|
+
if (entry.viewId === viewId) {
|
|
169
|
+
framesResolvers.delete(reqId);
|
|
170
|
+
entry.resolve({ ok: false, code: "runtime_error", message: "view destroyed" });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type ResolveAndClickPending = { viewId: number; resolve: (result: ResolveAndClickResult) => void };
|
|
176
|
+
let nextResolveAndClickRequestId = 1;
|
|
177
|
+
const resolveAndClickResolvers = new Map<number, ResolveAndClickPending>();
|
|
178
|
+
function registerResolveAndClickRequest(viewId: number, resolve: (result: ResolveAndClickResult) => void): number {
|
|
179
|
+
const id = nextResolveAndClickRequestId++;
|
|
180
|
+
resolveAndClickResolvers.set(id, { viewId, resolve });
|
|
181
|
+
return id;
|
|
182
|
+
}
|
|
183
|
+
function rejectResolveAndClickForView(viewId: number) {
|
|
184
|
+
for (const [reqId, entry] of resolveAndClickResolvers) {
|
|
185
|
+
if (entry.viewId === viewId) {
|
|
186
|
+
resolveAndClickResolvers.delete(reqId);
|
|
187
|
+
entry.resolve({ ok: false, code: "runtime_error", message: "view destroyed" });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function flattenFrameTree(raw: any): { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string }[] {
|
|
193
|
+
const out: { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string }[] = [];
|
|
194
|
+
const walk = (node: any, parent: string | null) => {
|
|
195
|
+
const f = node?.frame;
|
|
196
|
+
if (!f) return;
|
|
197
|
+
const entry: { frameId: string; parentFrameId: string | null; origin: string; url: string; name?: string } = {
|
|
198
|
+
frameId: String(f.id ?? ""),
|
|
199
|
+
parentFrameId: parent,
|
|
200
|
+
origin: String(f.securityOrigin ?? ""),
|
|
201
|
+
url: String(f.url ?? ""),
|
|
202
|
+
};
|
|
203
|
+
if (typeof f.name === "string" && f.name.length > 0) entry.name = f.name;
|
|
204
|
+
out.push(entry);
|
|
205
|
+
if (Array.isArray(node.childFrames)) for (const c of node.childFrames) walk(c, entry.frameId);
|
|
206
|
+
};
|
|
207
|
+
const root = raw?.frameTree;
|
|
208
|
+
if (root) walk(root, null);
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
setListFramesResultHandler((viewId, raw: NativeListFramesResult) => {
|
|
213
|
+
const entry = framesResolvers.get(raw.requestId);
|
|
214
|
+
if (!entry || entry.viewId !== viewId) return;
|
|
215
|
+
framesResolvers.delete(raw.requestId);
|
|
216
|
+
if (raw.ok && raw.raw) {
|
|
217
|
+
try {
|
|
218
|
+
const frames = flattenFrameTree(raw.raw);
|
|
219
|
+
entry.resolve({ ok: true, frames });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
entry.resolve({ ok: false, code: "runtime_error", message: `frame tree flatten failed: ${(e as Error).message}` });
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
entry.resolve({
|
|
225
|
+
ok: false,
|
|
226
|
+
code: (raw.code as "not_supported" | "runtime_error") ?? "runtime_error",
|
|
227
|
+
message: raw.message ?? "list frames failed",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
setAccessibilityResultHandler((viewId, raw: NativeAccessibilityResult) => {
|
|
233
|
+
const entry = axResolvers.get(raw.requestId);
|
|
234
|
+
if (!entry || entry.viewId !== viewId) return;
|
|
235
|
+
axResolvers.delete(raw.requestId);
|
|
236
|
+
if (raw.ok && raw.tree) {
|
|
237
|
+
try { entry.resolve({ ok: true, tree: convertAxTree(raw.tree as any, entry.interestingOnly) }); }
|
|
238
|
+
catch (e) { entry.resolve({ ok: false, code: "runtime_error", message: `ax tree convert failed: ${(e as Error).message}` }); }
|
|
239
|
+
} else {
|
|
240
|
+
entry.resolve({
|
|
241
|
+
ok: false,
|
|
242
|
+
code: (raw.code as "not_supported" | "runtime_error" | "timeout") ?? "runtime_error",
|
|
243
|
+
message: raw.message ?? "accessibility snapshot failed",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
72
248
|
function decodeBase64(b64: string): Uint8Array {
|
|
73
249
|
const bin = atob(b64);
|
|
74
250
|
const out = new Uint8Array(bin.length);
|
|
@@ -96,6 +272,19 @@ setScreenshotResultHandler((viewId, raw: NativeScreenshotResult) => {
|
|
|
96
272
|
}
|
|
97
273
|
});
|
|
98
274
|
|
|
275
|
+
setResolveAndClickResultHandler((viewId, raw: NativeResolveAndClickResult) => {
|
|
276
|
+
const entry = resolveAndClickResolvers.get(raw.requestId);
|
|
277
|
+
if (!entry || entry.viewId !== viewId) return;
|
|
278
|
+
resolveAndClickResolvers.delete(raw.requestId);
|
|
279
|
+
if (raw.ok && raw.rect) {
|
|
280
|
+
entry.resolve({ ok: true, rect: raw.rect, isTrustedEvent: !!raw.isTrustedEvent });
|
|
281
|
+
} else {
|
|
282
|
+
type FailCode = "not_found" | "not_visible" | "runtime_error" | "cross_origin" | "not_supported";
|
|
283
|
+
const code = (raw.code as FailCode | undefined) ?? "runtime_error";
|
|
284
|
+
entry.resolve({ ok: false, code, message: raw.message ?? "resolveAndClick failed" });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
99
288
|
setEvaluateResultHandler((viewId, raw: NativeEvaluateResult) => {
|
|
100
289
|
const entry = evaluateResolvers.get(raw.requestId);
|
|
101
290
|
if (!entry) return;
|
|
@@ -131,6 +320,12 @@ const CAP_FORMAT_JPEG = 1 << 10;
|
|
|
131
320
|
const CAP_MOUSE = 1 << 11;
|
|
132
321
|
const CAP_DIALOGS = 1 << 12;
|
|
133
322
|
const CAP_CONSOLE = 1 << 13;
|
|
323
|
+
const CAP_AX = 1 << 15;
|
|
324
|
+
const CAP_BOUNDING_RECT = 1 << 16;
|
|
325
|
+
const CAP_FRAMES = 1 << 17;
|
|
326
|
+
const CAP_DOWNLOADS = 1 << 18;
|
|
327
|
+
const CAP_POPUPS = 1 << 19;
|
|
328
|
+
const CAP_RESOLVE_AND_CLICK = 1 << 20;
|
|
134
329
|
|
|
135
330
|
function decodeCapabilityBits(bits: number): SurfaceCapabilities {
|
|
136
331
|
const formats: ("png" | "jpeg")[] = [];
|
|
@@ -149,6 +344,12 @@ function decodeCapabilityBits(bits: number): SurfaceCapabilities {
|
|
|
149
344
|
dialogs: !!(bits & CAP_DIALOGS),
|
|
150
345
|
console: !!(bits & CAP_CONSOLE),
|
|
151
346
|
screenshot: !!(bits & CAP_SCREENSHOT),
|
|
347
|
+
accessibilitySnapshot: !!(bits & CAP_AX),
|
|
348
|
+
getBoundingRect: !!(bits & CAP_BOUNDING_RECT),
|
|
349
|
+
frames: !!(bits & CAP_FRAMES),
|
|
350
|
+
downloads: !!(bits & CAP_DOWNLOADS),
|
|
351
|
+
popups: !!(bits & CAP_POPUPS),
|
|
352
|
+
resolveAndClick: !!(bits & CAP_RESOLVE_AND_CLICK),
|
|
152
353
|
...(formats.length > 0 ? { formats } : {}),
|
|
153
354
|
};
|
|
154
355
|
}
|
|
@@ -208,9 +409,36 @@ export class BrowserView {
|
|
|
208
409
|
sandbox: boolean;
|
|
209
410
|
secretKey: Uint8Array;
|
|
210
411
|
|
|
211
|
-
|
|
412
|
+
/** Wrap a pre-existing native view (popup mint). Skips `bunite_view_create`;
|
|
413
|
+
* the new view is then attached to the host window via `bunite_view_popup_accept`. */
|
|
414
|
+
static adopt(args: {
|
|
415
|
+
nativeViewId: number;
|
|
416
|
+
hostWindowId: number;
|
|
417
|
+
bounds: { x: number; y: number; width: number; height: number };
|
|
418
|
+
appresRoot: string | null;
|
|
419
|
+
}): BrowserView {
|
|
420
|
+
return new BrowserView({
|
|
421
|
+
adoptNativeViewId: args.nativeViewId,
|
|
422
|
+
windowId: args.hostWindowId,
|
|
423
|
+
frame: args.bounds,
|
|
424
|
+
appresRoot: args.appresRoot,
|
|
425
|
+
autoResize: false,
|
|
426
|
+
} as Partial<BrowserViewOptions> & { adoptNativeViewId: number });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
static dismissPopupById(newSurfaceId: number) {
|
|
430
|
+
getNativeLibrary()?.symbols.bunite_view_popup_dismiss(newSurfaceId);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
constructor(options: Partial<BrowserViewOptions> & { adoptNativeViewId?: number }) {
|
|
212
434
|
ensureNativeRuntime();
|
|
213
435
|
|
|
436
|
+
const adopting = options.adoptNativeViewId != null;
|
|
437
|
+
if (adopting) {
|
|
438
|
+
this.id = options.adoptNativeViewId!;
|
|
439
|
+
// Adopted IDs live in the upper u32 half (popup namespace). Keep TS's
|
|
440
|
+
// sequential allocator untouched so normal creates stay below 0x80000000.
|
|
441
|
+
}
|
|
214
442
|
this.windowId = options.windowId ?? defaultOptions.windowId;
|
|
215
443
|
this.url = options.url ?? defaultOptions.url;
|
|
216
444
|
this.html = options.html ?? defaultOptions.html;
|
|
@@ -242,23 +470,33 @@ export class BrowserView {
|
|
|
242
470
|
|
|
243
471
|
BrowserViewMap[this.id] = this;
|
|
244
472
|
this._readyPromise = waitForViewReady(this.id);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
473
|
+
if (adopting) {
|
|
474
|
+
// Native popup mint already created the view; bind to host window + bounds.
|
|
475
|
+
const lib = getNativeLibrary();
|
|
476
|
+
lib?.symbols.bunite_view_popup_accept(
|
|
477
|
+
this.id, this.windowId,
|
|
478
|
+
this.frame.x, this.frame.y, this.frame.width, this.frame.height,
|
|
479
|
+
);
|
|
480
|
+
this.nativeAttached = true;
|
|
481
|
+
} else {
|
|
482
|
+
this.nativeAttached =
|
|
483
|
+
getNativeLibrary()?.symbols.bunite_view_create(
|
|
484
|
+
this.id,
|
|
485
|
+
this.windowId,
|
|
486
|
+
toCString(this.url ?? ""),
|
|
487
|
+
toCString(this.html ?? ""),
|
|
488
|
+
toCString(preloadScript),
|
|
489
|
+
toCString(this.appresRoot ?? ""),
|
|
490
|
+
toCString(this.navigationRules ? JSON.stringify(this.navigationRules) : ""),
|
|
491
|
+
this.frame.x,
|
|
492
|
+
this.frame.y,
|
|
493
|
+
this.frame.width,
|
|
494
|
+
this.frame.height,
|
|
495
|
+
this.autoResize,
|
|
496
|
+
this.sandbox,
|
|
497
|
+
toCString(this.preloadOrigins ? JSON.stringify(this.preloadOrigins) : "")
|
|
498
|
+
) ?? false;
|
|
499
|
+
}
|
|
262
500
|
|
|
263
501
|
if (this.nativeAttached) {
|
|
264
502
|
this.on("did-navigate", (event: any) => {
|
|
@@ -342,13 +580,74 @@ export class BrowserView {
|
|
|
342
580
|
}
|
|
343
581
|
}
|
|
344
582
|
|
|
345
|
-
evaluate(script: string): Promise<EvaluateResult> {
|
|
583
|
+
evaluate(script: string, frameId?: string): Promise<EvaluateResult> {
|
|
346
584
|
if (!this.nativeAttached) {
|
|
347
585
|
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
348
586
|
}
|
|
349
587
|
return new Promise<EvaluateResult>((resolve) => {
|
|
350
|
-
|
|
351
|
-
|
|
588
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
589
|
+
const wrappedResolve = (r: EvaluateResult) => { if (timer) clearTimeout(timer); resolve(r); };
|
|
590
|
+
const requestId = registerEvaluateRequest(this.id, wrappedResolve);
|
|
591
|
+
// 30s timeout — two-hop CDP path (createIsolatedWorld → Runtime.evaluate)
|
|
592
|
+
// can silently hang if the frame is destroyed mid-flight.
|
|
593
|
+
timer = setTimeout(() => {
|
|
594
|
+
if (evaluateResolvers.delete(requestId)) {
|
|
595
|
+
resolve({ ok: false, code: "timeout", message: "evaluate timed out after 30s" });
|
|
596
|
+
}
|
|
597
|
+
}, 30_000);
|
|
598
|
+
const lib = getNativeLibrary();
|
|
599
|
+
if (frameId) {
|
|
600
|
+
lib?.symbols.bunite_view_evaluate_in_frame(this.id, requestId, toCString(script), toCString(frameId));
|
|
601
|
+
} else {
|
|
602
|
+
lib?.symbols.bunite_view_evaluate(this.id, requestId, toCString(script));
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
setDownloadPolicy(policy: "auto" | "ask" | "block", downloadDir?: string) {
|
|
608
|
+
if (!this.nativeAttached) return;
|
|
609
|
+
const policyCode = policy === "auto" ? 0 : policy === "ask" ? 1 : 2;
|
|
610
|
+
getNativeLibrary()?.symbols.bunite_view_set_download_policy(
|
|
611
|
+
this.id, policyCode, toCString(downloadDir ?? "")
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
listFrames(): Promise<ListFramesResult> {
|
|
616
|
+
if (!this.nativeAttached) {
|
|
617
|
+
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
618
|
+
}
|
|
619
|
+
return new Promise<ListFramesResult>((resolve) => {
|
|
620
|
+
const requestId = registerFramesRequest(this.id, resolve);
|
|
621
|
+
const timer = setTimeout(() => {
|
|
622
|
+
if (framesResolvers.delete(requestId)) {
|
|
623
|
+
resolve({ ok: false, code: "runtime_error", message: "list frames timed out after 10s" });
|
|
624
|
+
}
|
|
625
|
+
}, 10_000);
|
|
626
|
+
const wrappedResolve = (r: ListFramesResult) => { clearTimeout(timer); resolve(r); };
|
|
627
|
+
framesResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
628
|
+
getNativeLibrary()?.symbols.bunite_view_list_frames(this.id, requestId);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
resolveAndClick(args: ResolveAndClickArgs): Promise<ResolveAndClickResult> {
|
|
633
|
+
if (!this.nativeAttached) {
|
|
634
|
+
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
635
|
+
}
|
|
636
|
+
return new Promise<ResolveAndClickResult>((resolve) => {
|
|
637
|
+
const requestId = registerResolveAndClickRequest(this.id, resolve);
|
|
638
|
+
const timer = setTimeout(() => {
|
|
639
|
+
if (resolveAndClickResolvers.delete(requestId)) {
|
|
640
|
+
resolve({ ok: false, code: "runtime_error", message: "resolveAndClick timed out after 5s" });
|
|
641
|
+
}
|
|
642
|
+
}, 5_000);
|
|
643
|
+
const wrappedResolve = (r: ResolveAndClickResult) => { clearTimeout(timer); resolve(r); };
|
|
644
|
+
resolveAndClickResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve });
|
|
645
|
+
const button = args.button === "right" ? 2 : args.button === "middle" ? 1 : 0;
|
|
646
|
+
getNativeLibrary()?.symbols.bunite_view_resolve_and_click(
|
|
647
|
+
this.id, requestId,
|
|
648
|
+
toCString(args.selector), toCString(args.frameId ?? ""),
|
|
649
|
+
button, args.clickCount ?? 1, encodeModifiers(args.modifiers),
|
|
650
|
+
);
|
|
352
651
|
});
|
|
353
652
|
}
|
|
354
653
|
|
|
@@ -440,6 +739,24 @@ export class BrowserView {
|
|
|
440
739
|
});
|
|
441
740
|
}
|
|
442
741
|
|
|
742
|
+
accessibilitySnapshot(interestingOnly: boolean): Promise<AccessibilitySnapshotResult> {
|
|
743
|
+
if (!this.nativeAttached) {
|
|
744
|
+
return Promise.resolve({ ok: false, code: "not_supported", message: "native runtime unavailable" });
|
|
745
|
+
}
|
|
746
|
+
return new Promise<AccessibilitySnapshotResult>((resolve) => {
|
|
747
|
+
const requestId = registerAxRequest(this.id, resolve, interestingOnly);
|
|
748
|
+
const timer = setTimeout(() => {
|
|
749
|
+
if (axResolvers.delete(requestId)) {
|
|
750
|
+
resolve({ ok: false, code: "timeout", message: "accessibility snapshot timed out after 30s" });
|
|
751
|
+
}
|
|
752
|
+
}, 30_000);
|
|
753
|
+
const wrappedResolve = (r: AccessibilitySnapshotResult) => { clearTimeout(timer); resolve(r); };
|
|
754
|
+
axResolvers.set(requestId, { viewId: this.id, resolve: wrappedResolve, interestingOnly });
|
|
755
|
+
// Native flag is currently unused (filter is TS-side); kept for ABI shape stability.
|
|
756
|
+
getNativeLibrary()?.symbols.bunite_view_accessibility_snapshot(this.id, requestId, interestingOnly ? 1 : 0);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
443
760
|
goBack() {
|
|
444
761
|
if (this.nativeAttached) {
|
|
445
762
|
getNativeLibrary()?.symbols.bunite_view_go_back(this.id);
|
|
@@ -546,9 +863,13 @@ export class BrowserView {
|
|
|
546
863
|
cancelWaitForViewReady(this.id);
|
|
547
864
|
rejectEvaluatesForView(this.id);
|
|
548
865
|
rejectScreenshotsForView(this.id);
|
|
866
|
+
rejectAxForView(this.id);
|
|
867
|
+
rejectFramesForView(this.id);
|
|
868
|
+
rejectResolveAndClickForView(this.id);
|
|
549
869
|
this.nativeAttached = false;
|
|
550
870
|
for (const eventName of [
|
|
551
|
-
"will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested", "title-changed"
|
|
871
|
+
"will-navigate", "did-navigate", "dom-ready", "new-window-open", "permission-requested", "title-changed",
|
|
872
|
+
"load-start", "load-finish", "load-fail", "dialog", "console-message", "download-event", "popup-requested",
|
|
552
873
|
]) {
|
|
553
874
|
buniteEventEmitter.removeAllListeners(`${eventName}-${this.id}`);
|
|
554
875
|
}
|
|
@@ -556,7 +877,7 @@ export class BrowserView {
|
|
|
556
877
|
}
|
|
557
878
|
|
|
558
879
|
on(
|
|
559
|
-
name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested" | "title-changed" | "load-start" | "load-finish" | "load-fail" | "dialog" | "console-message",
|
|
880
|
+
name: "will-navigate" | "did-navigate" | "dom-ready" | "new-window-open" | "permission-requested" | "title-changed" | "load-start" | "load-finish" | "load-fail" | "dialog" | "console-message" | "download-event" | "popup-requested",
|
|
560
881
|
handler: (event: unknown) => void
|
|
561
882
|
) {
|
|
562
883
|
const specificName = `${name}-${this.id}`;
|
|
@@ -22,6 +22,7 @@ export type WindowOptionsType = {
|
|
|
22
22
|
preload: string | null;
|
|
23
23
|
appresRoot: string | null;
|
|
24
24
|
preloadOrigins?: string[];
|
|
25
|
+
label?: string;
|
|
25
26
|
/** Setup callback fired when the window's renderer connection attaches. */
|
|
26
27
|
serve?: (conn: Connection) => void;
|
|
27
28
|
titleBarStyle: "hidden" | "hiddenInset" | "default";
|
|
@@ -63,6 +64,7 @@ export class BrowserWindow {
|
|
|
63
64
|
id = getNextWindowId();
|
|
64
65
|
private nativeAttached = false;
|
|
65
66
|
title: string;
|
|
67
|
+
label = "";
|
|
66
68
|
frame: WindowOptionsType["frame"];
|
|
67
69
|
url: string | null;
|
|
68
70
|
html: string | null;
|
|
@@ -77,6 +79,9 @@ export class BrowserWindow {
|
|
|
77
79
|
webviewId: number;
|
|
78
80
|
private closed = false;
|
|
79
81
|
private restoreMaximizedAfterMinimize = false;
|
|
82
|
+
private _focused = false;
|
|
83
|
+
private readonly handleNativeFocus = () => { lastFocusedWindowId = this.id; this._focused = true; };
|
|
84
|
+
private readonly handleNativeBlur = () => { this._focused = false; };
|
|
80
85
|
private readonly handleNativeMove = (event: unknown) => {
|
|
81
86
|
const data = (event as {
|
|
82
87
|
data?: { x?: number; y?: number; maximized?: boolean; minimized?: boolean };
|
|
@@ -122,6 +127,8 @@ export class BrowserWindow {
|
|
|
122
127
|
}
|
|
123
128
|
BrowserView.getById(this.webviewId)?.detachFromNative();
|
|
124
129
|
delete BrowserWindowMap[this.id];
|
|
130
|
+
buniteEventEmitter.off(`focus-${this.id}`, this.handleNativeFocus);
|
|
131
|
+
buniteEventEmitter.off(`blur-${this.id}`, this.handleNativeBlur);
|
|
125
132
|
buniteEventEmitter.off(`move-${this.id}`, this.handleNativeMove);
|
|
126
133
|
buniteEventEmitter.off(`resize-${this.id}`, this.handleNativeResize);
|
|
127
134
|
buniteEventEmitter.off(`close-${this.id}`, this.handleNativeClose);
|
|
@@ -132,6 +139,7 @@ export class BrowserWindow {
|
|
|
132
139
|
ensureNativeRuntime();
|
|
133
140
|
|
|
134
141
|
this.title = options.title ?? defaultOptions.title;
|
|
142
|
+
this.label = options.label ?? "";
|
|
135
143
|
this.frame = { ...defaultOptions.frame, ...options.frame };
|
|
136
144
|
this.html = options.html ?? defaultOptions.html;
|
|
137
145
|
this.preload = options.preload ?? defaultOptions.preload;
|
|
@@ -160,6 +168,15 @@ export class BrowserWindow {
|
|
|
160
168
|
this.navigationRules = options.navigationRules ?? defaultOptions.navigationRules;
|
|
161
169
|
this.sandbox = options.sandbox ?? defaultOptions.sandbox;
|
|
162
170
|
|
|
171
|
+
// Register before native create — create shows the window and the initial
|
|
172
|
+
// WM_ACTIVATE fires synchronously, so listeners must be in place first.
|
|
173
|
+
BrowserWindowMap[this.id] = this;
|
|
174
|
+
buniteEventEmitter.on(`focus-${this.id}`, this.handleNativeFocus);
|
|
175
|
+
buniteEventEmitter.on(`blur-${this.id}`, this.handleNativeBlur);
|
|
176
|
+
buniteEventEmitter.on(`move-${this.id}`, this.handleNativeMove);
|
|
177
|
+
buniteEventEmitter.on(`resize-${this.id}`, this.handleNativeResize);
|
|
178
|
+
buniteEventEmitter.on(`close-${this.id}`, this.handleNativeClose);
|
|
179
|
+
|
|
163
180
|
const native = getNativeLibrary();
|
|
164
181
|
this.nativeAttached =
|
|
165
182
|
native?.symbols.bunite_window_create(
|
|
@@ -182,12 +199,6 @@ export class BrowserWindow {
|
|
|
182
199
|
);
|
|
183
200
|
}
|
|
184
201
|
|
|
185
|
-
BrowserWindowMap[this.id] = this;
|
|
186
|
-
buniteEventEmitter.on(`focus-${this.id}`, () => { lastFocusedWindowId = this.id; });
|
|
187
|
-
buniteEventEmitter.on(`move-${this.id}`, this.handleNativeMove);
|
|
188
|
-
buniteEventEmitter.on(`resize-${this.id}`, this.handleNativeResize);
|
|
189
|
-
buniteEventEmitter.on(`close-${this.id}`, this.handleNativeClose);
|
|
190
|
-
|
|
191
202
|
const webview = new BrowserView({
|
|
192
203
|
url: this.url,
|
|
193
204
|
html: this.html,
|
|
@@ -223,6 +234,15 @@ export class BrowserWindow {
|
|
|
223
234
|
return Object.values(BrowserWindowMap);
|
|
224
235
|
}
|
|
225
236
|
|
|
237
|
+
/** The window owning a given view id — its main webview, or a surface view
|
|
238
|
+
* whose `windowId` points back to the window. */
|
|
239
|
+
static getByWebviewId(viewId: number) {
|
|
240
|
+
const direct = Object.values(BrowserWindowMap).find((w) => w.webviewId === viewId);
|
|
241
|
+
if (direct) return direct;
|
|
242
|
+
const view = BrowserView.getById(viewId) as { windowId?: number } | undefined;
|
|
243
|
+
return view?.windowId != null ? BrowserWindowMap[view.windowId] : undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
226
246
|
get webview(): BrowserView | undefined {
|
|
227
247
|
return BrowserView.getById(this.webviewId) as BrowserView | undefined;
|
|
228
248
|
}
|
|
@@ -234,6 +254,11 @@ export class BrowserWindow {
|
|
|
234
254
|
}
|
|
235
255
|
}
|
|
236
256
|
|
|
257
|
+
/** Best-effort — no native focus FFI yet; show() brings the window up. */
|
|
258
|
+
focus() {
|
|
259
|
+
this.show();
|
|
260
|
+
}
|
|
261
|
+
|
|
237
262
|
close() {
|
|
238
263
|
if (this.closed) {
|
|
239
264
|
return;
|
|
@@ -258,6 +283,8 @@ export class BrowserWindow {
|
|
|
258
283
|
this.nativeAttached = false;
|
|
259
284
|
}
|
|
260
285
|
delete BrowserWindowMap[this.id];
|
|
286
|
+
buniteEventEmitter.off(`focus-${this.id}`, this.handleNativeFocus);
|
|
287
|
+
buniteEventEmitter.off(`blur-${this.id}`, this.handleNativeBlur);
|
|
261
288
|
buniteEventEmitter.off(`move-${this.id}`, this.handleNativeMove);
|
|
262
289
|
buniteEventEmitter.off(`resize-${this.id}`, this.handleNativeResize);
|
|
263
290
|
buniteEventEmitter.off(`close-${this.id}`, this.handleNativeClose);
|
|
@@ -356,6 +383,18 @@ export class BrowserWindow {
|
|
|
356
383
|
return minimized;
|
|
357
384
|
}
|
|
358
385
|
|
|
386
|
+
isFocused() {
|
|
387
|
+
return this._focused;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
toggleMaximize() {
|
|
391
|
+
if (this.isMaximized()) this.unmaximize(); else this.maximize();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
getState() {
|
|
395
|
+
return { maximized: this.isMaximized(), minimized: this.isMinimized(), focused: this._focused };
|
|
396
|
+
}
|
|
397
|
+
|
|
359
398
|
setTitle(title: string) {
|
|
360
399
|
this.title = title;
|
|
361
400
|
if (this.nativeAttached) {
|
|
@@ -374,6 +413,13 @@ export class BrowserWindow {
|
|
|
374
413
|
return this.frame;
|
|
375
414
|
}
|
|
376
415
|
|
|
416
|
+
/** Start an OS-driven window move drag. Call from a renderer mousedown on a
|
|
417
|
+
* custom titlebar region; the OS tracks the drag through mouse-up. */
|
|
418
|
+
beginMoveDrag() {
|
|
419
|
+
if (!this.nativeAttached) return;
|
|
420
|
+
getNativeLibrary()?.symbols.bunite_window_begin_move_drag(this.id);
|
|
421
|
+
}
|
|
422
|
+
|
|
377
423
|
on(name: "close-requested" | "close" | "focus" | "blur" | "move" | "resize", handler: (event: unknown) => void) {
|
|
378
424
|
const specificName = `${name}-${this.id}`;
|
|
379
425
|
buniteEventEmitter.on(specificName, handler);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
-
onSurfaceInit, emitSurfaceEvent, emitConsole,
|
|
2
|
+
onSurfaceInit, emitSurfaceEvent, emitConsole, emitDownload, emitPopupRequested,
|
|
3
3
|
registerDialogRequest, disposeSurfaceState, clearConsoleBuffer,
|
|
4
4
|
} from "./SurfaceManager";
|
|
5
|
+
import { log } from "../log";
|
|
5
6
|
|
|
6
7
|
onSurfaceInit((surfaceId, hostViewId, view) => {
|
|
8
|
+
log.debug("surface/init surfaceId=" + surfaceId + " hostViewId=" + hostViewId);
|
|
7
9
|
view.on("did-navigate", (event: any) => {
|
|
8
10
|
emitSurfaceEvent(hostViewId, surfaceId, { type: "navigate", url: event.data.detail });
|
|
9
11
|
});
|
|
@@ -32,6 +34,7 @@ onSurfaceInit((surfaceId, hostViewId, view) => {
|
|
|
32
34
|
message: string;
|
|
33
35
|
defaultPrompt?: string;
|
|
34
36
|
};
|
|
37
|
+
log.debug("surface/dialog-ipc surfaceId=" + surfaceId + " hostViewId=" + hostViewId + " kind=" + d.kind);
|
|
35
38
|
registerDialogRequest(hostViewId, surfaceId, d);
|
|
36
39
|
});
|
|
37
40
|
view.on("console-message", (event: any) => {
|
|
@@ -39,6 +42,12 @@ onSurfaceInit((surfaceId, hostViewId, view) => {
|
|
|
39
42
|
// microtask — no extra deferral needed at the listener level.
|
|
40
43
|
emitConsole(hostViewId, surfaceId, event.data);
|
|
41
44
|
});
|
|
45
|
+
view.on("download-event", (event: any) => {
|
|
46
|
+
emitDownload(hostViewId, surfaceId, event.data);
|
|
47
|
+
});
|
|
48
|
+
view.on("popup-requested", (event: any) => {
|
|
49
|
+
emitPopupRequested(hostViewId, surfaceId, event.data);
|
|
50
|
+
});
|
|
42
51
|
});
|
|
43
52
|
|
|
44
53
|
// Surface registry's untrackSurface doesn't fire a teardown event — wire it
|