bunite-core 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -126,6 +126,26 @@ type NativeSymbols = {
126
126
  bunite_view_screenshot: (
127
127
  viewId: number, requestId: number, format: CStringPointer, quality: number
128
128
  ) => void;
129
+ bunite_view_accessibility_snapshot: (
130
+ viewId: number, requestId: number, interestingOnly: number
131
+ ) => void;
132
+ bunite_view_list_frames: (viewId: number, requestId: number) => void;
133
+ bunite_view_evaluate_in_frame: (
134
+ viewId: number, requestId: number, script: CStringPointer, frameId: CStringPointer
135
+ ) => void;
136
+ bunite_view_resolve_and_click: (
137
+ viewId: number, requestId: number,
138
+ selector: CStringPointer, frameId: CStringPointer,
139
+ button: number, clickCount: number, modifiers: number
140
+ ) => void;
141
+ bunite_view_set_download_policy: (
142
+ viewId: number, policy: number, downloadDir: CStringPointer
143
+ ) => void;
144
+ bunite_view_popup_accept: (
145
+ newViewId: number, hostWindowId: number,
146
+ x: number, y: number, w: number, h: number,
147
+ ) => void;
148
+ bunite_view_popup_dismiss: (newViewId: number) => void;
129
149
  bunite_view_capabilities: (viewId: number) => number;
130
150
  bunite_view_load_url: (viewId: number, url: CStringPointer) => void;
131
151
  bunite_view_load_html: (viewId: number, html: CStringPointer) => void;
@@ -342,6 +362,34 @@ const nativeSymbolDefinitions = {
342
362
  args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.i32],
343
363
  returns: FFIType.void
344
364
  },
365
+ bunite_view_accessibility_snapshot: {
366
+ args: [FFIType.u32, FFIType.u32, FFIType.i32],
367
+ returns: FFIType.void
368
+ },
369
+ bunite_view_list_frames: {
370
+ args: [FFIType.u32, FFIType.u32],
371
+ returns: FFIType.void
372
+ },
373
+ bunite_view_evaluate_in_frame: {
374
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.cstring],
375
+ returns: FFIType.void
376
+ },
377
+ bunite_view_resolve_and_click: {
378
+ args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.cstring, FFIType.i32, FFIType.i32, FFIType.u32],
379
+ returns: FFIType.void
380
+ },
381
+ bunite_view_set_download_policy: {
382
+ args: [FFIType.u32, FFIType.i32, FFIType.cstring],
383
+ returns: FFIType.void
384
+ },
385
+ bunite_view_popup_accept: {
386
+ args: [FFIType.u32, FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64],
387
+ returns: FFIType.void
388
+ },
389
+ bunite_view_popup_dismiss: {
390
+ args: [FFIType.u32],
391
+ returns: FFIType.void
392
+ },
345
393
  bunite_view_capabilities: {
346
394
  args: [FFIType.u32],
347
395
  returns: FFIType.u32
@@ -421,6 +469,61 @@ export function setScreenshotResultHandler(handler: (viewId: number, result: Nat
421
469
  screenshotResultHandler = handler;
422
470
  }
423
471
 
472
+ export type NativeAccessibilityResult = {
473
+ requestId: number;
474
+ ok: boolean;
475
+ tree?: unknown; // CDP Accessibility.AXNode tree as JSON value
476
+ code?: string;
477
+ message?: string;
478
+ };
479
+ let accessibilityResultHandler: ((viewId: number, result: NativeAccessibilityResult) => void) | null = null;
480
+ export function setAccessibilityResultHandler(handler: (viewId: number, result: NativeAccessibilityResult) => void) {
481
+ accessibilityResultHandler = handler;
482
+ }
483
+
484
+ export type NativeListFramesResult = {
485
+ requestId: number;
486
+ ok: boolean;
487
+ /** Raw CDP `Page.getFrameTree` result (`{frameTree: {frame, childFrames}}`). TS flattens. */
488
+ raw?: unknown;
489
+ code?: string;
490
+ message?: string;
491
+ };
492
+ let listFramesResultHandler: ((viewId: number, result: NativeListFramesResult) => void) | null = null;
493
+ export function setListFramesResultHandler(handler: (viewId: number, result: NativeListFramesResult) => void) {
494
+ listFramesResultHandler = handler;
495
+ }
496
+
497
+ export type NativeResolveAndClickResult = {
498
+ requestId: number;
499
+ ok: boolean;
500
+ rect?: { x: number; y: number; width: number; height: number };
501
+ isTrustedEvent?: boolean;
502
+ code?: string;
503
+ message?: string;
504
+ };
505
+ let resolveAndClickResultHandler: ((viewId: number, result: NativeResolveAndClickResult) => void) | null = null;
506
+ export function setResolveAndClickResultHandler(handler: (viewId: number, result: NativeResolveAndClickResult) => void) {
507
+ resolveAndClickResultHandler = handler;
508
+ }
509
+
510
+ export type NativeDownloadEvent = {
511
+ kind: "started" | "progress" | "completed" | "failed" | "blocked";
512
+ id: string;
513
+ url?: string;
514
+ suggestedFilename?: string;
515
+ mimeType?: string;
516
+ sizeBytes?: number;
517
+ receivedBytes?: number;
518
+ totalBytes?: number;
519
+ localPath?: string;
520
+ reason?: string;
521
+ };
522
+ let downloadEventHandler: ((viewId: number, event: NativeDownloadEvent) => void) | null = null;
523
+ export function setDownloadEventHandler(handler: (viewId: number, event: NativeDownloadEvent) => void) {
524
+ downloadEventHandler = handler;
525
+ }
526
+
424
527
  // Per-view deferred resolvers for "view-ready" (OnAfterCreated).
425
528
  const viewReadyResolvers = new Map<number, () => void>();
426
529
 
@@ -575,6 +678,42 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
575
678
  screenshotResultHandler?.(viewId, parsed);
576
679
  break;
577
680
  }
681
+ case "accessibility-result": {
682
+ const parsed = maybeParsePayload(payload) as NativeAccessibilityResult;
683
+ accessibilityResultHandler?.(viewId, parsed);
684
+ break;
685
+ }
686
+ case "list-frames-result": {
687
+ const parsed = maybeParsePayload(payload) as NativeListFramesResult;
688
+ listFramesResultHandler?.(viewId, parsed);
689
+ break;
690
+ }
691
+ case "resolve-and-click-result": {
692
+ const parsed = maybeParsePayload(payload) as NativeResolveAndClickResult;
693
+ resolveAndClickResultHandler?.(viewId, parsed);
694
+ break;
695
+ }
696
+ case "download-event": {
697
+ const parsed = maybeParsePayload(payload) as NativeDownloadEvent;
698
+ downloadEventHandler?.(viewId, parsed);
699
+ buniteEventEmitter.emitEvent(
700
+ buniteEventEmitter.events.webview.downloadEvent(parsed),
701
+ viewId
702
+ );
703
+ break;
704
+ }
705
+ case "popup-requested": {
706
+ const parsed = maybeParsePayload(payload) as {
707
+ newSurfaceId: number;
708
+ url: string;
709
+ disposition: "tab" | "window" | "popup";
710
+ };
711
+ buniteEventEmitter.emitEvent(
712
+ buniteEventEmitter.events.webview.popupRequested(parsed),
713
+ viewId
714
+ );
715
+ break;
716
+ }
578
717
  case "title-changed": {
579
718
  const parsed = maybeParsePayload(payload) as { title: string };
580
719
  buniteEventEmitter.emitEvent(
@@ -779,7 +918,7 @@ export async function initNativeRuntime(
779
918
  throw new Error(`bunite: failed to load native library at ${artifacts.nativeLibPath}.`);
780
919
  }
781
920
 
782
- const EXPECTED_ABI = 9;
921
+ const EXPECTED_ABI = 11;
783
922
  const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
784
923
  if (nativeAbi !== EXPECTED_ABI) {
785
924
  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
  }
@@ -356,6 +356,15 @@ extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t, int32_t, int32_t, cons
356
356
  extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t, double, double, double, double, uint32_t) {}
357
357
  extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t, int32_t, double, double, int32_t, uint32_t) {}
358
358
 
359
+ extern "C" BUNITE_EXPORT void bunite_view_resolve_and_click(
360
+ uint32_t view_id, uint32_t request_id, const char* /*selector*/, const char* /*frame_id*/,
361
+ int32_t /*button*/, int32_t /*click_count*/, uint32_t /*modifiers*/) {
362
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
363
+ ",\"ok\":false,\"code\":\"not_supported\","
364
+ "\"message\":\"WebKitGTK has no synthetic input API\"}";
365
+ bunite_linux::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
366
+ }
367
+
359
368
  extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
360
369
  bool accept, const char* text) {
361
370
  bunite_linux::respondToDialogRequest(view_id, request_id, accept, text ? text : "");
@@ -452,7 +461,202 @@ extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
452
461
  if (!v) return 0;
453
462
  return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
454
463
  BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
455
- BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
464
+ BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
465
+ BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
466
+ BUNITE_CAP_AX | BUNITE_CAP_FRAMES;
467
+ }
468
+
469
+ namespace {
470
+
471
+ struct AxCtx { uint32_t view_id; uint32_t request_id; };
472
+
473
+ // JS-bridge ax tree: walks DOM + reads ARIA attrs. Emits CDP-shaped flat list
474
+ // so TS-side convertAxTree works unchanged. `ignored` is always false — TS
475
+ // `interestingOnly` filter is a no-op on this backend (limitation).
476
+ const char* kAxScript = R"((function(){
477
+ var nodes=[];
478
+ function add(el,parentId){
479
+ if(!el||el.nodeType!==1)return null;
480
+ var id=String(nodes.length+1);
481
+ var node={nodeId:id,parentId:parentId,
482
+ role:{value:el.getAttribute('role')||el.tagName.toLowerCase()},
483
+ name:{value:el.getAttribute('aria-label')||
484
+ (el.tagName==='INPUT'||el.tagName==='TEXTAREA'?'':
485
+ (el.firstChild&&el.firstChild.nodeType===3?el.firstChild.textContent.trim().slice(0,100):''))},
486
+ properties:[],childIds:[],ignored:false};
487
+ var d=el.getAttribute('aria-description');if(d)node.description={value:d};
488
+ if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){if(el.value)node.value={value:el.value};}
489
+ if(el.getAttribute('aria-disabled')==='true'||el.disabled)node.properties.push({name:'disabled',value:{value:true}});
490
+ var ck=el.getAttribute('aria-checked');
491
+ if(ck==='true')node.properties.push({name:'checked',value:{value:true}});
492
+ else if(ck==='false')node.properties.push({name:'checked',value:{value:false}});
493
+ else if(ck==='mixed')node.properties.push({name:'checked',value:{value:'mixed'}});
494
+ var pr=el.getAttribute('aria-pressed');
495
+ if(pr==='true')node.properties.push({name:'pressed',value:{value:true}});
496
+ else if(pr==='false')node.properties.push({name:'pressed',value:{value:false}});
497
+ else if(pr==='mixed')node.properties.push({name:'pressed',value:{value:'mixed'}});
498
+ if(el.getAttribute('aria-expanded')==='true')node.properties.push({name:'expanded',value:{value:true}});
499
+ if(el.getAttribute('aria-selected')==='true')node.properties.push({name:'selected',value:{value:true}});
500
+ if(el.getAttribute('aria-required')==='true')node.properties.push({name:'required',value:{value:true}});
501
+ if(el.getAttribute('aria-invalid')==='true')node.properties.push({name:'invalid',value:{value:true}});
502
+ var lv=el.getAttribute('aria-level');if(lv)node.properties.push({name:'level',value:{value:parseInt(lv,10)}});
503
+ if(document.activeElement===el)node.properties.push({name:'focused',value:{value:true}});
504
+ nodes.push(node);
505
+ for(var i=0;i<el.children.length;i++){var cid=add(el.children[i],id);if(cid)node.childIds.push(cid);}
506
+ return id;
507
+ }
508
+ add(document.documentElement,null);
509
+ return JSON.stringify({nodes:nodes});
510
+ })())";
511
+
512
+ void on_ax_done(GObject* source, GAsyncResult* res, gpointer user_data) {
513
+ auto* ctx = static_cast<AxCtx*>(user_data);
514
+ WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
515
+ GError* err = nullptr;
516
+ JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
517
+ std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
518
+ if (err || !value) {
519
+ std::string msg = err ? err->message : "evaluate failed";
520
+ if (err) g_error_free(err);
521
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
522
+ "\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
523
+ } else if (!jsc_value_is_string(value)) {
524
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
525
+ "\"message\":\"non-string ax result\"}";
526
+ } else {
527
+ char* raw = jsc_value_to_string(value);
528
+ std::string tree_json = raw ? raw : "{}";
529
+ if (raw) g_free(raw);
530
+ payload += ",\"ok\":true,\"tree\":" + tree_json + "}";
531
+ }
532
+ if (value) g_object_unref(value);
533
+ bunite_linux::emitWebviewEvent(ctx->view_id, "accessibility-result", payload);
534
+ delete ctx;
535
+ }
536
+
537
+ } // namespace
538
+
539
+ extern "C" BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
540
+ int32_t /*interesting_only*/) {
541
+ auto* st = bunite_linux::findView(view_id);
542
+ if (!st || !st->webview) {
543
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
544
+ ",\"ok\":false,\"code\":\"not_supported\","
545
+ "\"message\":\"view not ready\"}";
546
+ bunite_linux::emitWebviewEvent(view_id, "accessibility-result", payload);
547
+ return;
548
+ }
549
+ auto* ctx = new AxCtx{view_id, request_id};
550
+ webkit_web_view_evaluate_javascript(st->webview, kAxScript, -1, nullptr, nullptr, nullptr, on_ax_done, ctx);
551
+ }
552
+
553
+ namespace {
554
+
555
+ struct FramesCtx { uint32_t view_id; uint32_t request_id; };
556
+
557
+ // JS-bridge frame tree: walks window.frames. Synthetic IDs are sequential —
558
+ // not stable across calls. Cross-origin frames are included with empty url/origin
559
+ // (SecurityError catch). Output matches CDP `Page.getFrameTree` shape so the
560
+ // TS-side flattenFrameTree works unchanged.
561
+ const char* kFramesScript = R"((function(){
562
+ var id=0;
563
+ function walk(win){
564
+ var fid=String(++id);
565
+ var frame={id:fid,securityOrigin:'',url:''};
566
+ try{
567
+ frame.url=win.location.href;
568
+ frame.securityOrigin=win.location.origin;
569
+ if(win.frameElement&&win.frameElement.name)frame.name=win.frameElement.name;
570
+ }catch(e){}
571
+ var children=[];
572
+ try{for(var i=0;i<win.frames.length;i++)children.push(walk(win.frames[i]));}catch(e){}
573
+ return {frame:frame,childFrames:children};
574
+ }
575
+ return JSON.stringify({frameTree:walk(window)});
576
+ })())";
577
+
578
+ void on_frames_done(GObject* source, GAsyncResult* res, gpointer user_data) {
579
+ auto* ctx = static_cast<FramesCtx*>(user_data);
580
+ WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
581
+ GError* err = nullptr;
582
+ JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
583
+ std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
584
+ if (err || !value) {
585
+ std::string msg = err ? err->message : "evaluate failed";
586
+ if (err) g_error_free(err);
587
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
588
+ "\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
589
+ } else if (!jsc_value_is_string(value)) {
590
+ payload += ",\"ok\":false,\"code\":\"runtime_error\","
591
+ "\"message\":\"non-string frames result\"}";
592
+ } else {
593
+ char* raw = jsc_value_to_string(value);
594
+ std::string tree_json = raw ? raw : "{}";
595
+ if (raw) g_free(raw);
596
+ payload += ",\"ok\":true,\"raw\":" + tree_json + "}";
597
+ }
598
+ if (value) g_object_unref(value);
599
+ bunite_linux::emitWebviewEvent(ctx->view_id, "list-frames-result", payload);
600
+ delete ctx;
601
+ }
602
+
603
+ } // namespace
604
+
605
+ extern "C" BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
606
+ auto* st = bunite_linux::findView(view_id);
607
+ if (!st || !st->webview) {
608
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
609
+ ",\"ok\":false,\"code\":\"not_supported\","
610
+ "\"message\":\"view not ready\"}";
611
+ bunite_linux::emitWebviewEvent(view_id, "list-frames-result", payload);
612
+ return;
613
+ }
614
+ auto* ctx = new FramesCtx{view_id, request_id};
615
+ webkit_web_view_evaluate_javascript(st->webview, kFramesScript, -1, nullptr, nullptr, nullptr, on_frames_done, ctx);
616
+ }
617
+
618
+ extern "C" BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
619
+ const char* script_c, const char* frame_id_c) {
620
+ std::string script = script_c ? script_c : "";
621
+ std::string frame_id = frame_id_c ? frame_id_c : "";
622
+ if (frame_id.empty()) {
623
+ bunite_view_evaluate(view_id, request_id, script_c);
624
+ return;
625
+ }
626
+ // JS-bridge: walk window.frames matching listFrames numbering, `eval` user
627
+ // script in target frame. frameIds are sequential per walk — caller must use
628
+ // them immediately after listFrames. The outer envelope from
629
+ // bunite_view_evaluate handles ok/cross_origin/runtime_error mapping; we
630
+ // surface SecurityError so cross-origin → cross_origin and re-throw missing
631
+ // frames as plain Error → runtime_error.
632
+ std::string js_target = bunite_linux::escapeJsonString(frame_id);
633
+ std::string js_script = bunite_linux::escapeJsonString(script);
634
+ std::string inner =
635
+ "(function(){var target=\"" + js_target + "\";var id=0;var found=null;"
636
+ "function walk(win){var fid=String(++id);if(fid===target){found=win;return;}"
637
+ "try{for(var i=0;i<win.frames.length;i++){walk(win.frames[i]);if(found)return;}}catch(e){}}"
638
+ "walk(window);"
639
+ "if(!found)throw new Error('frame not found');"
640
+ "return found.eval(\"(\"+\"" + js_script + "\"+\")\");"
641
+ "})()";
642
+ bunite_view_evaluate(view_id, request_id, inner.c_str());
643
+ }
644
+
645
+ extern "C" BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
646
+ auto* st = bunite_linux::findView(view_id);
647
+ if (!st) return;
648
+ if (policy < 0 || policy > 2) policy = 2;
649
+ st->download_policy.store(policy);
650
+ st->download_dir = download_dir ? download_dir : "";
651
+ }
652
+
653
+ extern "C" BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
654
+ double x, double y, double w, double h) {
655
+ runOnUiThreadSync([=]() { bunite_linux::acceptParkedPopup(new_view_id, host_window_id, x, y, w, h); });
656
+ }
657
+
658
+ extern "C" BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
659
+ runOnUiThreadSync([=]() { bunite_linux::dismissParkedPopup(new_view_id); });
456
660
  }
457
661
 
458
662
  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 = 11;
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
  }