bunite-core 0.12.0 → 0.14.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 +17 -1
- package/src/host/core/BrowserView.ts +197 -28
- package/src/host/core/SurfaceBrowserIPC.ts +44 -3
- package/src/host/core/SurfaceManager.ts +260 -28
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +8 -1
- package/src/host/native.ts +124 -1
- package/src/native/linux/bunite_linux_ffi.cpp +223 -6
- package/src/native/linux/bunite_linux_internal.h +6 -0
- package/src/native/linux/bunite_linux_runtime.cpp +1 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +85 -0
- package/src/native/mac/bunite_mac_ffi.mm +356 -8
- package/src/native/mac/bunite_mac_internal.h +6 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +144 -2
- package/src/native/shared/ffi_exports.h +135 -0
- package/src/native/win/native_host_cef.cpp +86 -3
- package/src/native/win/native_host_ffi.cpp +378 -1
- package/src/native/win/native_host_internal.h +13 -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 +303 -9
- package/src/native/win-webview2/webview2_internal.h +11 -0
- package/src/native/win-webview2/webview2_runtime.cpp +128 -12
- 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 +173 -4
- package/src/rpc/index.ts +21 -0
- package/src/webview/native.ts +126 -25
- package/src/webview/polyfill.ts +196 -12
|
@@ -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 = 9;
|
|
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,284 @@ 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 + window sendEvent: (full responder chain).
|
|
517
|
+
// `isTrusted` is false (synthetic), so `nativeInputTrusted` capability stays false.
|
|
518
|
+
namespace {
|
|
519
|
+
|
|
520
|
+
NSEventModifierFlags macModifiers(uint32_t bits) {
|
|
521
|
+
NSEventModifierFlags m = 0;
|
|
522
|
+
if (bits & 8) m |= NSEventModifierFlagShift;
|
|
523
|
+
if (bits & 2) m |= NSEventModifierFlagControl;
|
|
524
|
+
if (bits & 1) m |= NSEventModifierFlagOption;
|
|
525
|
+
if (bits & 4) m |= NSEventModifierFlagCommand;
|
|
526
|
+
return m;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
NSEventType macMouseDownType(int32_t button) {
|
|
530
|
+
switch (button) {
|
|
531
|
+
case 1: return NSEventTypeOtherMouseDown;
|
|
532
|
+
case 2: return NSEventTypeRightMouseDown;
|
|
533
|
+
default: return NSEventTypeLeftMouseDown;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
NSEventType macMouseUpType(int32_t button) {
|
|
537
|
+
switch (button) {
|
|
538
|
+
case 1: return NSEventTypeOtherMouseUp;
|
|
539
|
+
case 2: return NSEventTypeRightMouseUp;
|
|
540
|
+
default: return NSEventTypeLeftMouseUp;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// FFI x/y is CSS px in top-left view space; convert to AppKit window coords.
|
|
545
|
+
// WKWebView is non-flipped, so y flips against bounds.height.
|
|
546
|
+
NSPoint viewPointToWindow(NSView* view, double x, double y) {
|
|
547
|
+
NSRect bounds = view.bounds;
|
|
548
|
+
NSPoint local = view.isFlipped ? NSMakePoint(x, y) : NSMakePoint(x, bounds.size.height - y);
|
|
549
|
+
return [view convertPoint:local toView:nil];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
} // namespace
|
|
553
|
+
|
|
554
|
+
extern "C" BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
|
|
555
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
556
|
+
if (click_count < 1) click_count = 1;
|
|
557
|
+
runOnUiThreadSync([=]() {
|
|
558
|
+
auto* v = bunite_mac::findView(view_id);
|
|
559
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
560
|
+
NSWindow* win = v->webview.window;
|
|
561
|
+
NSPoint loc = viewPointToWindow(v->webview, x, y);
|
|
562
|
+
// Strip the Control bit from mouse-event modifierFlags: AppKit converts
|
|
563
|
+
// Ctrl+leftMouseDown into a secondary click (context-menu modal),
|
|
564
|
+
// deadlocking the cooperative pump. Page-side `ctrlKey` will read false
|
|
565
|
+
// for ctrl-click on mac as a result — callers needing ctrl-click should
|
|
566
|
+
// wrap with explicit `press("Control")` keydown/keyup.
|
|
567
|
+
NSEventModifierFlags flags = macModifiers(modifiers) & ~NSEventModifierFlagControl;
|
|
568
|
+
// dblclick parity: ascending clickCount per pair. Dispatch directly to
|
|
569
|
+
// the WKWebView — `[win sendEvent:]` enters AppKit's modal mouse-tracking
|
|
570
|
+
// loop and deadlocks the cooperative pump.
|
|
571
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
572
|
+
NSEvent* down = [NSEvent mouseEventWithType:macMouseDownType(button)
|
|
573
|
+
location:loc modifierFlags:flags
|
|
574
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
575
|
+
windowNumber:win.windowNumber context:nil
|
|
576
|
+
eventNumber:0 clickCount:i pressure:1.0];
|
|
577
|
+
NSEvent* up = [NSEvent mouseEventWithType:macMouseUpType(button)
|
|
578
|
+
location:loc modifierFlags:flags
|
|
579
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
580
|
+
windowNumber:win.windowNumber context:nil
|
|
581
|
+
eventNumber:0 clickCount:i pressure:0.0];
|
|
582
|
+
// Dispatch directly to WKWebView — `[NSApp sendEvent:]` would translate
|
|
583
|
+
// Ctrl+leftMouseDown into rightMouseDown (AppKit's emulated secondary
|
|
584
|
+
// click), entering the context-menu modal run-loop mode and stalling
|
|
585
|
+
// the cooperative pump indefinitely.
|
|
586
|
+
if (down) {
|
|
587
|
+
if (button == 0) [v->webview mouseDown:down];
|
|
588
|
+
else if (button == 2) [v->webview rightMouseDown:down];
|
|
589
|
+
else [v->webview otherMouseDown:down];
|
|
590
|
+
}
|
|
591
|
+
if (up) {
|
|
592
|
+
if (button == 0) [v->webview mouseUp:up];
|
|
593
|
+
else if (button == 2) [v->webview rightMouseUp:up];
|
|
594
|
+
else [v->webview otherMouseUp:up];
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
extern "C" BUNITE_EXPORT void bunite_view_type(uint32_t view_id, const char* text) {
|
|
601
|
+
std::string s = text ? text : "";
|
|
602
|
+
runOnUiThreadSync([=]() {
|
|
603
|
+
auto* v = bunite_mac::findView(view_id);
|
|
604
|
+
if (!v || !v->webview) return;
|
|
605
|
+
NSString* ns = [NSString stringWithUTF8String:s.c_str()];
|
|
606
|
+
if (!ns.length) return;
|
|
607
|
+
// WKWebView conforms to NSTextInputClient — insertText: routes through
|
|
608
|
+
// its IME chain so DOM input events fire on focused editable elements.
|
|
609
|
+
if ([v->webview respondsToSelector:@selector(insertText:)]) {
|
|
610
|
+
[(id)v->webview insertText:ns];
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t /*windows_vk_code*/,
|
|
616
|
+
int32_t mac_key_code, const char* /*key*/, const char* /*code*/,
|
|
617
|
+
const char* character, uint32_t modifiers,
|
|
618
|
+
int32_t action, bool /*extended*/, int32_t /*location*/) {
|
|
619
|
+
std::string char_str = character ? character : "";
|
|
620
|
+
runOnUiThreadSync([=]() {
|
|
621
|
+
auto* v = bunite_mac::findView(view_id);
|
|
622
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
623
|
+
NSString* chars = char_str.empty() ? @"" : [NSString stringWithUTF8String:char_str.c_str()];
|
|
624
|
+
NSEventModifierFlags flags = macModifiers(modifiers);
|
|
625
|
+
NSWindow* win = v->webview.window;
|
|
626
|
+
if (action != 1) {
|
|
627
|
+
NSEvent* down = [NSEvent keyEventWithType:NSEventTypeKeyDown
|
|
628
|
+
location:NSZeroPoint modifierFlags:flags
|
|
629
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
630
|
+
windowNumber:win.windowNumber context:nil
|
|
631
|
+
characters:chars charactersIgnoringModifiers:chars
|
|
632
|
+
isARepeat:NO keyCode:(unsigned short)mac_key_code];
|
|
633
|
+
if (down) [v->webview keyDown:down];
|
|
634
|
+
}
|
|
635
|
+
if (action != 0) {
|
|
636
|
+
NSEvent* up = [NSEvent keyEventWithType:NSEventTypeKeyUp
|
|
637
|
+
location:NSZeroPoint modifierFlags:flags
|
|
638
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
639
|
+
windowNumber:win.windowNumber context:nil
|
|
640
|
+
characters:chars charactersIgnoringModifiers:chars
|
|
641
|
+
isARepeat:NO keyCode:(unsigned short)mac_key_code];
|
|
642
|
+
if (up) [v->webview keyUp:up];
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t view_id, double dx, double dy,
|
|
648
|
+
double x, double y, uint32_t modifiers) {
|
|
649
|
+
runOnUiThreadSync([=]() {
|
|
650
|
+
auto* v = bunite_mac::findView(view_id);
|
|
651
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
652
|
+
CGEventRef cg = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitPixel, 2,
|
|
653
|
+
static_cast<int32_t>(-dy),
|
|
654
|
+
static_cast<int32_t>(-dx));
|
|
655
|
+
if (!cg) return;
|
|
656
|
+
// CGEvent location is screen coords; CSS px → window → screen.
|
|
657
|
+
NSPoint inWin = viewPointToWindow(v->webview, x, y);
|
|
658
|
+
NSPoint onScreen = [v->webview.window convertPointToScreen:inWin];
|
|
659
|
+
CGEventSetLocation(cg, CGPointMake(onScreen.x, onScreen.y));
|
|
660
|
+
CGEventSetFlags(cg, (CGEventFlags)macModifiers(modifiers));
|
|
661
|
+
NSEvent* ev = [NSEvent eventWithCGEvent:cg];
|
|
662
|
+
CFRelease(cg);
|
|
663
|
+
if (ev) [v->webview scrollWheel:ev];
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
|
|
668
|
+
bool accept, const char* text) {
|
|
669
|
+
std::string text_str = text ? text : "";
|
|
670
|
+
runOnUiThreadSync([=]() {
|
|
671
|
+
auto* v = bunite_mac::findView(view_id);
|
|
672
|
+
if (!v) return;
|
|
673
|
+
auto it = v->pending_dialogs.find(request_id);
|
|
674
|
+
if (it == v->pending_dialogs.end()) return;
|
|
675
|
+
auto cb = it->second;
|
|
676
|
+
v->pending_dialogs.erase(it);
|
|
677
|
+
if (cb) cb(accept, text_str);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t view_id, int32_t action,
|
|
682
|
+
double x, double y, int32_t button,
|
|
683
|
+
uint32_t modifiers) {
|
|
684
|
+
runOnUiThreadSync([=]() {
|
|
685
|
+
auto* v = bunite_mac::findView(view_id);
|
|
686
|
+
if (!v || !v->webview || !v->webview.window) return;
|
|
687
|
+
NSWindow* win = v->webview.window;
|
|
688
|
+
NSPoint loc = viewPointToWindow(v->webview, x, y);
|
|
689
|
+
// Strip Control bit on mouse events for the same reason as click —
|
|
690
|
+
// AppKit Ctrl+leftMouseDown maps to secondary-click which enters context
|
|
691
|
+
// menu modal mode and stalls the cooperative pump.
|
|
692
|
+
NSEventModifierFlags flags = macModifiers(modifiers) & ~NSEventModifierFlagControl;
|
|
693
|
+
NSEventType type;
|
|
694
|
+
if (action == 0) {
|
|
695
|
+
type = NSEventTypeMouseMoved;
|
|
696
|
+
} else if (action == 1) {
|
|
697
|
+
type = macMouseDownType(button);
|
|
698
|
+
} else {
|
|
699
|
+
type = macMouseUpType(button);
|
|
700
|
+
}
|
|
701
|
+
NSEvent* ev = [NSEvent mouseEventWithType:type
|
|
702
|
+
location:loc modifierFlags:flags
|
|
703
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
704
|
+
windowNumber:win.windowNumber context:nil
|
|
705
|
+
eventNumber:0 clickCount:(action == 0 ? 0 : 1)
|
|
706
|
+
pressure:(action == 1 ? 1.0 : 0.0)];
|
|
707
|
+
if (!ev) return;
|
|
708
|
+
// Direct WKWebView dispatch — same modal-tracking concerns as click.
|
|
709
|
+
if (action == 0) {
|
|
710
|
+
[v->webview mouseMoved:ev];
|
|
711
|
+
} else if (action == 1) {
|
|
712
|
+
if (button == 0) [v->webview mouseDown:ev];
|
|
713
|
+
else if (button == 1) [v->webview otherMouseDown:ev];
|
|
714
|
+
else [v->webview rightMouseDown:ev];
|
|
715
|
+
} else {
|
|
716
|
+
if (button == 0) [v->webview mouseUp:ev];
|
|
717
|
+
else if (button == 1) [v->webview otherMouseUp:ev];
|
|
718
|
+
else [v->webview rightMouseUp:ev];
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Screenshot — WKWebView.takeSnapshotWithConfiguration: + NSBitmapImageRep PNG/JPEG.
|
|
724
|
+
namespace {
|
|
725
|
+
|
|
726
|
+
void emitMacScreenshotError(uint32_t view_id, uint32_t request_id, const char* code, NSString* msg) {
|
|
727
|
+
std::string m = msg ? (msg.UTF8String ?: "") : "";
|
|
728
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
729
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
730
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(m) + "\"}";
|
|
731
|
+
bunite_mac::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
} // namespace
|
|
735
|
+
|
|
736
|
+
extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
737
|
+
// WKWebView — synthetic NSEvent dispatched directly to the view. Empirically
|
|
738
|
+
// WebKit marks these events `isTrusted=true` on the page (the synthesis bit
|
|
739
|
+
// matters for AppKit responder routing, not for the DOM trust flag).
|
|
740
|
+
auto* v = bunite_mac::findView(view_id);
|
|
741
|
+
if (!v) return 0;
|
|
742
|
+
return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
|
|
743
|
+
BUNITE_CAP_NATIVE_INPUT_TRUSTED |
|
|
744
|
+
BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
|
|
745
|
+
BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
746
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
750
|
+
const char* format, int32_t quality) {
|
|
751
|
+
std::string fmt = format ? format : "png";
|
|
752
|
+
runOnUiThreadSync([=]() {
|
|
753
|
+
auto* v = bunite_mac::findView(view_id);
|
|
754
|
+
if (!v || !v->webview) {
|
|
755
|
+
emitMacScreenshotError(view_id, request_id, "not_supported", @"view not ready");
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
WKSnapshotConfiguration* cfg = [[WKSnapshotConfiguration alloc] init];
|
|
759
|
+
cfg.afterScreenUpdates = YES;
|
|
760
|
+
const bool jpeg = (fmt == "jpeg" || fmt == "jpg");
|
|
761
|
+
NSString* outFmt = jpeg ? @"jpeg" : @"png";
|
|
762
|
+
NSString* mime = jpeg ? @"image/jpeg" : @"image/png";
|
|
763
|
+
[v->webview takeSnapshotWithConfiguration:cfg
|
|
764
|
+
completionHandler:^(NSImage* img, NSError* err) {
|
|
765
|
+
if (err || !img) {
|
|
766
|
+
emitMacScreenshotError(view_id, request_id, "runtime_error", err ? err.localizedDescription : @"takeSnapshot returned nil");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
CGImageRef cgImg = [img CGImageForProposedRect:nullptr context:nil hints:nil];
|
|
770
|
+
if (!cgImg) {
|
|
771
|
+
emitMacScreenshotError(view_id, request_id, "runtime_error", @"CGImageForProposedRect failed");
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImg];
|
|
775
|
+
NSDictionary* props = jpeg
|
|
776
|
+
? @{ NSImageCompressionFactor: @((quality < 0 ? 0.9 : std::min(quality, 100) / 100.0)) }
|
|
777
|
+
: @{};
|
|
778
|
+
NSData* data = [rep representationUsingType:(jpeg ? NSBitmapImageFileTypeJPEG : NSBitmapImageFileTypePNG)
|
|
779
|
+
properties:props];
|
|
780
|
+
if (!data || data.length == 0) {
|
|
781
|
+
emitMacScreenshotError(view_id, request_id, "runtime_error", @"encode failed");
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
NSString* b64 = [data base64EncodedStringWithOptions:0];
|
|
785
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
786
|
+
",\"ok\":true,\"format\":\"" + outFmt.UTF8String +
|
|
787
|
+
"\",\"mime\":\"" + mime.UTF8String +
|
|
788
|
+
"\",\"dataBase64\":\"" + (b64.UTF8String ?: "") + "\"}";
|
|
789
|
+
bunite_mac::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
790
|
+
}];
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
446
794
|
extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
447
795
|
runOnUiThreadSync([=]() {
|
|
448
796
|
if (@available(macOS 13.3, *)) {
|
|
@@ -61,6 +61,12 @@ struct ViewState {
|
|
|
61
61
|
// HTML stashed by load_html; appres handler serves at internal/index.html.
|
|
62
62
|
std::string stored_html;
|
|
63
63
|
std::vector<std::string> navigation_rules;
|
|
64
|
+
|
|
65
|
+
// Page-initiated dialogs awaiting host response (alert/confirm/prompt).
|
|
66
|
+
// WKUIDelegate completion handlers are held in `__strong` blocks until
|
|
67
|
+
// respondToDialog invokes them; the page execution is paused meanwhile.
|
|
68
|
+
std::unordered_map<uint32_t, void(^)(bool /*accept*/, const std::string& /*text*/)> pending_dialogs;
|
|
69
|
+
uint32_t next_dialog_request_id = 1;
|
|
64
70
|
};
|
|
65
71
|
|
|
66
72
|
// ---------------------------------------------------------------------------
|
|
@@ -100,8 +100,8 @@ std::vector<std::string> parseNavigationRulesJson(NSString* json) {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Exact-match — prefix would let `../../evil` style paths bypass scrutiny.
|
|
104
|
+
return url == "about:blank" || url == "appres://app.internal/internal/index.html";
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
bool shouldAllowNavigation(const ViewState* view, const std::string& url) {
|
|
@@ -49,14 +49,77 @@ decidePolicyForNavigationAction:(WKNavigationAction*)action
|
|
|
49
49
|
decisionHandler(allow ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
- (void)webView:(WKWebView*)wv
|
|
52
|
+
- (void)webView:(WKWebView*)wv didStartProvisionalNavigation:(WKNavigation*)nav {
|
|
53
53
|
(void)nav;
|
|
54
54
|
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
55
55
|
NSString* url = wv.URL.absoluteString ?: @"";
|
|
56
|
+
bunite_mac::emitWebviewEvent(view_id, "load-start", url.UTF8String);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (void)webView:(WKWebView*)wv didCommitNavigation:(WKNavigation*)nav {
|
|
60
|
+
(void)nav;
|
|
61
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
62
|
+
NSString* url = wv.URL.absoluteString ?: @"";
|
|
63
|
+
// URL commit point — surfaceEvents `navigate` arm. WKWebView fires this
|
|
64
|
+
// when the document begins loading after server response (post-redirect).
|
|
56
65
|
bunite_mac::emitWebviewEvent(view_id, "did-navigate", url.UTF8String);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
- (void)webView:(WKWebView*)wv didFinishNavigation:(WKNavigation*)nav {
|
|
69
|
+
(void)nav;
|
|
70
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
71
|
+
NSString* url = wv.URL.absoluteString ?: @"";
|
|
72
|
+
bunite_mac::emitWebviewEvent(view_id, "load-finish", url.UTF8String);
|
|
57
73
|
bunite_mac::emitWebviewEvent(view_id, "dom-ready", url.UTF8String);
|
|
58
74
|
}
|
|
59
75
|
|
|
76
|
+
- (void)webView:(WKWebView*)wv didFailNavigation:(WKNavigation*)nav withError:(NSError*)error {
|
|
77
|
+
(void)nav;
|
|
78
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
79
|
+
NSString* url = wv.URL.absoluteString ?: @"";
|
|
80
|
+
std::string payload = "{\"url\":\"" + bunite_mac::escapeJsonString(url.UTF8String ?: "") +
|
|
81
|
+
"\",\"reason\":\"" + bunite_mac::escapeJsonString(error.localizedDescription.UTF8String ?: "") + "\"}";
|
|
82
|
+
bunite_mac::emitWebviewEvent(view_id, "load-fail", payload);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
- (void)webView:(WKWebView*)wv didFailProvisionalNavigation:(WKNavigation*)nav withError:(NSError*)error {
|
|
86
|
+
(void)nav;
|
|
87
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
88
|
+
NSString* failingUrl = ((NSURL*)error.userInfo[NSURLErrorFailingURLErrorKey]).absoluteString
|
|
89
|
+
?: (wv.URL.absoluteString ?: @"");
|
|
90
|
+
std::string payload = "{\"url\":\"" + bunite_mac::escapeJsonString(failingUrl.UTF8String ?: "") +
|
|
91
|
+
"\",\"reason\":\"" + bunite_mac::escapeJsonString(error.localizedDescription.UTF8String ?: "") + "\"}";
|
|
92
|
+
bunite_mac::emitWebviewEvent(view_id, "load-fail", payload);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@end
|
|
96
|
+
|
|
97
|
+
@interface BuniteTitleObserver : NSObject
|
|
98
|
+
+ (instancetype)shared;
|
|
99
|
+
@end
|
|
100
|
+
|
|
101
|
+
@implementation BuniteTitleObserver
|
|
102
|
+
+ (instancetype)shared {
|
|
103
|
+
static BuniteTitleObserver* o = nil;
|
|
104
|
+
static dispatch_once_t once;
|
|
105
|
+
dispatch_once(&once, ^{ o = [[BuniteTitleObserver alloc] init]; });
|
|
106
|
+
return o;
|
|
107
|
+
}
|
|
108
|
+
- (void)observeValueForKeyPath:(NSString*)keyPath
|
|
109
|
+
ofObject:(id)object
|
|
110
|
+
change:(NSDictionary<NSKeyValueChangeKey, id>*)change
|
|
111
|
+
context:(void*)context
|
|
112
|
+
{
|
|
113
|
+
(void)change; (void)context;
|
|
114
|
+
if (![keyPath isEqualToString:@"title"]) return;
|
|
115
|
+
WKWebView* wv = (WKWebView*)object;
|
|
116
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
117
|
+
if (!view_id) return;
|
|
118
|
+
NSString* title = wv.title ?: @"";
|
|
119
|
+
std::string payload = "{\"title\":\"" +
|
|
120
|
+
bunite_mac::escapeJsonString(title.UTF8String ?: "") + "\"}";
|
|
121
|
+
bunite_mac::emitWebviewEvent(view_id, "title-changed", payload);
|
|
122
|
+
}
|
|
60
123
|
@end
|
|
61
124
|
|
|
62
125
|
@interface BuniteUIDelegate : NSObject <WKUIDelegate>
|
|
@@ -79,6 +142,80 @@ createWebViewWithConfiguration:(WKWebViewConfiguration*)config
|
|
|
79
142
|
return nil;
|
|
80
143
|
}
|
|
81
144
|
|
|
145
|
+
// --- Dialog handlers (alert / confirm / prompt). beforeunload is not
|
|
146
|
+
// surfaced by WKUIDelegate on macOS — the cancel/proceed decision is made
|
|
147
|
+
// through the navigation-policy delegate's `decidePolicyForNavigationAction:`
|
|
148
|
+
// which we already drive via the `will-navigate` allow-list. Hence no
|
|
149
|
+
// beforeunload arm on this backend.
|
|
150
|
+
|
|
151
|
+
- (void)webView:(WKWebView*)wv
|
|
152
|
+
runJavaScriptAlertPanelWithMessage:(NSString*)message
|
|
153
|
+
initiatedByFrame:(WKFrameInfo*)frame
|
|
154
|
+
completionHandler:(void (^)(void))completionHandler
|
|
155
|
+
{
|
|
156
|
+
(void)frame;
|
|
157
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
158
|
+
auto* v = bunite_mac::findView(view_id);
|
|
159
|
+
if (!v) { completionHandler(); return; }
|
|
160
|
+
uint32_t rid = v->next_dialog_request_id++;
|
|
161
|
+
v->pending_dialogs[rid] = ^(bool /*accept*/, const std::string& /*text*/) {
|
|
162
|
+
completionHandler();
|
|
163
|
+
};
|
|
164
|
+
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
165
|
+
",\"kind\":\"alert\",\"message\":\"" +
|
|
166
|
+
bunite_mac::escapeJsonString(message.UTF8String ?: "") + "\"}";
|
|
167
|
+
// Post emit to next runloop turn so a heavy host-side listener cannot stall
|
|
168
|
+
// the cooperative pump while the page is awaiting the completion handler.
|
|
169
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
170
|
+
bunite_mac::emitWebviewEvent(view_id, "dialog", payload);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
- (void)webView:(WKWebView*)wv
|
|
175
|
+
runJavaScriptConfirmPanelWithMessage:(NSString*)message
|
|
176
|
+
initiatedByFrame:(WKFrameInfo*)frame
|
|
177
|
+
completionHandler:(void (^)(BOOL))completionHandler
|
|
178
|
+
{
|
|
179
|
+
(void)frame;
|
|
180
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
181
|
+
auto* v = bunite_mac::findView(view_id);
|
|
182
|
+
if (!v) { completionHandler(NO); return; }
|
|
183
|
+
uint32_t rid = v->next_dialog_request_id++;
|
|
184
|
+
v->pending_dialogs[rid] = ^(bool accept, const std::string& /*text*/) {
|
|
185
|
+
completionHandler(accept ? YES : NO);
|
|
186
|
+
};
|
|
187
|
+
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
188
|
+
",\"kind\":\"confirm\",\"message\":\"" +
|
|
189
|
+
bunite_mac::escapeJsonString(message.UTF8String ?: "") + "\"}";
|
|
190
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
191
|
+
bunite_mac::emitWebviewEvent(view_id, "dialog", payload);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
- (void)webView:(WKWebView*)wv
|
|
196
|
+
runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
|
|
197
|
+
defaultText:(NSString*)defaultText
|
|
198
|
+
initiatedByFrame:(WKFrameInfo*)frame
|
|
199
|
+
completionHandler:(void (^)(NSString*))completionHandler
|
|
200
|
+
{
|
|
201
|
+
(void)frame;
|
|
202
|
+
uint32_t view_id = bunite_mac::viewIdForWebView(wv);
|
|
203
|
+
auto* v = bunite_mac::findView(view_id);
|
|
204
|
+
if (!v) { completionHandler(nil); return; }
|
|
205
|
+
uint32_t rid = v->next_dialog_request_id++;
|
|
206
|
+
v->pending_dialogs[rid] = ^(bool accept, const std::string& text) {
|
|
207
|
+
completionHandler(accept ? [NSString stringWithUTF8String:text.c_str()] : nil);
|
|
208
|
+
};
|
|
209
|
+
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
210
|
+
",\"kind\":\"prompt\",\"message\":\"" +
|
|
211
|
+
bunite_mac::escapeJsonString(prompt.UTF8String ?: "") +
|
|
212
|
+
"\",\"defaultPrompt\":\"" +
|
|
213
|
+
bunite_mac::escapeJsonString(defaultText.UTF8String ?: "") + "\"}";
|
|
214
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
215
|
+
bunite_mac::emitWebviewEvent(view_id, "dialog", payload);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
82
219
|
- (void)webView:(WKWebView*)wv
|
|
83
220
|
requestMediaCapturePermissionForOrigin:(WKSecurityOrigin*)origin
|
|
84
221
|
initiatedByFrame:(WKFrameInfo*)frame
|
|
@@ -196,6 +333,7 @@ bool createView(uint32_t view_id, uint32_t window_id,
|
|
|
196
333
|
static __strong BuniteUIDelegate* uiDelegate = [[BuniteUIDelegate alloc] init];
|
|
197
334
|
wv.navigationDelegate = navDelegate;
|
|
198
335
|
wv.UIDelegate = uiDelegate;
|
|
336
|
+
[wv addObserver:[BuniteTitleObserver shared] forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:NULL];
|
|
199
337
|
|
|
200
338
|
[container addSubview:wv];
|
|
201
339
|
[window_state->window.contentView addSubview:container];
|
|
@@ -272,7 +410,11 @@ void removeView(uint32_t view_id) {
|
|
|
272
410
|
for (BunitePendingPermission* p in to_deny) p.handler(WKPermissionDecisionDeny);
|
|
273
411
|
}
|
|
274
412
|
|
|
275
|
-
if (wv)
|
|
413
|
+
if (wv) {
|
|
414
|
+
@try { [wv removeObserver:[BuniteTitleObserver shared] forKeyPath:@"title"]; }
|
|
415
|
+
@catch (NSException*) { /* never registered (race) */ }
|
|
416
|
+
[webviewIdTable() removeObjectForKey:wv];
|
|
417
|
+
}
|
|
276
418
|
if (container) [container removeFromSuperview];
|
|
277
419
|
}
|
|
278
420
|
|