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.
- package/package.json +4 -4
- package/src/host/core/App.ts +6 -3
- package/src/host/core/BrowserView.ts +345 -24
- package/src/host/core/BrowserWindow.ts +52 -6
- package/src/host/core/SurfaceBrowserIPC.ts +10 -1
- package/src/host/core/SurfaceManager.ts +357 -16
- package/src/host/core/windowCap.ts +69 -0
- package/src/host/events/webviewEvents.ts +18 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +145 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +225 -1
- package/src/native/linux/bunite_linux_internal.h +12 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_view.cpp +211 -5
- package/src/native/mac/bunite_mac_ffi.mm +293 -4
- package/src/native/mac/bunite_mac_internal.h +13 -0
- package/src/native/mac/bunite_mac_view.mm +227 -7
- package/src/native/shared/ffi_exports.h +97 -30
- package/src/native/win/native_host_cef.cpp +107 -13
- package/src/native/win/native_host_ffi.cpp +831 -2
- package/src/native/win/native_host_internal.h +22 -0
- package/src/native/win/native_host_runtime.cpp +34 -0
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +827 -5
- package/src/native/win-webview2/webview2_internal.h +19 -0
- package/src/native/win-webview2/webview2_runtime.cpp +383 -31
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +39 -0
- package/src/rpc/framework.ts +194 -12
- package/src/rpc/index.ts +12 -0
- package/src/rpc/peer.ts +1 -1
- package/src/webview/native.ts +142 -32
- package/src/webview/polyfill.ts +91 -14
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#include "webview2_internal.h"
|
|
2
2
|
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <array>
|
|
3
5
|
#include <cstring>
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <vector>
|
|
4
8
|
#include <wincrypt.h> // CryptBinaryToStringA — base64 encoding for screenshot payload.
|
|
5
9
|
|
|
6
10
|
using namespace bunite_webview2;
|
|
@@ -12,7 +16,7 @@ void setViewInputPassthrough(ViewHost* v, bool passthrough);
|
|
|
12
16
|
|
|
13
17
|
extern "C" {
|
|
14
18
|
|
|
15
|
-
BUNITE_EXPORT int32_t bunite_abi_version(void) { return
|
|
19
|
+
BUNITE_EXPORT int32_t bunite_abi_version(void) { return 12; }
|
|
16
20
|
|
|
17
21
|
BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
|
|
18
22
|
if (level < 0) level = 0;
|
|
@@ -86,8 +90,11 @@ BUNITE_EXPORT void bunite_window_show(uint32_t window_id) {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
BUNITE_EXPORT void bunite_window_close(uint32_t window_id) {
|
|
93
|
+
// WM_CLOSE (not DestroyWindow) so it routes through the vetoable
|
|
94
|
+
// close-requested path + destroyWindow cleanup (windows_by_id erase +
|
|
95
|
+
// all-windows-closed), matching the CEF backend.
|
|
89
96
|
WindowHost* w = getWindow(window_id);
|
|
90
|
-
if (w && w->hwnd)
|
|
97
|
+
if (w && w->hwnd) SendMessageW(w->hwnd, WM_CLOSE, 0, 0);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
BUNITE_EXPORT void bunite_window_set_title(uint32_t window_id, const char* title) {
|
|
@@ -136,6 +143,41 @@ BUNITE_EXPORT void bunite_window_set_frame(uint32_t window_id,
|
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
|
|
146
|
+
// Capture-based move-drag — windowProc follows WM_MOUSEMOVE, ends on
|
|
147
|
+
// WM_LBUTTONUP/CAPTURECHANGED/CANCELMODE/DESTROY (webview2_runtime.cpp). No
|
|
148
|
+
// WM_NCLBUTTONDOWN modal loop: it would freeze the shared Bun/UI thread for
|
|
149
|
+
// the whole drag. Trade-off: no Win11 snap/aero-shake.
|
|
150
|
+
BUNITE_EXPORT void bunite_window_begin_move_drag(uint32_t window_id) {
|
|
151
|
+
WindowHost* win = getWindow(window_id);
|
|
152
|
+
if (!win || !win->hwnd || win->drag_active) return;
|
|
153
|
+
if (!(GetAsyncKeyState(VK_LBUTTON) & 0x8000)) return; // button already released
|
|
154
|
+
POINT cur;
|
|
155
|
+
if (!GetCursorPos(&cur)) return;
|
|
156
|
+
|
|
157
|
+
if (IsZoomed(win->hwnd)) { // restore under the cursor before dragging
|
|
158
|
+
RECT maxRect{};
|
|
159
|
+
WINDOWPLACEMENT wp{ sizeof(wp) };
|
|
160
|
+
GetWindowRect(win->hwnd, &maxRect);
|
|
161
|
+
GetWindowPlacement(win->hwnd, &wp);
|
|
162
|
+
const int maxW = maxRect.right - maxRect.left;
|
|
163
|
+
const int restoredW = wp.rcNormalPosition.right - wp.rcNormalPosition.left;
|
|
164
|
+
const double fx = maxW > 0 ? double(cur.x - maxRect.left) / maxW : 0.5;
|
|
165
|
+
ShowWindow(win->hwnd, SW_RESTORE);
|
|
166
|
+
win->maximized = false;
|
|
167
|
+
SetWindowPos(win->hwnd, nullptr, cur.x - static_cast<int>(restoredW * fx),
|
|
168
|
+
maxRect.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
169
|
+
win = getWindow(window_id); // re-validate: SW_RESTORE dispatched messages
|
|
170
|
+
if (!win || !win->hwnd) return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
RECT r;
|
|
174
|
+
if (!GetWindowRect(win->hwnd, &r)) return;
|
|
175
|
+
win->drag_anchor_cursor = cur;
|
|
176
|
+
win->drag_anchor_origin = { r.left, r.top };
|
|
177
|
+
win->drag_active = true;
|
|
178
|
+
SetCapture(win->hwnd);
|
|
179
|
+
}
|
|
180
|
+
|
|
139
181
|
// ---- views ------------------------------------------------------------
|
|
140
182
|
|
|
141
183
|
BUNITE_EXPORT bool bunite_view_create(
|
|
@@ -379,7 +421,8 @@ BUNITE_EXPORT void bunite_view_reload(uint32_t view_id) {
|
|
|
379
421
|
BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) { destroyView(view_id); }
|
|
380
422
|
|
|
381
423
|
// Input dispatch — CDP via CallDevToolsProtocolMethod (Playwright pattern).
|
|
382
|
-
//
|
|
424
|
+
// Edge runtime injects below DevTools surface — DOM `isTrusted=true`
|
|
425
|
+
// (see `bunite_view_capabilities` note).
|
|
383
426
|
namespace {
|
|
384
427
|
|
|
385
428
|
const char* cdpMouseButton(int32_t b) {
|
|
@@ -392,6 +435,23 @@ void cdpCall(ViewHost* v, const wchar_t* method, const std::string& json) {
|
|
|
392
435
|
method, utf8ToWide(json).c_str(), nullptr);
|
|
393
436
|
}
|
|
394
437
|
|
|
438
|
+
// Result-aware CDP call. `cb(ok, json)` runs on the UI thread once the result
|
|
439
|
+
// arrives; on failure `ok=false` and json is an error description.
|
|
440
|
+
void cdpCallWithResult(ViewHost* v, const wchar_t* method, const std::string& json,
|
|
441
|
+
std::function<void(bool, std::string)> cb) {
|
|
442
|
+
if (!v || !v->webview) { if (cb) cb(false, "view not ready"); return; }
|
|
443
|
+
auto lifetime = g_runtime.lifetime;
|
|
444
|
+
v->webview->CallDevToolsProtocolMethod(
|
|
445
|
+
method, utf8ToWide(json).c_str(),
|
|
446
|
+
Microsoft::WRL::Callback<ICoreWebView2CallDevToolsProtocolMethodCompletedHandler>(
|
|
447
|
+
[lifetime, cb](HRESULT hr, LPCWSTR result) -> HRESULT {
|
|
448
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
449
|
+
if (FAILED(hr) || !result) { cb(false, "CDP call failed"); return S_OK; }
|
|
450
|
+
cb(true, wideToUtf8(result));
|
|
451
|
+
return S_OK;
|
|
452
|
+
}).Get());
|
|
453
|
+
}
|
|
454
|
+
|
|
395
455
|
} // namespace
|
|
396
456
|
|
|
397
457
|
BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
|
|
@@ -428,6 +488,10 @@ BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
|
|
|
428
488
|
std::string key_str = key ? key : "";
|
|
429
489
|
std::string code_str = code ? code : "";
|
|
430
490
|
|
|
491
|
+
// CDP modifier mask: 1=alt, 2=ctrl, 4=meta, 8=shift. Suppress text when any
|
|
492
|
+
// non-shift modifier is set so shortcuts like Ctrl+A don't insert "a".
|
|
493
|
+
const bool has_non_shift_modifier = (modifiers & ~static_cast<uint32_t>(8)) != 0;
|
|
494
|
+
|
|
431
495
|
auto buildPart = [&](const char* type, bool include_text) {
|
|
432
496
|
std::string out = "{\"type\":\"";
|
|
433
497
|
out += type;
|
|
@@ -437,7 +501,7 @@ BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
|
|
|
437
501
|
if (!code_str.empty()) out += ",\"code\":\"" + escapeJsonString(code_str) + "\"";
|
|
438
502
|
// CDP `location`: 0 standard, 1 left mod, 2 right mod, 3 numpad.
|
|
439
503
|
if (location > 0) out += ",\"location\":" + std::to_string(location);
|
|
440
|
-
if (include_text && !char_str.empty())
|
|
504
|
+
if (include_text && !char_str.empty() && !has_non_shift_modifier)
|
|
441
505
|
out += ",\"text\":\"" + escapeJsonString(char_str) + "\"";
|
|
442
506
|
out += "}";
|
|
443
507
|
return out;
|
|
@@ -485,7 +549,765 @@ BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
|
485
549
|
BUNITE_CAP_NATIVE_INPUT_TRUSTED |
|
|
486
550
|
BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
|
|
487
551
|
BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
488
|
-
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG
|
|
552
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
|
|
553
|
+
BUNITE_CAP_AX | BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_FRAMES |
|
|
554
|
+
BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
|
|
555
|
+
BUNITE_CAP_RESOLVE_AND_CLICK;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
|
|
559
|
+
ViewHost* v = getView(view_id);
|
|
560
|
+
if (!v) return;
|
|
561
|
+
if (policy < 0 || policy > 2) policy = 2;
|
|
562
|
+
v->download_policy.store(policy);
|
|
563
|
+
v->download_dir = download_dir ? download_dir : "";
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
|
|
567
|
+
double x, double y, double w, double h) {
|
|
568
|
+
ViewHost* v = getView(new_view_id);
|
|
569
|
+
if (!v || !v->controller || !v->container_hwnd) return;
|
|
570
|
+
WindowHost* host_window = nullptr;
|
|
571
|
+
{
|
|
572
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
573
|
+
auto wit = g_runtime.windows_by_id.find(host_window_id);
|
|
574
|
+
if (wit == g_runtime.windows_by_id.end()) return;
|
|
575
|
+
host_window = wit->second;
|
|
576
|
+
}
|
|
577
|
+
if (!host_window || !host_window->hwnd) return;
|
|
578
|
+
v->window = host_window;
|
|
579
|
+
host_window->views.push_back(v);
|
|
580
|
+
SetParent(v->container_hwnd, host_window->hwnd);
|
|
581
|
+
MoveWindow(v->container_hwnd, static_cast<int>(x), static_cast<int>(y),
|
|
582
|
+
static_cast<int>(w), static_cast<int>(h), TRUE);
|
|
583
|
+
ShowWindow(v->container_hwnd, SW_SHOW);
|
|
584
|
+
RECT bounds{0, 0, static_cast<LONG>(w), static_cast<LONG>(h)};
|
|
585
|
+
v->controller->put_Bounds(bounds);
|
|
586
|
+
v->controller->put_IsVisible(TRUE);
|
|
587
|
+
emitWebviewEvent(new_view_id, "view-ready", "");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
|
|
591
|
+
ViewHost* v = nullptr;
|
|
592
|
+
{
|
|
593
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
594
|
+
auto it = g_runtime.views_by_id.find(new_view_id);
|
|
595
|
+
if (it == g_runtime.views_by_id.end()) return;
|
|
596
|
+
v = it->second;
|
|
597
|
+
g_runtime.views_by_id.erase(it);
|
|
598
|
+
}
|
|
599
|
+
if (!v) return;
|
|
600
|
+
if (v->controller) v->controller->Close();
|
|
601
|
+
if (v->container_hwnd) DestroyWindow(v->container_hwnd);
|
|
602
|
+
delete v;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
namespace {
|
|
606
|
+
void emitAxError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
607
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
608
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
609
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
610
|
+
emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
611
|
+
}
|
|
612
|
+
} // namespace
|
|
613
|
+
|
|
614
|
+
namespace {
|
|
615
|
+
// Extract integer field from a CDP JSON response — used to pluck
|
|
616
|
+
// `executionContextId` without pulling in a JSON parser.
|
|
617
|
+
bool extractJsonInt(const std::string& json, const std::string& key, int& out) {
|
|
618
|
+
std::string needle = "\"" + key + "\":";
|
|
619
|
+
auto p = json.find(needle);
|
|
620
|
+
if (p == std::string::npos) return false;
|
|
621
|
+
p += needle.size();
|
|
622
|
+
while (p < json.size() && (json[p] == ' ' || json[p] == '\t')) ++p;
|
|
623
|
+
auto end = p;
|
|
624
|
+
while (end < json.size() && (json[end] == '-' || (json[end] >= '0' && json[end] <= '9'))) ++end;
|
|
625
|
+
if (end == p) return false;
|
|
626
|
+
out = std::stoi(json.substr(p, end - p));
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
void emitListFramesErrorWv2(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
630
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
631
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
632
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
633
|
+
emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
634
|
+
}
|
|
635
|
+
} // namespace
|
|
636
|
+
|
|
637
|
+
BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
|
|
638
|
+
ViewHost* v = getView(view_id);
|
|
639
|
+
if (!v || !v->webview) { emitListFramesErrorWv2(view_id, request_id, "not_supported", "view not ready"); return; }
|
|
640
|
+
cdpCallWithResult(v, L"Page.getFrameTree", "{}",
|
|
641
|
+
[view_id, request_id](bool ok, std::string result) {
|
|
642
|
+
if (!ok) { emitListFramesErrorWv2(view_id, request_id, "runtime_error", "getFrameTree failed: " + result); return; }
|
|
643
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
644
|
+
",\"ok\":true,\"raw\":" + result + "}";
|
|
645
|
+
emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
|
|
650
|
+
const char* script_c, const char* frame_id_c) {
|
|
651
|
+
std::string script = script_c ? script_c : "";
|
|
652
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
653
|
+
if (frameId.empty()) {
|
|
654
|
+
bunite_view_evaluate(view_id, request_id, script_c);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
ViewHost* v = getView(view_id);
|
|
658
|
+
if (!v || !v->webview) {
|
|
659
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
660
|
+
",\"ok\":false,\"code\":\"not_supported\",\"message\":\"view not ready\"}";
|
|
661
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
std::string isoParams = "{\"frameId\":\"" + escapeJsonString(frameId) + "\",\"worldName\":\"bunite-eval\"}";
|
|
665
|
+
cdpCallWithResult(v, L"Page.createIsolatedWorld", isoParams,
|
|
666
|
+
[view_id, request_id, script](bool ok, std::string isoResult) {
|
|
667
|
+
if (!ok) {
|
|
668
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
669
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
670
|
+
"\"message\":\"createIsolatedWorld failed\"}";
|
|
671
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
int contextId = 0;
|
|
675
|
+
if (!extractJsonInt(isoResult, "executionContextId", contextId)) {
|
|
676
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
677
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
678
|
+
"\"message\":\"missing executionContextId\"}";
|
|
679
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Re-lookup — the async gap could have torn down the view.
|
|
683
|
+
ViewHost* v2 = getView(view_id);
|
|
684
|
+
if (!v2 || !v2->webview) {
|
|
685
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
686
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
687
|
+
"\"message\":\"view destroyed\"}";
|
|
688
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
|
|
692
|
+
"\",\"contextId\":" + std::to_string(contextId) +
|
|
693
|
+
",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
694
|
+
cdpCallWithResult(v2, L"Runtime.evaluate", evalParams,
|
|
695
|
+
[view_id, request_id](bool ok2, std::string evalResult) {
|
|
696
|
+
if (!ok2) {
|
|
697
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
698
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
699
|
+
"\"message\":\"Runtime.evaluate failed\"}";
|
|
700
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
// Normalize CDP shape to the flat `{requestId, ok, value/code/message}`
|
|
704
|
+
// that the host's evaluate-result handler expects. exceptionDetails
|
|
705
|
+
// at the top level is preceded by `},` — distinguishes from a value
|
|
706
|
+
// that happens to contain the literal token inside a string.
|
|
707
|
+
auto excPos = evalResult.find("},\"exceptionDetails\"");
|
|
708
|
+
if (excPos == std::string::npos) excPos = evalResult.find("}, \"exceptionDetails\"");
|
|
709
|
+
if (excPos != std::string::npos) {
|
|
710
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
711
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
712
|
+
"\"message\":\"evaluate threw\"}";
|
|
713
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// Extract result.value as a JSON substring. CDP returns
|
|
717
|
+
// `{"result":{"type":"...","value":<json>}}`. We find the `"value":`
|
|
718
|
+
// token inside the inner object and slice until the matching close.
|
|
719
|
+
auto resPos = evalResult.find("\"result\":");
|
|
720
|
+
if (resPos == std::string::npos) {
|
|
721
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
722
|
+
",\"ok\":true,\"value\":\"null\"}";
|
|
723
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
auto valKey = evalResult.find("\"value\":", resPos);
|
|
727
|
+
if (valKey == std::string::npos) {
|
|
728
|
+
// type === "undefined" — no value field.
|
|
729
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
730
|
+
",\"ok\":true,\"value\":\"null\"}";
|
|
731
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
size_t start = valKey + 8; // past `"value":`
|
|
735
|
+
// Skip whitespace.
|
|
736
|
+
while (start < evalResult.size() && (evalResult[start] == ' ' || evalResult[start] == '\t')) ++start;
|
|
737
|
+
// Find balanced end of the JSON value.
|
|
738
|
+
size_t end = start;
|
|
739
|
+
if (start < evalResult.size()) {
|
|
740
|
+
char c = evalResult[start];
|
|
741
|
+
if (c == '"') {
|
|
742
|
+
++end;
|
|
743
|
+
while (end < evalResult.size() && evalResult[end] != '"') {
|
|
744
|
+
if (evalResult[end] == '\\' && end + 1 < evalResult.size()) ++end;
|
|
745
|
+
++end;
|
|
746
|
+
}
|
|
747
|
+
if (end < evalResult.size()) ++end;
|
|
748
|
+
} else if (c == '{' || c == '[') {
|
|
749
|
+
int depth = 0;
|
|
750
|
+
bool inStr = false;
|
|
751
|
+
while (end < evalResult.size()) {
|
|
752
|
+
char ch = evalResult[end];
|
|
753
|
+
if (inStr) {
|
|
754
|
+
if (ch == '\\' && end + 1 < evalResult.size()) ++end;
|
|
755
|
+
else if (ch == '"') inStr = false;
|
|
756
|
+
} else if (ch == '"') inStr = true;
|
|
757
|
+
else if (ch == '{' || ch == '[') ++depth;
|
|
758
|
+
else if (ch == '}' || ch == ']') { --depth; if (depth == 0) { ++end; break; } }
|
|
759
|
+
++end;
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
// Number / true / false / null — read until non-token char.
|
|
763
|
+
while (end < evalResult.size()) {
|
|
764
|
+
char ch = evalResult[end];
|
|
765
|
+
if (ch == ',' || ch == '}' || ch == ' ' || ch == '\t' || ch == '\n') break;
|
|
766
|
+
++end;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
std::string valueJson = evalResult.substr(start, end - start);
|
|
771
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
772
|
+
",\"ok\":true,\"value\":\"" + escapeJsonString(valueJson) + "\"}";
|
|
773
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
} // extern "C" — exit so the helpers below can return std::string.
|
|
779
|
+
|
|
780
|
+
namespace {
|
|
781
|
+
|
|
782
|
+
void emitResolveAndClickErrorWv2(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
783
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
784
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
785
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
786
|
+
emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const char* cdpButtonNameWv2(int32_t b) {
|
|
790
|
+
switch (b) { case 1: return "middle"; case 2: return "right"; default: return "left"; }
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
std::string extractJsonValueRaw(const std::string& s, const std::string& key) {
|
|
794
|
+
std::string needle = "\"" + key + "\":";
|
|
795
|
+
size_t p = s.find(needle);
|
|
796
|
+
if (p == std::string::npos) return {};
|
|
797
|
+
p += needle.size();
|
|
798
|
+
while (p < s.size() && (s[p] == ' ' || s[p] == '\t')) ++p;
|
|
799
|
+
if (p >= s.size()) return {};
|
|
800
|
+
size_t end = p;
|
|
801
|
+
char c = s[p];
|
|
802
|
+
if (c == '"') {
|
|
803
|
+
++end;
|
|
804
|
+
while (end < s.size() && s[end] != '"') { if (s[end] == '\\' && end + 1 < s.size()) ++end; ++end; }
|
|
805
|
+
if (end < s.size()) ++end;
|
|
806
|
+
} else if (c == '{' || c == '[') {
|
|
807
|
+
int depth = 0; bool inStr = false;
|
|
808
|
+
while (end < s.size()) {
|
|
809
|
+
char ch = s[end];
|
|
810
|
+
if (inStr) { if (ch == '\\' && end + 1 < s.size()) ++end; else if (ch == '"') inStr = false; }
|
|
811
|
+
else if (ch == '"') inStr = true;
|
|
812
|
+
else if (ch == '{' || ch == '[') ++depth;
|
|
813
|
+
else if (ch == '}' || ch == ']') { --depth; if (depth == 0) { ++end; break; } }
|
|
814
|
+
++end;
|
|
815
|
+
}
|
|
816
|
+
} else {
|
|
817
|
+
while (end < s.size()) {
|
|
818
|
+
char ch = s[end];
|
|
819
|
+
if (ch == ',' || ch == '}' || ch == ' ' || ch == '\t' || ch == '\n') break;
|
|
820
|
+
++end;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return s.substr(p, end - p);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
double extractJsonDouble(const std::string& s, const std::string& key, double dflt = 0.0) {
|
|
827
|
+
std::string raw = extractJsonValueRaw(s, key);
|
|
828
|
+
if (raw.empty()) return dflt;
|
|
829
|
+
try { return std::stod(raw); } catch (...) { return dflt; }
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
std::string extractJsonStringDecoded(const std::string& s, const std::string& key) {
|
|
833
|
+
std::string raw = extractJsonValueRaw(s, key);
|
|
834
|
+
if (raw.size() < 2 || raw.front() != '"' || raw.back() != '"') return {};
|
|
835
|
+
std::string out; out.reserve(raw.size());
|
|
836
|
+
for (size_t i = 1; i + 1 < raw.size(); ++i) {
|
|
837
|
+
if (raw[i] == '\\' && i + 1 < raw.size() - 1) {
|
|
838
|
+
char nxt = raw[++i];
|
|
839
|
+
switch (nxt) {
|
|
840
|
+
case '"': out += '"'; break;
|
|
841
|
+
case '\\': out += '\\'; break;
|
|
842
|
+
case 'n': out += '\n'; break;
|
|
843
|
+
case 'r': out += '\r'; break;
|
|
844
|
+
case 't': out += '\t'; break;
|
|
845
|
+
case '/': out += '/'; break;
|
|
846
|
+
default: out += nxt; break;
|
|
847
|
+
}
|
|
848
|
+
} else out += raw[i];
|
|
849
|
+
}
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
std::string escapeForJsStringWv2(const std::string& s) {
|
|
854
|
+
std::string out; out.reserve(s.size() + 2);
|
|
855
|
+
for (char c : s) {
|
|
856
|
+
if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
|
|
857
|
+
else if (c == '\n') out += "\\n";
|
|
858
|
+
else if (c == '\r') out += "\\r";
|
|
859
|
+
else if (c == '\t') out += "\\t";
|
|
860
|
+
else out.push_back(c);
|
|
861
|
+
}
|
|
862
|
+
return out;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
std::string buildResolveScriptWv2(const std::string& selector) {
|
|
866
|
+
// Frame-local rect + innerWidth/innerHeight for bilinear mapping when the
|
|
867
|
+
// frame is transformed (rotate/scale). Main frame uses iw/ih harmlessly.
|
|
868
|
+
std::string sel_lit = "\"" + escapeForJsStringWv2(selector) + "\"";
|
|
869
|
+
return
|
|
870
|
+
"(function(){"
|
|
871
|
+
"var el=document.querySelector(" + sel_lit + ");"
|
|
872
|
+
"if(!el)return{ok:false,code:\"not_found\"};"
|
|
873
|
+
"el.scrollIntoView({block:\"nearest\",inline:\"nearest\",behavior:\"instant\"});"
|
|
874
|
+
"var r=el.getBoundingClientRect();"
|
|
875
|
+
"var vis=r.width>0&&r.height>0&&r.bottom>0&&r.right>0"
|
|
876
|
+
"&&r.top<innerHeight&&r.left<innerWidth;"
|
|
877
|
+
"if(!vis)return{ok:false,code:\"not_visible\"};"
|
|
878
|
+
"return{ok:true,x:r.x,y:r.y,w:r.width,h:r.height,"
|
|
879
|
+
"cx:r.x+r.width/2,cy:r.y+r.height/2,"
|
|
880
|
+
"iw:innerWidth,ih:innerHeight};"
|
|
881
|
+
"})()";
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
void dispatchCdpClickWv2(ViewHost* v, double cx, double cy,
|
|
885
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
886
|
+
if (click_count < 1) click_count = 1;
|
|
887
|
+
const char* btn = cdpButtonNameWv2(button);
|
|
888
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
889
|
+
std::string base = "\"x\":" + std::to_string(cx) + ",\"y\":" + std::to_string(cy) +
|
|
890
|
+
",\"button\":\"" + btn + "\",\"clickCount\":" + std::to_string(i) +
|
|
891
|
+
",\"modifiers\":" + std::to_string(modifiers);
|
|
892
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mousePressed\"," + base + "}");
|
|
893
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mouseReleased\"," + base + "}");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// CDP call routed to a child target session (flatten:true). Requires
|
|
898
|
+
// ICoreWebView2_11.
|
|
899
|
+
void cdpCallForSession(ViewHost* v, const std::string& session_id, const wchar_t* method,
|
|
900
|
+
const std::string& params_json,
|
|
901
|
+
std::function<void(bool, std::string)> cb) {
|
|
902
|
+
if (!v || !v->webview) { if (cb) cb(false, "view not ready"); return; }
|
|
903
|
+
ComPtr<ICoreWebView2_11> wv11;
|
|
904
|
+
if (FAILED(v->webview.As(&wv11)) || !wv11) { if (cb) cb(false, "ICoreWebView2_11 unavailable"); return; }
|
|
905
|
+
auto lifetime = g_runtime.lifetime;
|
|
906
|
+
wv11->CallDevToolsProtocolMethodForSession(
|
|
907
|
+
utf8ToWide(session_id).c_str(), method, utf8ToWide(params_json).c_str(),
|
|
908
|
+
Microsoft::WRL::Callback<ICoreWebView2CallDevToolsProtocolMethodCompletedHandler>(
|
|
909
|
+
[lifetime, cb](HRESULT hr, LPCWSTR result) -> HRESULT {
|
|
910
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
911
|
+
if (FAILED(hr) || !result) { cb(false, "CDP-for-session call failed"); return S_OK; }
|
|
912
|
+
cb(true, wideToUtf8(result));
|
|
913
|
+
return S_OK;
|
|
914
|
+
}).Get());
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Register one-time listeners for Target.attachedToTarget / detachedFromTarget.
|
|
918
|
+
// Populates view->oopif_sessions as OOPIF child sessions attach.
|
|
919
|
+
void armOopifEvents(ViewHost* v) {
|
|
920
|
+
if (v->oopif_event_tokens_registered) return;
|
|
921
|
+
using namespace Microsoft::WRL;
|
|
922
|
+
ComPtr<ICoreWebView2DevToolsProtocolEventReceiver> attached_r, detached_r;
|
|
923
|
+
if (FAILED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.attachedToTarget", &attached_r))
|
|
924
|
+
|| FAILED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.detachedFromTarget", &detached_r))) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
uint32_t view_id = v->id;
|
|
928
|
+
attached_r->add_DevToolsProtocolEventReceived(
|
|
929
|
+
Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
|
|
930
|
+
[view_id](ICoreWebView2*, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT {
|
|
931
|
+
LPWSTR raw = nullptr;
|
|
932
|
+
if (FAILED(args->get_ParameterObjectAsJson(&raw)) || !raw) return S_OK;
|
|
933
|
+
std::string p = wideToUtf8(raw); CoTaskMemFree(raw);
|
|
934
|
+
std::string session_id = extractJsonStringDecoded(p, "sessionId");
|
|
935
|
+
std::string info = extractJsonValueRaw(p, "targetInfo");
|
|
936
|
+
std::string type = extractJsonStringDecoded(info, "type");
|
|
937
|
+
std::string target_id = extractJsonStringDecoded(info, "targetId");
|
|
938
|
+
if (session_id.empty() || target_id.empty() || type != "iframe") return S_OK;
|
|
939
|
+
auto* v = getView(view_id);
|
|
940
|
+
if (!v) return S_OK;
|
|
941
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
942
|
+
v->oopif_sessions[target_id] = session_id;
|
|
943
|
+
return S_OK;
|
|
944
|
+
}).Get(),
|
|
945
|
+
&v->target_attached_token);
|
|
946
|
+
detached_r->add_DevToolsProtocolEventReceived(
|
|
947
|
+
Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
|
|
948
|
+
[view_id](ICoreWebView2*, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT {
|
|
949
|
+
LPWSTR raw = nullptr;
|
|
950
|
+
if (FAILED(args->get_ParameterObjectAsJson(&raw)) || !raw) return S_OK;
|
|
951
|
+
std::string p = wideToUtf8(raw); CoTaskMemFree(raw);
|
|
952
|
+
std::string session_id = extractJsonStringDecoded(p, "sessionId");
|
|
953
|
+
if (session_id.empty()) return S_OK;
|
|
954
|
+
auto* v = getView(view_id);
|
|
955
|
+
if (!v) return S_OK;
|
|
956
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
957
|
+
for (auto it = v->oopif_sessions.begin(); it != v->oopif_sessions.end(); ) {
|
|
958
|
+
if (it->second == session_id) it = v->oopif_sessions.erase(it);
|
|
959
|
+
else ++it;
|
|
960
|
+
}
|
|
961
|
+
return S_OK;
|
|
962
|
+
}).Get(),
|
|
963
|
+
&v->target_detached_token);
|
|
964
|
+
v->oopif_event_tokens_registered = true;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
void finishResolveAndClickWv2(uint32_t view_id, uint32_t request_id, double x, double y,
|
|
968
|
+
double w, double h, double cx, double cy,
|
|
969
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
970
|
+
ViewHost* v = getView(view_id);
|
|
971
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
972
|
+
dispatchCdpClickWv2(v, cx, cy, button, click_count, modifiers);
|
|
973
|
+
// Edge runtime CDP → DOM trust=true (empirical; matches existing click cap).
|
|
974
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
975
|
+
",\"ok\":true,\"rect\":{\"x\":" + std::to_string(x) +
|
|
976
|
+
",\"y\":" + std::to_string(y) +
|
|
977
|
+
",\"width\":" + std::to_string(w) +
|
|
978
|
+
",\"height\":" + std::to_string(h) + "},"
|
|
979
|
+
"\"isTrustedEvent\":true}";
|
|
980
|
+
emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
struct FrameResolveOkWv2 { double x, y, w, h, cx, cy, iw, ih; };
|
|
984
|
+
|
|
985
|
+
// Parse a Runtime.evaluate response (regardless of session); on success forwards
|
|
986
|
+
// frame-local fields to `onOk`. Script's failure branch routes through error emit.
|
|
987
|
+
void parseEvalAndContinueWv2(uint32_t view_id, uint32_t request_id, bool ok, const std::string& evalResult,
|
|
988
|
+
std::function<void(const FrameResolveOkWv2&)> onOk) {
|
|
989
|
+
if (!ok) {
|
|
990
|
+
BUNITE_INFO("webview2/eval: Runtime.evaluate failed view=%u request=%u body=%.300s%s",
|
|
991
|
+
view_id, request_id, evalResult.c_str(),
|
|
992
|
+
evalResult.size() > 300 ? "..." : "");
|
|
993
|
+
emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "Runtime.evaluate failed"); return;
|
|
994
|
+
}
|
|
995
|
+
if (evalResult.find("\"exceptionDetails\"") != std::string::npos) {
|
|
996
|
+
emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "evaluate threw"); return;
|
|
997
|
+
}
|
|
998
|
+
std::string value = extractJsonValueRaw(evalResult, "value");
|
|
999
|
+
if (value.empty()) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "evaluate returned no value"); return; }
|
|
1000
|
+
std::string okRaw = extractJsonValueRaw(value, "ok");
|
|
1001
|
+
if (okRaw != "true") {
|
|
1002
|
+
std::string code = extractJsonStringDecoded(value, "code");
|
|
1003
|
+
if (code.empty()) code = "runtime_error";
|
|
1004
|
+
emitResolveAndClickErrorWv2(view_id, request_id, code.c_str(), "");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
onOk(FrameResolveOkWv2{
|
|
1008
|
+
extractJsonDouble(value, "x"), extractJsonDouble(value, "y"),
|
|
1009
|
+
extractJsonDouble(value, "w"), extractJsonDouble(value, "h"),
|
|
1010
|
+
extractJsonDouble(value, "cx"), extractJsonDouble(value, "cy"),
|
|
1011
|
+
extractJsonDouble(value, "iw"), extractJsonDouble(value, "ih"),
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
void evalInFrameWv2(uint32_t view_id, uint32_t request_id, const std::string& frameId,
|
|
1016
|
+
const std::string& script,
|
|
1017
|
+
std::function<void(const FrameResolveOkWv2&)> onOk) {
|
|
1018
|
+
ViewHost* v = getView(view_id);
|
|
1019
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1020
|
+
std::string session_id;
|
|
1021
|
+
{
|
|
1022
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
1023
|
+
auto it = v->oopif_sessions.find(frameId);
|
|
1024
|
+
if (it != v->oopif_sessions.end()) session_id = it->second;
|
|
1025
|
+
}
|
|
1026
|
+
if (!session_id.empty()) {
|
|
1027
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
|
|
1028
|
+
"\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1029
|
+
cdpCallForSession(v, session_id, L"Runtime.evaluate", evalParams,
|
|
1030
|
+
[view_id, request_id, onOk](bool ok, std::string r) {
|
|
1031
|
+
parseEvalAndContinueWv2(view_id, request_id, ok, r, onOk);
|
|
1032
|
+
});
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
// In-process: createIsolatedWorld + Runtime.evaluate via main session.
|
|
1036
|
+
std::string isoParams = "{\"frameId\":\"" + escapeJsonString(frameId) + "\",\"worldName\":\"bunite-rac\"}";
|
|
1037
|
+
cdpCallWithResult(v, L"Page.createIsolatedWorld", isoParams,
|
|
1038
|
+
[view_id, request_id, script, onOk](bool ok, std::string isoResult) {
|
|
1039
|
+
if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "createIsolatedWorld failed"); return; }
|
|
1040
|
+
int contextId = 0;
|
|
1041
|
+
if (!extractJsonInt(isoResult, "executionContextId", contextId)) {
|
|
1042
|
+
emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "missing executionContextId"); return;
|
|
1043
|
+
}
|
|
1044
|
+
ViewHost* v2 = getView(view_id);
|
|
1045
|
+
if (!v2 || !v2->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1046
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
|
|
1047
|
+
"\",\"contextId\":" + std::to_string(contextId) +
|
|
1048
|
+
",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1049
|
+
cdpCallWithResult(v2, L"Runtime.evaluate", evalParams,
|
|
1050
|
+
[view_id, request_id, onOk](bool ok2, std::string r) {
|
|
1051
|
+
parseEvalAndContinueWv2(view_id, request_id, ok2, r, onOk);
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
inline void bilinearMapWv2(const std::array<double, 8>& q, double iw, double ih,
|
|
1057
|
+
double fx, double fy, double& px, double& py) {
|
|
1058
|
+
const double u = (iw > 0) ? (fx / iw) : 0.0;
|
|
1059
|
+
const double v = (ih > 0) ? (fy / ih) : 0.0;
|
|
1060
|
+
px = (1-u)*(1-v)*q[0] + u*(1-v)*q[2] + u*v*q[4] + (1-u)*v*q[6];
|
|
1061
|
+
py = (1-u)*(1-v)*q[1] + u*(1-v)*q[3] + u*v*q[5] + (1-u)*v*q[7];
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Recursive frame path lookup in Page.getFrameTree response (text-based parser).
|
|
1065
|
+
// Returns [main_frame_id, ..., target_frame_id]; empty if target not found.
|
|
1066
|
+
std::vector<std::string> findFramePathWv2(const std::string& node, const std::string& target) {
|
|
1067
|
+
std::string frame = extractJsonValueRaw(node, "frame");
|
|
1068
|
+
std::string this_id = extractJsonStringDecoded(frame, "id");
|
|
1069
|
+
if (this_id.empty()) return {};
|
|
1070
|
+
if (this_id == target) return {this_id};
|
|
1071
|
+
std::string children = extractJsonValueRaw(node, "childFrames");
|
|
1072
|
+
if (children.size() < 2 || children.front() != '[') return {};
|
|
1073
|
+
// Walk child array — each element is a JSON object {frame, childFrames?}.
|
|
1074
|
+
size_t pos = 1;
|
|
1075
|
+
while (pos < children.size() && children[pos] != ']') {
|
|
1076
|
+
while (pos < children.size() && (children[pos] == ' ' || children[pos] == ',')) ++pos;
|
|
1077
|
+
if (pos >= children.size() || children[pos] != '{') break;
|
|
1078
|
+
int depth = 0;
|
|
1079
|
+
size_t end = pos;
|
|
1080
|
+
bool inStr = false;
|
|
1081
|
+
while (end < children.size()) {
|
|
1082
|
+
char ch = children[end];
|
|
1083
|
+
if (inStr) { if (ch == '\\' && end + 1 < children.size()) ++end; else if (ch == '"') inStr = false; }
|
|
1084
|
+
else if (ch == '"') inStr = true;
|
|
1085
|
+
else if (ch == '{') ++depth;
|
|
1086
|
+
else if (ch == '}') { --depth; if (depth == 0) { ++end; break; } }
|
|
1087
|
+
++end;
|
|
1088
|
+
}
|
|
1089
|
+
auto sub = findFramePathWv2(children.substr(pos, end - pos), target);
|
|
1090
|
+
if (!sub.empty()) { sub.insert(sub.begin(), this_id); return sub; }
|
|
1091
|
+
pos = end;
|
|
1092
|
+
}
|
|
1093
|
+
return {};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
bool parseQuad8(const std::string& content, std::array<double, 8>& out) {
|
|
1097
|
+
if (content.size() < 2 || content.front() != '[' || content.back() != ']') return false;
|
|
1098
|
+
size_t pos = 1;
|
|
1099
|
+
for (int i = 0; i < 8; ++i) {
|
|
1100
|
+
while (pos < content.size() && (content[pos] == ' ' || content[pos] == ',')) ++pos;
|
|
1101
|
+
size_t end = pos;
|
|
1102
|
+
while (end < content.size() && content[end] != ',' && content[end] != ']') ++end;
|
|
1103
|
+
if (end == pos) return false;
|
|
1104
|
+
try { out[i] = std::stod(content.substr(pos, end - pos)); } catch (...) { return false; }
|
|
1105
|
+
pos = end;
|
|
1106
|
+
}
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Issue CDP on a specific OOPIF session, or main session if `session_id` empty.
|
|
1111
|
+
void cdpForChain(ViewHost* v, const std::string& session_id, const wchar_t* method,
|
|
1112
|
+
const std::string& params_json,
|
|
1113
|
+
std::function<void(bool, std::string)> cb) {
|
|
1114
|
+
if (session_id.empty()) cdpCallWithResult(v, method, params_json, std::move(cb));
|
|
1115
|
+
else cdpCallForSession(v, session_id, method, params_json, std::move(cb));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
struct ChainStateWv2 {
|
|
1119
|
+
uint32_t view_id;
|
|
1120
|
+
uint32_t request_id;
|
|
1121
|
+
std::string targetFrameId;
|
|
1122
|
+
std::string script;
|
|
1123
|
+
int32_t button, click_count;
|
|
1124
|
+
uint32_t modifiers;
|
|
1125
|
+
std::vector<std::string> chain; // [main, ..., target]
|
|
1126
|
+
std::vector<std::array<double, 8>> link_quads;
|
|
1127
|
+
std::vector<std::pair<double, double>> ancestor_inner; // chain[1..N-2]'s iw/ih
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
void composeAndDispatchWv2(std::shared_ptr<ChainStateWv2> s, const FrameResolveOkWv2& fr);
|
|
1131
|
+
void fetchTargetEvalWv2(std::shared_ptr<ChainStateWv2> s);
|
|
1132
|
+
void fetchAncestorInnerWv2(std::shared_ptr<ChainStateWv2> s, size_t i);
|
|
1133
|
+
void fetchLinkWv2(std::shared_ptr<ChainStateWv2> s, size_t link_idx);
|
|
1134
|
+
|
|
1135
|
+
std::string sessionForChainIdxWv2(uint32_t view_id, const std::vector<std::string>& chain, size_t idx) {
|
|
1136
|
+
if (idx == 0) return {};
|
|
1137
|
+
ViewHost* v = getView(view_id);
|
|
1138
|
+
if (!v) return {};
|
|
1139
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
1140
|
+
auto it = v->oopif_sessions.find(chain[idx]);
|
|
1141
|
+
return (it != v->oopif_sessions.end()) ? it->second : std::string{};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
void fetchLinkWv2(std::shared_ptr<ChainStateWv2> s, size_t link_idx) {
|
|
1145
|
+
if (link_idx + 1 >= s->chain.size()) { fetchAncestorInnerWv2(s, 1); return; }
|
|
1146
|
+
const std::string parent_session = sessionForChainIdxWv2(s->view_id, s->chain, link_idx);
|
|
1147
|
+
const std::string& child_frameId = s->chain[link_idx + 1];
|
|
1148
|
+
ViewHost* v = getView(s->view_id);
|
|
1149
|
+
if (!v) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1150
|
+
std::string ownerParams = "{\"frameId\":\"" + escapeJsonString(child_frameId) + "\"}";
|
|
1151
|
+
cdpForChain(v, parent_session, L"DOM.getFrameOwner", ownerParams,
|
|
1152
|
+
[s, link_idx, parent_session](bool ok, std::string r) {
|
|
1153
|
+
if (!ok) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_found", "getFrameOwner failed"); return; }
|
|
1154
|
+
int backendNodeId = 0;
|
|
1155
|
+
if (!extractJsonInt(r, "backendNodeId", backendNodeId) || !backendNodeId) {
|
|
1156
|
+
emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_found", "no backendNodeId"); return;
|
|
1157
|
+
}
|
|
1158
|
+
ViewHost* v2 = getView(s->view_id);
|
|
1159
|
+
if (!v2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1160
|
+
std::string boxParams = "{\"backendNodeId\":" + std::to_string(backendNodeId) + "}";
|
|
1161
|
+
cdpForChain(v2, parent_session, L"DOM.getBoxModel", boxParams,
|
|
1162
|
+
[s, link_idx](bool ok2, std::string rb) {
|
|
1163
|
+
if (!ok2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_visible", "iframe has no box"); return; }
|
|
1164
|
+
std::string model = extractJsonValueRaw(rb, "model");
|
|
1165
|
+
std::string content = extractJsonValueRaw(model, "content");
|
|
1166
|
+
std::array<double, 8> quad{};
|
|
1167
|
+
if (!parseQuad8(content, quad)) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "bad quad"); return; }
|
|
1168
|
+
s->link_quads.push_back(quad);
|
|
1169
|
+
fetchLinkWv2(s, link_idx + 1);
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
void fetchAncestorInnerWv2(std::shared_ptr<ChainStateWv2> s, size_t i) {
|
|
1175
|
+
if (i + 1 >= s->chain.size()) { fetchTargetEvalWv2(s); return; }
|
|
1176
|
+
const std::string sid = sessionForChainIdxWv2(s->view_id, s->chain, i);
|
|
1177
|
+
ViewHost* v = getView(s->view_id);
|
|
1178
|
+
if (!v) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1179
|
+
std::string params = "{\"expression\":\"JSON.stringify({iw:innerWidth,ih:innerHeight})\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1180
|
+
cdpForChain(v, sid, L"Runtime.evaluate", params,
|
|
1181
|
+
[s, i](bool ok, std::string r) {
|
|
1182
|
+
if (!ok) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "ancestor eval failed"); return; }
|
|
1183
|
+
// Result: {"result":{"type":"string","value":"<json>"}}
|
|
1184
|
+
std::string value = extractJsonValueRaw(r, "value");
|
|
1185
|
+
if (value.size() < 2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "ancestor eval no value"); return; }
|
|
1186
|
+
// value is a JSON string literal — extractJsonValueRaw returns it with quotes; decode.
|
|
1187
|
+
std::string inner;
|
|
1188
|
+
for (size_t p = 1; p + 1 < value.size(); ++p) {
|
|
1189
|
+
if (value[p] == '\\' && p + 2 < value.size()) {
|
|
1190
|
+
char nxt = value[++p];
|
|
1191
|
+
switch (nxt) { case '"': inner += '"'; break; case '\\': inner += '\\'; break;
|
|
1192
|
+
case 'n': inner += '\n'; break; default: inner += nxt; }
|
|
1193
|
+
} else inner += value[p];
|
|
1194
|
+
}
|
|
1195
|
+
double iw = extractJsonDouble(inner, "iw"), ih = extractJsonDouble(inner, "ih");
|
|
1196
|
+
s->ancestor_inner.push_back({iw, ih});
|
|
1197
|
+
fetchAncestorInnerWv2(s, i + 1);
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
void fetchTargetEvalWv2(std::shared_ptr<ChainStateWv2> s) {
|
|
1202
|
+
evalInFrameWv2(s->view_id, s->request_id, s->targetFrameId, s->script,
|
|
1203
|
+
[s](const FrameResolveOkWv2& fr) { composeAndDispatchWv2(s, fr); });
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
void composeAndDispatchWv2(std::shared_ptr<ChainStateWv2> s, const FrameResolveOkWv2& fr) {
|
|
1207
|
+
auto mapCorner = [&](double fx, double fy, double& px, double& py) {
|
|
1208
|
+
double cur_x = fx, cur_y = fy;
|
|
1209
|
+
double cur_iw = fr.iw, cur_ih = fr.ih;
|
|
1210
|
+
for (size_t i = s->link_quads.size(); i-- > 0; ) {
|
|
1211
|
+
double mx, my;
|
|
1212
|
+
bilinearMapWv2(s->link_quads[i], cur_iw, cur_ih, cur_x, cur_y, mx, my);
|
|
1213
|
+
cur_x = mx; cur_y = my;
|
|
1214
|
+
if (i == 0) break;
|
|
1215
|
+
cur_iw = s->ancestor_inner[i - 1].first;
|
|
1216
|
+
cur_ih = s->ancestor_inner[i - 1].second;
|
|
1217
|
+
}
|
|
1218
|
+
px = cur_x; py = cur_y;
|
|
1219
|
+
};
|
|
1220
|
+
double pcx, pcy; mapCorner(fr.cx, fr.cy, pcx, pcy);
|
|
1221
|
+
double cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3;
|
|
1222
|
+
mapCorner(fr.x, fr.y, cx0, cy0);
|
|
1223
|
+
mapCorner(fr.x + fr.w, fr.y, cx1, cy1);
|
|
1224
|
+
mapCorner(fr.x + fr.w, fr.y + fr.h, cx2, cy2);
|
|
1225
|
+
mapCorner(fr.x, fr.y + fr.h, cx3, cy3);
|
|
1226
|
+
const double min_x = std::min(std::min(cx0, cx1), std::min(cx2, cx3));
|
|
1227
|
+
const double max_x = std::max(std::max(cx0, cx1), std::max(cx2, cx3));
|
|
1228
|
+
const double min_y = std::min(std::min(cy0, cy1), std::min(cy2, cy3));
|
|
1229
|
+
const double max_y = std::max(std::max(cy0, cy1), std::max(cy2, cy3));
|
|
1230
|
+
finishResolveAndClickWv2(s->view_id, s->request_id,
|
|
1231
|
+
min_x, min_y, max_x - min_x, max_y - min_y, pcx, pcy,
|
|
1232
|
+
s->button, s->click_count, s->modifiers);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Walk ancestor chain via Page.getFrameTree, compose bilinear transforms across
|
|
1236
|
+
// nested OOPIF/same-origin frames, dispatch click in main-page coords.
|
|
1237
|
+
void runFrameTargetedWv2(uint32_t view_id, uint32_t request_id, const std::string& frameId,
|
|
1238
|
+
const std::string& script,
|
|
1239
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1240
|
+
ViewHost* v = getView(view_id);
|
|
1241
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1242
|
+
cdpCallWithResult(v, L"Page.getFrameTree", "{}",
|
|
1243
|
+
[view_id, request_id, frameId, script, button, click_count, modifiers](bool ok, std::string r) {
|
|
1244
|
+
if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "getFrameTree failed"); return; }
|
|
1245
|
+
std::string root = extractJsonValueRaw(r, "frameTree");
|
|
1246
|
+
std::vector<std::string> chain = findFramePathWv2(root, frameId);
|
|
1247
|
+
if (chain.size() < 2) { emitResolveAndClickErrorWv2(view_id, request_id, "not_found", "frame not in tree"); return; }
|
|
1248
|
+
auto s = std::make_shared<ChainStateWv2>();
|
|
1249
|
+
s->view_id = view_id; s->request_id = request_id;
|
|
1250
|
+
s->targetFrameId = frameId; s->script = script;
|
|
1251
|
+
s->button = button; s->click_count = click_count; s->modifiers = modifiers;
|
|
1252
|
+
s->chain = std::move(chain);
|
|
1253
|
+
fetchLinkWv2(s, 0);
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
} // namespace
|
|
1257
|
+
|
|
1258
|
+
extern "C" {
|
|
1259
|
+
|
|
1260
|
+
BUNITE_EXPORT void bunite_view_resolve_and_click(
|
|
1261
|
+
uint32_t view_id, uint32_t request_id,
|
|
1262
|
+
const char* selector_c, const char* frame_id_c,
|
|
1263
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1264
|
+
ViewHost* v = getView(view_id);
|
|
1265
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view not ready"); return; }
|
|
1266
|
+
std::string selector = selector_c ? selector_c : "";
|
|
1267
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
1268
|
+
std::string script = buildResolveScriptWv2(selector);
|
|
1269
|
+
|
|
1270
|
+
if (frameId.empty()) {
|
|
1271
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) + "\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1272
|
+
cdpCallWithResult(v, L"Runtime.evaluate", evalParams,
|
|
1273
|
+
[view_id, request_id, button, click_count, modifiers](bool ok, std::string r) {
|
|
1274
|
+
parseEvalAndContinueWv2(view_id, request_id, ok, r,
|
|
1275
|
+
[view_id, request_id, button, click_count, modifiers](const FrameResolveOkWv2& fr) {
|
|
1276
|
+
finishResolveAndClickWv2(view_id, request_id,
|
|
1277
|
+
fr.x, fr.y, fr.w, fr.h, fr.cx, fr.cy,
|
|
1278
|
+
button, click_count, modifiers);
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Frame-targeted: lazy event subscription + setAutoAttach so OOPIF child
|
|
1285
|
+
// sessions populate v->oopif_sessions before frame eval routes.
|
|
1286
|
+
armOopifEvents(v);
|
|
1287
|
+
if (!v->oopif_autoattach_armed.exchange(true)) {
|
|
1288
|
+
cdpCallWithResult(v, L"Target.setAutoAttach",
|
|
1289
|
+
"{\"autoAttach\":true,\"flatten\":true,\"waitForDebuggerOnStart\":false}",
|
|
1290
|
+
[view_id, request_id, frameId, script, button, click_count, modifiers](bool ok, std::string) {
|
|
1291
|
+
if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "setAutoAttach failed"); return; }
|
|
1292
|
+
runFrameTargetedWv2(view_id, request_id, frameId, script, button, click_count, modifiers);
|
|
1293
|
+
});
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
runFrameTargetedWv2(view_id, request_id, frameId, script, button, click_count, modifiers);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
|
|
1300
|
+
int32_t /*interesting_only*/) {
|
|
1301
|
+
// CDP getFullAXTree accepts only depth/frameId — interesting-only filter is TS-side.
|
|
1302
|
+
ViewHost* v = getView(view_id);
|
|
1303
|
+
if (!v || !v->webview) { emitAxError(view_id, request_id, "not_supported", "view not ready"); return; }
|
|
1304
|
+
cdpCallWithResult(v, L"Accessibility.getFullAXTree", "{}",
|
|
1305
|
+
[view_id, request_id](bool ok, std::string result) {
|
|
1306
|
+
if (!ok) { emitAxError(view_id, request_id, "runtime_error", "getFullAXTree failed: " + result); return; }
|
|
1307
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1308
|
+
",\"ok\":true,\"tree\":" + result + "}";
|
|
1309
|
+
emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
1310
|
+
});
|
|
489
1311
|
}
|
|
490
1312
|
|
|
491
1313
|
BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|