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.
@@ -11,16 +11,20 @@
11
11
  // (verified empirically on Win 11 / CEF 119+).
12
12
  // screenshot: PrintWindow PW_RENDERFULLCONTENT misses hardware-composited
13
13
  // surfaces (returns all-black). Page.captureScreenshot is compositor-aware.
14
+ #include <algorithm>
15
+ #include <array>
14
16
  #include <atomic>
15
17
  #include <functional>
18
+ #include <memory>
16
19
  #include <mutex>
17
20
  #include <unordered_map>
21
+ #include <vector>
18
22
 
19
23
 
20
24
  using bunite_win::runOnUiThreadSync;
21
25
  using bunite_win::runOnCefUiThreadSync;
22
26
 
23
- static constexpr int32_t BUNITE_ABI_VERSION = 9;
27
+ static constexpr int32_t BUNITE_ABI_VERSION = 11;
24
28
 
25
29
  namespace {
26
30
 
@@ -45,6 +49,48 @@ public:
45
49
  if (result && result_size) r.assign(static_cast<const char*>(result), result_size);
46
50
  cb(success, std::move(r));
47
51
  }
52
+ void OnDevToolsEvent(CefRefPtr<CefBrowser> browser,
53
+ const CefString& method,
54
+ const void* params,
55
+ size_t params_size) override {
56
+ std::string m = method.ToString();
57
+ if (m != "Target.attachedToTarget" && m != "Target.detachedFromTarget") return;
58
+ std::string p;
59
+ if (params && params_size) p.assign(static_cast<const char*>(params), params_size);
60
+ CefRefPtr<CefValue> val = CefParseJSON(p, JSON_PARSER_RFC);
61
+ if (!val || val->GetType() != VTYPE_DICTIONARY) return;
62
+ auto d = val->GetDictionary();
63
+ uint32_t view_id = 0;
64
+ {
65
+ std::lock_guard<std::mutex> lk(g_runtime.object_mutex);
66
+ auto it = g_runtime.browser_to_view_id.find(browser->GetIdentifier());
67
+ if (it == g_runtime.browser_to_view_id.end()) return;
68
+ view_id = it->second;
69
+ }
70
+ auto* view = bunite_win::getViewHostById(view_id);
71
+ if (!view) return;
72
+ if (m == "Target.attachedToTarget") {
73
+ std::string session_id = d->HasKey("sessionId") ? d->GetString("sessionId").ToString() : "";
74
+ if (session_id.empty()) return;
75
+ auto info = d->HasKey("targetInfo") ? d->GetDictionary("targetInfo") : nullptr;
76
+ if (!info) return;
77
+ std::string type = info->HasKey("type") ? info->GetString("type").ToString() : "";
78
+ std::string target_id = info->HasKey("targetId") ? info->GetString("targetId").ToString() : "";
79
+ // For iframe targets in modern Chromium, targetId is the devtools frame
80
+ // token — identical to Page.FrameId. Spike-verified per OOPIF plan.
81
+ if (type != "iframe" || target_id.empty()) return;
82
+ std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
83
+ view->oopif_sessions[target_id] = session_id;
84
+ } else {
85
+ std::string session_id = d->HasKey("sessionId") ? d->GetString("sessionId").ToString() : "";
86
+ if (session_id.empty()) return;
87
+ std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
88
+ for (auto it = view->oopif_sessions.begin(); it != view->oopif_sessions.end(); ) {
89
+ if (it->second == session_id) it = view->oopif_sessions.erase(it);
90
+ else ++it;
91
+ }
92
+ }
93
+ }
48
94
  void OnDevToolsAgentDetached(CefRefPtr<CefBrowser>) override {
49
95
  // Pending method results are dropped by CEF on detach (browser crash,
50
96
  // process restart). Fire all callbacks with a failure result to prevent
@@ -64,6 +110,29 @@ CefRefPtr<CefDevToolsMessageObserver> getDevToolsObserver() {
64
110
  return obs;
65
111
  }
66
112
 
113
+ // Raw-id space for SendDevToolsMessage (sessionId-routed calls). High range
114
+ // avoids collision with CEF's internal counter used by ExecuteDevToolsMethod.
115
+ std::atomic<int> g_raw_cdp_id_counter{0x40000000};
116
+ int nextRawCdpId() { return ++g_raw_cdp_id_counter; }
117
+
118
+ void cefSendRaw(ViewHost* v, const std::string& message, int id_for_cb,
119
+ std::function<void(bool, std::string)> cb) {
120
+ if (!v || !v->browser) { if (cb) cb(false, "{\"error\":\"view not ready\"}"); return; }
121
+ if (cb) {
122
+ std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
123
+ g_cdp_callbacks[id_for_cb] = std::move(cb);
124
+ }
125
+ if (!v->browser->GetHost()->SendDevToolsMessage(message.data(), message.size())) {
126
+ std::function<void(bool, std::string)> orphan;
127
+ {
128
+ std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
129
+ auto it = g_cdp_callbacks.find(id_for_cb);
130
+ if (it != g_cdp_callbacks.end()) { orphan = std::move(it->second); g_cdp_callbacks.erase(it); }
131
+ }
132
+ if (orphan) orphan(false, "{\"error\":\"SendDevToolsMessage failed\"}");
133
+ }
134
+ }
135
+
67
136
  void cefCdpCall(ViewHost* v, const std::string& method, const std::string& params_json,
68
137
  std::function<void(bool, std::string)> cb = nullptr) {
69
138
  if (!v || !v->browser) {
@@ -195,6 +264,7 @@ extern "C" BUNITE_EXPORT bool bunite_init(
195
264
  bool popup_blocking,
196
265
  const char* engine_config_json
197
266
  ) {
267
+ buniteApplyEnvLogLevel();
198
268
  reapChildrenOnExit();
199
269
  {
200
270
  std::lock_guard<std::mutex> lock(g_runtime.lifecycle_mutex);
@@ -1189,7 +1259,79 @@ extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
1189
1259
  BUNITE_CAP_NATIVE_INPUT_TRUSTED |
1190
1260
  BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
1191
1261
  BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
1192
- BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
1262
+ BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
1263
+ BUNITE_CAP_AX | BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_FRAMES |
1264
+ BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
1265
+ BUNITE_CAP_RESOLVE_AND_CLICK;
1266
+ }
1267
+
1268
+ extern "C" BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
1269
+ bunite_win::postCefUiTask([view_id, policy, dir = std::string(download_dir ? download_dir : "")]() {
1270
+ auto* view = bunite_win::getViewHostById(view_id);
1271
+ if (!view) return;
1272
+ int32_t p = policy;
1273
+ if (p < 0 || p > 2) p = 2;
1274
+ view->download_policy.store(p);
1275
+ view->download_dir = dir;
1276
+ });
1277
+ }
1278
+
1279
+ namespace bunite_win {
1280
+
1281
+ void applyPopupAccept(ViewHost* view, uint32_t host_window_id, double x, double y, double w, double h) {
1282
+ if (!view || !view->browser) return;
1283
+ auto* host = getWindowHostById(host_window_id);
1284
+ if (!host || !host->hwnd) return;
1285
+ view->window = host;
1286
+ view->is_popup_pending = false;
1287
+ host->views.push_back(view);
1288
+ HWND browser_hwnd = view->browser->GetHost()->GetWindowHandle();
1289
+ if (browser_hwnd) {
1290
+ SetParent(browser_hwnd, host->hwnd);
1291
+ SetWindowPos(browser_hwnd, HWND_TOP,
1292
+ static_cast<int>(x), static_cast<int>(y),
1293
+ static_cast<int>(w), static_cast<int>(h),
1294
+ SWP_SHOWWINDOW | SWP_NOACTIVATE);
1295
+ view->bounds = RECT{
1296
+ static_cast<LONG>(x), static_cast<LONG>(y),
1297
+ static_cast<LONG>(x + w), static_cast<LONG>(y + h)
1298
+ };
1299
+ }
1300
+ // Re-emit view-ready so the TS-side BrowserView.adopt resolves its
1301
+ // `_readyPromise` (the original view-ready from OnAfterCreated fired before
1302
+ // the TS resolver was registered).
1303
+ emitWebviewEvent(view->id, "view-ready", "");
1304
+ }
1305
+
1306
+ } // namespace bunite_win
1307
+
1308
+ extern "C" BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
1309
+ double x, double y, double w, double h) {
1310
+ bunite_win::postCefUiTask([new_view_id, host_window_id, x, y, w, h]() {
1311
+ auto* view = bunite_win::getViewHostById(new_view_id);
1312
+ if (!view) return;
1313
+ if (!view->browser) {
1314
+ // OnAfterCreated hasn't fired yet; stash the accept and apply when it does.
1315
+ view->pending_popup_accept = ViewHost::PendingPopupAccept{host_window_id, x, y, w, h};
1316
+ return;
1317
+ }
1318
+ bunite_win::applyPopupAccept(view, host_window_id, x, y, w, h);
1319
+ });
1320
+ }
1321
+
1322
+ extern "C" BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
1323
+ bunite_win::postCefUiTask([new_view_id]() {
1324
+ auto* view = bunite_win::getViewHostById(new_view_id);
1325
+ if (!view) return;
1326
+ if (!view->is_popup_pending && view->window) return; // already adopted — caller responsibility, ignore.
1327
+ if (view->browser) {
1328
+ view->closing.store(true);
1329
+ view->browser->GetHost()->CloseBrowser(true);
1330
+ } else {
1331
+ // Browser not yet created — let OnAfterCreated handle dismissal.
1332
+ view->popup_dismiss_requested = true;
1333
+ }
1334
+ });
1193
1335
  }
1194
1336
 
1195
1337
  extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
@@ -1234,6 +1376,680 @@ extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t
1234
1376
  });
1235
1377
  }
1236
1378
 
1379
+ static void emitAxError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
1380
+ std::string esc; esc.reserve(message.size());
1381
+ for (char c : message) {
1382
+ if (c == '"' || c == '\\') { esc.push_back('\\'); esc.push_back(c); }
1383
+ else if (c == '\n') esc += "\\n";
1384
+ else if (c == '\r') esc += "\\r";
1385
+ else if (c == '\t') esc += "\\t";
1386
+ else esc.push_back(c);
1387
+ }
1388
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1389
+ ",\"ok\":false,\"code\":\"" + code +
1390
+ "\",\"message\":\"" + esc + "\"}";
1391
+ bunite_win::emitWebviewEvent(view_id, "accessibility-result", payload);
1392
+ }
1393
+
1394
+ extern "C" BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
1395
+ int32_t /*interesting_only*/) {
1396
+ // CDP `Accessibility.getFullAXTree` takes `depth`/`frameId` only; the
1397
+ // interesting-only filter is applied TS-side on the flat node list.
1398
+ bunite_win::postCefUiTask([view_id, request_id]() {
1399
+ auto* view = bunite_win::getViewHostById(view_id);
1400
+ if (!view) { emitAxError(view_id, request_id, "not_supported", "view not ready"); return; }
1401
+ cefCdpCall(view, "Accessibility.getFullAXTree", "{}",
1402
+ [view_id, request_id](bool ok, std::string result) {
1403
+ if (!ok) { emitAxError(view_id, request_id, "runtime_error", "getFullAXTree failed: " + result); return; }
1404
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1405
+ ",\"ok\":true,\"tree\":" + result + "}";
1406
+ bunite_win::emitWebviewEvent(view_id, "accessibility-result", payload);
1407
+ });
1408
+ });
1409
+ }
1410
+
1411
+ static void emitListFramesError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
1412
+ std::string esc; esc.reserve(message.size());
1413
+ for (char c : message) {
1414
+ if (c == '"' || c == '\\') { esc.push_back('\\'); esc.push_back(c); }
1415
+ else esc.push_back(c);
1416
+ }
1417
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1418
+ ",\"ok\":false,\"code\":\"" + code +
1419
+ "\",\"message\":\"" + esc + "\"}";
1420
+ bunite_win::emitWebviewEvent(view_id, "list-frames-result", payload);
1421
+ }
1422
+
1423
+ extern "C" BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
1424
+ bunite_win::postCefUiTask([view_id, request_id]() {
1425
+ auto* view = bunite_win::getViewHostById(view_id);
1426
+ if (!view) { emitListFramesError(view_id, request_id, "not_supported", "view not ready"); return; }
1427
+ cefCdpCall(view, "Page.getFrameTree", "{}",
1428
+ [view_id, request_id](bool ok, std::string result) {
1429
+ if (!ok) { emitListFramesError(view_id, request_id, "runtime_error", "getFrameTree failed: " + result); return; }
1430
+ // Raw CDP `{frameTree:{frame,childFrames}}` — TS flattens.
1431
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1432
+ ",\"ok\":true,\"raw\":" + result + "}";
1433
+ bunite_win::emitWebviewEvent(view_id, "list-frames-result", payload);
1434
+ });
1435
+ });
1436
+ }
1437
+
1438
+ extern "C" BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
1439
+ const char* script_c, const char* frame_id_c) {
1440
+ std::string script = script_c ? script_c : "";
1441
+ std::string frameId = frame_id_c ? frame_id_c : "";
1442
+ if (frameId.empty()) {
1443
+ bunite_view_evaluate(view_id, request_id, script_c);
1444
+ return;
1445
+ }
1446
+ bunite_win::postCefUiTask([view_id, request_id, script, frameId]() {
1447
+ auto* view = bunite_win::getViewHostById(view_id);
1448
+ if (!view) {
1449
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1450
+ ",\"ok\":false,\"code\":\"not_supported\",\"message\":\"view not ready\"}";
1451
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1452
+ return;
1453
+ }
1454
+ // Step 1: create an isolated world in the target frame.
1455
+ std::string isoParams = "{\"frameId\":\"" + bunite_win::escapeJsonString(frameId) + "\",\"worldName\":\"bunite-eval\"}";
1456
+ cefCdpCall(view, "Page.createIsolatedWorld", isoParams,
1457
+ [view_id, request_id, script](bool ok, std::string isoResult) {
1458
+ if (!ok) {
1459
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1460
+ ",\"ok\":false,\"code\":\"runtime_error\","
1461
+ "\"message\":\"createIsolatedWorld failed\"}";
1462
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1463
+ return;
1464
+ }
1465
+ CefRefPtr<CefValue> val = CefParseJSON(isoResult, JSON_PARSER_RFC);
1466
+ if (!val || val->GetType() != VTYPE_DICTIONARY) {
1467
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1468
+ ",\"ok\":false,\"code\":\"runtime_error\","
1469
+ "\"message\":\"createIsolatedWorld malformed\"}";
1470
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1471
+ return;
1472
+ }
1473
+ int contextId = val->GetDictionary()->GetInt("executionContextId");
1474
+ // Re-lookup view — async gap may have destroyed it.
1475
+ auto* view2 = bunite_win::getViewHostById(view_id);
1476
+ if (!view2) {
1477
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1478
+ ",\"ok\":false,\"code\":\"not_supported\","
1479
+ "\"message\":\"view destroyed\"}";
1480
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1481
+ return;
1482
+ }
1483
+ // Step 2: Runtime.evaluate in that context.
1484
+ std::string escScript; escScript.reserve(script.size());
1485
+ for (char c : script) {
1486
+ if (c == '"' || c == '\\') { escScript.push_back('\\'); escScript.push_back(c); }
1487
+ else if (c == '\n') escScript += "\\n";
1488
+ else if (c == '\r') escScript += "\\r";
1489
+ else if (c == '\t') escScript += "\\t";
1490
+ else escScript.push_back(c);
1491
+ }
1492
+ std::string evalParams = "{\"expression\":\"" + escScript +
1493
+ "\",\"contextId\":" + std::to_string(contextId) +
1494
+ ",\"returnByValue\":true,\"awaitPromise\":true}";
1495
+ cefCdpCall(view2, "Runtime.evaluate", evalParams,
1496
+ [view_id, request_id](bool ok2, std::string evalResult) {
1497
+ if (!ok2) {
1498
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1499
+ ",\"ok\":false,\"code\":\"runtime_error\","
1500
+ "\"message\":\"Runtime.evaluate failed\"}";
1501
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1502
+ return;
1503
+ }
1504
+ CefRefPtr<CefValue> ev = CefParseJSON(evalResult, JSON_PARSER_RFC);
1505
+ if (!ev || ev->GetType() != VTYPE_DICTIONARY) {
1506
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1507
+ ",\"ok\":false,\"code\":\"runtime_error\","
1508
+ "\"message\":\"Runtime.evaluate malformed\"}";
1509
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1510
+ return;
1511
+ }
1512
+ CefRefPtr<CefDictionaryValue> d = ev->GetDictionary();
1513
+ if (d->HasKey("exceptionDetails")) {
1514
+ CefRefPtr<CefDictionaryValue> ex = d->GetDictionary("exceptionDetails");
1515
+ std::string msg = ex && ex->HasKey("text") ? ex->GetString("text").ToString() : "runtime exception";
1516
+ std::string escMsg; escMsg.reserve(msg.size());
1517
+ for (char c : msg) {
1518
+ if (c == '"' || c == '\\') { escMsg.push_back('\\'); escMsg.push_back(c); }
1519
+ else escMsg.push_back(c);
1520
+ }
1521
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1522
+ ",\"ok\":false,\"code\":\"runtime_error\","
1523
+ "\"message\":\"" + escMsg + "\"}";
1524
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1525
+ return;
1526
+ }
1527
+ // result.value (JSON-serialized into "value") -> stringify.
1528
+ CefRefPtr<CefDictionaryValue> result = d->GetDictionary("result");
1529
+ std::string valueJson = "null";
1530
+ if (result && result->HasKey("value")) {
1531
+ CefRefPtr<CefValue> v = result->GetValue("value");
1532
+ if (v) valueJson = CefWriteJSON(v, JSON_WRITER_DEFAULT);
1533
+ }
1534
+ // The host expects "value" to be a JSON STRING (it re-parses).
1535
+ std::string escVal; escVal.reserve(valueJson.size());
1536
+ for (char c : valueJson) {
1537
+ if (c == '"' || c == '\\') { escVal.push_back('\\'); escVal.push_back(c); }
1538
+ else if (c == '\n') escVal += "\\n";
1539
+ else if (c == '\r') escVal += "\\r";
1540
+ else if (c == '\t') escVal += "\\t";
1541
+ else escVal.push_back(c);
1542
+ }
1543
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1544
+ ",\"ok\":true,\"value\":\"" + escVal + "\"}";
1545
+ bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
1546
+ });
1547
+ });
1548
+ });
1549
+ }
1550
+
1551
+ namespace {
1552
+
1553
+ void emitResolveAndClickError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
1554
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1555
+ ",\"ok\":false,\"code\":\"" + code + "\","
1556
+ "\"message\":\"" + bunite_win::escapeJsonString(msg) + "\"}";
1557
+ bunite_win::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
1558
+ }
1559
+
1560
+ const char* cdpButtonName(int32_t b) {
1561
+ switch (b) { case 1: return "middle"; case 2: return "right"; default: return "left"; }
1562
+ }
1563
+
1564
+ std::string escapeForJsString(const std::string& s) {
1565
+ std::string out; out.reserve(s.size() + 2);
1566
+ for (char c : s) {
1567
+ if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
1568
+ else if (c == '\n') out += "\\n";
1569
+ else if (c == '\r') out += "\\r";
1570
+ else if (c == '\t') out += "\\t";
1571
+ else out.push_back(c);
1572
+ }
1573
+ return out;
1574
+ }
1575
+
1576
+ std::string escapeForJsonString(const std::string& s) {
1577
+ std::string out; out.reserve(s.size());
1578
+ for (char c : s) {
1579
+ if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
1580
+ else if (c == '\n') out += "\\n";
1581
+ else if (c == '\r') out += "\\r";
1582
+ else if (c == '\t') out += "\\t";
1583
+ else out.push_back(c);
1584
+ }
1585
+ return out;
1586
+ }
1587
+
1588
+ std::string buildResolveScript(const std::string& selector) {
1589
+ // Frame-local rect + innerWidth/innerHeight for bilinear mapping when the
1590
+ // frame is transformed (rotate/scale). Main frame uses iw/ih harmlessly.
1591
+ std::string sel_lit = "\"" + escapeForJsString(selector) + "\"";
1592
+ return
1593
+ "(function(){"
1594
+ "var el=document.querySelector(" + sel_lit + ");"
1595
+ "if(!el)return{ok:false,code:\"not_found\"};"
1596
+ "el.scrollIntoView({block:\"nearest\",inline:\"nearest\",behavior:\"instant\"});"
1597
+ "var r=el.getBoundingClientRect();"
1598
+ "var vis=r.width>0&&r.height>0&&r.bottom>0&&r.right>0"
1599
+ "&&r.top<innerHeight&&r.left<innerWidth;"
1600
+ "if(!vis)return{ok:false,code:\"not_visible\"};"
1601
+ "return{ok:true,x:r.x,y:r.y,w:r.width,h:r.height,"
1602
+ "cx:r.x+r.width/2,cy:r.y+r.height/2,"
1603
+ "iw:innerWidth,ih:innerHeight};"
1604
+ "})()";
1605
+ }
1606
+
1607
+ // Extract the user-returned value dict from a Runtime.evaluate response.
1608
+ // Complex (dict/list) CefValue handles reference parent storage — round-trip
1609
+ // through JSON so the returned dict has independent lifetime.
1610
+ CefRefPtr<CefDictionaryValue> parseEvaluateValue(
1611
+ const std::string& evalResult,
1612
+ std::function<void(const char*, const std::string&)> onErr) {
1613
+ CefRefPtr<CefValue> ev = CefParseJSON(evalResult, JSON_PARSER_RFC);
1614
+ if (!ev || ev->GetType() != VTYPE_DICTIONARY) { onErr("runtime_error", "Runtime.evaluate malformed"); return nullptr; }
1615
+ CefRefPtr<CefDictionaryValue> d = ev->GetDictionary();
1616
+ if (d->HasKey("exceptionDetails")) {
1617
+ CefRefPtr<CefDictionaryValue> ex = d->GetDictionary("exceptionDetails");
1618
+ std::string msg = ex && ex->HasKey("text") ? ex->GetString("text").ToString() : "runtime exception";
1619
+ onErr("runtime_error", msg);
1620
+ return nullptr;
1621
+ }
1622
+ CefRefPtr<CefDictionaryValue> result = d->GetDictionary("result");
1623
+ if (!result || !result->HasKey("value")) { onErr("runtime_error", "evaluate returned no value"); return nullptr; }
1624
+ CefRefPtr<CefValue> v = result->GetValue("value");
1625
+ if (!v || v->GetType() != VTYPE_DICTIONARY) { onErr("runtime_error", "evaluate returned non-object"); return nullptr; }
1626
+ std::string userJson = CefWriteJSON(v, JSON_WRITER_DEFAULT);
1627
+ CefRefPtr<CefValue> independent = CefParseJSON(userJson, JSON_PARSER_RFC);
1628
+ if (!independent || independent->GetType() != VTYPE_DICTIONARY) {
1629
+ onErr("runtime_error", "evaluate value re-parse failed"); return nullptr;
1630
+ }
1631
+ return independent->GetDictionary();
1632
+ }
1633
+
1634
+ void dispatchCdpClick(ViewHost* view, double cx, double cy,
1635
+ int32_t button, int32_t click_count, uint32_t modifiers) {
1636
+ if (click_count < 1) click_count = 1;
1637
+ const char* btn = cdpButtonName(button);
1638
+ for (int i = 1; i <= click_count; ++i) {
1639
+ std::string base = "\"x\":" + std::to_string(cx) + ",\"y\":" + std::to_string(cy) +
1640
+ ",\"button\":\"" + btn + "\",\"clickCount\":" + std::to_string(i) +
1641
+ ",\"modifiers\":" + std::to_string(modifiers);
1642
+ cefCdpCall(view, "Input.dispatchMouseEvent", "{\"type\":\"mousePressed\"," + base + "}");
1643
+ cefCdpCall(view, "Input.dispatchMouseEvent", "{\"type\":\"mouseReleased\"," + base + "}");
1644
+ }
1645
+ }
1646
+
1647
+ } // namespace
1648
+
1649
+ namespace {
1650
+
1651
+ // Dispatch native click at page coords + emit success envelope.
1652
+ void finishResolveAndClick(uint32_t view_id, uint32_t request_id, double x, double y,
1653
+ double w, double h, double cx, double cy,
1654
+ int32_t button, int32_t click_count, uint32_t modifiers) {
1655
+ auto* v = bunite_win::getViewHostById(view_id);
1656
+ if (!v || !v->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
1657
+ dispatchCdpClick(v, cx, cy, button, click_count, modifiers);
1658
+ // CEF CDP `Input.dispatchMouseEvent` produces DOM events with isTrusted=true
1659
+ // (empirical — `e.isTrusted` on page-side listener reports true).
1660
+ std::string payload = "{\"requestId\":" + std::to_string(request_id) +
1661
+ ",\"ok\":true,\"rect\":{\"x\":" + std::to_string(x) +
1662
+ ",\"y\":" + std::to_string(y) +
1663
+ ",\"width\":" + std::to_string(w) +
1664
+ ",\"height\":" + std::to_string(h) + "},"
1665
+ "\"isTrustedEvent\":true}";
1666
+ bunite_win::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
1667
+ }
1668
+
1669
+ struct FrameResolveOk { double x, y, w, h, cx, cy, iw, ih; };
1670
+
1671
+ // CefDictionaryValue::GetDouble returns 0 for VTYPE_INT — JSON.stringify
1672
+ // serializes integer-valued numbers without `.0` so values like rect.height==35
1673
+ // re-parse as VTYPE_INT. Coerce here.
1674
+ double dictNumber(CefRefPtr<CefDictionaryValue> d, const char* key) {
1675
+ if (!d || !d->HasKey(key)) return 0.0;
1676
+ switch (d->GetType(key)) {
1677
+ case VTYPE_INT: return static_cast<double>(d->GetInt(key));
1678
+ case VTYPE_DOUBLE: return d->GetDouble(key);
1679
+ default: return 0.0;
1680
+ }
1681
+ }
1682
+
1683
+ // Parse a Runtime.evaluate response (either main-session or sessionId-routed)
1684
+ // and forward the script's frame-local fields to `onOk`. The script's failure
1685
+ // branch (`{ok:false,code:...}`) routes through `onErr`.
1686
+ void parseEvalAndContinue(uint32_t view_id, uint32_t request_id, bool ok, const std::string& evalResult,
1687
+ std::function<void(const FrameResolveOk&)> onOk) {
1688
+ auto onErr = [view_id, request_id](const char* code, const std::string& msg) {
1689
+ emitResolveAndClickError(view_id, request_id, code, msg);
1690
+ };
1691
+ if (!ok) {
1692
+ BUNITE_INFO("cef/eval: Runtime.evaluate failed view=%u request=%u body=%.300s%s",
1693
+ view_id, request_id, evalResult.c_str(),
1694
+ evalResult.size() > 300 ? "..." : "");
1695
+ onErr("runtime_error", "Runtime.evaluate failed"); return;
1696
+ }
1697
+ auto value = parseEvaluateValue(evalResult, onErr);
1698
+ if (!value) return;
1699
+ if (!value->HasKey("ok") || !value->GetBool("ok")) {
1700
+ std::string code = value->HasKey("code") ? value->GetString("code").ToString() : "runtime_error";
1701
+ onErr(code.c_str(), "");
1702
+ return;
1703
+ }
1704
+ onOk(FrameResolveOk{
1705
+ dictNumber(value, "x"), dictNumber(value, "y"),
1706
+ dictNumber(value, "w"), dictNumber(value, "h"),
1707
+ dictNumber(value, "cx"), dictNumber(value, "cy"),
1708
+ dictNumber(value, "iw"), dictNumber(value, "ih"),
1709
+ });
1710
+ }
1711
+
1712
+ // Issue Runtime.evaluate inside the target frame — sessionId-routed (OOPIF) or
1713
+ // isolated-world via main session (same-renderer cross-origin or same-origin).
1714
+ void evalInFrame(uint32_t view_id, uint32_t request_id, const std::string& frameId,
1715
+ const std::string& escScript,
1716
+ std::function<void(const FrameResolveOk&)> onOk) {
1717
+ auto* view = bunite_win::getViewHostById(view_id);
1718
+ if (!view || !view->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
1719
+ // Look up auto-attached OOPIF session.
1720
+ std::string session_id;
1721
+ {
1722
+ std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
1723
+ auto it = view->oopif_sessions.find(frameId);
1724
+ if (it != view->oopif_sessions.end()) session_id = it->second;
1725
+ }
1726
+ if (!session_id.empty()) {
1727
+ int id = nextRawCdpId();
1728
+ std::string msg = "{\"id\":" + std::to_string(id) +
1729
+ ",\"sessionId\":\"" + session_id +
1730
+ "\",\"method\":\"Runtime.evaluate\""
1731
+ ",\"params\":{\"expression\":\"" + escScript +
1732
+ "\",\"returnByValue\":true,\"awaitPromise\":true}}";
1733
+ cefSendRaw(view, msg, id,
1734
+ [view_id, request_id, onOk](bool ok, std::string r) {
1735
+ parseEvalAndContinue(view_id, request_id, ok, r, onOk);
1736
+ });
1737
+ return;
1738
+ }
1739
+ // In-process: createIsolatedWorld + Runtime.evaluate via main session.
1740
+ std::string isoParams = "{\"frameId\":\"" + bunite_win::escapeJsonString(frameId) + "\",\"worldName\":\"bunite-rac\"}";
1741
+ cefCdpCall(view, "Page.createIsolatedWorld", isoParams,
1742
+ [view_id, request_id, escScript, onOk](bool ok, std::string isoResult) {
1743
+ if (!ok) { emitResolveAndClickError(view_id, request_id, "runtime_error", "createIsolatedWorld failed"); return; }
1744
+ CefRefPtr<CefValue> val = CefParseJSON(isoResult, JSON_PARSER_RFC);
1745
+ if (!val || val->GetType() != VTYPE_DICTIONARY) {
1746
+ emitResolveAndClickError(view_id, request_id, "runtime_error", "createIsolatedWorld malformed"); return;
1747
+ }
1748
+ int contextId = val->GetDictionary()->GetInt("executionContextId");
1749
+ auto* v2 = bunite_win::getViewHostById(view_id);
1750
+ if (!v2) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
1751
+ std::string evalParams = "{\"expression\":\"" + escScript +
1752
+ "\",\"contextId\":" + std::to_string(contextId) +
1753
+ ",\"returnByValue\":true,\"awaitPromise\":true}";
1754
+ cefCdpCall(v2, "Runtime.evaluate", evalParams,
1755
+ [view_id, request_id, onOk](bool ok2, std::string r) {
1756
+ parseEvalAndContinue(view_id, request_id, ok2, r, onOk);
1757
+ });
1758
+ });
1759
+ }
1760
+
1761
+ // Bilinear: (fx, fy) ∈ [0, iw] × [0, ih] → page coord using clockwise quad
1762
+ // TL/TR/BR/BL. Handles axis-aligned, scaled, rotated, and skewed iframes.
1763
+ inline void bilinearMap(const std::array<double, 8>& q, double iw, double ih,
1764
+ double fx, double fy, double& px, double& py) {
1765
+ const double u = (iw > 0) ? (fx / iw) : 0.0;
1766
+ const double v = (ih > 0) ? (fy / ih) : 0.0;
1767
+ px = (1-u)*(1-v)*q[0] + u*(1-v)*q[2] + u*v*q[4] + (1-u)*v*q[6];
1768
+ py = (1-u)*(1-v)*q[1] + u*(1-v)*q[3] + u*v*q[5] + (1-u)*v*q[7];
1769
+ }
1770
+
1771
+ // Recursive frame path lookup in Page.getFrameTree response.
1772
+ // Returns [outermost frame id, ..., target_frame_id] including main; empty if
1773
+ // target not in tree.
1774
+ std::vector<std::string> findFramePath(CefRefPtr<CefDictionaryValue> node, const std::string& target) {
1775
+ if (!node) return {};
1776
+ CefRefPtr<CefDictionaryValue> frame = node->HasKey("frame") ? node->GetDictionary("frame") : nullptr;
1777
+ if (!frame) return {};
1778
+ std::string this_id = frame->GetString("id").ToString();
1779
+ if (this_id == target) return {this_id};
1780
+ CefRefPtr<CefListValue> children = node->HasKey("childFrames") ? node->GetList("childFrames") : nullptr;
1781
+ if (!children) return {};
1782
+ for (size_t i = 0; i < children->GetSize(); ++i) {
1783
+ auto v = children->GetValue(i);
1784
+ if (!v || v->GetType() != VTYPE_DICTIONARY) continue;
1785
+ auto sub = findFramePath(v->GetDictionary(), target);
1786
+ if (!sub.empty()) { sub.insert(sub.begin(), this_id); return sub; }
1787
+ }
1788
+ return {};
1789
+ }
1790
+
1791
+ bool parseQuadFromBoxModel(const std::string& result, std::array<double, 8>& out) {
1792
+ CefRefPtr<CefValue> bv = CefParseJSON(result, JSON_PARSER_RFC);
1793
+ if (!bv || bv->GetType() != VTYPE_DICTIONARY) return false;
1794
+ auto model = bv->GetDictionary()->HasKey("model") ? bv->GetDictionary()->GetDictionary("model") : nullptr;
1795
+ if (!model) return false;
1796
+ auto content = model->HasKey("content") ? model->GetList("content") : nullptr;
1797
+ if (!content || content->GetSize() < 8) return false;
1798
+ for (int i = 0; i < 8; ++i) {
1799
+ // CDP serializes integer pixel positions without `.0`; coerce from INT.
1800
+ switch (content->GetType(i)) {
1801
+ case VTYPE_INT: out[i] = static_cast<double>(content->GetInt(i)); break;
1802
+ case VTYPE_DOUBLE: out[i] = content->GetDouble(i); break;
1803
+ default: out[i] = 0.0;
1804
+ }
1805
+ }
1806
+ return true;
1807
+ }
1808
+
1809
+ // Eval a script on a specific session (OOPIF) or main session (empty session_id).
1810
+ // Result delivered as raw JSON of `Runtime.evaluate` response.
1811
+ void evalRaw(ViewHost* view, const std::string& session_id, const std::string& escScript,
1812
+ std::function<void(bool, std::string)> cb) {
1813
+ if (session_id.empty()) {
1814
+ std::string params = "{\"expression\":\"" + escScript + "\",\"returnByValue\":true,\"awaitPromise\":true}";
1815
+ cefCdpCall(view, "Runtime.evaluate", params, std::move(cb));
1816
+ return;
1817
+ }
1818
+ int id = nextRawCdpId();
1819
+ std::string msg = "{\"id\":" + std::to_string(id) +
1820
+ ",\"sessionId\":\"" + session_id +
1821
+ "\",\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"" +
1822
+ escScript + "\",\"returnByValue\":true,\"awaitPromise\":true}}";
1823
+ cefSendRaw(view, msg, id, std::move(cb));
1824
+ }
1825
+
1826
+ // State threaded through chain-walk continuations.
1827
+ struct ChainState {
1828
+ uint32_t view_id;
1829
+ uint32_t request_id;
1830
+ std::string targetFrameId;
1831
+ std::string escScript;
1832
+ int32_t button, click_count;
1833
+ uint32_t modifiers;
1834
+ // chain[0] = main frameId, chain[1..N-1] = outermost..target. N >= 2.
1835
+ std::vector<std::string> chain;
1836
+ // For each link i (parent = chain[i], child = chain[i+1]): quad in parent's coord system.
1837
+ std::vector<std::array<double, 8>> link_quads;
1838
+ // For chain[i] (i in [1..N-2]): innerWidth/innerHeight of that ancestor frame.
1839
+ // Used when mapping FROM chain[i+1]'s coords up to chain[i]'s coords.
1840
+ // Indexed by ancestor's chain position; chain[N-1] (target) iw/ih comes from
1841
+ // the target eval, not stored here.
1842
+ std::vector<std::pair<double, double>> ancestor_inner;
1843
+ };
1844
+
1845
+ void composeAndDispatch(std::shared_ptr<ChainState> s, const FrameResolveOk& fr);
1846
+ void fetchTargetEval(std::shared_ptr<ChainState> s);
1847
+ void fetchAncestorInner(std::shared_ptr<ChainState> s, size_t i);
1848
+ void fetchLink(std::shared_ptr<ChainState> s, size_t link_idx);
1849
+
1850
+ // Look up the session for an ancestor frame (chain[idx]). idx == 0 → main (empty).
1851
+ std::string sessionForChainIdx(uint32_t view_id, const std::vector<std::string>& chain, size_t idx) {
1852
+ if (idx == 0) return {};
1853
+ auto* view = bunite_win::getViewHostById(view_id);
1854
+ if (!view) return {};
1855
+ std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
1856
+ auto it = view->oopif_sessions.find(chain[idx]);
1857
+ return (it != view->oopif_sessions.end()) ? it->second : std::string{};
1858
+ }
1859
+
1860
+ void fetchLink(std::shared_ptr<ChainState> s, size_t link_idx) {
1861
+ if (link_idx + 1 >= s->chain.size()) {
1862
+ // All links collected. Move to ancestor inner sizes.
1863
+ fetchAncestorInner(s, 1);
1864
+ return;
1865
+ }
1866
+ const std::string parent_session = sessionForChainIdx(s->view_id, s->chain, link_idx);
1867
+ const std::string& child_frameId = s->chain[link_idx + 1];
1868
+ auto* view = bunite_win::getViewHostById(s->view_id);
1869
+ if (!view) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
1870
+ // DOM.getFrameOwner on parent's session.
1871
+ std::string ownerParams = "{\"frameId\":\"" + bunite_win::escapeJsonString(child_frameId) + "\"}";
1872
+ auto onOwner = [s, link_idx, parent_session](bool ok, std::string r) {
1873
+ if (!ok) { emitResolveAndClickError(s->view_id, s->request_id, "not_found", "getFrameOwner failed"); return; }
1874
+ CefRefPtr<CefValue> val = CefParseJSON(r, JSON_PARSER_RFC);
1875
+ if (!val || val->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "getFrameOwner malformed"); return; }
1876
+ int backendNodeId = val->GetDictionary()->HasKey("backendNodeId") ? val->GetDictionary()->GetInt("backendNodeId") : 0;
1877
+ if (!backendNodeId) { emitResolveAndClickError(s->view_id, s->request_id, "not_found", "no backendNodeId"); return; }
1878
+ auto* v2 = bunite_win::getViewHostById(s->view_id);
1879
+ if (!v2) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
1880
+ std::string boxParams = "{\"backendNodeId\":" + std::to_string(backendNodeId) + "}";
1881
+ auto onBox = [s, link_idx](bool ok2, std::string rb) {
1882
+ if (!ok2) { emitResolveAndClickError(s->view_id, s->request_id, "not_visible", "iframe has no box"); return; }
1883
+ std::array<double, 8> quad{};
1884
+ if (!parseQuadFromBoxModel(rb, quad)) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "bad quad"); return; }
1885
+ s->link_quads.push_back(quad);
1886
+ fetchLink(s, link_idx + 1);
1887
+ };
1888
+ if (parent_session.empty()) {
1889
+ cefCdpCall(v2, "DOM.getBoxModel", boxParams, onBox);
1890
+ } else {
1891
+ int id = nextRawCdpId();
1892
+ std::string msg = "{\"id\":" + std::to_string(id) +
1893
+ ",\"sessionId\":\"" + parent_session +
1894
+ "\",\"method\":\"DOM.getBoxModel\",\"params\":" + boxParams + "}";
1895
+ cefSendRaw(v2, msg, id, onBox);
1896
+ }
1897
+ };
1898
+ if (parent_session.empty()) {
1899
+ cefCdpCall(view, "DOM.getFrameOwner", ownerParams, onOwner);
1900
+ } else {
1901
+ int id = nextRawCdpId();
1902
+ std::string msg = "{\"id\":" + std::to_string(id) +
1903
+ ",\"sessionId\":\"" + parent_session +
1904
+ "\",\"method\":\"DOM.getFrameOwner\",\"params\":" + ownerParams + "}";
1905
+ cefSendRaw(view, msg, id, onOwner);
1906
+ }
1907
+ }
1908
+
1909
+ void fetchAncestorInner(std::shared_ptr<ChainState> s, size_t i) {
1910
+ // i ranges [1, N-2]. Skip N-1 (target — iw/ih from target eval).
1911
+ if (i + 1 >= s->chain.size()) {
1912
+ // Done with ancestors. Eval target script.
1913
+ fetchTargetEval(s);
1914
+ return;
1915
+ }
1916
+ const std::string sid = sessionForChainIdx(s->view_id, s->chain, i);
1917
+ auto* view = bunite_win::getViewHostById(s->view_id);
1918
+ if (!view) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
1919
+ const std::string& script = "JSON.stringify({iw:innerWidth,ih:innerHeight})";
1920
+ std::string escScript = escapeForJsonString(script);
1921
+ evalRaw(view, sid, escScript,
1922
+ [s, i](bool ok, std::string r) {
1923
+ if (!ok) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor eval failed"); return; }
1924
+ // Result envelope: {"result":{"type":"string","value":"<json string>"}}
1925
+ CefRefPtr<CefValue> v = CefParseJSON(r, JSON_PARSER_RFC);
1926
+ if (!v || v->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor eval malformed"); return; }
1927
+ auto result = v->GetDictionary()->GetDictionary("result");
1928
+ if (!result || !result->HasKey("value")) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor eval no value"); return; }
1929
+ std::string vs = result->GetString("value").ToString();
1930
+ CefRefPtr<CefValue> inner = CefParseJSON(vs, JSON_PARSER_RFC);
1931
+ if (!inner || inner->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor inner malformed"); return; }
1932
+ s->ancestor_inner.push_back({dictNumber(inner->GetDictionary(), "iw"), dictNumber(inner->GetDictionary(), "ih")});
1933
+ fetchAncestorInner(s, i + 1);
1934
+ });
1935
+ }
1936
+
1937
+ void fetchTargetEval(std::shared_ptr<ChainState> s) {
1938
+ evalInFrame(s->view_id, s->request_id, s->targetFrameId, s->escScript,
1939
+ [s](const FrameResolveOk& fr) { composeAndDispatch(s, fr); });
1940
+ }
1941
+
1942
+ void composeAndDispatch(std::shared_ptr<ChainState> s, const FrameResolveOk& fr) {
1943
+ // Stack iw/ih per chain level chain[1..N-1] for the bilinear walk.
1944
+ // chain[N-1] = target → fr.iw, fr.ih.
1945
+ // chain[i] (1 <= i < N-1) → s->ancestor_inner[i-1].
1946
+ // link_quads[i] = quad of chain[i+1]'s iframe element, in chain[i]'s coords.
1947
+ // Map order: from target up to main, applying bilinear at each link.
1948
+ auto mapCorner = [&](double fx, double fy, double& px, double& py) {
1949
+ double cur_x = fx, cur_y = fy;
1950
+ double cur_iw = fr.iw, cur_ih = fr.ih;
1951
+ // link i (chain[i+1] in chain[i]'s coords) for i = N-2 down to 0.
1952
+ for (size_t i = s->link_quads.size(); i-- > 0; ) {
1953
+ double mapped_x, mapped_y;
1954
+ bilinearMap(s->link_quads[i], cur_iw, cur_ih, cur_x, cur_y, mapped_x, mapped_y);
1955
+ cur_x = mapped_x; cur_y = mapped_y;
1956
+ if (i == 0) break; // chain[i] is main-direct child handled; next would be main itself
1957
+ // Now in chain[i]'s coords; next iteration maps to chain[i-1]'s coords.
1958
+ cur_iw = s->ancestor_inner[i - 1].first;
1959
+ cur_ih = s->ancestor_inner[i - 1].second;
1960
+ }
1961
+ px = cur_x; py = cur_y;
1962
+ };
1963
+ double pcx, pcy; mapCorner(fr.cx, fr.cy, pcx, pcy);
1964
+ double cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3;
1965
+ mapCorner(fr.x, fr.y, cx0, cy0);
1966
+ mapCorner(fr.x + fr.w, fr.y, cx1, cy1);
1967
+ mapCorner(fr.x + fr.w, fr.y + fr.h, cx2, cy2);
1968
+ mapCorner(fr.x, fr.y + fr.h, cx3, cy3);
1969
+ const double min_x = std::min(std::min(cx0, cx1), std::min(cx2, cx3));
1970
+ const double max_x = std::max(std::max(cx0, cx1), std::max(cx2, cx3));
1971
+ const double min_y = std::min(std::min(cy0, cy1), std::min(cy2, cy3));
1972
+ const double max_y = std::max(std::max(cy0, cy1), std::max(cy2, cy3));
1973
+ finishResolveAndClick(s->view_id, s->request_id,
1974
+ min_x, min_y, max_x - min_x, max_y - min_y, pcx, pcy,
1975
+ s->button, s->click_count, s->modifiers);
1976
+ }
1977
+
1978
+ // `frameId` non-empty: walk ancestor chain via Page.getFrameTree, compose
1979
+ // bilinear transforms across nested OOPIF/same-origin frames, dispatch click
1980
+ // in main-page coords.
1981
+ void runFrameTargeted(uint32_t view_id, uint32_t request_id, const std::string& frameId,
1982
+ const std::string& escScript,
1983
+ int32_t button, int32_t click_count, uint32_t modifiers) {
1984
+ auto* view = bunite_win::getViewHostById(view_id);
1985
+ if (!view || !view->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
1986
+ cefCdpCall(view, "Page.getFrameTree", "{}",
1987
+ [view_id, request_id, frameId, escScript, button, click_count, modifiers](bool ok, std::string r) {
1988
+ if (!ok) { emitResolveAndClickError(view_id, request_id, "runtime_error", "getFrameTree failed"); return; }
1989
+ CefRefPtr<CefValue> val = CefParseJSON(r, JSON_PARSER_RFC);
1990
+ if (!val || val->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(view_id, request_id, "runtime_error", "getFrameTree malformed"); return; }
1991
+ auto root = val->GetDictionary()->GetDictionary("frameTree");
1992
+ std::vector<std::string> chain = findFramePath(root, frameId);
1993
+ if (chain.size() < 2) { emitResolveAndClickError(view_id, request_id, "not_found", "frame not in tree"); return; }
1994
+ auto s = std::make_shared<ChainState>();
1995
+ s->view_id = view_id;
1996
+ s->request_id = request_id;
1997
+ s->targetFrameId = frameId;
1998
+ s->escScript = escScript;
1999
+ s->button = button;
2000
+ s->click_count = click_count;
2001
+ s->modifiers = modifiers;
2002
+ s->chain = std::move(chain); // chain[0] = main, chain[N-1] = target
2003
+ fetchLink(s, 0);
2004
+ });
2005
+ }
2006
+
2007
+ } // namespace
2008
+
2009
+ extern "C" BUNITE_EXPORT void bunite_view_resolve_and_click(
2010
+ uint32_t view_id, uint32_t request_id,
2011
+ const char* selector_c, const char* frame_id_c,
2012
+ int32_t button, int32_t click_count, uint32_t modifiers) {
2013
+ std::string selector = selector_c ? selector_c : "";
2014
+ std::string frameId = frame_id_c ? frame_id_c : "";
2015
+ bunite_win::postCefUiTask([view_id, request_id, selector, frameId, button, click_count, modifiers]() {
2016
+ auto* view = bunite_win::getViewHostById(view_id);
2017
+ if (!view || !view->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view not ready"); return; }
2018
+
2019
+ std::string script = buildResolveScript(selector);
2020
+ std::string escScript = escapeForJsonString(script);
2021
+
2022
+ if (frameId.empty()) {
2023
+ // Main frame — fr.x/y/w/h are already page-viewport coords (iw/ih unused).
2024
+ std::string evalParams = "{\"expression\":\"" + escScript + "\",\"returnByValue\":true,\"awaitPromise\":true}";
2025
+ cefCdpCall(view, "Runtime.evaluate", evalParams,
2026
+ [view_id, request_id, button, click_count, modifiers](bool ok, std::string r) {
2027
+ parseEvalAndContinue(view_id, request_id, ok, r,
2028
+ [view_id, request_id, button, click_count, modifiers](const FrameResolveOk& fr) {
2029
+ finishResolveAndClick(view_id, request_id,
2030
+ fr.x, fr.y, fr.w, fr.h, fr.cx, fr.cy,
2031
+ button, click_count, modifiers);
2032
+ });
2033
+ });
2034
+ return;
2035
+ }
2036
+
2037
+ // Frame-targeted: lazy Target.setAutoAttach so OOPIF frames get sessionIds
2038
+ // populated into view->oopif_sessions via OnDevToolsEvent. Wait for response
2039
+ // so attachedToTarget events fire before we proceed.
2040
+ if (!view->oopif_autoattach_armed.exchange(true)) {
2041
+ cefCdpCall(view, "Target.setAutoAttach",
2042
+ "{\"autoAttach\":true,\"flatten\":true,\"waitForDebuggerOnStart\":false}",
2043
+ [view_id, request_id, frameId, escScript, button, click_count, modifiers](bool ok, std::string) {
2044
+ if (!ok) { emitResolveAndClickError(view_id, request_id, "runtime_error", "setAutoAttach failed"); return; }
2045
+ runFrameTargeted(view_id, request_id, frameId, escScript, button, click_count, modifiers);
2046
+ });
2047
+ return;
2048
+ }
2049
+ runFrameTargeted(view_id, request_id, frameId, escScript, button, click_count, modifiers);
2050
+ });
2051
+ }
2052
+
1237
2053
  extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
1238
2054
  bunite_win::postCefUiTask([view_id]() { bunite_win::openDevToolsForView(bunite_win::getViewHostById(view_id)); });
1239
2055
  }