bunite-core 0.14.0 → 0.16.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 +2 -1
- package/src/host/core/BrowserView.ts +345 -24
- package/src/host/core/SurfaceBrowserIPC.ts +10 -1
- package/src/host/core/SurfaceManager.ts +357 -16
- package/src/host/events/webviewEvents.ts +18 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +140 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +205 -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 +278 -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 +93 -30
- package/src/native/win/native_host_cef.cpp +102 -10
- package/src/native/win/native_host_ffi.cpp +818 -2
- package/src/native/win/native_host_internal.h +22 -0
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +788 -4
- package/src/native/win-webview2/webview2_internal.h +14 -0
- package/src/native/win-webview2/webview2_runtime.cpp +276 -23
- package/src/preload/runtime.built.js +1 -1
- package/src/rpc/framework.ts +174 -11
- package/src/rpc/index.ts +11 -0
- package/src/webview/native.ts +142 -32
- package/src/webview/polyfill.ts +91 -14
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#import "bunite_mac_internal.h"
|
|
4
4
|
|
|
5
5
|
#include "webview_storage.h"
|
|
6
|
+
#include <objc/runtime.h>
|
|
6
7
|
|
|
7
8
|
@implementation BunitePendingPermission
|
|
8
9
|
@end
|
|
@@ -27,6 +28,125 @@
|
|
|
27
28
|
using bunite_mac::g_runtime;
|
|
28
29
|
using bunite_mac::utf8ToNSString;
|
|
29
30
|
|
|
31
|
+
// WKDownload delegate — decideDestination gates host policy + emits
|
|
32
|
+
// started/blocked, KVO on download.progress emits progress, didFinish/didFail
|
|
33
|
+
// emit terminal events.
|
|
34
|
+
@interface BuniteDownloadDelegate : NSObject <WKDownloadDelegate>
|
|
35
|
+
@property (nonatomic) uint32_t viewId;
|
|
36
|
+
@property (nonatomic) uint64_t downloadId;
|
|
37
|
+
@property (nonatomic, strong) NSString* destinationPath;
|
|
38
|
+
@property (nonatomic, weak) NSProgress* observedProgress;
|
|
39
|
+
@property (nonatomic) int64_t lastReportedBytes;
|
|
40
|
+
@property (atomic) BOOL terminalReached;
|
|
41
|
+
@end
|
|
42
|
+
|
|
43
|
+
static char kBuniteProgressCtx;
|
|
44
|
+
|
|
45
|
+
@implementation BuniteDownloadDelegate
|
|
46
|
+
|
|
47
|
+
- (void)download:(WKDownload*)download
|
|
48
|
+
decideDestinationUsingResponse:(NSURLResponse*)response
|
|
49
|
+
suggestedFilename:(NSString*)suggested
|
|
50
|
+
completionHandler:(void (^)(NSURL*))completionHandler
|
|
51
|
+
{
|
|
52
|
+
auto* st = bunite_mac::findView(self.viewId);
|
|
53
|
+
if (!st) { completionHandler(nil); return; }
|
|
54
|
+
const int32_t policy = st->download_policy.load();
|
|
55
|
+
NSString* url = download.originalRequest.URL.absoluteString ?: @"";
|
|
56
|
+
std::string url_s = url.UTF8String ?: "";
|
|
57
|
+
if (policy != 0) {
|
|
58
|
+
const char* reason = (policy == 1) ? "ask-not-implemented" : "host-policy";
|
|
59
|
+
std::string payload = "{\"kind\":\"blocked\",\"id\":\"mac-" + std::to_string(self.downloadId) +
|
|
60
|
+
"\",\"url\":\"" + bunite_mac::escapeJsonString(url_s) +
|
|
61
|
+
"\",\"reason\":\"" + reason + "\"}";
|
|
62
|
+
bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
|
|
63
|
+
completionHandler(nil);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
NSString* dirStr = st->download_dir.empty() ? nil : [NSString stringWithUTF8String:st->download_dir.c_str()];
|
|
67
|
+
if (!dirStr || dirStr.length == 0) {
|
|
68
|
+
NSArray<NSURL*>* paths = [NSFileManager.defaultManager URLsForDirectory:NSDownloadsDirectory inDomains:NSUserDomainMask];
|
|
69
|
+
dirStr = paths.firstObject.path ?: NSTemporaryDirectory();
|
|
70
|
+
}
|
|
71
|
+
NSString* fname = suggested.length > 0 ? suggested : @"download";
|
|
72
|
+
NSString* path = [dirStr stringByAppendingPathComponent:fname];
|
|
73
|
+
self.destinationPath = path;
|
|
74
|
+
std::string mime = (response.MIMEType ?: @"").UTF8String ?: "";
|
|
75
|
+
long long total = response.expectedContentLength > 0 ? (long long)response.expectedContentLength : 0;
|
|
76
|
+
std::string started = "{\"kind\":\"started\",\"id\":\"mac-" + std::to_string(self.downloadId) +
|
|
77
|
+
"\",\"url\":\"" + bunite_mac::escapeJsonString(url_s) +
|
|
78
|
+
"\",\"suggestedFilename\":\"" + bunite_mac::escapeJsonString(fname.UTF8String ?: "") +
|
|
79
|
+
"\",\"mimeType\":\"" + bunite_mac::escapeJsonString(mime) + "\"";
|
|
80
|
+
if (total > 0) started += ",\"sizeBytes\":" + std::to_string(total);
|
|
81
|
+
started += "}";
|
|
82
|
+
bunite_mac::emitWebviewEvent(self.viewId, "download-event", started);
|
|
83
|
+
NSProgress* prog = download.progress;
|
|
84
|
+
if (prog) {
|
|
85
|
+
self.observedProgress = prog;
|
|
86
|
+
self.lastReportedBytes = 0;
|
|
87
|
+
[prog addObserver:self
|
|
88
|
+
forKeyPath:@"completedUnitCount"
|
|
89
|
+
options:NSKeyValueObservingOptionNew
|
|
90
|
+
context:&kBuniteProgressCtx];
|
|
91
|
+
}
|
|
92
|
+
completionHandler([NSURL fileURLWithPath:path]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
- (void)observeValueForKeyPath:(NSString*)keyPath
|
|
96
|
+
ofObject:(id)object
|
|
97
|
+
change:(NSDictionary*)change
|
|
98
|
+
context:(void*)context
|
|
99
|
+
{
|
|
100
|
+
if (context != &kBuniteProgressCtx) {
|
|
101
|
+
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (self.terminalReached) return;
|
|
105
|
+
NSProgress* prog = (NSProgress*)object;
|
|
106
|
+
int64_t received = prog.completedUnitCount;
|
|
107
|
+
int64_t total = prog.totalUnitCount;
|
|
108
|
+
if (received <= self.lastReportedBytes) return;
|
|
109
|
+
self.lastReportedBytes = received;
|
|
110
|
+
std::string payload = "{\"kind\":\"progress\",\"id\":\"mac-" + std::to_string(self.downloadId) +
|
|
111
|
+
"\",\"receivedBytes\":" + std::to_string(received);
|
|
112
|
+
if (total > 0) payload += ",\"totalBytes\":" + std::to_string(total);
|
|
113
|
+
payload += "}";
|
|
114
|
+
bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
- (void)_detachProgressObserver {
|
|
118
|
+
NSProgress* prog = self.observedProgress;
|
|
119
|
+
if (!prog) return;
|
|
120
|
+
@try {
|
|
121
|
+
[prog removeObserver:self forKeyPath:@"completedUnitCount" context:&kBuniteProgressCtx];
|
|
122
|
+
} @catch (NSException* ex) {} // tolerate over-remove on edge lifecycles
|
|
123
|
+
self.observedProgress = nil;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
- (void)downloadDidFinish:(WKDownload*)download {
|
|
127
|
+
self.terminalReached = YES;
|
|
128
|
+
[self _detachProgressObserver];
|
|
129
|
+
std::string dest = self.destinationPath ? std::string(self.destinationPath.UTF8String ?: "") : "";
|
|
130
|
+
std::string payload = "{\"kind\":\"completed\",\"id\":\"mac-" + std::to_string(self.downloadId) +
|
|
131
|
+
"\",\"localPath\":\"" + bunite_mac::escapeJsonString(dest) + "\"}";
|
|
132
|
+
bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
- (void)download:(WKDownload*)download didFailWithError:(NSError*)error resumeData:(NSData*)resumeData {
|
|
136
|
+
self.terminalReached = YES;
|
|
137
|
+
[self _detachProgressObserver];
|
|
138
|
+
std::string reason = error ? std::string(error.localizedDescription.UTF8String ?: "unknown") : "unknown";
|
|
139
|
+
std::string payload = "{\"kind\":\"failed\",\"id\":\"mac-" + std::to_string(self.downloadId) +
|
|
140
|
+
"\",\"reason\":\"" + bunite_mac::escapeJsonString(reason) + "\"}";
|
|
141
|
+
bunite_mac::emitWebviewEvent(self.viewId, "download-event", payload);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
- (void)dealloc {
|
|
145
|
+
[self _detachProgressObserver];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@end
|
|
149
|
+
|
|
30
150
|
@interface BuniteNavigationDelegate : NSObject <WKNavigationDelegate>
|
|
31
151
|
@end
|
|
32
152
|
|
|
@@ -92,6 +212,24 @@ decidePolicyForNavigationAction:(WKNavigationAction*)action
|
|
|
92
212
|
bunite_mac::emitWebviewEvent(view_id, "load-fail", payload);
|
|
93
213
|
}
|
|
94
214
|
|
|
215
|
+
- (void)webView:(WKWebView*)wv navigationAction:(WKNavigationAction*)action didBecomeDownload:(WKDownload*)download {
|
|
216
|
+
static std::atomic<uint64_t> g_seq{1};
|
|
217
|
+
BuniteDownloadDelegate* d = [BuniteDownloadDelegate new];
|
|
218
|
+
d.viewId = bunite_mac::viewIdForWebView(wv);
|
|
219
|
+
d.downloadId = g_seq.fetch_add(1);
|
|
220
|
+
download.delegate = d;
|
|
221
|
+
objc_setAssociatedObject(download, "bunite.dl.delegate", d, OBJC_ASSOCIATION_RETAIN);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
- (void)webView:(WKWebView*)wv navigationResponse:(WKNavigationResponse*)response didBecomeDownload:(WKDownload*)download {
|
|
225
|
+
static std::atomic<uint64_t> g_seq{0x80000000ULL};
|
|
226
|
+
BuniteDownloadDelegate* d = [BuniteDownloadDelegate new];
|
|
227
|
+
d.viewId = bunite_mac::viewIdForWebView(wv);
|
|
228
|
+
d.downloadId = g_seq.fetch_add(1);
|
|
229
|
+
download.delegate = d;
|
|
230
|
+
objc_setAssociatedObject(download, "bunite.dl.delegate", d, OBJC_ASSOCIATION_RETAIN);
|
|
231
|
+
}
|
|
232
|
+
|
|
95
233
|
@end
|
|
96
234
|
|
|
97
235
|
@interface BuniteTitleObserver : NSObject
|
|
@@ -132,14 +270,44 @@ createWebViewWithConfiguration:(WKWebViewConfiguration*)config
|
|
|
132
270
|
forNavigationAction:(WKNavigationAction*)action
|
|
133
271
|
windowFeatures:(WKWindowFeatures*)features
|
|
134
272
|
{
|
|
135
|
-
(void)
|
|
136
|
-
uint32_t
|
|
273
|
+
(void)features;
|
|
274
|
+
uint32_t opener_view_id = bunite_mac::viewIdForWebView(wv);
|
|
137
275
|
NSString* url = action.request.URL.absoluteString ?: @"";
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
bunite_mac::
|
|
141
|
-
|
|
142
|
-
|
|
276
|
+
if (bunite_mac::g_runtime.popup_blocking) {
|
|
277
|
+
std::string payload = "{\"url\":\"" + bunite_mac::escapeJsonString(url.UTF8String ?: "") + "\"}";
|
|
278
|
+
bunite_mac::emitWebviewEvent(opener_view_id, "new-window-open", payload);
|
|
279
|
+
return nil;
|
|
280
|
+
}
|
|
281
|
+
static std::atomic<uint32_t> g_popup_seq{0x80000000u};
|
|
282
|
+
const uint32_t new_view_id = g_popup_seq.fetch_add(1);
|
|
283
|
+
WKWebView* popup = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 1, 1) configuration:config];
|
|
284
|
+
popup.navigationDelegate = wv.navigationDelegate;
|
|
285
|
+
popup.UIDelegate = wv.UIDelegate;
|
|
286
|
+
if (!bunite_mac::g_runtime.parked_popups) {
|
|
287
|
+
bunite_mac::g_runtime.parked_popups = [NSMutableDictionary dictionary];
|
|
288
|
+
}
|
|
289
|
+
if (!bunite_mac::g_runtime.popup_parent) {
|
|
290
|
+
bunite_mac::g_runtime.popup_parent =
|
|
291
|
+
[[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 1, 1)
|
|
292
|
+
styleMask:NSWindowStyleMaskBorderless
|
|
293
|
+
backing:NSBackingStoreBuffered
|
|
294
|
+
defer:YES];
|
|
295
|
+
}
|
|
296
|
+
bunite_mac::g_runtime.parked_popups[@(new_view_id)] = popup;
|
|
297
|
+
[bunite_mac::g_runtime.popup_parent.contentView addSubview:popup];
|
|
298
|
+
bunite_mac::registerWebViewId(popup, new_view_id);
|
|
299
|
+
{
|
|
300
|
+
std::lock_guard<std::mutex> lock(bunite_mac::g_runtime.object_mutex);
|
|
301
|
+
auto& st = bunite_mac::g_runtime.views[new_view_id];
|
|
302
|
+
st.webview = popup;
|
|
303
|
+
st.container = nil; // bound on adoption
|
|
304
|
+
st.window_id = 0;
|
|
305
|
+
}
|
|
306
|
+
std::string payload = "{\"newSurfaceId\":" + std::to_string(new_view_id) +
|
|
307
|
+
",\"url\":\"" + bunite_mac::escapeJsonString(url.UTF8String ?: "") +
|
|
308
|
+
"\",\"disposition\":\"popup\"}";
|
|
309
|
+
bunite_mac::emitWebviewEvent(opener_view_id, "popup-requested", payload);
|
|
310
|
+
return popup;
|
|
143
311
|
}
|
|
144
312
|
|
|
145
313
|
// --- Dialog handlers (alert / confirm / prompt). beforeunload is not
|
|
@@ -279,6 +447,14 @@ uint32_t viewIdForWebView(WKWebView* wv) {
|
|
|
279
447
|
return id ? id.unsignedIntValue : 0;
|
|
280
448
|
}
|
|
281
449
|
|
|
450
|
+
void registerWebViewId(WKWebView* wv, uint32_t view_id) {
|
|
451
|
+
[webviewIdTable() setObject:@(view_id) forKey:wv];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
void unregisterWebViewId(WKWebView* wv) {
|
|
455
|
+
[webviewIdTable() removeObjectForKey:wv];
|
|
456
|
+
}
|
|
457
|
+
|
|
282
458
|
bool createView(uint32_t view_id, uint32_t window_id,
|
|
283
459
|
NSString* url, NSString* html, NSString* preload, NSString* appres_root,
|
|
284
460
|
NSString* navigation_rules_json, NSString* preload_origins_json,
|
|
@@ -364,6 +540,50 @@ bool createView(uint32_t view_id, uint32_t window_id,
|
|
|
364
540
|
return true;
|
|
365
541
|
}
|
|
366
542
|
|
|
543
|
+
bool acceptParkedPopup(uint32_t new_view_id, uint32_t host_window_id, double x, double y, double w, double h) {
|
|
544
|
+
WKWebView* popup = nil;
|
|
545
|
+
if (g_runtime.parked_popups) {
|
|
546
|
+
popup = g_runtime.parked_popups[@(new_view_id)];
|
|
547
|
+
if (popup) [g_runtime.parked_popups removeObjectForKey:@(new_view_id)];
|
|
548
|
+
}
|
|
549
|
+
if (!popup) return false;
|
|
550
|
+
WindowState* host = findWindow(host_window_id);
|
|
551
|
+
if (!host || !host->window) return false;
|
|
552
|
+
[popup removeFromSuperview];
|
|
553
|
+
BunitePassthroughContainer* container = [[BunitePassthroughContainer alloc] init];
|
|
554
|
+
container.frame = NSMakeRect(x, y, w, h);
|
|
555
|
+
popup.frame = NSMakeRect(0, 0, w, h);
|
|
556
|
+
popup.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
|
557
|
+
[container addSubview:popup];
|
|
558
|
+
[host->window.contentView addSubview:container];
|
|
559
|
+
{
|
|
560
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
561
|
+
auto& st = g_runtime.views[new_view_id];
|
|
562
|
+
st.container = container;
|
|
563
|
+
st.webview = popup;
|
|
564
|
+
st.window_id = host_window_id;
|
|
565
|
+
}
|
|
566
|
+
// Re-emit view-ready so TS BrowserView.adopt resolves — the initial
|
|
567
|
+
// did-navigate fired before the adopter registered.
|
|
568
|
+
emitWebviewEvent(new_view_id, "view-ready", "");
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
void dismissParkedPopup(uint32_t new_view_id) {
|
|
573
|
+
WKWebView* popup = nil;
|
|
574
|
+
if (g_runtime.parked_popups) {
|
|
575
|
+
popup = g_runtime.parked_popups[@(new_view_id)];
|
|
576
|
+
if (popup) [g_runtime.parked_popups removeObjectForKey:@(new_view_id)];
|
|
577
|
+
}
|
|
578
|
+
if (!popup) return;
|
|
579
|
+
[popup removeFromSuperview];
|
|
580
|
+
unregisterWebViewId(popup);
|
|
581
|
+
{
|
|
582
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
583
|
+
g_runtime.views.erase(new_view_id);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
367
587
|
void removeView(uint32_t view_id) {
|
|
368
588
|
__strong WKWebView* wv = nil;
|
|
369
589
|
__strong NSView* container = nil;
|
|
@@ -15,29 +15,7 @@
|
|
|
15
15
|
extern "C" {
|
|
16
16
|
#endif
|
|
17
17
|
|
|
18
|
-
/** ABI version. Bump on any breaking change to symbol set / signatures.
|
|
19
|
-
* v9 (2026-05): adds `bunite_view_mouse` (move/down/up primitives for drag &
|
|
20
|
-
* hover) + `bunite_view_respond_dialog`. Webview event names
|
|
21
|
-
* expand to include `dialog` (alert/confirm/prompt/beforeunload)
|
|
22
|
-
* and `console-message` (the latter is RPC-pushed by preload,
|
|
23
|
-
* not emitted by native — listed here for the host-side event
|
|
24
|
-
* channel completeness). Capability bits add `MOUSE` (1<<11),
|
|
25
|
-
* `DIALOGS` (1<<12), `CONSOLE` (1<<13). `NATIVE_INPUT_TRUSTED`
|
|
26
|
-
* meaning stretches to include `mouse` (click/type/press/mouse
|
|
27
|
-
* all produce isTrusted=true on supporting backends; scroll and
|
|
28
|
-
* screenshot remain outside that guarantee).
|
|
29
|
-
* v8 (2026-05): `bunite_view_press` gains `action` (down/up/both), `extended`
|
|
30
|
-
* (Win 0xE0 scancode prefix), `location` (DOM KeyboardEvent
|
|
31
|
-
* location, 0/1/2/3) params. Webview event names expand to
|
|
32
|
-
* include `load-start` / `load-finish` / `load-fail`. Capability
|
|
33
|
-
* bit 2 renamed `TITLE_CHANGED` → `SURFACE_EVENTS` — value
|
|
34
|
-
* unchanged but semantic is now "unified surfaceEvents stream
|
|
35
|
-
* supported". A surface fires `load-finish` OR `load-fail` per
|
|
36
|
-
* navigation, never both.
|
|
37
|
-
* v7 (2026-05): adds `bunite_view_screenshot` + `bunite_view_capabilities`
|
|
38
|
-
* + capability bitset (`BuniteCapBit`).
|
|
39
|
-
* v6 (2026-05): adds input dispatch — `bunite_view_click/type/press/scroll`.
|
|
40
|
-
* v5 (2026-05): adds `bunite_view_evaluate` + `evaluate-result` webview event. */
|
|
18
|
+
/** ABI version. Bump on any breaking change to symbol set / signatures. */
|
|
41
19
|
BUNITE_EXPORT int32_t bunite_abi_version(void);
|
|
42
20
|
BUNITE_EXPORT void bunite_set_log_level(int32_t level);
|
|
43
21
|
BUNITE_EXPORT bool bunite_init(
|
|
@@ -222,24 +200,33 @@ BUNITE_EXPORT void bunite_view_respond_dialog(
|
|
|
222
200
|
const char* text
|
|
223
201
|
);
|
|
224
202
|
|
|
225
|
-
/** Per-view automation capability bitset.
|
|
226
|
-
*
|
|
227
|
-
*
|
|
203
|
+
/** Per-view automation capability bitset. Two categories: method gate
|
|
204
|
+
* (bit=false → method must not be called) and property advertise (bit=false
|
|
205
|
+
* → method runs, property degrades). See `.agents/architecture.md`. */
|
|
228
206
|
enum BuniteCapBit {
|
|
207
|
+
/* method gate */
|
|
229
208
|
BUNITE_CAP_EVALUATE = 1u << 0,
|
|
230
|
-
BUNITE_CAP_CROSS_ORIGIN_EVAL = 1u << 1,
|
|
231
209
|
BUNITE_CAP_SURFACE_EVENTS = 1u << 2,
|
|
232
|
-
BUNITE_CAP_NATIVE_INPUT_TRUSTED = 1u << 3, /* click/type/press/mouse all isTrusted=true */
|
|
233
210
|
BUNITE_CAP_CLICK = 1u << 4,
|
|
234
211
|
BUNITE_CAP_TYPE = 1u << 5,
|
|
235
212
|
BUNITE_CAP_PRESS = 1u << 6,
|
|
236
213
|
BUNITE_CAP_SCROLL = 1u << 7,
|
|
237
214
|
BUNITE_CAP_SCREENSHOT = 1u << 8,
|
|
238
|
-
BUNITE_CAP_FORMAT_PNG = 1u << 9,
|
|
239
|
-
BUNITE_CAP_FORMAT_JPEG = 1u << 10,
|
|
240
215
|
BUNITE_CAP_MOUSE = 1u << 11,
|
|
241
216
|
BUNITE_CAP_DIALOGS = 1u << 12,
|
|
242
217
|
BUNITE_CAP_CONSOLE = 1u << 13,
|
|
218
|
+
BUNITE_CAP_AX = 1u << 15,
|
|
219
|
+
BUNITE_CAP_BOUNDING_RECT = 1u << 16,
|
|
220
|
+
BUNITE_CAP_FRAMES = 1u << 17,
|
|
221
|
+
BUNITE_CAP_DOWNLOADS = 1u << 18,
|
|
222
|
+
BUNITE_CAP_POPUPS = 1u << 19,
|
|
223
|
+
BUNITE_CAP_RESOLVE_AND_CLICK = 1u << 20,
|
|
224
|
+
|
|
225
|
+
/* property advertise */
|
|
226
|
+
BUNITE_CAP_CROSS_ORIGIN_EVAL = 1u << 1,
|
|
227
|
+
BUNITE_CAP_NATIVE_INPUT_TRUSTED = 1u << 3, /* click/type/press/mouse isTrusted */
|
|
228
|
+
BUNITE_CAP_FORMAT_PNG = 1u << 9,
|
|
229
|
+
BUNITE_CAP_FORMAT_JPEG = 1u << 10,
|
|
243
230
|
};
|
|
244
231
|
BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id);
|
|
245
232
|
|
|
@@ -259,6 +246,82 @@ BUNITE_EXPORT void bunite_view_screenshot(
|
|
|
259
246
|
int32_t quality
|
|
260
247
|
);
|
|
261
248
|
|
|
249
|
+
/** Snapshot the accessibility tree (CDP `Accessibility.getFullAXTree`). Async —
|
|
250
|
+
* result reported via webview event handler as `accessibility-result` payload
|
|
251
|
+
* { requestId, ok: true, tree: {nodes: [<CDP AXNode flat list>]} }
|
|
252
|
+
* { requestId, ok: false, code, message }
|
|
253
|
+
* TS builds the nested tree from `childIds`. mac/linux always emit
|
|
254
|
+
* `not_supported` (no public ax tree API). `interesting_only` is reserved and
|
|
255
|
+
* currently unused on the native side (filter is TS-side). */
|
|
256
|
+
BUNITE_EXPORT void bunite_view_accessibility_snapshot(
|
|
257
|
+
uint32_t view_id,
|
|
258
|
+
uint32_t request_id,
|
|
259
|
+
int32_t interesting_only
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
/** Enumerate frames in the view. Async — result reported via webview event
|
|
263
|
+
* `list-frames-result` payload
|
|
264
|
+
* { requestId, ok: true, frames: [{frameId, parentFrameId, origin, url, name?}] }
|
|
265
|
+
* { requestId, ok: false, code, message }
|
|
266
|
+
* Codes: `not_supported`, `runtime_error`. mac/linux emit `not_supported`. */
|
|
267
|
+
BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id);
|
|
268
|
+
|
|
269
|
+
/** Evaluate `script` in the target frame's isolated world (CDP
|
|
270
|
+
* `Page.createIsolatedWorld` + `Runtime.evaluate`). Page main-world JS
|
|
271
|
+
* variables are NOT visible; DOM access works. Result reused via the
|
|
272
|
+
* existing `evaluate-result` event. `frame_id` empty/null delegates to
|
|
273
|
+
* `bunite_view_evaluate` (main frame, main world). */
|
|
274
|
+
BUNITE_EXPORT void bunite_view_evaluate_in_frame(
|
|
275
|
+
uint32_t view_id,
|
|
276
|
+
uint32_t request_id,
|
|
277
|
+
const char* script,
|
|
278
|
+
const char* frame_id
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
/** Atomic selector resolve + native click. Async via `resolve-and-click-result`:
|
|
282
|
+
* { requestId, ok: true, rect, isTrustedEvent }
|
|
283
|
+
* { requestId, ok: false, code, message }
|
|
284
|
+
* Codes: not_found / not_visible / runtime_error / cross_origin / not_supported.
|
|
285
|
+
* `frame_id` non-empty selects a same-origin iframe (rect viewport-normalized);
|
|
286
|
+
* cross-origin → `cross_origin`, mac/linux → `not_supported`. scrollIntoView is
|
|
287
|
+
* automatic. `isTrustedEvent` is empirical per backend; CEF/WV2 CDP path and
|
|
288
|
+
* mac NSEvent direct dispatch all produce trusted events (all `true`). */
|
|
289
|
+
BUNITE_EXPORT void bunite_view_resolve_and_click(
|
|
290
|
+
uint32_t view_id,
|
|
291
|
+
uint32_t request_id,
|
|
292
|
+
const char* selector,
|
|
293
|
+
const char* frame_id,
|
|
294
|
+
int32_t button,
|
|
295
|
+
int32_t click_count,
|
|
296
|
+
uint32_t modifiers
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
/** Set per-view download policy. `policy`: 0=auto (allow + emit lifecycle),
|
|
300
|
+
* 1=ask (not implemented for v10, treated as block), 2=block (default).
|
|
301
|
+
* `download_dir` (utf-8) optionally overrides backend default save dir.
|
|
302
|
+
* Lifecycle events emit as `download-event` payloads
|
|
303
|
+
* { kind: "started"|"progress"|"completed"|"failed"|"blocked", id, ...fields }
|
|
304
|
+
* See `DownloadEvent` (TS) for the per-kind field set. */
|
|
305
|
+
BUNITE_EXPORT void bunite_view_set_download_policy(
|
|
306
|
+
uint32_t view_id,
|
|
307
|
+
int32_t policy,
|
|
308
|
+
const char* download_dir
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
/** Adopt a popup-minted view. Native must have previously emitted a
|
|
312
|
+
* `popup-requested` event (carrying `newSurfaceId`); host calls this to attach
|
|
313
|
+
* the pre-minted view to the target window + bounds. `host_window_id` is the
|
|
314
|
+
* destination `WindowHost.id`. */
|
|
315
|
+
BUNITE_EXPORT void bunite_view_popup_accept(
|
|
316
|
+
uint32_t new_view_id,
|
|
317
|
+
uint32_t host_window_id,
|
|
318
|
+
double x, double y, double w, double h
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
/** Discard a popup-minted view that wasn't adopted (or that host wants to
|
|
322
|
+
* reject). Native destroys the controller/browser. Idempotent. */
|
|
323
|
+
BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id);
|
|
324
|
+
|
|
262
325
|
BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id);
|
|
263
326
|
BUNITE_EXPORT void bunite_view_close_devtools(uint32_t view_id);
|
|
264
327
|
BUNITE_EXPORT void bunite_view_toggle_devtools(uint32_t view_id);
|
|
@@ -86,7 +86,8 @@ class BuniteCefClient
|
|
|
86
86
|
public CefResourceRequestHandler,
|
|
87
87
|
public CefPermissionHandler,
|
|
88
88
|
public CefDisplayHandler,
|
|
89
|
-
public CefJSDialogHandler
|
|
89
|
+
public CefJSDialogHandler,
|
|
90
|
+
public CefDownloadHandler {
|
|
90
91
|
public:
|
|
91
92
|
// BuniteCefClient is constructed 1:1 with a `ViewHost*`; `last_title_` is
|
|
92
93
|
// therefore per-view. OnTitleChange runs on the CEF UI thread (single).
|
|
@@ -99,6 +100,68 @@ public:
|
|
|
99
100
|
CefRefPtr<CefPermissionHandler> GetPermissionHandler() override { return this; }
|
|
100
101
|
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
|
|
101
102
|
CefRefPtr<CefJSDialogHandler> GetJSDialogHandler() override { return this; }
|
|
103
|
+
CefRefPtr<CefDownloadHandler> GetDownloadHandler() override { return this; }
|
|
104
|
+
|
|
105
|
+
bool OnBeforeDownload(CefRefPtr<CefBrowser>, CefRefPtr<CefDownloadItem> item,
|
|
106
|
+
const CefString& suggested_name,
|
|
107
|
+
CefRefPtr<CefBeforeDownloadCallback> callback) override {
|
|
108
|
+
CEF_REQUIRE_UI_THREAD();
|
|
109
|
+
int32_t policy = view_->download_policy.load();
|
|
110
|
+
std::string id = "cef-" + std::to_string(item->GetId());
|
|
111
|
+
std::string url = item->GetURL().ToString();
|
|
112
|
+
std::string mime = item->GetMimeType().ToString();
|
|
113
|
+
int64_t total = item->GetTotalBytes();
|
|
114
|
+
std::string suggested = suggested_name.ToString();
|
|
115
|
+
// Only policy=0 (auto) allows. `ask` (1) falls back to block — distinguished
|
|
116
|
+
// via blocked.reason so callers can detect the unsupported policy path.
|
|
117
|
+
if (policy != 0) {
|
|
118
|
+
const char* reason = (policy == 1) ? "ask-not-implemented" : "host-policy";
|
|
119
|
+
std::string payload = "{\"kind\":\"blocked\",\"id\":\"" + id +
|
|
120
|
+
"\",\"url\":\"" + bunite_win::escapeJsonString(url) +
|
|
121
|
+
"\",\"reason\":\"" + reason + "\"}";
|
|
122
|
+
bunite_win::emitWebviewEvent(view_->id, "download-event", payload);
|
|
123
|
+
return true; // not calling callback->Continue → cancels.
|
|
124
|
+
}
|
|
125
|
+
// auto: build target path. If host set download_dir, use it; else CEF defaults to user Downloads.
|
|
126
|
+
std::string target = view_->download_dir;
|
|
127
|
+
if (!target.empty()) {
|
|
128
|
+
if (target.back() != '\\' && target.back() != '/') target.push_back('\\');
|
|
129
|
+
target += suggested;
|
|
130
|
+
}
|
|
131
|
+
callback->Continue(target, false); // false = no Save-As dialog.
|
|
132
|
+
std::string payload = "{\"kind\":\"started\",\"id\":\"" + id +
|
|
133
|
+
"\",\"url\":\"" + bunite_win::escapeJsonString(url) +
|
|
134
|
+
"\",\"suggestedFilename\":\"" + bunite_win::escapeJsonString(suggested) +
|
|
135
|
+
"\",\"mimeType\":\"" + bunite_win::escapeJsonString(mime) + "\"";
|
|
136
|
+
if (total > 0) payload += ",\"sizeBytes\":" + std::to_string(total);
|
|
137
|
+
payload += "}";
|
|
138
|
+
bunite_win::emitWebviewEvent(view_->id, "download-event", payload);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
void OnDownloadUpdated(CefRefPtr<CefBrowser>, CefRefPtr<CefDownloadItem> item,
|
|
143
|
+
CefRefPtr<CefDownloadItemCallback> /*callback*/) override {
|
|
144
|
+
CEF_REQUIRE_UI_THREAD();
|
|
145
|
+
std::string id = "cef-" + std::to_string(item->GetId());
|
|
146
|
+
if (item->IsComplete()) {
|
|
147
|
+
std::string path = item->GetFullPath().ToString();
|
|
148
|
+
std::string payload = "{\"kind\":\"completed\",\"id\":\"" + id +
|
|
149
|
+
"\",\"localPath\":\"" + bunite_win::escapeJsonString(path) + "\"}";
|
|
150
|
+
bunite_win::emitWebviewEvent(view_->id, "download-event", payload);
|
|
151
|
+
} else if (item->IsCanceled()) {
|
|
152
|
+
std::string payload = "{\"kind\":\"failed\",\"id\":\"" + id +
|
|
153
|
+
"\",\"reason\":\"canceled\"}";
|
|
154
|
+
bunite_win::emitWebviewEvent(view_->id, "download-event", payload);
|
|
155
|
+
} else if (item->IsInProgress()) {
|
|
156
|
+
int64_t rec = item->GetReceivedBytes();
|
|
157
|
+
int64_t tot = item->GetTotalBytes();
|
|
158
|
+
std::string payload = "{\"kind\":\"progress\",\"id\":\"" + id +
|
|
159
|
+
"\",\"receivedBytes\":" + std::to_string(rec);
|
|
160
|
+
if (tot > 0) payload += ",\"totalBytes\":" + std::to_string(tot);
|
|
161
|
+
payload += "}";
|
|
162
|
+
bunite_win::emitWebviewEvent(view_->id, "download-event", payload);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
102
165
|
|
|
103
166
|
bool OnJSDialog(CefRefPtr<CefBrowser>, const CefString& /*origin_url*/,
|
|
104
167
|
JSDialogType dialog_type, const CefString& message_text,
|
|
@@ -110,7 +173,9 @@ public:
|
|
|
110
173
|
: (dialog_type == JSDIALOGTYPE_CONFIRM) ? "confirm" : "prompt";
|
|
111
174
|
const uint32_t rid = view_->next_dialog_request_id++;
|
|
112
175
|
view_->pending_dialogs[rid] = callback;
|
|
113
|
-
|
|
176
|
+
BUNITE_INFO("cef/dialog: OnJSDialog view=%u kind=%s rid=%u", view_->id, kind, rid);
|
|
177
|
+
// CEF asserts !suppress_message when return=true (custom dialog path).
|
|
178
|
+
suppress_message = false;
|
|
114
179
|
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
115
180
|
",\"kind\":\"" + kind +
|
|
116
181
|
"\",\"message\":\"" + bunite_win::escapeJsonString(message_text.ToString()) + "\"";
|
|
@@ -254,6 +319,19 @@ public:
|
|
|
254
319
|
}
|
|
255
320
|
|
|
256
321
|
bunite_win::emitWebviewEvent(view_->id, "view-ready");
|
|
322
|
+
|
|
323
|
+
if (view_->is_popup_pending) {
|
|
324
|
+
if (view_->popup_dismiss_requested) {
|
|
325
|
+
view_->closing.store(true);
|
|
326
|
+
browser->GetHost()->CloseBrowser(true);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (view_->pending_popup_accept) {
|
|
330
|
+
auto p = *view_->pending_popup_accept;
|
|
331
|
+
view_->pending_popup_accept.reset();
|
|
332
|
+
bunite_win::applyPopupAccept(view_, p.host_window_id, p.x, p.y, p.w, p.h);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
257
335
|
}
|
|
258
336
|
|
|
259
337
|
bool DoClose(CefRefPtr<CefBrowser>) override {
|
|
@@ -317,19 +395,33 @@ public:
|
|
|
317
395
|
CefLifeSpanHandler::WindowOpenDisposition,
|
|
318
396
|
bool,
|
|
319
397
|
const CefPopupFeatures&,
|
|
320
|
-
CefWindowInfo
|
|
321
|
-
CefRefPtr<CefClient
|
|
398
|
+
CefWindowInfo& window_info,
|
|
399
|
+
CefRefPtr<CefClient>& client,
|
|
322
400
|
CefBrowserSettings&,
|
|
323
401
|
CefRefPtr<CefDictionaryValue>&,
|
|
324
402
|
bool*
|
|
325
403
|
) override {
|
|
326
404
|
CEF_REQUIRE_UI_THREAD();
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
405
|
+
// Popup IDs live in the upper u32 half; TS allocator stays below.
|
|
406
|
+
static std::atomic<uint32_t> g_popup_seq{0x80000000u};
|
|
407
|
+
uint32_t new_view_id = g_popup_seq.fetch_add(1);
|
|
408
|
+
auto* popup = new ViewHost();
|
|
409
|
+
popup->id = new_view_id;
|
|
410
|
+
popup->window = nullptr;
|
|
411
|
+
popup->is_popup_pending = true;
|
|
412
|
+
{
|
|
413
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
414
|
+
g_runtime.views_by_id[new_view_id] = popup;
|
|
415
|
+
}
|
|
416
|
+
// Initial parent = runtime message window; host adopts and reparents later.
|
|
417
|
+
window_info.SetAsChild(g_runtime.message_window, CefRect{0, 0, 0, 0});
|
|
418
|
+
window_info.style = WS_CHILD;
|
|
419
|
+
client = new BuniteCefClient(popup);
|
|
420
|
+
std::string payload = "{\"newSurfaceId\":" + std::to_string(new_view_id) +
|
|
421
|
+
",\"url\":\"" + bunite_win::escapeJsonString(target_url.ToString()) +
|
|
422
|
+
"\",\"disposition\":\"popup\"}";
|
|
423
|
+
bunite_win::emitWebviewEvent(view_->id, "popup-requested", payload);
|
|
424
|
+
return false; // allow CEF to create the popup browser.
|
|
333
425
|
}
|
|
334
426
|
|
|
335
427
|
void OnLoadStart(CefRefPtr<CefBrowser>, CefRefPtr<CefFrame> frame, TransitionType) override {
|