bunite-core 0.14.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +4 -4
  2. package/src/host/core/App.ts +6 -3
  3. package/src/host/core/BrowserView.ts +345 -24
  4. package/src/host/core/BrowserWindow.ts +52 -6
  5. package/src/host/core/SurfaceBrowserIPC.ts +10 -1
  6. package/src/host/core/SurfaceManager.ts +357 -16
  7. package/src/host/core/windowCap.ts +69 -0
  8. package/src/host/events/webviewEvents.ts +18 -1
  9. package/src/host/log.ts +6 -1
  10. package/src/host/native.ts +145 -1
  11. package/src/host/preloadBundle.ts +7 -2
  12. package/src/native/linux/bunite_linux_ffi.cpp +225 -1
  13. package/src/native/linux/bunite_linux_internal.h +12 -0
  14. package/src/native/linux/bunite_linux_runtime.cpp +6 -1
  15. package/src/native/linux/bunite_linux_view.cpp +211 -5
  16. package/src/native/mac/bunite_mac_ffi.mm +293 -4
  17. package/src/native/mac/bunite_mac_internal.h +13 -0
  18. package/src/native/mac/bunite_mac_view.mm +227 -7
  19. package/src/native/shared/ffi_exports.h +97 -30
  20. package/src/native/win/native_host_cef.cpp +107 -13
  21. package/src/native/win/native_host_ffi.cpp +831 -2
  22. package/src/native/win/native_host_internal.h +22 -0
  23. package/src/native/win/native_host_runtime.cpp +34 -0
  24. package/src/native/win-webview2/bunite_webview2_ffi.cpp +827 -5
  25. package/src/native/win-webview2/webview2_internal.h +19 -0
  26. package/src/native/win-webview2/webview2_runtime.cpp +383 -31
  27. package/src/preload/runtime.built.js +1 -1
  28. package/src/preload/runtime.ts +39 -0
  29. package/src/rpc/framework.ts +194 -12
  30. package/src/rpc/index.ts +12 -0
  31. package/src/rpc/peer.ts +1 -1
  32. package/src/webview/native.ts +142 -32
  33. package/src/webview/polyfill.ts +91 -14
@@ -73,6 +73,7 @@ type NativeSymbols = {
73
73
  width: number,
74
74
  height: number
75
75
  ) => void;
76
+ bunite_window_begin_move_drag: (windowId: number) => void;
76
77
  bunite_view_create: (
77
78
  viewId: number,
78
79
  windowId: number,
@@ -126,6 +127,26 @@ type NativeSymbols = {
126
127
  bunite_view_screenshot: (
127
128
  viewId: number, requestId: number, format: CStringPointer, quality: number
128
129
  ) => void;
130
+ bunite_view_accessibility_snapshot: (
131
+ viewId: number, requestId: number, interestingOnly: number
132
+ ) => void;
133
+ bunite_view_list_frames: (viewId: number, requestId: number) => void;
134
+ bunite_view_evaluate_in_frame: (
135
+ viewId: number, requestId: number, script: CStringPointer, frameId: CStringPointer
136
+ ) => void;
137
+ bunite_view_resolve_and_click: (
138
+ viewId: number, requestId: number,
139
+ selector: CStringPointer, frameId: CStringPointer,
140
+ button: number, clickCount: number, modifiers: number
141
+ ) => void;
142
+ bunite_view_set_download_policy: (
143
+ viewId: number, policy: number, downloadDir: CStringPointer
144
+ ) => void;
145
+ bunite_view_popup_accept: (
146
+ newViewId: number, hostWindowId: number,
147
+ x: number, y: number, w: number, h: number,
148
+ ) => void;
149
+ bunite_view_popup_dismiss: (newViewId: number) => void;
129
150
  bunite_view_capabilities: (viewId: number) => number;
130
151
  bunite_view_load_url: (viewId: number, url: CStringPointer) => void;
131
152
  bunite_view_load_html: (viewId: number, html: CStringPointer) => void;
@@ -243,6 +264,10 @@ const nativeSymbolDefinitions = {
243
264
  args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64],
244
265
  returns: FFIType.void
245
266
  },
267
+ bunite_window_begin_move_drag: {
268
+ args: [FFIType.u32],
269
+ returns: FFIType.void
270
+ },
246
271
  bunite_view_create: {
247
272
  args: [
248
273
  FFIType.u32,
@@ -342,6 +367,34 @@ const nativeSymbolDefinitions = {
342
367
  args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.i32],
343
368
  returns: FFIType.void
344
369
  },
370
+ bunite_view_accessibility_snapshot: {
371
+ args: [FFIType.u32, FFIType.u32, FFIType.i32],
372
+ returns: FFIType.void
373
+ },
374
+ bunite_view_list_frames: {
375
+ args: [FFIType.u32, FFIType.u32],
376
+ returns: FFIType.void
377
+ },
378
+ bunite_view_evaluate_in_frame: {
379
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.cstring],
380
+ returns: FFIType.void
381
+ },
382
+ bunite_view_resolve_and_click: {
383
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.cstring, FFIType.i32, FFIType.i32, FFIType.u32],
384
+ returns: FFIType.void
385
+ },
386
+ bunite_view_set_download_policy: {
387
+ args: [FFIType.u32, FFIType.i32, FFIType.cstring],
388
+ returns: FFIType.void
389
+ },
390
+ bunite_view_popup_accept: {
391
+ args: [FFIType.u32, FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64],
392
+ returns: FFIType.void
393
+ },
394
+ bunite_view_popup_dismiss: {
395
+ args: [FFIType.u32],
396
+ returns: FFIType.void
397
+ },
345
398
  bunite_view_capabilities: {
346
399
  args: [FFIType.u32],
347
400
  returns: FFIType.u32
@@ -421,6 +474,61 @@ export function setScreenshotResultHandler(handler: (viewId: number, result: Nat
421
474
  screenshotResultHandler = handler;
422
475
  }
423
476
 
477
+ export type NativeAccessibilityResult = {
478
+ requestId: number;
479
+ ok: boolean;
480
+ tree?: unknown; // CDP Accessibility.AXNode tree as JSON value
481
+ code?: string;
482
+ message?: string;
483
+ };
484
+ let accessibilityResultHandler: ((viewId: number, result: NativeAccessibilityResult) => void) | null = null;
485
+ export function setAccessibilityResultHandler(handler: (viewId: number, result: NativeAccessibilityResult) => void) {
486
+ accessibilityResultHandler = handler;
487
+ }
488
+
489
+ export type NativeListFramesResult = {
490
+ requestId: number;
491
+ ok: boolean;
492
+ /** Raw CDP `Page.getFrameTree` result (`{frameTree: {frame, childFrames}}`). TS flattens. */
493
+ raw?: unknown;
494
+ code?: string;
495
+ message?: string;
496
+ };
497
+ let listFramesResultHandler: ((viewId: number, result: NativeListFramesResult) => void) | null = null;
498
+ export function setListFramesResultHandler(handler: (viewId: number, result: NativeListFramesResult) => void) {
499
+ listFramesResultHandler = handler;
500
+ }
501
+
502
+ export type NativeResolveAndClickResult = {
503
+ requestId: number;
504
+ ok: boolean;
505
+ rect?: { x: number; y: number; width: number; height: number };
506
+ isTrustedEvent?: boolean;
507
+ code?: string;
508
+ message?: string;
509
+ };
510
+ let resolveAndClickResultHandler: ((viewId: number, result: NativeResolveAndClickResult) => void) | null = null;
511
+ export function setResolveAndClickResultHandler(handler: (viewId: number, result: NativeResolveAndClickResult) => void) {
512
+ resolveAndClickResultHandler = handler;
513
+ }
514
+
515
+ export type NativeDownloadEvent = {
516
+ kind: "started" | "progress" | "completed" | "failed" | "blocked";
517
+ id: string;
518
+ url?: string;
519
+ suggestedFilename?: string;
520
+ mimeType?: string;
521
+ sizeBytes?: number;
522
+ receivedBytes?: number;
523
+ totalBytes?: number;
524
+ localPath?: string;
525
+ reason?: string;
526
+ };
527
+ let downloadEventHandler: ((viewId: number, event: NativeDownloadEvent) => void) | null = null;
528
+ export function setDownloadEventHandler(handler: (viewId: number, event: NativeDownloadEvent) => void) {
529
+ downloadEventHandler = handler;
530
+ }
531
+
424
532
  // Per-view deferred resolvers for "view-ready" (OnAfterCreated).
425
533
  const viewReadyResolvers = new Map<number, () => void>();
426
534
 
@@ -575,6 +683,42 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
575
683
  screenshotResultHandler?.(viewId, parsed);
576
684
  break;
577
685
  }
686
+ case "accessibility-result": {
687
+ const parsed = maybeParsePayload(payload) as NativeAccessibilityResult;
688
+ accessibilityResultHandler?.(viewId, parsed);
689
+ break;
690
+ }
691
+ case "list-frames-result": {
692
+ const parsed = maybeParsePayload(payload) as NativeListFramesResult;
693
+ listFramesResultHandler?.(viewId, parsed);
694
+ break;
695
+ }
696
+ case "resolve-and-click-result": {
697
+ const parsed = maybeParsePayload(payload) as NativeResolveAndClickResult;
698
+ resolveAndClickResultHandler?.(viewId, parsed);
699
+ break;
700
+ }
701
+ case "download-event": {
702
+ const parsed = maybeParsePayload(payload) as NativeDownloadEvent;
703
+ downloadEventHandler?.(viewId, parsed);
704
+ buniteEventEmitter.emitEvent(
705
+ buniteEventEmitter.events.webview.downloadEvent(parsed),
706
+ viewId
707
+ );
708
+ break;
709
+ }
710
+ case "popup-requested": {
711
+ const parsed = maybeParsePayload(payload) as {
712
+ newSurfaceId: number;
713
+ url: string;
714
+ disposition: "tab" | "window" | "popup";
715
+ };
716
+ buniteEventEmitter.emitEvent(
717
+ buniteEventEmitter.events.webview.popupRequested(parsed),
718
+ viewId
719
+ );
720
+ break;
721
+ }
578
722
  case "title-changed": {
579
723
  const parsed = maybeParsePayload(payload) as { title: string };
580
724
  buniteEventEmitter.emitEvent(
@@ -779,7 +923,7 @@ export async function initNativeRuntime(
779
923
  throw new Error(`bunite: failed to load native library at ${artifacts.nativeLibPath}.`);
780
924
  }
781
925
 
782
- const EXPECTED_ABI = 9;
926
+ const EXPECTED_ABI = 12;
783
927
  const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
784
928
  if (nativeAbi !== EXPECTED_ABI) {
785
929
  throw new Error(
@@ -70,11 +70,16 @@ export function buildViewPreloadScript(options: {
70
70
  }) {
71
71
  const secretKeyBase64 = Buffer.from(options.secretKey).toString("base64");
72
72
 
73
- // Per-view config — these globals are consumed by the pre-built runtime
73
+ // Per-view config — these globals are consumed by the pre-built runtime.
74
74
  const config = `var __buniteWebviewId=${options.webviewId},__buniteRpcSocketPort=${options.rpcSocketPort},__buniteSecretKeyBase64=${JSON.stringify(secretKeyBase64)};`;
75
75
 
76
76
  const runtime = getPreloadRuntime();
77
77
  const customPreload = readCustomPreload(options.preload, options.appresRoot).trim();
78
78
 
79
- return [config, runtime, customPreload].filter(Boolean).join("\n");
79
+ // `;\n` between segments guards against ASI / token-boundary issues.
80
+ const inner = [config, runtime, customPreload].filter(Boolean).join(";\n");
81
+ // Catch top-level errors so a broken segment doesn't abort the surrounding
82
+ // native IIFE wrapper. Stashes the error on globalThis for programmatic
83
+ // inspection in addition to console.error.
84
+ return `try{${inner}\n}catch(e){try{globalThis.__bunitePreloadError=e}catch(_){}try{console.error("[bunite preload] failed:",e&&e.stack||e)}catch(_){}}`;
80
85
  }
@@ -113,6 +113,26 @@ extern "C" BUNITE_EXPORT void bunite_window_set_frame(
113
113
  });
114
114
  }
115
115
 
116
+ extern "C" BUNITE_EXPORT void bunite_window_begin_move_drag(uint32_t window_id) {
117
+ // GTK4 begin_move via default seat pointer (no-arg API → best-effort).
118
+ // GDK_CURRENT_TIME may be rejected by Wayland (validates the event serial).
119
+ runOnUiThreadSync([=]() {
120
+ auto* s = bunite_linux::findWindow(window_id);
121
+ if (!s) return;
122
+ GdkSurface* surface = gtk_native_get_surface(GTK_NATIVE(s->window));
123
+ if (!surface || !GDK_IS_TOPLEVEL(surface)) return;
124
+ GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(s->window));
125
+ GdkSeat* seat = gdk_display_get_default_seat(display);
126
+ GdkDevice* pointer = seat ? gdk_seat_get_pointer(seat) : nullptr;
127
+ if (!pointer) return;
128
+ double px = 0, py = 0;
129
+ GdkModifierType mask;
130
+ if (!gdk_surface_get_device_position(surface, pointer, &px, &py, &mask)) return;
131
+ gdk_toplevel_begin_move(GDK_TOPLEVEL(surface), pointer, /*button=*/1,
132
+ px, py, GDK_CURRENT_TIME);
133
+ });
134
+ }
135
+
116
136
  extern "C" BUNITE_EXPORT bool bunite_view_create(
117
137
  uint32_t view_id, uint32_t window_id,
118
138
  const char* url, const char* html, const char* preload,
@@ -356,6 +376,15 @@ extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t, int32_t, int32_t, cons
356
376
  extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t, double, double, double, double, uint32_t) {}
357
377
  extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t, int32_t, double, double, int32_t, uint32_t) {}
358
378
 
379
+ extern "C" BUNITE_EXPORT void bunite_view_resolve_and_click(
380
+ uint32_t view_id, uint32_t request_id, const char* /*selector*/, const char* /*frame_id*/,
381
+ int32_t /*button*/, int32_t /*click_count*/, uint32_t /*modifiers*/) {
382
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
383
+ ",\"ok\":false,\"code\":\"not_supported\","
384
+ "\"message\":\"WebKitGTK has no synthetic input API\"}";
385
+ bunite_linux::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
386
+ }
387
+
359
388
  extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
360
389
  bool accept, const char* text) {
361
390
  bunite_linux::respondToDialogRequest(view_id, request_id, accept, text ? text : "");
@@ -452,7 +481,202 @@ extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
452
481
  if (!v) return 0;
453
482
  return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
454
483
  BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
455
- BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
484
+ BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
485
+ BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
486
+ BUNITE_CAP_AX | BUNITE_CAP_FRAMES;
487
+ }
488
+
489
+ namespace {
490
+
491
+ struct AxCtx { uint32_t view_id; uint32_t request_id; };
492
+
493
+ // JS-bridge ax tree: walks DOM + reads ARIA attrs. Emits CDP-shaped flat list
494
+ // so TS-side convertAxTree works unchanged. `ignored` is always false — TS
495
+ // `interestingOnly` filter is a no-op on this backend (limitation).
496
+ const char* kAxScript = R"((function(){
497
+ var nodes=[];
498
+ function add(el,parentId){
499
+ if(!el||el.nodeType!==1)return null;
500
+ var id=String(nodes.length+1);
501
+ var node={nodeId:id,parentId:parentId,
502
+ role:{value:el.getAttribute('role')||el.tagName.toLowerCase()},
503
+ name:{value:el.getAttribute('aria-label')||
504
+ (el.tagName==='INPUT'||el.tagName==='TEXTAREA'?'':
505
+ (el.firstChild&&el.firstChild.nodeType===3?el.firstChild.textContent.trim().slice(0,100):''))},
506
+ properties:[],childIds:[],ignored:false};
507
+ var d=el.getAttribute('aria-description');if(d)node.description={value:d};
508
+ if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){if(el.value)node.value={value:el.value};}
509
+ if(el.getAttribute('aria-disabled')==='true'||el.disabled)node.properties.push({name:'disabled',value:{value:true}});
510
+ var ck=el.getAttribute('aria-checked');
511
+ if(ck==='true')node.properties.push({name:'checked',value:{value:true}});
512
+ else if(ck==='false')node.properties.push({name:'checked',value:{value:false}});
513
+ else if(ck==='mixed')node.properties.push({name:'checked',value:{value:'mixed'}});
514
+ var pr=el.getAttribute('aria-pressed');
515
+ if(pr==='true')node.properties.push({name:'pressed',value:{value:true}});
516
+ else if(pr==='false')node.properties.push({name:'pressed',value:{value:false}});
517
+ else if(pr==='mixed')node.properties.push({name:'pressed',value:{value:'mixed'}});
518
+ if(el.getAttribute('aria-expanded')==='true')node.properties.push({name:'expanded',value:{value:true}});
519
+ if(el.getAttribute('aria-selected')==='true')node.properties.push({name:'selected',value:{value:true}});
520
+ if(el.getAttribute('aria-required')==='true')node.properties.push({name:'required',value:{value:true}});
521
+ if(el.getAttribute('aria-invalid')==='true')node.properties.push({name:'invalid',value:{value:true}});
522
+ var lv=el.getAttribute('aria-level');if(lv)node.properties.push({name:'level',value:{value:parseInt(lv,10)}});
523
+ if(document.activeElement===el)node.properties.push({name:'focused',value:{value:true}});
524
+ nodes.push(node);
525
+ for(var i=0;i<el.children.length;i++){var cid=add(el.children[i],id);if(cid)node.childIds.push(cid);}
526
+ return id;
527
+ }
528
+ add(document.documentElement,null);
529
+ return JSON.stringify({nodes:nodes});
530
+ })())";
531
+
532
+ void on_ax_done(GObject* source, GAsyncResult* res, gpointer user_data) {
533
+ auto* ctx = static_cast<AxCtx*>(user_data);
534
+ WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
535
+ GError* err = nullptr;
536
+ JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
537
+ std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
538
+ if (err || !value) {
539
+ std::string msg = err ? err->message : "evaluate failed";
540
+ if (err) g_error_free(err);
541
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
542
+ "\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
543
+ } else if (!jsc_value_is_string(value)) {
544
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
545
+ "\"message\":\"non-string ax result\"}";
546
+ } else {
547
+ char* raw = jsc_value_to_string(value);
548
+ std::string tree_json = raw ? raw : "{}";
549
+ if (raw) g_free(raw);
550
+ payload += ",\"ok\":true,\"tree\":" + tree_json + "}";
551
+ }
552
+ if (value) g_object_unref(value);
553
+ bunite_linux::emitWebviewEvent(ctx->view_id, "accessibility-result", payload);
554
+ delete ctx;
555
+ }
556
+
557
+ } // namespace
558
+
559
+ extern "C" BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
560
+ int32_t /*interesting_only*/) {
561
+ auto* st = bunite_linux::findView(view_id);
562
+ if (!st || !st->webview) {
563
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
564
+ ",\"ok\":false,\"code\":\"not_supported\","
565
+ "\"message\":\"view not ready\"}";
566
+ bunite_linux::emitWebviewEvent(view_id, "accessibility-result", payload);
567
+ return;
568
+ }
569
+ auto* ctx = new AxCtx{view_id, request_id};
570
+ webkit_web_view_evaluate_javascript(st->webview, kAxScript, -1, nullptr, nullptr, nullptr, on_ax_done, ctx);
571
+ }
572
+
573
+ namespace {
574
+
575
+ struct FramesCtx { uint32_t view_id; uint32_t request_id; };
576
+
577
+ // JS-bridge frame tree: walks window.frames. Synthetic IDs are sequential —
578
+ // not stable across calls. Cross-origin frames are included with empty url/origin
579
+ // (SecurityError catch). Output matches CDP `Page.getFrameTree` shape so the
580
+ // TS-side flattenFrameTree works unchanged.
581
+ const char* kFramesScript = R"((function(){
582
+ var id=0;
583
+ function walk(win){
584
+ var fid=String(++id);
585
+ var frame={id:fid,securityOrigin:'',url:''};
586
+ try{
587
+ frame.url=win.location.href;
588
+ frame.securityOrigin=win.location.origin;
589
+ if(win.frameElement&&win.frameElement.name)frame.name=win.frameElement.name;
590
+ }catch(e){}
591
+ var children=[];
592
+ try{for(var i=0;i<win.frames.length;i++)children.push(walk(win.frames[i]));}catch(e){}
593
+ return {frame:frame,childFrames:children};
594
+ }
595
+ return JSON.stringify({frameTree:walk(window)});
596
+ })())";
597
+
598
+ void on_frames_done(GObject* source, GAsyncResult* res, gpointer user_data) {
599
+ auto* ctx = static_cast<FramesCtx*>(user_data);
600
+ WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
601
+ GError* err = nullptr;
602
+ JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
603
+ std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
604
+ if (err || !value) {
605
+ std::string msg = err ? err->message : "evaluate failed";
606
+ if (err) g_error_free(err);
607
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
608
+ "\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
609
+ } else if (!jsc_value_is_string(value)) {
610
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
611
+ "\"message\":\"non-string frames result\"}";
612
+ } else {
613
+ char* raw = jsc_value_to_string(value);
614
+ std::string tree_json = raw ? raw : "{}";
615
+ if (raw) g_free(raw);
616
+ payload += ",\"ok\":true,\"raw\":" + tree_json + "}";
617
+ }
618
+ if (value) g_object_unref(value);
619
+ bunite_linux::emitWebviewEvent(ctx->view_id, "list-frames-result", payload);
620
+ delete ctx;
621
+ }
622
+
623
+ } // namespace
624
+
625
+ extern "C" BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
626
+ auto* st = bunite_linux::findView(view_id);
627
+ if (!st || !st->webview) {
628
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
629
+ ",\"ok\":false,\"code\":\"not_supported\","
630
+ "\"message\":\"view not ready\"}";
631
+ bunite_linux::emitWebviewEvent(view_id, "list-frames-result", payload);
632
+ return;
633
+ }
634
+ auto* ctx = new FramesCtx{view_id, request_id};
635
+ webkit_web_view_evaluate_javascript(st->webview, kFramesScript, -1, nullptr, nullptr, nullptr, on_frames_done, ctx);
636
+ }
637
+
638
+ extern "C" BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
639
+ const char* script_c, const char* frame_id_c) {
640
+ std::string script = script_c ? script_c : "";
641
+ std::string frame_id = frame_id_c ? frame_id_c : "";
642
+ if (frame_id.empty()) {
643
+ bunite_view_evaluate(view_id, request_id, script_c);
644
+ return;
645
+ }
646
+ // JS-bridge: walk window.frames matching listFrames numbering, `eval` user
647
+ // script in target frame. frameIds are sequential per walk — caller must use
648
+ // them immediately after listFrames. The outer envelope from
649
+ // bunite_view_evaluate handles ok/cross_origin/runtime_error mapping; we
650
+ // surface SecurityError so cross-origin → cross_origin and re-throw missing
651
+ // frames as plain Error → runtime_error.
652
+ std::string js_target = bunite_linux::escapeJsonString(frame_id);
653
+ std::string js_script = bunite_linux::escapeJsonString(script);
654
+ std::string inner =
655
+ "(function(){var target=\"" + js_target + "\";var id=0;var found=null;"
656
+ "function walk(win){var fid=String(++id);if(fid===target){found=win;return;}"
657
+ "try{for(var i=0;i<win.frames.length;i++){walk(win.frames[i]);if(found)return;}}catch(e){}}"
658
+ "walk(window);"
659
+ "if(!found)throw new Error('frame not found');"
660
+ "return found.eval(\"(\"+\"" + js_script + "\"+\")\");"
661
+ "})()";
662
+ bunite_view_evaluate(view_id, request_id, inner.c_str());
663
+ }
664
+
665
+ extern "C" BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
666
+ auto* st = bunite_linux::findView(view_id);
667
+ if (!st) return;
668
+ if (policy < 0 || policy > 2) policy = 2;
669
+ st->download_policy.store(policy);
670
+ st->download_dir = download_dir ? download_dir : "";
671
+ }
672
+
673
+ extern "C" BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
674
+ double x, double y, double w, double h) {
675
+ runOnUiThreadSync([=]() { bunite_linux::acceptParkedPopup(new_view_id, host_window_id, x, y, w, h); });
676
+ }
677
+
678
+ extern "C" BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
679
+ runOnUiThreadSync([=]() { bunite_linux::dismissParkedPopup(new_view_id); });
456
680
  }
457
681
 
458
682
  extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
@@ -43,6 +43,10 @@ struct ViewState {
43
43
  // held until we mark confirmed/text + emit signal completion.
44
44
  std::unordered_map<uint32_t, WebKitScriptDialog*> pending_dialogs;
45
45
  uint32_t next_dialog_request_id = 1;
46
+
47
+ // Download policy: 0=auto, 1=ask (treated as block), 2=block (default).
48
+ std::atomic<int32_t> download_policy{2};
49
+ std::string download_dir;
46
50
  };
47
51
 
48
52
  struct RuntimeState {
@@ -59,6 +63,12 @@ struct RuntimeState {
59
63
  pthread_t ui_thread = 0;
60
64
  bool ui_thread_set = false;
61
65
 
66
+ // Hidden top-level window — popup-minted WebViews park here until adoption.
67
+ GtkWindow* popup_parent = nullptr;
68
+ // Parked popup-minted views awaiting acceptPopup/dismissPopup. Keyed by
69
+ // popup view_id (>= 0x80000000).
70
+ std::unordered_map<uint32_t, WebKitWebView*> parked_popups;
71
+
62
72
  std::unordered_map<uint32_t, WebKitPermissionRequest*> pending_permissions;
63
73
  uint32_t next_permission_request_id = 1;
64
74
 
@@ -147,6 +157,8 @@ bool createView(uint32_t view_id, uint32_t window_id,
147
157
  void removeView(uint32_t view_id);
148
158
  void detachViewSideState(uint32_t view_id);
149
159
  void applyViewBounds(uint32_t view_id, double x, double y, double width, double height);
160
+ bool acceptParkedPopup(uint32_t new_view_id, uint32_t host_window_id, double x, double y, double w, double h);
161
+ void dismissParkedPopup(uint32_t new_view_id);
150
162
  void queueViewRedraw(WebKitWebView* wv);
151
163
 
152
164
  void registerAppresScheme(WebKitWebContext* ctx);
@@ -16,7 +16,7 @@ bool isOnMainThread() {
16
16
 
17
17
  namespace {
18
18
 
19
- constexpr int32_t kBuniteAbiVersion = 9;
19
+ constexpr int32_t kBuniteAbiVersion = 12;
20
20
 
21
21
  } // namespace
22
22
 
@@ -59,6 +59,11 @@ extern "C" BUNITE_EXPORT bool bunite_init(
59
59
  }
60
60
 
61
61
  rt.app = app;
62
+ // Hidden offscreen parent — popup-minted views park here until host adoption.
63
+ rt.popup_parent = GTK_WINDOW(gtk_window_new());
64
+ gtk_window_set_decorated(rt.popup_parent, FALSE);
65
+ gtk_window_set_default_size(rt.popup_parent, 1, 1);
66
+ gtk_window_set_child(rt.popup_parent, gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
62
67
  rt.initialized = true;
63
68
  return true;
64
69
  }