bunite-core 0.12.1 → 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 +19 -2
- package/src/host/core/BrowserView.ts +515 -38
- package/src/host/core/SurfaceBrowserIPC.ts +53 -3
- package/src/host/core/SurfaceManager.ts +603 -30
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +25 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +263 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +427 -6
- package/src/native/linux/bunite_linux_internal.h +18 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +296 -5
- package/src/native/mac/bunite_mac_ffi.mm +630 -8
- package/src/native/mac/bunite_mac_internal.h +19 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +371 -9
- package/src/native/shared/ffi_exports.h +200 -2
- package/src/native/win/native_host_cef.cpp +186 -11
- package/src/native/win/native_host_ffi.cpp +1194 -1
- package/src/native/win/native_host_internal.h +35 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
- package/src/native/win-webview2/webview2_internal.h +25 -0
- package/src/native/win-webview2/webview2_runtime.cpp +403 -34
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +340 -8
- package/src/rpc/index.ts +32 -0
- package/src/webview/native.ts +253 -51
- package/src/webview/polyfill.ts +283 -22
|
@@ -20,7 +20,7 @@ using bunite_mac::runOnUiThreadSync;
|
|
|
20
20
|
|
|
21
21
|
namespace {
|
|
22
22
|
|
|
23
|
-
constexpr int32_t kBuniteAbiVersion =
|
|
23
|
+
constexpr int32_t kBuniteAbiVersion = 11;
|
|
24
24
|
|
|
25
25
|
// warn-once — avoid log spam from tight JS call loops.
|
|
26
26
|
#define BUNITE_MAC_TODO(name) \
|
|
@@ -273,13 +273,83 @@ extern "C" BUNITE_EXPORT void bunite_view_execute_javascript(uint32_t view_id, c
|
|
|
273
273
|
});
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
extern "C" BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, const char*
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
276
|
+
extern "C" BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, const char* script) {
|
|
277
|
+
// Wrapper matches WebView2/CEF: try/catch returns JSON envelope string.
|
|
278
|
+
// WKWebView's `evaluateJavaScript:` delivers the string directly (no outer
|
|
279
|
+
// re-stringify), so the inner envelope is the completion result.
|
|
280
|
+
if (!script) {
|
|
281
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
282
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
283
|
+
"\"message\":\"null script\"}";
|
|
284
|
+
bunite_mac::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
std::string wrapped =
|
|
288
|
+
"(function(){try{return JSON.stringify({__bunite_ok:true,value:("
|
|
289
|
+
+ std::string(script) +
|
|
290
|
+
")})}catch(e){var c=(e&&e.name===\"SecurityError\")?\"cross_origin\":\"runtime_error\";"
|
|
291
|
+
"return JSON.stringify({__bunite_ok:false,code:c,"
|
|
292
|
+
"message:(e&&e.message)?e.message:String(e),"
|
|
293
|
+
"name:(e&&e.name)||\"\"})}})()";
|
|
294
|
+
NSString* nsScript = [NSString stringWithUTF8String:wrapped.c_str()];
|
|
295
|
+
runOnUiThreadSync([=]() {
|
|
296
|
+
auto* v = bunite_mac::findView(view_id);
|
|
297
|
+
if (!v || !v->webview) {
|
|
298
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
299
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
300
|
+
"\"message\":\"view not ready\"}";
|
|
301
|
+
bunite_mac::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
[v->webview evaluateJavaScript:nsScript completionHandler:^(id result, NSError* error) {
|
|
305
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id);
|
|
306
|
+
if (error) {
|
|
307
|
+
std::string msg = error.localizedDescription.UTF8String ?: "evaluate failed";
|
|
308
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
309
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
310
|
+
} else if (![result isKindOfClass:[NSString class]]) {
|
|
311
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
312
|
+
"\"message\":\"wrapper returned non-string\"}";
|
|
313
|
+
} else {
|
|
314
|
+
std::string inner = ((NSString*)result).UTF8String ?: "";
|
|
315
|
+
if (inner.find("\"__bunite_ok\":true") != std::string::npos) {
|
|
316
|
+
static const std::string prefix = "{\"__bunite_ok\":true,\"value\":";
|
|
317
|
+
std::string value_json = "null";
|
|
318
|
+
if (inner.compare(0, prefix.size(), prefix) == 0 &&
|
|
319
|
+
inner.size() > prefix.size() + 1) {
|
|
320
|
+
value_json = inner.substr(prefix.size(), inner.size() - prefix.size() - 1);
|
|
321
|
+
}
|
|
322
|
+
payload += ",\"ok\":true,\"value\":\"" + bunite_mac::escapeJsonString(value_json) + "\"}";
|
|
323
|
+
} else {
|
|
324
|
+
// Anchor extraction at the fixed envelope prefix — user-controlled
|
|
325
|
+
// e.message could otherwise inject a fake "code" via substring match.
|
|
326
|
+
static const std::string codePrefix = "{\"__bunite_ok\":false,\"code\":\"";
|
|
327
|
+
std::string code = "runtime_error";
|
|
328
|
+
std::string msg = "script threw";
|
|
329
|
+
if (inner.compare(0, codePrefix.size(), codePrefix) == 0) {
|
|
330
|
+
size_t start = codePrefix.size();
|
|
331
|
+
size_t end = start;
|
|
332
|
+
while (end < inner.size() && inner[end] != '"') ++end;
|
|
333
|
+
if (end > start) code = inner.substr(start, end - start);
|
|
334
|
+
static const std::string msgKey = "\",\"message\":\"";
|
|
335
|
+
if (end + msgKey.size() <= inner.size() &&
|
|
336
|
+
inner.compare(end, msgKey.size(), msgKey) == 0) {
|
|
337
|
+
size_t mstart = end + msgKey.size();
|
|
338
|
+
size_t mend = mstart;
|
|
339
|
+
while (mend < inner.size()) {
|
|
340
|
+
if (inner[mend] == '"' && (mend == mstart || inner[mend - 1] != '\\')) break;
|
|
341
|
+
++mend;
|
|
342
|
+
}
|
|
343
|
+
if (mend > mstart) msg = inner.substr(mstart, mend - mstart);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
payload += ",\"ok\":false,\"code\":\"" + bunite_mac::escapeJsonString(code) + "\","
|
|
347
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
bunite_mac::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
351
|
+
}];
|
|
352
|
+
});
|
|
283
353
|
}
|
|
284
354
|
|
|
285
355
|
extern "C" BUNITE_EXPORT void bunite_view_load_url(uint32_t view_id, const char* url) {
|
|
@@ -443,6 +513,558 @@ extern "C" BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) {
|
|
|
443
513
|
runOnUiThreadSync([=]() { bunite_mac::removeView(view_id); });
|
|
444
514
|
}
|
|
445
515
|
|
|
516
|
+
// Input dispatch — synthesized NSEvent directly to the WKWebView (not via
|
|
517
|
+
// `[NSApp sendEvent:]` — see ctrl-strip note in `bunite_view_click`). WebKit
|
|
518
|
+
// marks these events `isTrusted=true` on the page; capabilities flag matches.
|
|
519
|
+
namespace {
|
|
520
|
+
|
|
521
|
+
NSEventModifierFlags macModifiers(uint32_t bits) {
|
|
522
|
+
NSEventModifierFlags m = 0;
|
|
523
|
+
if (bits & 8) m |= NSEventModifierFlagShift;
|
|
524
|
+
if (bits & 2) m |= NSEventModifierFlagControl;
|
|
525
|
+
if (bits & 1) m |= NSEventModifierFlagOption;
|
|
526
|
+
if (bits & 4) m |= NSEventModifierFlagCommand;
|
|
527
|
+
return m;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
NSEventType macMouseDownType(int32_t button) {
|
|
531
|
+
switch (button) {
|
|
532
|
+
case 1: return NSEventTypeOtherMouseDown;
|
|
533
|
+
case 2: return NSEventTypeRightMouseDown;
|
|
534
|
+
default: return NSEventTypeLeftMouseDown;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
NSEventType macMouseUpType(int32_t button) {
|
|
538
|
+
switch (button) {
|
|
539
|
+
case 1: return NSEventTypeOtherMouseUp;
|
|
540
|
+
case 2: return NSEventTypeRightMouseUp;
|
|
541
|
+
default: return NSEventTypeLeftMouseUp;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// FFI x/y is CSS px in top-left view space; convert to AppKit window coords.
|
|
546
|
+
// WKWebView is non-flipped, so y flips against bounds.height.
|
|
547
|
+
NSPoint viewPointToWindow(NSView* view, double x, double y) {
|
|
548
|
+
NSRect bounds = view.bounds;
|
|
549
|
+
NSPoint local = view.isFlipped ? NSMakePoint(x, y) : NSMakePoint(x, bounds.size.height - y);
|
|
550
|
+
return [view convertPoint:local toView:nil];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
} // namespace
|
|
554
|
+
|
|
555
|
+
extern "C" BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
|
|
556
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
557
|
+
if (click_count < 1) click_count = 1;
|
|
558
|
+
runOnUiThreadSync([=]() {
|
|
559
|
+
auto* v = bunite_mac::findView(view_id);
|
|
560
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
561
|
+
NSWindow* win = v->webview.window;
|
|
562
|
+
NSPoint loc = viewPointToWindow(v->webview, x, y);
|
|
563
|
+
// Strip the Control bit from mouse-event modifierFlags: AppKit converts
|
|
564
|
+
// Ctrl+leftMouseDown into a secondary click (context-menu modal),
|
|
565
|
+
// deadlocking the cooperative pump. Page-side `ctrlKey` will read false
|
|
566
|
+
// for ctrl-click on mac as a result — callers needing ctrl-click should
|
|
567
|
+
// wrap with explicit `press("Control")` keydown/keyup.
|
|
568
|
+
NSEventModifierFlags flags = macModifiers(modifiers) & ~NSEventModifierFlagControl;
|
|
569
|
+
// dblclick parity: ascending clickCount per pair. Dispatch directly to
|
|
570
|
+
// the WKWebView — `[win sendEvent:]` enters AppKit's modal mouse-tracking
|
|
571
|
+
// loop and deadlocks the cooperative pump.
|
|
572
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
573
|
+
NSEvent* down = [NSEvent mouseEventWithType:macMouseDownType(button)
|
|
574
|
+
location:loc modifierFlags:flags
|
|
575
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
576
|
+
windowNumber:win.windowNumber context:nil
|
|
577
|
+
eventNumber:0 clickCount:i pressure:1.0];
|
|
578
|
+
NSEvent* up = [NSEvent mouseEventWithType:macMouseUpType(button)
|
|
579
|
+
location:loc modifierFlags:flags
|
|
580
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
581
|
+
windowNumber:win.windowNumber context:nil
|
|
582
|
+
eventNumber:0 clickCount:i pressure:0.0];
|
|
583
|
+
// Dispatch directly to WKWebView — `[NSApp sendEvent:]` would translate
|
|
584
|
+
// Ctrl+leftMouseDown into rightMouseDown (AppKit's emulated secondary
|
|
585
|
+
// click), entering the context-menu modal run-loop mode and stalling
|
|
586
|
+
// the cooperative pump indefinitely.
|
|
587
|
+
if (down) {
|
|
588
|
+
if (button == 0) [v->webview mouseDown:down];
|
|
589
|
+
else if (button == 2) [v->webview rightMouseDown:down];
|
|
590
|
+
else [v->webview otherMouseDown:down];
|
|
591
|
+
}
|
|
592
|
+
if (up) {
|
|
593
|
+
if (button == 0) [v->webview mouseUp:up];
|
|
594
|
+
else if (button == 2) [v->webview rightMouseUp:up];
|
|
595
|
+
else [v->webview otherMouseUp:up];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
extern "C" BUNITE_EXPORT void bunite_view_type(uint32_t view_id, const char* text) {
|
|
602
|
+
std::string s = text ? text : "";
|
|
603
|
+
runOnUiThreadSync([=]() {
|
|
604
|
+
auto* v = bunite_mac::findView(view_id);
|
|
605
|
+
if (!v || !v->webview) return;
|
|
606
|
+
NSString* ns = [NSString stringWithUTF8String:s.c_str()];
|
|
607
|
+
if (!ns.length) return;
|
|
608
|
+
// WKWebView conforms to NSTextInputClient — insertText: routes through
|
|
609
|
+
// its IME chain so DOM input events fire on focused editable elements.
|
|
610
|
+
if ([v->webview respondsToSelector:@selector(insertText:)]) {
|
|
611
|
+
[(id)v->webview insertText:ns];
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t /*windows_vk_code*/,
|
|
617
|
+
int32_t mac_key_code, const char* /*key*/, const char* /*code*/,
|
|
618
|
+
const char* character, uint32_t modifiers,
|
|
619
|
+
int32_t action, bool /*extended*/, int32_t /*location*/) {
|
|
620
|
+
std::string char_str = character ? character : "";
|
|
621
|
+
runOnUiThreadSync([=]() {
|
|
622
|
+
auto* v = bunite_mac::findView(view_id);
|
|
623
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
624
|
+
NSString* chars = char_str.empty() ? @"" : [NSString stringWithUTF8String:char_str.c_str()];
|
|
625
|
+
NSEventModifierFlags flags = macModifiers(modifiers);
|
|
626
|
+
NSWindow* win = v->webview.window;
|
|
627
|
+
if (action != 1) {
|
|
628
|
+
NSEvent* down = [NSEvent keyEventWithType:NSEventTypeKeyDown
|
|
629
|
+
location:NSZeroPoint modifierFlags:flags
|
|
630
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
631
|
+
windowNumber:win.windowNumber context:nil
|
|
632
|
+
characters:chars charactersIgnoringModifiers:chars
|
|
633
|
+
isARepeat:NO keyCode:(unsigned short)mac_key_code];
|
|
634
|
+
if (down) [v->webview keyDown:down];
|
|
635
|
+
}
|
|
636
|
+
if (action != 0) {
|
|
637
|
+
NSEvent* up = [NSEvent keyEventWithType:NSEventTypeKeyUp
|
|
638
|
+
location:NSZeroPoint modifierFlags:flags
|
|
639
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
640
|
+
windowNumber:win.windowNumber context:nil
|
|
641
|
+
characters:chars charactersIgnoringModifiers:chars
|
|
642
|
+
isARepeat:NO keyCode:(unsigned short)mac_key_code];
|
|
643
|
+
if (up) [v->webview keyUp:up];
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t view_id, double dx, double dy,
|
|
649
|
+
double x, double y, uint32_t modifiers) {
|
|
650
|
+
runOnUiThreadSync([=]() {
|
|
651
|
+
auto* v = bunite_mac::findView(view_id);
|
|
652
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
653
|
+
CGEventRef cg = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 2,
|
|
654
|
+
static_cast<int32_t>(-dy),
|
|
655
|
+
static_cast<int32_t>(-dx));
|
|
656
|
+
if (!cg) return;
|
|
657
|
+
// CGEvent location is screen coords; CSS px → window → screen.
|
|
658
|
+
NSPoint inWin = viewPointToWindow(v->webview, x, y);
|
|
659
|
+
NSPoint onScreen = [v->webview.window convertPointToScreen:inWin];
|
|
660
|
+
CGEventSetLocation(cg, CGPointMake(onScreen.x, onScreen.y));
|
|
661
|
+
CGEventSetFlags(cg, (CGEventFlags)macModifiers(modifiers));
|
|
662
|
+
NSEvent* ev = [NSEvent eventWithCGEvent:cg];
|
|
663
|
+
CFRelease(cg);
|
|
664
|
+
if (ev) [v->webview scrollWheel:ev];
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
|
|
669
|
+
bool accept, const char* text) {
|
|
670
|
+
std::string text_str = text ? text : "";
|
|
671
|
+
runOnUiThreadSync([=]() {
|
|
672
|
+
auto* v = bunite_mac::findView(view_id);
|
|
673
|
+
if (!v) return;
|
|
674
|
+
auto it = v->pending_dialogs.find(request_id);
|
|
675
|
+
if (it == v->pending_dialogs.end()) return;
|
|
676
|
+
auto cb = it->second;
|
|
677
|
+
v->pending_dialogs.erase(it);
|
|
678
|
+
if (cb) cb(accept, text_str);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t view_id, int32_t action,
|
|
683
|
+
double x, double y, int32_t button,
|
|
684
|
+
uint32_t modifiers) {
|
|
685
|
+
runOnUiThreadSync([=]() {
|
|
686
|
+
auto* v = bunite_mac::findView(view_id);
|
|
687
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
688
|
+
NSWindow* win = v->webview.window;
|
|
689
|
+
NSPoint loc = viewPointToWindow(v->webview, x, y);
|
|
690
|
+
// Strip Control bit on mouse events for the same reason as click —
|
|
691
|
+
// AppKit Ctrl+leftMouseDown maps to secondary-click which enters context
|
|
692
|
+
// menu modal mode and stalls the cooperative pump.
|
|
693
|
+
NSEventModifierFlags flags = macModifiers(modifiers) & ~NSEventModifierFlagControl;
|
|
694
|
+
NSEventType type;
|
|
695
|
+
if (action == 0) {
|
|
696
|
+
type = NSEventTypeMouseMoved;
|
|
697
|
+
} else if (action == 1) {
|
|
698
|
+
type = macMouseDownType(button);
|
|
699
|
+
} else {
|
|
700
|
+
type = macMouseUpType(button);
|
|
701
|
+
}
|
|
702
|
+
NSEvent* ev = [NSEvent mouseEventWithType:type
|
|
703
|
+
location:loc modifierFlags:flags
|
|
704
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
705
|
+
windowNumber:win.windowNumber context:nil
|
|
706
|
+
eventNumber:0 clickCount:(action == 0 ? 0 : 1)
|
|
707
|
+
pressure:(action == 1 ? 1.0 : 0.0)];
|
|
708
|
+
if (!ev) return;
|
|
709
|
+
// Direct WKWebView dispatch — same modal-tracking concerns as click.
|
|
710
|
+
if (action == 0) {
|
|
711
|
+
[v->webview mouseMoved:ev];
|
|
712
|
+
} else if (action == 1) {
|
|
713
|
+
if (button == 0) [v->webview mouseDown:ev];
|
|
714
|
+
else if (button == 1) [v->webview otherMouseDown:ev];
|
|
715
|
+
else [v->webview rightMouseDown:ev];
|
|
716
|
+
} else {
|
|
717
|
+
if (button == 0) [v->webview mouseUp:ev];
|
|
718
|
+
else if (button == 1) [v->webview otherMouseUp:ev];
|
|
719
|
+
else [v->webview rightMouseUp:ev];
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Screenshot — WKWebView.takeSnapshotWithConfiguration: + NSBitmapImageRep PNG/JPEG.
|
|
725
|
+
namespace {
|
|
726
|
+
|
|
727
|
+
void emitMacScreenshotError(uint32_t view_id, uint32_t request_id, const char* code, NSString* msg) {
|
|
728
|
+
std::string m = msg ? (msg.UTF8String ?: "") : "";
|
|
729
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
730
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
731
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(m) + "\"}";
|
|
732
|
+
bunite_mac::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
} // namespace
|
|
736
|
+
|
|
737
|
+
extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
738
|
+
// WKWebView — synthetic NSEvent dispatched directly to the view. Empirically
|
|
739
|
+
// WebKit marks these events `isTrusted=true` on the page (the synthesis bit
|
|
740
|
+
// matters for AppKit responder routing, not for the DOM trust flag).
|
|
741
|
+
auto* v = bunite_mac::findView(view_id);
|
|
742
|
+
if (!v) return 0;
|
|
743
|
+
return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
|
|
744
|
+
BUNITE_CAP_NATIVE_INPUT_TRUSTED |
|
|
745
|
+
BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
|
|
746
|
+
BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
747
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
|
|
748
|
+
BUNITE_CAP_BOUNDING_RECT |
|
|
749
|
+
BUNITE_CAP_RESOLVE_AND_CLICK | BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
|
|
750
|
+
BUNITE_CAP_AX | BUNITE_CAP_FRAMES;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
namespace {
|
|
754
|
+
|
|
755
|
+
// JS-bridge ax tree (mirrors linux backend) — WKWebView has no public host-side
|
|
756
|
+
// ax API. Walks DOM + ARIA attrs, emits CDP-shaped flat list so TS-side
|
|
757
|
+
// convertAxTree works unchanged. `ignored` always false (interestingOnly no-op).
|
|
758
|
+
NSString* const kAxScript = @"(function(){"
|
|
759
|
+
"var nodes=[];"
|
|
760
|
+
"function add(el,parentId){"
|
|
761
|
+
"if(!el||el.nodeType!==1)return null;"
|
|
762
|
+
"var id=String(nodes.length+1);"
|
|
763
|
+
"var node={nodeId:id,parentId:parentId,"
|
|
764
|
+
"role:{value:el.getAttribute('role')||el.tagName.toLowerCase()},"
|
|
765
|
+
"name:{value:el.getAttribute('aria-label')||"
|
|
766
|
+
"(el.tagName==='INPUT'||el.tagName==='TEXTAREA'?'':"
|
|
767
|
+
"(el.firstChild&&el.firstChild.nodeType===3?el.firstChild.textContent.trim().slice(0,100):''))},"
|
|
768
|
+
"properties:[],childIds:[],ignored:false};"
|
|
769
|
+
"var d=el.getAttribute('aria-description');if(d)node.description={value:d};"
|
|
770
|
+
"if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){if(el.value)node.value={value:el.value};}"
|
|
771
|
+
"if(el.getAttribute('aria-disabled')==='true'||el.disabled)node.properties.push({name:'disabled',value:{value:true}});"
|
|
772
|
+
"var ck=el.getAttribute('aria-checked');"
|
|
773
|
+
"if(ck==='true')node.properties.push({name:'checked',value:{value:true}});"
|
|
774
|
+
"else if(ck==='false')node.properties.push({name:'checked',value:{value:false}});"
|
|
775
|
+
"else if(ck==='mixed')node.properties.push({name:'checked',value:{value:'mixed'}});"
|
|
776
|
+
"var pr=el.getAttribute('aria-pressed');"
|
|
777
|
+
"if(pr==='true')node.properties.push({name:'pressed',value:{value:true}});"
|
|
778
|
+
"else if(pr==='false')node.properties.push({name:'pressed',value:{value:false}});"
|
|
779
|
+
"else if(pr==='mixed')node.properties.push({name:'pressed',value:{value:'mixed'}});"
|
|
780
|
+
"if(el.getAttribute('aria-expanded')==='true')node.properties.push({name:'expanded',value:{value:true}});"
|
|
781
|
+
"if(el.getAttribute('aria-selected')==='true')node.properties.push({name:'selected',value:{value:true}});"
|
|
782
|
+
"if(el.getAttribute('aria-required')==='true')node.properties.push({name:'required',value:{value:true}});"
|
|
783
|
+
"if(el.getAttribute('aria-invalid')==='true')node.properties.push({name:'invalid',value:{value:true}});"
|
|
784
|
+
"var lv=el.getAttribute('aria-level');if(lv)node.properties.push({name:'level',value:{value:parseInt(lv,10)}});"
|
|
785
|
+
"if(document.activeElement===el)node.properties.push({name:'focused',value:{value:true}});"
|
|
786
|
+
"nodes.push(node);"
|
|
787
|
+
"for(var i=0;i<el.children.length;i++){var cid=add(el.children[i],id);if(cid)node.childIds.push(cid);}"
|
|
788
|
+
"return id;"
|
|
789
|
+
"}"
|
|
790
|
+
"add(document.documentElement,null);"
|
|
791
|
+
"return JSON.stringify({nodes:nodes});"
|
|
792
|
+
"})()";
|
|
793
|
+
|
|
794
|
+
// JS-bridge frame tree — walks window.frames into CDP `Page.getFrameTree` shape.
|
|
795
|
+
NSString* const kFramesScript = @"(function(){"
|
|
796
|
+
"var id=0;"
|
|
797
|
+
"function walk(win){"
|
|
798
|
+
"var fid=String(++id);"
|
|
799
|
+
"var frame={id:fid,securityOrigin:'',url:''};"
|
|
800
|
+
"try{frame.url=win.location.href;frame.securityOrigin=win.location.origin;"
|
|
801
|
+
"if(win.frameElement&&win.frameElement.name)frame.name=win.frameElement.name;}catch(e){}"
|
|
802
|
+
"var children=[];"
|
|
803
|
+
"try{for(var i=0;i<win.frames.length;i++)children.push(walk(win.frames[i]));}catch(e){}"
|
|
804
|
+
"return {frame:frame,childFrames:children};"
|
|
805
|
+
"}"
|
|
806
|
+
"return JSON.stringify({frameTree:walk(window)});"
|
|
807
|
+
"})()";
|
|
808
|
+
|
|
809
|
+
} // namespace
|
|
810
|
+
|
|
811
|
+
extern "C" BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
|
|
812
|
+
int32_t /*interesting_only*/) {
|
|
813
|
+
runOnUiThreadSync([=]() {
|
|
814
|
+
auto* v = bunite_mac::findView(view_id);
|
|
815
|
+
if (!v || !v->webview) {
|
|
816
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
817
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
818
|
+
"\"message\":\"view not ready\"}";
|
|
819
|
+
bunite_mac::emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
[v->webview evaluateJavaScript:kAxScript completionHandler:^(id result, NSError* error) {
|
|
823
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id);
|
|
824
|
+
if (error || ![result isKindOfClass:[NSString class]]) {
|
|
825
|
+
std::string msg = error ? std::string(error.localizedDescription.UTF8String ?: "evaluate failed")
|
|
826
|
+
: std::string("non-string ax result");
|
|
827
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
828
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
829
|
+
} else {
|
|
830
|
+
std::string tree_json = ((NSString*)result).UTF8String ?: "{}";
|
|
831
|
+
payload += ",\"ok\":true,\"tree\":" + tree_json + "}";
|
|
832
|
+
}
|
|
833
|
+
bunite_mac::emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
834
|
+
}];
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
extern "C" BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
|
|
839
|
+
runOnUiThreadSync([=]() {
|
|
840
|
+
auto* v = bunite_mac::findView(view_id);
|
|
841
|
+
if (!v || !v->webview) {
|
|
842
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
843
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
844
|
+
"\"message\":\"view not ready\"}";
|
|
845
|
+
bunite_mac::emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
[v->webview evaluateJavaScript:kFramesScript completionHandler:^(id result, NSError* error) {
|
|
849
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id);
|
|
850
|
+
if (error || ![result isKindOfClass:[NSString class]]) {
|
|
851
|
+
std::string msg = error ? std::string(error.localizedDescription.UTF8String ?: "evaluate failed")
|
|
852
|
+
: std::string("non-string frames result");
|
|
853
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
854
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
855
|
+
} else {
|
|
856
|
+
std::string tree_json = ((NSString*)result).UTF8String ?: "{}";
|
|
857
|
+
payload += ",\"ok\":true,\"raw\":" + tree_json + "}";
|
|
858
|
+
}
|
|
859
|
+
bunite_mac::emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
860
|
+
}];
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
extern "C" BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
|
|
865
|
+
const char* script_c, const char* frame_id_c) {
|
|
866
|
+
std::string script = script_c ? script_c : "";
|
|
867
|
+
std::string frame_id = frame_id_c ? frame_id_c : "";
|
|
868
|
+
if (frame_id.empty()) {
|
|
869
|
+
bunite_view_evaluate(view_id, request_id, script_c);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
std::string js_target = bunite_mac::escapeJsonString(frame_id);
|
|
873
|
+
std::string js_script = bunite_mac::escapeJsonString(script);
|
|
874
|
+
std::string inner =
|
|
875
|
+
"(function(){var target=\"" + js_target + "\";var id=0;var found=null;"
|
|
876
|
+
"function walk(win){var fid=String(++id);if(fid===target){found=win;return;}"
|
|
877
|
+
"try{for(var i=0;i<win.frames.length;i++){walk(win.frames[i]);if(found)return;}}catch(e){}}"
|
|
878
|
+
"walk(window);"
|
|
879
|
+
"if(!found)throw new Error('frame not found');"
|
|
880
|
+
"return found.eval(\"(\"+\"" + js_script + "\"+\")\");"
|
|
881
|
+
"})()";
|
|
882
|
+
bunite_view_evaluate(view_id, request_id, inner.c_str());
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
namespace {
|
|
886
|
+
|
|
887
|
+
void emitResolveAndClickErrorMac(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
|
|
888
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
889
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
890
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
891
|
+
bunite_mac::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
std::string escapeForJsStringMac(const std::string& s) {
|
|
895
|
+
std::string out; out.reserve(s.size() + 2);
|
|
896
|
+
for (char c : s) {
|
|
897
|
+
if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
|
|
898
|
+
else if (c == '\n') out += "\\n";
|
|
899
|
+
else if (c == '\r') out += "\\r";
|
|
900
|
+
else if (c == '\t') out += "\\t";
|
|
901
|
+
else out.push_back(c);
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
} // namespace
|
|
907
|
+
|
|
908
|
+
extern "C" BUNITE_EXPORT void bunite_view_resolve_and_click(
|
|
909
|
+
uint32_t view_id, uint32_t request_id,
|
|
910
|
+
const char* selector_c, const char* frame_id_c,
|
|
911
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
912
|
+
std::string selector = selector_c ? selector_c : "";
|
|
913
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
914
|
+
if (!frameId.empty()) {
|
|
915
|
+
// No frame addressing on WKWebView — sync reject before UI hop.
|
|
916
|
+
emitResolveAndClickErrorMac(view_id, request_id, "not_supported",
|
|
917
|
+
"WKWebView has no frame addressing API");
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (click_count < 1) click_count = 1;
|
|
921
|
+
std::string sel_lit = "\"" + escapeForJsStringMac(selector) + "\"";
|
|
922
|
+
std::string script =
|
|
923
|
+
"(function(){"
|
|
924
|
+
"var el=document.querySelector(" + sel_lit + ");"
|
|
925
|
+
"if(!el)return{ok:false,code:\"not_found\"};"
|
|
926
|
+
"el.scrollIntoView({block:\"nearest\",inline:\"nearest\",behavior:\"instant\"});"
|
|
927
|
+
"var r=el.getBoundingClientRect();"
|
|
928
|
+
"var vis=r.width>0&&r.height>0&&r.bottom>0&&r.right>0"
|
|
929
|
+
"&&r.top<innerHeight&&r.left<innerWidth;"
|
|
930
|
+
"if(!vis)return{ok:false,code:\"not_visible\"};"
|
|
931
|
+
"return{ok:true,x:r.x,y:r.y,w:r.width,h:r.height,"
|
|
932
|
+
"cx:r.x+r.width/2,cy:r.y+r.height/2};"
|
|
933
|
+
"})()";
|
|
934
|
+
NSString* nsScript = [NSString stringWithUTF8String:script.c_str()];
|
|
935
|
+
runOnUiThreadSync([=]() {
|
|
936
|
+
auto* v = bunite_mac::findView(view_id);
|
|
937
|
+
if (!v || !v->webview || !v->webview.window) {
|
|
938
|
+
emitResolveAndClickErrorMac(view_id, request_id, "runtime_error", "view not ready");
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
[v->webview evaluateJavaScript:nsScript completionHandler:^(id result, NSError* error) {
|
|
942
|
+
if (error || ![result isKindOfClass:[NSDictionary class]]) {
|
|
943
|
+
std::string msg = error ? std::string(error.localizedDescription.UTF8String ?: "evaluate failed")
|
|
944
|
+
: std::string("evaluate returned non-object");
|
|
945
|
+
emitResolveAndClickErrorMac(view_id, request_id, "runtime_error", msg);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
NSDictionary* d = (NSDictionary*)result;
|
|
949
|
+
id okVal = d[@"ok"];
|
|
950
|
+
const bool ok = [okVal respondsToSelector:@selector(boolValue)] && [okVal boolValue];
|
|
951
|
+
if (!ok) {
|
|
952
|
+
id codeVal = d[@"code"];
|
|
953
|
+
std::string code = [codeVal isKindOfClass:[NSString class]]
|
|
954
|
+
? std::string([(NSString*)codeVal UTF8String] ?: "runtime_error")
|
|
955
|
+
: std::string("runtime_error");
|
|
956
|
+
emitResolveAndClickErrorMac(view_id, request_id, code.c_str(), "");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
auto pickD = [&](NSString* k)->double {
|
|
960
|
+
id x = d[k];
|
|
961
|
+
return [x respondsToSelector:@selector(doubleValue)] ? [x doubleValue] : 0.0;
|
|
962
|
+
};
|
|
963
|
+
double x = pickD(@"x"), y = pickD(@"y"), w = pickD(@"w"), h = pickD(@"h");
|
|
964
|
+
double cx = pickD(@"cx"), cy = pickD(@"cy");
|
|
965
|
+
auto* v2 = bunite_mac::findView(view_id);
|
|
966
|
+
if (!v2 || !v2->webview || !v2->webview.window) {
|
|
967
|
+
emitResolveAndClickErrorMac(view_id, request_id, "runtime_error", "view destroyed");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
NSWindow* win = v2->webview.window;
|
|
971
|
+
NSPoint loc = viewPointToWindow(v2->webview, cx, cy);
|
|
972
|
+
NSEventModifierFlags flags = macModifiers(modifiers) & ~NSEventModifierFlagControl;
|
|
973
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
974
|
+
NSEvent* down = [NSEvent mouseEventWithType:macMouseDownType(button)
|
|
975
|
+
location:loc modifierFlags:flags
|
|
976
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
977
|
+
windowNumber:win.windowNumber context:nil
|
|
978
|
+
eventNumber:0 clickCount:i pressure:1.0];
|
|
979
|
+
NSEvent* up = [NSEvent mouseEventWithType:macMouseUpType(button)
|
|
980
|
+
location:loc modifierFlags:flags
|
|
981
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
982
|
+
windowNumber:win.windowNumber context:nil
|
|
983
|
+
eventNumber:0 clickCount:i pressure:0.0];
|
|
984
|
+
if (down) {
|
|
985
|
+
if (button == 0) [v2->webview mouseDown:down];
|
|
986
|
+
else if (button == 2) [v2->webview rightMouseDown:down];
|
|
987
|
+
else [v2->webview otherMouseDown:down];
|
|
988
|
+
}
|
|
989
|
+
if (up) {
|
|
990
|
+
if (button == 0) [v2->webview mouseUp:up];
|
|
991
|
+
else if (button == 2) [v2->webview rightMouseUp:up];
|
|
992
|
+
else [v2->webview otherMouseUp:up];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
996
|
+
",\"ok\":true,\"rect\":{\"x\":" + std::to_string(x) +
|
|
997
|
+
",\"y\":" + std::to_string(y) +
|
|
998
|
+
",\"width\":" + std::to_string(w) +
|
|
999
|
+
",\"height\":" + std::to_string(h) + "},"
|
|
1000
|
+
"\"isTrustedEvent\":true}";
|
|
1001
|
+
bunite_mac::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
1002
|
+
}];
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
|
|
1007
|
+
auto* st = bunite_mac::findView(view_id);
|
|
1008
|
+
if (!st) return;
|
|
1009
|
+
if (policy < 0 || policy > 2) policy = 2;
|
|
1010
|
+
st->download_policy.store(policy);
|
|
1011
|
+
st->download_dir = download_dir ? download_dir : "";
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
|
|
1015
|
+
double x, double y, double w, double h) {
|
|
1016
|
+
runOnUiThreadSync([=]() { bunite_mac::acceptParkedPopup(new_view_id, host_window_id, x, y, w, h); });
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
|
|
1020
|
+
runOnUiThreadSync([=]() { bunite_mac::dismissParkedPopup(new_view_id); });
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
1024
|
+
const char* format, int32_t quality) {
|
|
1025
|
+
std::string fmt = format ? format : "png";
|
|
1026
|
+
runOnUiThreadSync([=]() {
|
|
1027
|
+
auto* v = bunite_mac::findView(view_id);
|
|
1028
|
+
if (!v || !v->webview) {
|
|
1029
|
+
emitMacScreenshotError(view_id, request_id, "not_supported", @"view not ready");
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
WKSnapshotConfiguration* cfg = [[WKSnapshotConfiguration alloc] init];
|
|
1033
|
+
cfg.afterScreenUpdates = YES;
|
|
1034
|
+
const bool jpeg = (fmt == "jpeg" || fmt == "jpg");
|
|
1035
|
+
NSString* outFmt = jpeg ? @"jpeg" : @"png";
|
|
1036
|
+
NSString* mime = jpeg ? @"image/jpeg" : @"image/png";
|
|
1037
|
+
[v->webview takeSnapshotWithConfiguration:cfg
|
|
1038
|
+
completionHandler:^(NSImage* img, NSError* err) {
|
|
1039
|
+
if (err || !img) {
|
|
1040
|
+
emitMacScreenshotError(view_id, request_id, "runtime_error", err ? err.localizedDescription : @"takeSnapshot returned nil");
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
CGImageRef cgImg = [img CGImageForProposedRect:nullptr context:nil hints:nil];
|
|
1044
|
+
if (!cgImg) {
|
|
1045
|
+
emitMacScreenshotError(view_id, request_id, "runtime_error", @"CGImageForProposedRect failed");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImg];
|
|
1049
|
+
NSDictionary* props = jpeg
|
|
1050
|
+
? @{ NSImageCompressionFactor: @((quality < 0 ? 0.9 : std::min(quality, 100) / 100.0)) }
|
|
1051
|
+
: @{};
|
|
1052
|
+
NSData* data = [rep representationUsingType:(jpeg ? NSBitmapImageFileTypeJPEG : NSBitmapImageFileTypePNG)
|
|
1053
|
+
properties:props];
|
|
1054
|
+
if (!data || data.length == 0) {
|
|
1055
|
+
emitMacScreenshotError(view_id, request_id, "runtime_error", @"encode failed");
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
NSString* b64 = [data base64EncodedStringWithOptions:0];
|
|
1059
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1060
|
+
",\"ok\":true,\"format\":\"" + outFmt.UTF8String +
|
|
1061
|
+
"\",\"mime\":\"" + mime.UTF8String +
|
|
1062
|
+
"\",\"dataBase64\":\"" + (b64.UTF8String ?: "") + "\"}";
|
|
1063
|
+
bunite_mac::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
1064
|
+
}];
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
446
1068
|
extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
447
1069
|
runOnUiThreadSync([=]() {
|
|
448
1070
|
if (@available(macOS 13.3, *)) {
|