bunite-core 0.12.1 → 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.
- package/package.json +4 -4
- package/src/host/core/App.ts +19 -2
- package/src/host/core/BrowserView.ts +515 -38
- package/src/host/core/SurfaceBrowserIPC.ts +53 -3
- package/src/host/core/SurfaceManager.ts +603 -30
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +25 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +263 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +427 -6
- package/src/native/linux/bunite_linux_internal.h +18 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +296 -5
- package/src/native/mac/bunite_mac_ffi.mm +630 -8
- package/src/native/mac/bunite_mac_internal.h +19 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +371 -9
- package/src/native/shared/ffi_exports.h +200 -2
- package/src/native/win/native_host_cef.cpp +186 -11
- package/src/native/win/native_host_ffi.cpp +1194 -1
- package/src/native/win/native_host_internal.h +35 -0
- package/src/native/win/native_host_utils.cpp +2 -1
- package/src/native/win/process_helper_win.cpp +54 -27
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +1023 -12
- package/src/native/win-webview2/webview2_internal.h +25 -0
- package/src/native/win-webview2/webview2_runtime.cpp +403 -34
- package/src/native/win-webview2/webview2_utils.cpp +30 -12
- package/src/preload/runtime.built.js +1 -1
- package/src/preload/runtime.ts +97 -0
- package/src/rpc/framework.ts +340 -8
- package/src/rpc/index.ts +32 -0
- package/src/webview/native.ts +253 -51
- package/src/webview/polyfill.ts +283 -22
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#include "webview2_internal.h"
|
|
2
2
|
|
|
3
|
+
#include <algorithm>
|
|
4
|
+
#include <array>
|
|
3
5
|
#include <cstring>
|
|
6
|
+
#include <memory>
|
|
7
|
+
#include <vector>
|
|
8
|
+
#include <wincrypt.h> // CryptBinaryToStringA — base64 encoding for screenshot payload.
|
|
4
9
|
|
|
5
10
|
using namespace bunite_webview2;
|
|
6
11
|
|
|
@@ -11,7 +16,7 @@ void setViewInputPassthrough(ViewHost* v, bool passthrough);
|
|
|
11
16
|
|
|
12
17
|
extern "C" {
|
|
13
18
|
|
|
14
|
-
BUNITE_EXPORT int32_t bunite_abi_version(void) { return
|
|
19
|
+
BUNITE_EXPORT int32_t bunite_abi_version(void) { return 11; }
|
|
15
20
|
|
|
16
21
|
BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
|
|
17
22
|
if (level < 0) level = 0;
|
|
@@ -171,7 +176,8 @@ BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, c
|
|
|
171
176
|
std::string wrapped =
|
|
172
177
|
"(function(){try{return JSON.stringify({__bunite_ok:true,value:("
|
|
173
178
|
+ std::string(script) +
|
|
174
|
-
")})}catch(e){
|
|
179
|
+
")})}catch(e){var c=(e&&e.name===\"SecurityError\")?\"cross_origin\":\"runtime_error\";"
|
|
180
|
+
"return JSON.stringify({__bunite_ok:false,code:c,"
|
|
175
181
|
"message:(e&&e.message)?e.message:String(e),"
|
|
176
182
|
"name:(e&&e.name)||\"\"})}})()";
|
|
177
183
|
|
|
@@ -235,21 +241,31 @@ BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, c
|
|
|
235
241
|
payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
236
242
|
",\"ok\":true,\"value\":\"" + escapeJsonString(value_json) + "\"}";
|
|
237
243
|
} else {
|
|
238
|
-
// __bunite_ok:false branch.
|
|
244
|
+
// __bunite_ok:false branch. Anchor extraction at fixed prefix —
|
|
245
|
+
// user-controlled e.message could otherwise inject a fake "code".
|
|
246
|
+
static const std::string codePrefix = "{\"__bunite_ok\":false,\"code\":\"";
|
|
247
|
+
std::string code = "runtime_error";
|
|
239
248
|
std::string msg = "script threw";
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
size_t start = key + std::strlen("\"message\":\"");
|
|
243
|
-
// Find unescaped closing quote.
|
|
249
|
+
if (inner.compare(0, codePrefix.size(), codePrefix) == 0) {
|
|
250
|
+
size_t start = codePrefix.size();
|
|
244
251
|
size_t end = start;
|
|
245
|
-
while (end < inner.size())
|
|
246
|
-
|
|
247
|
-
|
|
252
|
+
while (end < inner.size() && inner[end] != '"') ++end;
|
|
253
|
+
if (end > start) code = inner.substr(start, end - start);
|
|
254
|
+
// message key follows immediately after `","` separator.
|
|
255
|
+
static const std::string msgKey = "\",\"message\":\"";
|
|
256
|
+
if (end + msgKey.size() <= inner.size() &&
|
|
257
|
+
inner.compare(end, msgKey.size(), msgKey) == 0) {
|
|
258
|
+
size_t mstart = end + msgKey.size();
|
|
259
|
+
size_t mend = mstart;
|
|
260
|
+
while (mend < inner.size()) {
|
|
261
|
+
if (inner[mend] == '"' && (mend == mstart || inner[mend - 1] != '\\')) break;
|
|
262
|
+
++mend;
|
|
263
|
+
}
|
|
264
|
+
if (mend > mstart) msg = inner.substr(mstart, mend - mstart);
|
|
248
265
|
}
|
|
249
|
-
if (end > start) msg = inner.substr(start, end - start);
|
|
250
266
|
}
|
|
251
267
|
payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
252
|
-
",\"ok\":false,\"code\":\"
|
|
268
|
+
",\"ok\":false,\"code\":\"" + escapeJsonString(code) + "\","
|
|
253
269
|
"\"message\":\"" + msg + "\"}";
|
|
254
270
|
}
|
|
255
271
|
}
|
|
@@ -366,6 +382,1001 @@ BUNITE_EXPORT void bunite_view_reload(uint32_t view_id) {
|
|
|
366
382
|
|
|
367
383
|
BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) { destroyView(view_id); }
|
|
368
384
|
|
|
385
|
+
// Input dispatch — CDP via CallDevToolsProtocolMethod (Playwright pattern).
|
|
386
|
+
// Edge runtime injects below DevTools surface — DOM `isTrusted=true`
|
|
387
|
+
// (see `bunite_view_capabilities` note).
|
|
388
|
+
namespace {
|
|
389
|
+
|
|
390
|
+
const char* cdpMouseButton(int32_t b) {
|
|
391
|
+
switch (b) { case 1: return "middle"; case 2: return "right"; default: return "left"; }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
void cdpCall(ViewHost* v, const wchar_t* method, const std::string& json) {
|
|
395
|
+
if (!v || !v->webview) return;
|
|
396
|
+
v->webview->CallDevToolsProtocolMethod(
|
|
397
|
+
method, utf8ToWide(json).c_str(), nullptr);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Result-aware CDP call. `cb(ok, json)` runs on the UI thread once the result
|
|
401
|
+
// arrives; on failure `ok=false` and json is an error description.
|
|
402
|
+
void cdpCallWithResult(ViewHost* v, const wchar_t* method, const std::string& json,
|
|
403
|
+
std::function<void(bool, std::string)> cb) {
|
|
404
|
+
if (!v || !v->webview) { if (cb) cb(false, "view not ready"); return; }
|
|
405
|
+
auto lifetime = g_runtime.lifetime;
|
|
406
|
+
v->webview->CallDevToolsProtocolMethod(
|
|
407
|
+
method, utf8ToWide(json).c_str(),
|
|
408
|
+
Microsoft::WRL::Callback<ICoreWebView2CallDevToolsProtocolMethodCompletedHandler>(
|
|
409
|
+
[lifetime, cb](HRESULT hr, LPCWSTR result) -> HRESULT {
|
|
410
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
411
|
+
if (FAILED(hr) || !result) { cb(false, "CDP call failed"); return S_OK; }
|
|
412
|
+
cb(true, wideToUtf8(result));
|
|
413
|
+
return S_OK;
|
|
414
|
+
}).Get());
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
} // namespace
|
|
418
|
+
|
|
419
|
+
BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
|
|
420
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
421
|
+
ViewHost* v = getView(view_id);
|
|
422
|
+
if (!v) return;
|
|
423
|
+
if (click_count < 1) click_count = 1;
|
|
424
|
+
// Multi-click → repeated pairs with increasing clickCount so the page sees
|
|
425
|
+
// a dblclick (Playwright convention).
|
|
426
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
427
|
+
std::string base = "\"x\":" + std::to_string(x) + ",\"y\":" + std::to_string(y) +
|
|
428
|
+
",\"button\":\"" + cdpMouseButton(button) + "\","
|
|
429
|
+
"\"clickCount\":" + std::to_string(i) +
|
|
430
|
+
",\"modifiers\":" + std::to_string(modifiers);
|
|
431
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mousePressed\"," + base + "}");
|
|
432
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mouseReleased\"," + base + "}");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
BUNITE_EXPORT void bunite_view_type(uint32_t view_id, const char* text) {
|
|
437
|
+
ViewHost* v = getView(view_id);
|
|
438
|
+
if (!v || !text) return;
|
|
439
|
+
std::string json = "{\"text\":\"" + escapeJsonString(text) + "\"}";
|
|
440
|
+
cdpCall(v, L"Input.insertText", json);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
|
|
444
|
+
int32_t /*mac_key_code*/, const char* key, const char* code,
|
|
445
|
+
const char* character, uint32_t modifiers,
|
|
446
|
+
int32_t action, bool /*extended*/, int32_t location) {
|
|
447
|
+
ViewHost* v = getView(view_id);
|
|
448
|
+
if (!v) return;
|
|
449
|
+
std::string char_str = character ? character : "";
|
|
450
|
+
std::string key_str = key ? key : "";
|
|
451
|
+
std::string code_str = code ? code : "";
|
|
452
|
+
|
|
453
|
+
// CDP modifier mask: 1=alt, 2=ctrl, 4=meta, 8=shift. Suppress text when any
|
|
454
|
+
// non-shift modifier is set so shortcuts like Ctrl+A don't insert "a".
|
|
455
|
+
const bool has_non_shift_modifier = (modifiers & ~static_cast<uint32_t>(8)) != 0;
|
|
456
|
+
|
|
457
|
+
auto buildPart = [&](const char* type, bool include_text) {
|
|
458
|
+
std::string out = "{\"type\":\"";
|
|
459
|
+
out += type;
|
|
460
|
+
out += "\",\"modifiers\":" + std::to_string(modifiers);
|
|
461
|
+
if (windows_vk_code != 0) out += ",\"windowsVirtualKeyCode\":" + std::to_string(windows_vk_code);
|
|
462
|
+
if (!key_str.empty()) out += ",\"key\":\"" + escapeJsonString(key_str) + "\"";
|
|
463
|
+
if (!code_str.empty()) out += ",\"code\":\"" + escapeJsonString(code_str) + "\"";
|
|
464
|
+
// CDP `location`: 0 standard, 1 left mod, 2 right mod, 3 numpad.
|
|
465
|
+
if (location > 0) out += ",\"location\":" + std::to_string(location);
|
|
466
|
+
if (include_text && !char_str.empty() && !has_non_shift_modifier)
|
|
467
|
+
out += ",\"text\":\"" + escapeJsonString(char_str) + "\"";
|
|
468
|
+
out += "}";
|
|
469
|
+
return out;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// Playwright convention: CHAR `text` rides with keyDown for printable keys.
|
|
473
|
+
if (action != 1) cdpCall(v, L"Input.dispatchKeyEvent", buildPart("keyDown", /*include_text=*/true));
|
|
474
|
+
if (action != 0) cdpCall(v, L"Input.dispatchKeyEvent", buildPart("keyUp", /*include_text=*/false));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
} // extern "C" — temporarily exit so C++ helpers below can return std::string.
|
|
478
|
+
|
|
479
|
+
namespace {
|
|
480
|
+
|
|
481
|
+
// Win CryptoAPI base64 — `bytes` → printable string (no line breaks).
|
|
482
|
+
std::string base64Encode(const BYTE* bytes, DWORD len) {
|
|
483
|
+
DWORD out_len = 0;
|
|
484
|
+
if (!CryptBinaryToStringA(bytes, len, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, nullptr, &out_len)) return {};
|
|
485
|
+
std::string out(out_len, '\0');
|
|
486
|
+
if (!CryptBinaryToStringA(bytes, len, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, out.data(), &out_len)) return {};
|
|
487
|
+
out.resize(out_len); // CryptBinaryToString writes including trailing null on some configs; trim.
|
|
488
|
+
while (!out.empty() && out.back() == '\0') out.pop_back();
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
void emitScreenshotError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
493
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
494
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
495
|
+
"\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
496
|
+
emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
} // namespace
|
|
500
|
+
|
|
501
|
+
extern "C" {
|
|
502
|
+
|
|
503
|
+
BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
504
|
+
// WebView2 — CDP input path. Empirically dispatchMouseEvent /
|
|
505
|
+
// dispatchKeyEvent / insertText produce events with `isTrusted=true` on
|
|
506
|
+
// the page (Edge runtime injects below DevTools surface; differs from
|
|
507
|
+
// browser-process CDP where isTrusted=false).
|
|
508
|
+
ViewHost* v = getView(view_id);
|
|
509
|
+
if (!v) return 0;
|
|
510
|
+
return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
|
|
511
|
+
BUNITE_CAP_NATIVE_INPUT_TRUSTED |
|
|
512
|
+
BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
|
|
513
|
+
BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
514
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
|
|
515
|
+
BUNITE_CAP_AX | BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_FRAMES |
|
|
516
|
+
BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
|
|
517
|
+
BUNITE_CAP_RESOLVE_AND_CLICK;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
|
|
521
|
+
ViewHost* v = getView(view_id);
|
|
522
|
+
if (!v) return;
|
|
523
|
+
if (policy < 0 || policy > 2) policy = 2;
|
|
524
|
+
v->download_policy.store(policy);
|
|
525
|
+
v->download_dir = download_dir ? download_dir : "";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
|
|
529
|
+
double x, double y, double w, double h) {
|
|
530
|
+
ViewHost* v = getView(new_view_id);
|
|
531
|
+
if (!v || !v->controller || !v->container_hwnd) return;
|
|
532
|
+
WindowHost* host_window = nullptr;
|
|
533
|
+
{
|
|
534
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
535
|
+
auto wit = g_runtime.windows_by_id.find(host_window_id);
|
|
536
|
+
if (wit == g_runtime.windows_by_id.end()) return;
|
|
537
|
+
host_window = wit->second;
|
|
538
|
+
}
|
|
539
|
+
if (!host_window || !host_window->hwnd) return;
|
|
540
|
+
v->window = host_window;
|
|
541
|
+
host_window->views.push_back(v);
|
|
542
|
+
SetParent(v->container_hwnd, host_window->hwnd);
|
|
543
|
+
MoveWindow(v->container_hwnd, static_cast<int>(x), static_cast<int>(y),
|
|
544
|
+
static_cast<int>(w), static_cast<int>(h), TRUE);
|
|
545
|
+
ShowWindow(v->container_hwnd, SW_SHOW);
|
|
546
|
+
RECT bounds{0, 0, static_cast<LONG>(w), static_cast<LONG>(h)};
|
|
547
|
+
v->controller->put_Bounds(bounds);
|
|
548
|
+
v->controller->put_IsVisible(TRUE);
|
|
549
|
+
emitWebviewEvent(new_view_id, "view-ready", "");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
|
|
553
|
+
ViewHost* v = nullptr;
|
|
554
|
+
{
|
|
555
|
+
std::lock_guard<std::mutex> g(g_runtime.object_mutex);
|
|
556
|
+
auto it = g_runtime.views_by_id.find(new_view_id);
|
|
557
|
+
if (it == g_runtime.views_by_id.end()) return;
|
|
558
|
+
v = it->second;
|
|
559
|
+
g_runtime.views_by_id.erase(it);
|
|
560
|
+
}
|
|
561
|
+
if (!v) return;
|
|
562
|
+
if (v->controller) v->controller->Close();
|
|
563
|
+
if (v->container_hwnd) DestroyWindow(v->container_hwnd);
|
|
564
|
+
delete v;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
namespace {
|
|
568
|
+
void emitAxError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
569
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
570
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
571
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
572
|
+
emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
573
|
+
}
|
|
574
|
+
} // namespace
|
|
575
|
+
|
|
576
|
+
namespace {
|
|
577
|
+
// Extract integer field from a CDP JSON response — used to pluck
|
|
578
|
+
// `executionContextId` without pulling in a JSON parser.
|
|
579
|
+
bool extractJsonInt(const std::string& json, const std::string& key, int& out) {
|
|
580
|
+
std::string needle = "\"" + key + "\":";
|
|
581
|
+
auto p = json.find(needle);
|
|
582
|
+
if (p == std::string::npos) return false;
|
|
583
|
+
p += needle.size();
|
|
584
|
+
while (p < json.size() && (json[p] == ' ' || json[p] == '\t')) ++p;
|
|
585
|
+
auto end = p;
|
|
586
|
+
while (end < json.size() && (json[end] == '-' || (json[end] >= '0' && json[end] <= '9'))) ++end;
|
|
587
|
+
if (end == p) return false;
|
|
588
|
+
out = std::stoi(json.substr(p, end - p));
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
void emitListFramesErrorWv2(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
592
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
593
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
594
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
595
|
+
emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
596
|
+
}
|
|
597
|
+
} // namespace
|
|
598
|
+
|
|
599
|
+
BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
|
|
600
|
+
ViewHost* v = getView(view_id);
|
|
601
|
+
if (!v || !v->webview) { emitListFramesErrorWv2(view_id, request_id, "not_supported", "view not ready"); return; }
|
|
602
|
+
cdpCallWithResult(v, L"Page.getFrameTree", "{}",
|
|
603
|
+
[view_id, request_id](bool ok, std::string result) {
|
|
604
|
+
if (!ok) { emitListFramesErrorWv2(view_id, request_id, "runtime_error", "getFrameTree failed: " + result); return; }
|
|
605
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
606
|
+
",\"ok\":true,\"raw\":" + result + "}";
|
|
607
|
+
emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
|
|
612
|
+
const char* script_c, const char* frame_id_c) {
|
|
613
|
+
std::string script = script_c ? script_c : "";
|
|
614
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
615
|
+
if (frameId.empty()) {
|
|
616
|
+
bunite_view_evaluate(view_id, request_id, script_c);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
ViewHost* v = getView(view_id);
|
|
620
|
+
if (!v || !v->webview) {
|
|
621
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
622
|
+
",\"ok\":false,\"code\":\"not_supported\",\"message\":\"view not ready\"}";
|
|
623
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
std::string isoParams = "{\"frameId\":\"" + escapeJsonString(frameId) + "\",\"worldName\":\"bunite-eval\"}";
|
|
627
|
+
cdpCallWithResult(v, L"Page.createIsolatedWorld", isoParams,
|
|
628
|
+
[view_id, request_id, script](bool ok, std::string isoResult) {
|
|
629
|
+
if (!ok) {
|
|
630
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
631
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
632
|
+
"\"message\":\"createIsolatedWorld failed\"}";
|
|
633
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
int contextId = 0;
|
|
637
|
+
if (!extractJsonInt(isoResult, "executionContextId", contextId)) {
|
|
638
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
639
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
640
|
+
"\"message\":\"missing executionContextId\"}";
|
|
641
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
// Re-lookup — the async gap could have torn down the view.
|
|
645
|
+
ViewHost* v2 = getView(view_id);
|
|
646
|
+
if (!v2 || !v2->webview) {
|
|
647
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
648
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
649
|
+
"\"message\":\"view destroyed\"}";
|
|
650
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
|
|
654
|
+
"\",\"contextId\":" + std::to_string(contextId) +
|
|
655
|
+
",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
656
|
+
cdpCallWithResult(v2, L"Runtime.evaluate", evalParams,
|
|
657
|
+
[view_id, request_id](bool ok2, std::string evalResult) {
|
|
658
|
+
if (!ok2) {
|
|
659
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
660
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
661
|
+
"\"message\":\"Runtime.evaluate failed\"}";
|
|
662
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
// Normalize CDP shape to the flat `{requestId, ok, value/code/message}`
|
|
666
|
+
// that the host's evaluate-result handler expects. exceptionDetails
|
|
667
|
+
// at the top level is preceded by `},` — distinguishes from a value
|
|
668
|
+
// that happens to contain the literal token inside a string.
|
|
669
|
+
auto excPos = evalResult.find("},\"exceptionDetails\"");
|
|
670
|
+
if (excPos == std::string::npos) excPos = evalResult.find("}, \"exceptionDetails\"");
|
|
671
|
+
if (excPos != std::string::npos) {
|
|
672
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
673
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
674
|
+
"\"message\":\"evaluate threw\"}";
|
|
675
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
// Extract result.value as a JSON substring. CDP returns
|
|
679
|
+
// `{"result":{"type":"...","value":<json>}}`. We find the `"value":`
|
|
680
|
+
// token inside the inner object and slice until the matching close.
|
|
681
|
+
auto resPos = evalResult.find("\"result\":");
|
|
682
|
+
if (resPos == std::string::npos) {
|
|
683
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
684
|
+
",\"ok\":true,\"value\":\"null\"}";
|
|
685
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
auto valKey = evalResult.find("\"value\":", resPos);
|
|
689
|
+
if (valKey == std::string::npos) {
|
|
690
|
+
// type === "undefined" — no value field.
|
|
691
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
692
|
+
",\"ok\":true,\"value\":\"null\"}";
|
|
693
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
size_t start = valKey + 8; // past `"value":`
|
|
697
|
+
// Skip whitespace.
|
|
698
|
+
while (start < evalResult.size() && (evalResult[start] == ' ' || evalResult[start] == '\t')) ++start;
|
|
699
|
+
// Find balanced end of the JSON value.
|
|
700
|
+
size_t end = start;
|
|
701
|
+
if (start < evalResult.size()) {
|
|
702
|
+
char c = evalResult[start];
|
|
703
|
+
if (c == '"') {
|
|
704
|
+
++end;
|
|
705
|
+
while (end < evalResult.size() && evalResult[end] != '"') {
|
|
706
|
+
if (evalResult[end] == '\\' && end + 1 < evalResult.size()) ++end;
|
|
707
|
+
++end;
|
|
708
|
+
}
|
|
709
|
+
if (end < evalResult.size()) ++end;
|
|
710
|
+
} else if (c == '{' || c == '[') {
|
|
711
|
+
int depth = 0;
|
|
712
|
+
bool inStr = false;
|
|
713
|
+
while (end < evalResult.size()) {
|
|
714
|
+
char ch = evalResult[end];
|
|
715
|
+
if (inStr) {
|
|
716
|
+
if (ch == '\\' && end + 1 < evalResult.size()) ++end;
|
|
717
|
+
else if (ch == '"') inStr = false;
|
|
718
|
+
} else if (ch == '"') inStr = true;
|
|
719
|
+
else if (ch == '{' || ch == '[') ++depth;
|
|
720
|
+
else if (ch == '}' || ch == ']') { --depth; if (depth == 0) { ++end; break; } }
|
|
721
|
+
++end;
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
// Number / true / false / null — read until non-token char.
|
|
725
|
+
while (end < evalResult.size()) {
|
|
726
|
+
char ch = evalResult[end];
|
|
727
|
+
if (ch == ',' || ch == '}' || ch == ' ' || ch == '\t' || ch == '\n') break;
|
|
728
|
+
++end;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
std::string valueJson = evalResult.substr(start, end - start);
|
|
733
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
734
|
+
",\"ok\":true,\"value\":\"" + escapeJsonString(valueJson) + "\"}";
|
|
735
|
+
emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
} // extern "C" — exit so the helpers below can return std::string.
|
|
741
|
+
|
|
742
|
+
namespace {
|
|
743
|
+
|
|
744
|
+
void emitResolveAndClickErrorWv2(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
745
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
746
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
747
|
+
"\",\"message\":\"" + escapeJsonString(message) + "\"}";
|
|
748
|
+
emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const char* cdpButtonNameWv2(int32_t b) {
|
|
752
|
+
switch (b) { case 1: return "middle"; case 2: return "right"; default: return "left"; }
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
std::string extractJsonValueRaw(const std::string& s, const std::string& key) {
|
|
756
|
+
std::string needle = "\"" + key + "\":";
|
|
757
|
+
size_t p = s.find(needle);
|
|
758
|
+
if (p == std::string::npos) return {};
|
|
759
|
+
p += needle.size();
|
|
760
|
+
while (p < s.size() && (s[p] == ' ' || s[p] == '\t')) ++p;
|
|
761
|
+
if (p >= s.size()) return {};
|
|
762
|
+
size_t end = p;
|
|
763
|
+
char c = s[p];
|
|
764
|
+
if (c == '"') {
|
|
765
|
+
++end;
|
|
766
|
+
while (end < s.size() && s[end] != '"') { if (s[end] == '\\' && end + 1 < s.size()) ++end; ++end; }
|
|
767
|
+
if (end < s.size()) ++end;
|
|
768
|
+
} else if (c == '{' || c == '[') {
|
|
769
|
+
int depth = 0; bool inStr = false;
|
|
770
|
+
while (end < s.size()) {
|
|
771
|
+
char ch = s[end];
|
|
772
|
+
if (inStr) { if (ch == '\\' && end + 1 < s.size()) ++end; else if (ch == '"') inStr = false; }
|
|
773
|
+
else if (ch == '"') inStr = true;
|
|
774
|
+
else if (ch == '{' || ch == '[') ++depth;
|
|
775
|
+
else if (ch == '}' || ch == ']') { --depth; if (depth == 0) { ++end; break; } }
|
|
776
|
+
++end;
|
|
777
|
+
}
|
|
778
|
+
} else {
|
|
779
|
+
while (end < s.size()) {
|
|
780
|
+
char ch = s[end];
|
|
781
|
+
if (ch == ',' || ch == '}' || ch == ' ' || ch == '\t' || ch == '\n') break;
|
|
782
|
+
++end;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return s.substr(p, end - p);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
double extractJsonDouble(const std::string& s, const std::string& key, double dflt = 0.0) {
|
|
789
|
+
std::string raw = extractJsonValueRaw(s, key);
|
|
790
|
+
if (raw.empty()) return dflt;
|
|
791
|
+
try { return std::stod(raw); } catch (...) { return dflt; }
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
std::string extractJsonStringDecoded(const std::string& s, const std::string& key) {
|
|
795
|
+
std::string raw = extractJsonValueRaw(s, key);
|
|
796
|
+
if (raw.size() < 2 || raw.front() != '"' || raw.back() != '"') return {};
|
|
797
|
+
std::string out; out.reserve(raw.size());
|
|
798
|
+
for (size_t i = 1; i + 1 < raw.size(); ++i) {
|
|
799
|
+
if (raw[i] == '\\' && i + 1 < raw.size() - 1) {
|
|
800
|
+
char nxt = raw[++i];
|
|
801
|
+
switch (nxt) {
|
|
802
|
+
case '"': out += '"'; break;
|
|
803
|
+
case '\\': out += '\\'; break;
|
|
804
|
+
case 'n': out += '\n'; break;
|
|
805
|
+
case 'r': out += '\r'; break;
|
|
806
|
+
case 't': out += '\t'; break;
|
|
807
|
+
case '/': out += '/'; break;
|
|
808
|
+
default: out += nxt; break;
|
|
809
|
+
}
|
|
810
|
+
} else out += raw[i];
|
|
811
|
+
}
|
|
812
|
+
return out;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
std::string escapeForJsStringWv2(const std::string& s) {
|
|
816
|
+
std::string out; out.reserve(s.size() + 2);
|
|
817
|
+
for (char c : s) {
|
|
818
|
+
if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
|
|
819
|
+
else if (c == '\n') out += "\\n";
|
|
820
|
+
else if (c == '\r') out += "\\r";
|
|
821
|
+
else if (c == '\t') out += "\\t";
|
|
822
|
+
else out.push_back(c);
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
std::string buildResolveScriptWv2(const std::string& selector) {
|
|
828
|
+
// Frame-local rect + innerWidth/innerHeight for bilinear mapping when the
|
|
829
|
+
// frame is transformed (rotate/scale). Main frame uses iw/ih harmlessly.
|
|
830
|
+
std::string sel_lit = "\"" + escapeForJsStringWv2(selector) + "\"";
|
|
831
|
+
return
|
|
832
|
+
"(function(){"
|
|
833
|
+
"var el=document.querySelector(" + sel_lit + ");"
|
|
834
|
+
"if(!el)return{ok:false,code:\"not_found\"};"
|
|
835
|
+
"el.scrollIntoView({block:\"nearest\",inline:\"nearest\",behavior:\"instant\"});"
|
|
836
|
+
"var r=el.getBoundingClientRect();"
|
|
837
|
+
"var vis=r.width>0&&r.height>0&&r.bottom>0&&r.right>0"
|
|
838
|
+
"&&r.top<innerHeight&&r.left<innerWidth;"
|
|
839
|
+
"if(!vis)return{ok:false,code:\"not_visible\"};"
|
|
840
|
+
"return{ok:true,x:r.x,y:r.y,w:r.width,h:r.height,"
|
|
841
|
+
"cx:r.x+r.width/2,cy:r.y+r.height/2,"
|
|
842
|
+
"iw:innerWidth,ih:innerHeight};"
|
|
843
|
+
"})()";
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
void dispatchCdpClickWv2(ViewHost* v, double cx, double cy,
|
|
847
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
848
|
+
if (click_count < 1) click_count = 1;
|
|
849
|
+
const char* btn = cdpButtonNameWv2(button);
|
|
850
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
851
|
+
std::string base = "\"x\":" + std::to_string(cx) + ",\"y\":" + std::to_string(cy) +
|
|
852
|
+
",\"button\":\"" + btn + "\",\"clickCount\":" + std::to_string(i) +
|
|
853
|
+
",\"modifiers\":" + std::to_string(modifiers);
|
|
854
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mousePressed\"," + base + "}");
|
|
855
|
+
cdpCall(v, L"Input.dispatchMouseEvent", "{\"type\":\"mouseReleased\"," + base + "}");
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// CDP call routed to a child target session (flatten:true). Requires
|
|
860
|
+
// ICoreWebView2_11.
|
|
861
|
+
void cdpCallForSession(ViewHost* v, const std::string& session_id, const wchar_t* method,
|
|
862
|
+
const std::string& params_json,
|
|
863
|
+
std::function<void(bool, std::string)> cb) {
|
|
864
|
+
if (!v || !v->webview) { if (cb) cb(false, "view not ready"); return; }
|
|
865
|
+
ComPtr<ICoreWebView2_11> wv11;
|
|
866
|
+
if (FAILED(v->webview.As(&wv11)) || !wv11) { if (cb) cb(false, "ICoreWebView2_11 unavailable"); return; }
|
|
867
|
+
auto lifetime = g_runtime.lifetime;
|
|
868
|
+
wv11->CallDevToolsProtocolMethodForSession(
|
|
869
|
+
utf8ToWide(session_id).c_str(), method, utf8ToWide(params_json).c_str(),
|
|
870
|
+
Microsoft::WRL::Callback<ICoreWebView2CallDevToolsProtocolMethodCompletedHandler>(
|
|
871
|
+
[lifetime, cb](HRESULT hr, LPCWSTR result) -> HRESULT {
|
|
872
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
873
|
+
if (FAILED(hr) || !result) { cb(false, "CDP-for-session call failed"); return S_OK; }
|
|
874
|
+
cb(true, wideToUtf8(result));
|
|
875
|
+
return S_OK;
|
|
876
|
+
}).Get());
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Register one-time listeners for Target.attachedToTarget / detachedFromTarget.
|
|
880
|
+
// Populates view->oopif_sessions as OOPIF child sessions attach.
|
|
881
|
+
void armOopifEvents(ViewHost* v) {
|
|
882
|
+
if (v->oopif_event_tokens_registered) return;
|
|
883
|
+
using namespace Microsoft::WRL;
|
|
884
|
+
ComPtr<ICoreWebView2DevToolsProtocolEventReceiver> attached_r, detached_r;
|
|
885
|
+
if (FAILED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.attachedToTarget", &attached_r))
|
|
886
|
+
|| FAILED(v->webview->GetDevToolsProtocolEventReceiver(L"Target.detachedFromTarget", &detached_r))) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
uint32_t view_id = v->id;
|
|
890
|
+
attached_r->add_DevToolsProtocolEventReceived(
|
|
891
|
+
Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
|
|
892
|
+
[view_id](ICoreWebView2*, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT {
|
|
893
|
+
LPWSTR raw = nullptr;
|
|
894
|
+
if (FAILED(args->get_ParameterObjectAsJson(&raw)) || !raw) return S_OK;
|
|
895
|
+
std::string p = wideToUtf8(raw); CoTaskMemFree(raw);
|
|
896
|
+
std::string session_id = extractJsonStringDecoded(p, "sessionId");
|
|
897
|
+
std::string info = extractJsonValueRaw(p, "targetInfo");
|
|
898
|
+
std::string type = extractJsonStringDecoded(info, "type");
|
|
899
|
+
std::string target_id = extractJsonStringDecoded(info, "targetId");
|
|
900
|
+
if (session_id.empty() || target_id.empty() || type != "iframe") return S_OK;
|
|
901
|
+
auto* v = getView(view_id);
|
|
902
|
+
if (!v) return S_OK;
|
|
903
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
904
|
+
v->oopif_sessions[target_id] = session_id;
|
|
905
|
+
return S_OK;
|
|
906
|
+
}).Get(),
|
|
907
|
+
&v->target_attached_token);
|
|
908
|
+
detached_r->add_DevToolsProtocolEventReceived(
|
|
909
|
+
Callback<ICoreWebView2DevToolsProtocolEventReceivedEventHandler>(
|
|
910
|
+
[view_id](ICoreWebView2*, ICoreWebView2DevToolsProtocolEventReceivedEventArgs* args) -> HRESULT {
|
|
911
|
+
LPWSTR raw = nullptr;
|
|
912
|
+
if (FAILED(args->get_ParameterObjectAsJson(&raw)) || !raw) return S_OK;
|
|
913
|
+
std::string p = wideToUtf8(raw); CoTaskMemFree(raw);
|
|
914
|
+
std::string session_id = extractJsonStringDecoded(p, "sessionId");
|
|
915
|
+
if (session_id.empty()) return S_OK;
|
|
916
|
+
auto* v = getView(view_id);
|
|
917
|
+
if (!v) return S_OK;
|
|
918
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
919
|
+
for (auto it = v->oopif_sessions.begin(); it != v->oopif_sessions.end(); ) {
|
|
920
|
+
if (it->second == session_id) it = v->oopif_sessions.erase(it);
|
|
921
|
+
else ++it;
|
|
922
|
+
}
|
|
923
|
+
return S_OK;
|
|
924
|
+
}).Get(),
|
|
925
|
+
&v->target_detached_token);
|
|
926
|
+
v->oopif_event_tokens_registered = true;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
void finishResolveAndClickWv2(uint32_t view_id, uint32_t request_id, double x, double y,
|
|
930
|
+
double w, double h, double cx, double cy,
|
|
931
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
932
|
+
ViewHost* v = getView(view_id);
|
|
933
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
934
|
+
dispatchCdpClickWv2(v, cx, cy, button, click_count, modifiers);
|
|
935
|
+
// Edge runtime CDP → DOM trust=true (empirical; matches existing click cap).
|
|
936
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
937
|
+
",\"ok\":true,\"rect\":{\"x\":" + std::to_string(x) +
|
|
938
|
+
",\"y\":" + std::to_string(y) +
|
|
939
|
+
",\"width\":" + std::to_string(w) +
|
|
940
|
+
",\"height\":" + std::to_string(h) + "},"
|
|
941
|
+
"\"isTrustedEvent\":true}";
|
|
942
|
+
emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
struct FrameResolveOkWv2 { double x, y, w, h, cx, cy, iw, ih; };
|
|
946
|
+
|
|
947
|
+
// Parse a Runtime.evaluate response (regardless of session); on success forwards
|
|
948
|
+
// frame-local fields to `onOk`. Script's failure branch routes through error emit.
|
|
949
|
+
void parseEvalAndContinueWv2(uint32_t view_id, uint32_t request_id, bool ok, const std::string& evalResult,
|
|
950
|
+
std::function<void(const FrameResolveOkWv2&)> onOk) {
|
|
951
|
+
if (!ok) {
|
|
952
|
+
BUNITE_INFO("webview2/eval: Runtime.evaluate failed view=%u request=%u body=%.300s%s",
|
|
953
|
+
view_id, request_id, evalResult.c_str(),
|
|
954
|
+
evalResult.size() > 300 ? "..." : "");
|
|
955
|
+
emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "Runtime.evaluate failed"); return;
|
|
956
|
+
}
|
|
957
|
+
if (evalResult.find("\"exceptionDetails\"") != std::string::npos) {
|
|
958
|
+
emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "evaluate threw"); return;
|
|
959
|
+
}
|
|
960
|
+
std::string value = extractJsonValueRaw(evalResult, "value");
|
|
961
|
+
if (value.empty()) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "evaluate returned no value"); return; }
|
|
962
|
+
std::string okRaw = extractJsonValueRaw(value, "ok");
|
|
963
|
+
if (okRaw != "true") {
|
|
964
|
+
std::string code = extractJsonStringDecoded(value, "code");
|
|
965
|
+
if (code.empty()) code = "runtime_error";
|
|
966
|
+
emitResolveAndClickErrorWv2(view_id, request_id, code.c_str(), "");
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
onOk(FrameResolveOkWv2{
|
|
970
|
+
extractJsonDouble(value, "x"), extractJsonDouble(value, "y"),
|
|
971
|
+
extractJsonDouble(value, "w"), extractJsonDouble(value, "h"),
|
|
972
|
+
extractJsonDouble(value, "cx"), extractJsonDouble(value, "cy"),
|
|
973
|
+
extractJsonDouble(value, "iw"), extractJsonDouble(value, "ih"),
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
void evalInFrameWv2(uint32_t view_id, uint32_t request_id, const std::string& frameId,
|
|
978
|
+
const std::string& script,
|
|
979
|
+
std::function<void(const FrameResolveOkWv2&)> onOk) {
|
|
980
|
+
ViewHost* v = getView(view_id);
|
|
981
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
982
|
+
std::string session_id;
|
|
983
|
+
{
|
|
984
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
985
|
+
auto it = v->oopif_sessions.find(frameId);
|
|
986
|
+
if (it != v->oopif_sessions.end()) session_id = it->second;
|
|
987
|
+
}
|
|
988
|
+
if (!session_id.empty()) {
|
|
989
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
|
|
990
|
+
"\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
991
|
+
cdpCallForSession(v, session_id, L"Runtime.evaluate", evalParams,
|
|
992
|
+
[view_id, request_id, onOk](bool ok, std::string r) {
|
|
993
|
+
parseEvalAndContinueWv2(view_id, request_id, ok, r, onOk);
|
|
994
|
+
});
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
// In-process: createIsolatedWorld + Runtime.evaluate via main session.
|
|
998
|
+
std::string isoParams = "{\"frameId\":\"" + escapeJsonString(frameId) + "\",\"worldName\":\"bunite-rac\"}";
|
|
999
|
+
cdpCallWithResult(v, L"Page.createIsolatedWorld", isoParams,
|
|
1000
|
+
[view_id, request_id, script, onOk](bool ok, std::string isoResult) {
|
|
1001
|
+
if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "createIsolatedWorld failed"); return; }
|
|
1002
|
+
int contextId = 0;
|
|
1003
|
+
if (!extractJsonInt(isoResult, "executionContextId", contextId)) {
|
|
1004
|
+
emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "missing executionContextId"); return;
|
|
1005
|
+
}
|
|
1006
|
+
ViewHost* v2 = getView(view_id);
|
|
1007
|
+
if (!v2 || !v2->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1008
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) +
|
|
1009
|
+
"\",\"contextId\":" + std::to_string(contextId) +
|
|
1010
|
+
",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1011
|
+
cdpCallWithResult(v2, L"Runtime.evaluate", evalParams,
|
|
1012
|
+
[view_id, request_id, onOk](bool ok2, std::string r) {
|
|
1013
|
+
parseEvalAndContinueWv2(view_id, request_id, ok2, r, onOk);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
inline void bilinearMapWv2(const std::array<double, 8>& q, double iw, double ih,
|
|
1019
|
+
double fx, double fy, double& px, double& py) {
|
|
1020
|
+
const double u = (iw > 0) ? (fx / iw) : 0.0;
|
|
1021
|
+
const double v = (ih > 0) ? (fy / ih) : 0.0;
|
|
1022
|
+
px = (1-u)*(1-v)*q[0] + u*(1-v)*q[2] + u*v*q[4] + (1-u)*v*q[6];
|
|
1023
|
+
py = (1-u)*(1-v)*q[1] + u*(1-v)*q[3] + u*v*q[5] + (1-u)*v*q[7];
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// Recursive frame path lookup in Page.getFrameTree response (text-based parser).
|
|
1027
|
+
// Returns [main_frame_id, ..., target_frame_id]; empty if target not found.
|
|
1028
|
+
std::vector<std::string> findFramePathWv2(const std::string& node, const std::string& target) {
|
|
1029
|
+
std::string frame = extractJsonValueRaw(node, "frame");
|
|
1030
|
+
std::string this_id = extractJsonStringDecoded(frame, "id");
|
|
1031
|
+
if (this_id.empty()) return {};
|
|
1032
|
+
if (this_id == target) return {this_id};
|
|
1033
|
+
std::string children = extractJsonValueRaw(node, "childFrames");
|
|
1034
|
+
if (children.size() < 2 || children.front() != '[') return {};
|
|
1035
|
+
// Walk child array — each element is a JSON object {frame, childFrames?}.
|
|
1036
|
+
size_t pos = 1;
|
|
1037
|
+
while (pos < children.size() && children[pos] != ']') {
|
|
1038
|
+
while (pos < children.size() && (children[pos] == ' ' || children[pos] == ',')) ++pos;
|
|
1039
|
+
if (pos >= children.size() || children[pos] != '{') break;
|
|
1040
|
+
int depth = 0;
|
|
1041
|
+
size_t end = pos;
|
|
1042
|
+
bool inStr = false;
|
|
1043
|
+
while (end < children.size()) {
|
|
1044
|
+
char ch = children[end];
|
|
1045
|
+
if (inStr) { if (ch == '\\' && end + 1 < children.size()) ++end; else if (ch == '"') inStr = false; }
|
|
1046
|
+
else if (ch == '"') inStr = true;
|
|
1047
|
+
else if (ch == '{') ++depth;
|
|
1048
|
+
else if (ch == '}') { --depth; if (depth == 0) { ++end; break; } }
|
|
1049
|
+
++end;
|
|
1050
|
+
}
|
|
1051
|
+
auto sub = findFramePathWv2(children.substr(pos, end - pos), target);
|
|
1052
|
+
if (!sub.empty()) { sub.insert(sub.begin(), this_id); return sub; }
|
|
1053
|
+
pos = end;
|
|
1054
|
+
}
|
|
1055
|
+
return {};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
bool parseQuad8(const std::string& content, std::array<double, 8>& out) {
|
|
1059
|
+
if (content.size() < 2 || content.front() != '[' || content.back() != ']') return false;
|
|
1060
|
+
size_t pos = 1;
|
|
1061
|
+
for (int i = 0; i < 8; ++i) {
|
|
1062
|
+
while (pos < content.size() && (content[pos] == ' ' || content[pos] == ',')) ++pos;
|
|
1063
|
+
size_t end = pos;
|
|
1064
|
+
while (end < content.size() && content[end] != ',' && content[end] != ']') ++end;
|
|
1065
|
+
if (end == pos) return false;
|
|
1066
|
+
try { out[i] = std::stod(content.substr(pos, end - pos)); } catch (...) { return false; }
|
|
1067
|
+
pos = end;
|
|
1068
|
+
}
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Issue CDP on a specific OOPIF session, or main session if `session_id` empty.
|
|
1073
|
+
void cdpForChain(ViewHost* v, const std::string& session_id, const wchar_t* method,
|
|
1074
|
+
const std::string& params_json,
|
|
1075
|
+
std::function<void(bool, std::string)> cb) {
|
|
1076
|
+
if (session_id.empty()) cdpCallWithResult(v, method, params_json, std::move(cb));
|
|
1077
|
+
else cdpCallForSession(v, session_id, method, params_json, std::move(cb));
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
struct ChainStateWv2 {
|
|
1081
|
+
uint32_t view_id;
|
|
1082
|
+
uint32_t request_id;
|
|
1083
|
+
std::string targetFrameId;
|
|
1084
|
+
std::string script;
|
|
1085
|
+
int32_t button, click_count;
|
|
1086
|
+
uint32_t modifiers;
|
|
1087
|
+
std::vector<std::string> chain; // [main, ..., target]
|
|
1088
|
+
std::vector<std::array<double, 8>> link_quads;
|
|
1089
|
+
std::vector<std::pair<double, double>> ancestor_inner; // chain[1..N-2]'s iw/ih
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
void composeAndDispatchWv2(std::shared_ptr<ChainStateWv2> s, const FrameResolveOkWv2& fr);
|
|
1093
|
+
void fetchTargetEvalWv2(std::shared_ptr<ChainStateWv2> s);
|
|
1094
|
+
void fetchAncestorInnerWv2(std::shared_ptr<ChainStateWv2> s, size_t i);
|
|
1095
|
+
void fetchLinkWv2(std::shared_ptr<ChainStateWv2> s, size_t link_idx);
|
|
1096
|
+
|
|
1097
|
+
std::string sessionForChainIdxWv2(uint32_t view_id, const std::vector<std::string>& chain, size_t idx) {
|
|
1098
|
+
if (idx == 0) return {};
|
|
1099
|
+
ViewHost* v = getView(view_id);
|
|
1100
|
+
if (!v) return {};
|
|
1101
|
+
std::lock_guard<std::mutex> lk(v->oopif_sessions_mutex);
|
|
1102
|
+
auto it = v->oopif_sessions.find(chain[idx]);
|
|
1103
|
+
return (it != v->oopif_sessions.end()) ? it->second : std::string{};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
void fetchLinkWv2(std::shared_ptr<ChainStateWv2> s, size_t link_idx) {
|
|
1107
|
+
if (link_idx + 1 >= s->chain.size()) { fetchAncestorInnerWv2(s, 1); return; }
|
|
1108
|
+
const std::string parent_session = sessionForChainIdxWv2(s->view_id, s->chain, link_idx);
|
|
1109
|
+
const std::string& child_frameId = s->chain[link_idx + 1];
|
|
1110
|
+
ViewHost* v = getView(s->view_id);
|
|
1111
|
+
if (!v) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1112
|
+
std::string ownerParams = "{\"frameId\":\"" + escapeJsonString(child_frameId) + "\"}";
|
|
1113
|
+
cdpForChain(v, parent_session, L"DOM.getFrameOwner", ownerParams,
|
|
1114
|
+
[s, link_idx, parent_session](bool ok, std::string r) {
|
|
1115
|
+
if (!ok) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_found", "getFrameOwner failed"); return; }
|
|
1116
|
+
int backendNodeId = 0;
|
|
1117
|
+
if (!extractJsonInt(r, "backendNodeId", backendNodeId) || !backendNodeId) {
|
|
1118
|
+
emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_found", "no backendNodeId"); return;
|
|
1119
|
+
}
|
|
1120
|
+
ViewHost* v2 = getView(s->view_id);
|
|
1121
|
+
if (!v2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1122
|
+
std::string boxParams = "{\"backendNodeId\":" + std::to_string(backendNodeId) + "}";
|
|
1123
|
+
cdpForChain(v2, parent_session, L"DOM.getBoxModel", boxParams,
|
|
1124
|
+
[s, link_idx](bool ok2, std::string rb) {
|
|
1125
|
+
if (!ok2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "not_visible", "iframe has no box"); return; }
|
|
1126
|
+
std::string model = extractJsonValueRaw(rb, "model");
|
|
1127
|
+
std::string content = extractJsonValueRaw(model, "content");
|
|
1128
|
+
std::array<double, 8> quad{};
|
|
1129
|
+
if (!parseQuad8(content, quad)) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "bad quad"); return; }
|
|
1130
|
+
s->link_quads.push_back(quad);
|
|
1131
|
+
fetchLinkWv2(s, link_idx + 1);
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
void fetchAncestorInnerWv2(std::shared_ptr<ChainStateWv2> s, size_t i) {
|
|
1137
|
+
if (i + 1 >= s->chain.size()) { fetchTargetEvalWv2(s); return; }
|
|
1138
|
+
const std::string sid = sessionForChainIdxWv2(s->view_id, s->chain, i);
|
|
1139
|
+
ViewHost* v = getView(s->view_id);
|
|
1140
|
+
if (!v) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1141
|
+
std::string params = "{\"expression\":\"JSON.stringify({iw:innerWidth,ih:innerHeight})\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1142
|
+
cdpForChain(v, sid, L"Runtime.evaluate", params,
|
|
1143
|
+
[s, i](bool ok, std::string r) {
|
|
1144
|
+
if (!ok) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "ancestor eval failed"); return; }
|
|
1145
|
+
// Result: {"result":{"type":"string","value":"<json>"}}
|
|
1146
|
+
std::string value = extractJsonValueRaw(r, "value");
|
|
1147
|
+
if (value.size() < 2) { emitResolveAndClickErrorWv2(s->view_id, s->request_id, "runtime_error", "ancestor eval no value"); return; }
|
|
1148
|
+
// value is a JSON string literal — extractJsonValueRaw returns it with quotes; decode.
|
|
1149
|
+
std::string inner;
|
|
1150
|
+
for (size_t p = 1; p + 1 < value.size(); ++p) {
|
|
1151
|
+
if (value[p] == '\\' && p + 2 < value.size()) {
|
|
1152
|
+
char nxt = value[++p];
|
|
1153
|
+
switch (nxt) { case '"': inner += '"'; break; case '\\': inner += '\\'; break;
|
|
1154
|
+
case 'n': inner += '\n'; break; default: inner += nxt; }
|
|
1155
|
+
} else inner += value[p];
|
|
1156
|
+
}
|
|
1157
|
+
double iw = extractJsonDouble(inner, "iw"), ih = extractJsonDouble(inner, "ih");
|
|
1158
|
+
s->ancestor_inner.push_back({iw, ih});
|
|
1159
|
+
fetchAncestorInnerWv2(s, i + 1);
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
void fetchTargetEvalWv2(std::shared_ptr<ChainStateWv2> s) {
|
|
1164
|
+
evalInFrameWv2(s->view_id, s->request_id, s->targetFrameId, s->script,
|
|
1165
|
+
[s](const FrameResolveOkWv2& fr) { composeAndDispatchWv2(s, fr); });
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
void composeAndDispatchWv2(std::shared_ptr<ChainStateWv2> s, const FrameResolveOkWv2& fr) {
|
|
1169
|
+
auto mapCorner = [&](double fx, double fy, double& px, double& py) {
|
|
1170
|
+
double cur_x = fx, cur_y = fy;
|
|
1171
|
+
double cur_iw = fr.iw, cur_ih = fr.ih;
|
|
1172
|
+
for (size_t i = s->link_quads.size(); i-- > 0; ) {
|
|
1173
|
+
double mx, my;
|
|
1174
|
+
bilinearMapWv2(s->link_quads[i], cur_iw, cur_ih, cur_x, cur_y, mx, my);
|
|
1175
|
+
cur_x = mx; cur_y = my;
|
|
1176
|
+
if (i == 0) break;
|
|
1177
|
+
cur_iw = s->ancestor_inner[i - 1].first;
|
|
1178
|
+
cur_ih = s->ancestor_inner[i - 1].second;
|
|
1179
|
+
}
|
|
1180
|
+
px = cur_x; py = cur_y;
|
|
1181
|
+
};
|
|
1182
|
+
double pcx, pcy; mapCorner(fr.cx, fr.cy, pcx, pcy);
|
|
1183
|
+
double cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3;
|
|
1184
|
+
mapCorner(fr.x, fr.y, cx0, cy0);
|
|
1185
|
+
mapCorner(fr.x + fr.w, fr.y, cx1, cy1);
|
|
1186
|
+
mapCorner(fr.x + fr.w, fr.y + fr.h, cx2, cy2);
|
|
1187
|
+
mapCorner(fr.x, fr.y + fr.h, cx3, cy3);
|
|
1188
|
+
const double min_x = std::min(std::min(cx0, cx1), std::min(cx2, cx3));
|
|
1189
|
+
const double max_x = std::max(std::max(cx0, cx1), std::max(cx2, cx3));
|
|
1190
|
+
const double min_y = std::min(std::min(cy0, cy1), std::min(cy2, cy3));
|
|
1191
|
+
const double max_y = std::max(std::max(cy0, cy1), std::max(cy2, cy3));
|
|
1192
|
+
finishResolveAndClickWv2(s->view_id, s->request_id,
|
|
1193
|
+
min_x, min_y, max_x - min_x, max_y - min_y, pcx, pcy,
|
|
1194
|
+
s->button, s->click_count, s->modifiers);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Walk ancestor chain via Page.getFrameTree, compose bilinear transforms across
|
|
1198
|
+
// nested OOPIF/same-origin frames, dispatch click in main-page coords.
|
|
1199
|
+
void runFrameTargetedWv2(uint32_t view_id, uint32_t request_id, const std::string& frameId,
|
|
1200
|
+
const std::string& script,
|
|
1201
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1202
|
+
ViewHost* v = getView(view_id);
|
|
1203
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1204
|
+
cdpCallWithResult(v, L"Page.getFrameTree", "{}",
|
|
1205
|
+
[view_id, request_id, frameId, script, button, click_count, modifiers](bool ok, std::string r) {
|
|
1206
|
+
if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "getFrameTree failed"); return; }
|
|
1207
|
+
std::string root = extractJsonValueRaw(r, "frameTree");
|
|
1208
|
+
std::vector<std::string> chain = findFramePathWv2(root, frameId);
|
|
1209
|
+
if (chain.size() < 2) { emitResolveAndClickErrorWv2(view_id, request_id, "not_found", "frame not in tree"); return; }
|
|
1210
|
+
auto s = std::make_shared<ChainStateWv2>();
|
|
1211
|
+
s->view_id = view_id; s->request_id = request_id;
|
|
1212
|
+
s->targetFrameId = frameId; s->script = script;
|
|
1213
|
+
s->button = button; s->click_count = click_count; s->modifiers = modifiers;
|
|
1214
|
+
s->chain = std::move(chain);
|
|
1215
|
+
fetchLinkWv2(s, 0);
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
} // namespace
|
|
1219
|
+
|
|
1220
|
+
extern "C" {
|
|
1221
|
+
|
|
1222
|
+
BUNITE_EXPORT void bunite_view_resolve_and_click(
|
|
1223
|
+
uint32_t view_id, uint32_t request_id,
|
|
1224
|
+
const char* selector_c, const char* frame_id_c,
|
|
1225
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1226
|
+
ViewHost* v = getView(view_id);
|
|
1227
|
+
if (!v || !v->webview) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "view not ready"); return; }
|
|
1228
|
+
std::string selector = selector_c ? selector_c : "";
|
|
1229
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
1230
|
+
std::string script = buildResolveScriptWv2(selector);
|
|
1231
|
+
|
|
1232
|
+
if (frameId.empty()) {
|
|
1233
|
+
std::string evalParams = "{\"expression\":\"" + escapeJsonString(script) + "\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1234
|
+
cdpCallWithResult(v, L"Runtime.evaluate", evalParams,
|
|
1235
|
+
[view_id, request_id, button, click_count, modifiers](bool ok, std::string r) {
|
|
1236
|
+
parseEvalAndContinueWv2(view_id, request_id, ok, r,
|
|
1237
|
+
[view_id, request_id, button, click_count, modifiers](const FrameResolveOkWv2& fr) {
|
|
1238
|
+
finishResolveAndClickWv2(view_id, request_id,
|
|
1239
|
+
fr.x, fr.y, fr.w, fr.h, fr.cx, fr.cy,
|
|
1240
|
+
button, click_count, modifiers);
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Frame-targeted: lazy event subscription + setAutoAttach so OOPIF child
|
|
1247
|
+
// sessions populate v->oopif_sessions before frame eval routes.
|
|
1248
|
+
armOopifEvents(v);
|
|
1249
|
+
if (!v->oopif_autoattach_armed.exchange(true)) {
|
|
1250
|
+
cdpCallWithResult(v, L"Target.setAutoAttach",
|
|
1251
|
+
"{\"autoAttach\":true,\"flatten\":true,\"waitForDebuggerOnStart\":false}",
|
|
1252
|
+
[view_id, request_id, frameId, script, button, click_count, modifiers](bool ok, std::string) {
|
|
1253
|
+
if (!ok) { emitResolveAndClickErrorWv2(view_id, request_id, "runtime_error", "setAutoAttach failed"); return; }
|
|
1254
|
+
runFrameTargetedWv2(view_id, request_id, frameId, script, button, click_count, modifiers);
|
|
1255
|
+
});
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
runFrameTargetedWv2(view_id, request_id, frameId, script, button, click_count, modifiers);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
|
|
1262
|
+
int32_t /*interesting_only*/) {
|
|
1263
|
+
// CDP getFullAXTree accepts only depth/frameId — interesting-only filter is TS-side.
|
|
1264
|
+
ViewHost* v = getView(view_id);
|
|
1265
|
+
if (!v || !v->webview) { emitAxError(view_id, request_id, "not_supported", "view not ready"); return; }
|
|
1266
|
+
cdpCallWithResult(v, L"Accessibility.getFullAXTree", "{}",
|
|
1267
|
+
[view_id, request_id](bool ok, std::string result) {
|
|
1268
|
+
if (!ok) { emitAxError(view_id, request_id, "runtime_error", "getFullAXTree failed: " + result); return; }
|
|
1269
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1270
|
+
",\"ok\":true,\"tree\":" + result + "}";
|
|
1271
|
+
emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
1276
|
+
const char* format, int32_t /*quality*/) {
|
|
1277
|
+
ViewHost* v = getView(view_id);
|
|
1278
|
+
if (!v || !v->webview) {
|
|
1279
|
+
emitScreenshotError(view_id, request_id, "not_supported", "view not ready");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
std::string fmt = format ? format : "png";
|
|
1283
|
+
COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT cwv_fmt;
|
|
1284
|
+
std::string mime;
|
|
1285
|
+
if (fmt == "jpeg" || fmt == "jpg") {
|
|
1286
|
+
cwv_fmt = COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_JPEG;
|
|
1287
|
+
fmt = "jpeg"; mime = "image/jpeg";
|
|
1288
|
+
} else {
|
|
1289
|
+
cwv_fmt = COREWEBVIEW2_CAPTURE_PREVIEW_IMAGE_FORMAT_PNG;
|
|
1290
|
+
fmt = "png"; mime = "image/png";
|
|
1291
|
+
}
|
|
1292
|
+
ComPtr<IStream> stream;
|
|
1293
|
+
HRESULT hr = CreateStreamOnHGlobal(nullptr, TRUE, &stream);
|
|
1294
|
+
if (FAILED(hr) || !stream) {
|
|
1295
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "CreateStreamOnHGlobal failed");
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
auto lifetime = g_runtime.lifetime;
|
|
1299
|
+
v->webview->CapturePreview(
|
|
1300
|
+
cwv_fmt, stream.Get(),
|
|
1301
|
+
Microsoft::WRL::Callback<ICoreWebView2CapturePreviewCompletedHandler>(
|
|
1302
|
+
[lifetime, view_id, request_id, fmt, mime, stream](HRESULT hr2) -> HRESULT {
|
|
1303
|
+
if (!lifetime || !lifetime->alive.load()) return S_OK;
|
|
1304
|
+
if (FAILED(hr2)) {
|
|
1305
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "CapturePreview failed");
|
|
1306
|
+
return S_OK;
|
|
1307
|
+
}
|
|
1308
|
+
HGLOBAL hg = nullptr;
|
|
1309
|
+
if (FAILED(GetHGlobalFromStream(stream.Get(), &hg)) || !hg) {
|
|
1310
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "GetHGlobalFromStream failed");
|
|
1311
|
+
return S_OK;
|
|
1312
|
+
}
|
|
1313
|
+
const SIZE_T size = GlobalSize(hg);
|
|
1314
|
+
void* ptr = GlobalLock(hg);
|
|
1315
|
+
std::string b64 = ptr ? base64Encode(static_cast<const BYTE*>(ptr), static_cast<DWORD>(size)) : std::string{};
|
|
1316
|
+
if (ptr) GlobalUnlock(hg);
|
|
1317
|
+
if (b64.empty()) {
|
|
1318
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "base64 encode failed");
|
|
1319
|
+
return S_OK;
|
|
1320
|
+
}
|
|
1321
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1322
|
+
",\"ok\":true,\"format\":\"" + fmt +
|
|
1323
|
+
"\",\"mime\":\"" + mime +
|
|
1324
|
+
"\",\"dataBase64\":\"" + b64 + "\"}";
|
|
1325
|
+
emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
1326
|
+
return S_OK;
|
|
1327
|
+
}).Get());
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
BUNITE_EXPORT void bunite_view_scroll(uint32_t view_id, double dx, double dy,
|
|
1331
|
+
double x, double y, uint32_t modifiers) {
|
|
1332
|
+
ViewHost* v = getView(view_id);
|
|
1333
|
+
if (!v) return;
|
|
1334
|
+
std::string json = "{\"type\":\"mouseWheel\",\"x\":" + std::to_string(x) +
|
|
1335
|
+
",\"y\":" + std::to_string(y) +
|
|
1336
|
+
",\"deltaX\":" + std::to_string(dx) +
|
|
1337
|
+
",\"deltaY\":" + std::to_string(dy) +
|
|
1338
|
+
",\"modifiers\":" + std::to_string(modifiers) + "}";
|
|
1339
|
+
cdpCall(v, L"Input.dispatchMouseEvent", json);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
BUNITE_EXPORT void bunite_view_mouse(uint32_t view_id, int32_t action,
|
|
1343
|
+
double x, double y, int32_t button,
|
|
1344
|
+
uint32_t modifiers) {
|
|
1345
|
+
ViewHost* v = getView(view_id);
|
|
1346
|
+
if (!v) return;
|
|
1347
|
+
// CDP mouseMoved / mousePressed / mouseReleased. WV2 produces isTrusted=true
|
|
1348
|
+
// on the page for these (Edge runtime injects below DevTools surface).
|
|
1349
|
+
const char* type = (action == 0) ? "mouseMoved"
|
|
1350
|
+
: (action == 1) ? "mousePressed" : "mouseReleased";
|
|
1351
|
+
std::string json = "{\"type\":\"" + std::string(type) +
|
|
1352
|
+
"\",\"x\":" + std::to_string(x) +
|
|
1353
|
+
",\"y\":" + std::to_string(y) +
|
|
1354
|
+
",\"modifiers\":" + std::to_string(modifiers);
|
|
1355
|
+
if (action != 0) {
|
|
1356
|
+
const char* btn = (button == 2) ? "right" : (button == 1) ? "middle" : "left";
|
|
1357
|
+
json += ",\"button\":\"" + std::string(btn) + "\",\"clickCount\":1";
|
|
1358
|
+
}
|
|
1359
|
+
json += "}";
|
|
1360
|
+
cdpCall(v, L"Input.dispatchMouseEvent", json);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
|
|
1364
|
+
bool accept, const char* text) {
|
|
1365
|
+
ViewHost* v = getView(view_id);
|
|
1366
|
+
if (!v) return;
|
|
1367
|
+
auto it = v->pending_dialogs.find(request_id);
|
|
1368
|
+
if (it == v->pending_dialogs.end()) return;
|
|
1369
|
+
ViewHost::PendingDialog entry = std::move(it->second);
|
|
1370
|
+
v->pending_dialogs.erase(it);
|
|
1371
|
+
if (entry.args && accept) {
|
|
1372
|
+
// prompt: feed user text. WV2 ignores put_ResultText for non-prompt kinds.
|
|
1373
|
+
if (text && *text) entry.args->put_ResultText(utf8ToWide(text).c_str());
|
|
1374
|
+
entry.args->Accept();
|
|
1375
|
+
}
|
|
1376
|
+
// accept=false → no Accept() call → WV2 treats as dismiss (default behavior).
|
|
1377
|
+
if (entry.deferral) entry.deferral->Complete();
|
|
1378
|
+
}
|
|
1379
|
+
|
|
369
1380
|
BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
370
1381
|
ViewHost* v = getView(view_id);
|
|
371
1382
|
if (v && v->webview) v->webview->OpenDevToolsWindow();
|