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
|
@@ -2,14 +2,163 @@
|
|
|
2
2
|
|
|
3
3
|
#include "include/cef_version.h"
|
|
4
4
|
#include "include/cef_version_info.h"
|
|
5
|
+
#include "include/cef_parser.h"
|
|
6
|
+
#include "include/cef_values.h"
|
|
7
|
+
|
|
8
|
+
// CDP path — input dispatch (mouse wheel) and screenshot use CefBrowserHost::
|
|
9
|
+
// ExecuteDevToolsMethod. Replies arrive on the CefDevToolsMessageObserver.
|
|
10
|
+
// scroll: SendMouseWheelEvent doesn't reach the page in windowed CEF
|
|
11
|
+
// (verified empirically on Win 11 / CEF 119+).
|
|
12
|
+
// screenshot: PrintWindow PW_RENDERFULLCONTENT misses hardware-composited
|
|
13
|
+
// surfaces (returns all-black). Page.captureScreenshot is compositor-aware.
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <array>
|
|
16
|
+
#include <atomic>
|
|
17
|
+
#include <functional>
|
|
18
|
+
#include <memory>
|
|
19
|
+
#include <mutex>
|
|
20
|
+
#include <unordered_map>
|
|
21
|
+
#include <vector>
|
|
22
|
+
|
|
5
23
|
|
|
6
24
|
using bunite_win::runOnUiThreadSync;
|
|
7
25
|
using bunite_win::runOnCefUiThreadSync;
|
|
8
26
|
|
|
9
|
-
static constexpr int32_t BUNITE_ABI_VERSION =
|
|
27
|
+
static constexpr int32_t BUNITE_ABI_VERSION = 11;
|
|
10
28
|
|
|
11
29
|
namespace {
|
|
12
30
|
|
|
31
|
+
// CDP message routing: ExecuteDevToolsMethod returns a message_id; the
|
|
32
|
+
// singleton observer routes OnDevToolsMethodResult back to a stashed callback.
|
|
33
|
+
std::mutex g_cdp_cb_mutex;
|
|
34
|
+
std::unordered_map<int, std::function<void(bool, std::string)>> g_cdp_callbacks;
|
|
35
|
+
|
|
36
|
+
class BuniteDevToolsObserver : public CefDevToolsMessageObserver {
|
|
37
|
+
public:
|
|
38
|
+
void OnDevToolsMethodResult(CefRefPtr<CefBrowser>, int message_id, bool success,
|
|
39
|
+
const void* result, size_t result_size) override {
|
|
40
|
+
std::function<void(bool, std::string)> cb;
|
|
41
|
+
{
|
|
42
|
+
std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
|
|
43
|
+
auto it = g_cdp_callbacks.find(message_id);
|
|
44
|
+
if (it == g_cdp_callbacks.end()) return;
|
|
45
|
+
cb = std::move(it->second);
|
|
46
|
+
g_cdp_callbacks.erase(it);
|
|
47
|
+
}
|
|
48
|
+
std::string r;
|
|
49
|
+
if (result && result_size) r.assign(static_cast<const char*>(result), result_size);
|
|
50
|
+
cb(success, std::move(r));
|
|
51
|
+
}
|
|
52
|
+
void OnDevToolsEvent(CefRefPtr<CefBrowser> browser,
|
|
53
|
+
const CefString& method,
|
|
54
|
+
const void* params,
|
|
55
|
+
size_t params_size) override {
|
|
56
|
+
std::string m = method.ToString();
|
|
57
|
+
if (m != "Target.attachedToTarget" && m != "Target.detachedFromTarget") return;
|
|
58
|
+
std::string p;
|
|
59
|
+
if (params && params_size) p.assign(static_cast<const char*>(params), params_size);
|
|
60
|
+
CefRefPtr<CefValue> val = CefParseJSON(p, JSON_PARSER_RFC);
|
|
61
|
+
if (!val || val->GetType() != VTYPE_DICTIONARY) return;
|
|
62
|
+
auto d = val->GetDictionary();
|
|
63
|
+
uint32_t view_id = 0;
|
|
64
|
+
{
|
|
65
|
+
std::lock_guard<std::mutex> lk(g_runtime.object_mutex);
|
|
66
|
+
auto it = g_runtime.browser_to_view_id.find(browser->GetIdentifier());
|
|
67
|
+
if (it == g_runtime.browser_to_view_id.end()) return;
|
|
68
|
+
view_id = it->second;
|
|
69
|
+
}
|
|
70
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
71
|
+
if (!view) return;
|
|
72
|
+
if (m == "Target.attachedToTarget") {
|
|
73
|
+
std::string session_id = d->HasKey("sessionId") ? d->GetString("sessionId").ToString() : "";
|
|
74
|
+
if (session_id.empty()) return;
|
|
75
|
+
auto info = d->HasKey("targetInfo") ? d->GetDictionary("targetInfo") : nullptr;
|
|
76
|
+
if (!info) return;
|
|
77
|
+
std::string type = info->HasKey("type") ? info->GetString("type").ToString() : "";
|
|
78
|
+
std::string target_id = info->HasKey("targetId") ? info->GetString("targetId").ToString() : "";
|
|
79
|
+
// For iframe targets in modern Chromium, targetId is the devtools frame
|
|
80
|
+
// token — identical to Page.FrameId. Spike-verified per OOPIF plan.
|
|
81
|
+
if (type != "iframe" || target_id.empty()) return;
|
|
82
|
+
std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
|
|
83
|
+
view->oopif_sessions[target_id] = session_id;
|
|
84
|
+
} else {
|
|
85
|
+
std::string session_id = d->HasKey("sessionId") ? d->GetString("sessionId").ToString() : "";
|
|
86
|
+
if (session_id.empty()) return;
|
|
87
|
+
std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
|
|
88
|
+
for (auto it = view->oopif_sessions.begin(); it != view->oopif_sessions.end(); ) {
|
|
89
|
+
if (it->second == session_id) it = view->oopif_sessions.erase(it);
|
|
90
|
+
else ++it;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
void OnDevToolsAgentDetached(CefRefPtr<CefBrowser>) override {
|
|
95
|
+
// Pending method results are dropped by CEF on detach (browser crash,
|
|
96
|
+
// process restart). Fire all callbacks with a failure result to prevent
|
|
97
|
+
// hung promises in the TS layer.
|
|
98
|
+
std::unordered_map<int, std::function<void(bool, std::string)>> orphans;
|
|
99
|
+
{
|
|
100
|
+
std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
|
|
101
|
+
orphans.swap(g_cdp_callbacks);
|
|
102
|
+
}
|
|
103
|
+
for (auto& kv : orphans) kv.second(false, "{\"error\":\"devtools_agent_detached\"}");
|
|
104
|
+
}
|
|
105
|
+
IMPLEMENT_REFCOUNTING(BuniteDevToolsObserver);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
CefRefPtr<CefDevToolsMessageObserver> getDevToolsObserver() {
|
|
109
|
+
static CefRefPtr<CefDevToolsMessageObserver> obs = new BuniteDevToolsObserver();
|
|
110
|
+
return obs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Raw-id space for SendDevToolsMessage (sessionId-routed calls). High range
|
|
114
|
+
// avoids collision with CEF's internal counter used by ExecuteDevToolsMethod.
|
|
115
|
+
std::atomic<int> g_raw_cdp_id_counter{0x40000000};
|
|
116
|
+
int nextRawCdpId() { return ++g_raw_cdp_id_counter; }
|
|
117
|
+
|
|
118
|
+
void cefSendRaw(ViewHost* v, const std::string& message, int id_for_cb,
|
|
119
|
+
std::function<void(bool, std::string)> cb) {
|
|
120
|
+
if (!v || !v->browser) { if (cb) cb(false, "{\"error\":\"view not ready\"}"); return; }
|
|
121
|
+
if (cb) {
|
|
122
|
+
std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
|
|
123
|
+
g_cdp_callbacks[id_for_cb] = std::move(cb);
|
|
124
|
+
}
|
|
125
|
+
if (!v->browser->GetHost()->SendDevToolsMessage(message.data(), message.size())) {
|
|
126
|
+
std::function<void(bool, std::string)> orphan;
|
|
127
|
+
{
|
|
128
|
+
std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
|
|
129
|
+
auto it = g_cdp_callbacks.find(id_for_cb);
|
|
130
|
+
if (it != g_cdp_callbacks.end()) { orphan = std::move(it->second); g_cdp_callbacks.erase(it); }
|
|
131
|
+
}
|
|
132
|
+
if (orphan) orphan(false, "{\"error\":\"SendDevToolsMessage failed\"}");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
void cefCdpCall(ViewHost* v, const std::string& method, const std::string& params_json,
|
|
137
|
+
std::function<void(bool, std::string)> cb = nullptr) {
|
|
138
|
+
if (!v || !v->browser) {
|
|
139
|
+
if (cb) cb(false, "{}");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
CefRefPtr<CefDictionaryValue> params;
|
|
143
|
+
if (!params_json.empty()) {
|
|
144
|
+
CefRefPtr<CefValue> val = CefParseJSON(params_json, JSON_PARSER_RFC);
|
|
145
|
+
if (val && val->GetType() == VTYPE_DICTIONARY) params = val->GetDictionary();
|
|
146
|
+
}
|
|
147
|
+
if (!params) params = CefDictionaryValue::Create();
|
|
148
|
+
// Register the callback under the *assigned* message id (ExecuteDevToolsMethod
|
|
149
|
+
// returns it). We supply 0 to let CEF assign so our counter can't collide
|
|
150
|
+
// with CEF's internal counter.
|
|
151
|
+
const int assigned_id = v->browser->GetHost()->ExecuteDevToolsMethod(0, method, params);
|
|
152
|
+
if (assigned_id == 0) {
|
|
153
|
+
if (cb) cb(false, "{\"error\":\"ExecuteDevToolsMethod failed\"}");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (cb) {
|
|
157
|
+
std::lock_guard<std::mutex> lk(g_cdp_cb_mutex);
|
|
158
|
+
g_cdp_callbacks[assigned_id] = std::move(cb);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
13
162
|
std::string wideToUtf8(const std::wstring& value) {
|
|
14
163
|
if (value.empty()) return {};
|
|
15
164
|
const int bytes = WideCharToMultiByte(
|
|
@@ -48,6 +197,24 @@ std::string resolveProcessHelperPath() {
|
|
|
48
197
|
|
|
49
198
|
} // namespace
|
|
50
199
|
|
|
200
|
+
namespace bunite_win {
|
|
201
|
+
void registerCdpObserverForView(ViewHost* view) {
|
|
202
|
+
if (!view || !view->browser) return;
|
|
203
|
+
view->devtools_registration =
|
|
204
|
+
view->browser->GetHost()->AddDevToolsMessageObserver(getDevToolsObserver());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
void respondToDialogRequest(ViewHost* view, uint32_t request_id,
|
|
208
|
+
bool accept, const std::string& text) {
|
|
209
|
+
if (!view) return;
|
|
210
|
+
auto it = view->pending_dialogs.find(request_id);
|
|
211
|
+
if (it == view->pending_dialogs.end()) return;
|
|
212
|
+
CefRefPtr<CefJSDialogCallback> cb = std::move(it->second);
|
|
213
|
+
view->pending_dialogs.erase(it);
|
|
214
|
+
if (cb) cb->Continue(accept, text);
|
|
215
|
+
}
|
|
216
|
+
} // namespace bunite_win
|
|
217
|
+
|
|
51
218
|
extern "C" BUNITE_EXPORT int32_t bunite_abi_version(void) {
|
|
52
219
|
return BUNITE_ABI_VERSION;
|
|
53
220
|
}
|
|
@@ -79,12 +246,26 @@ extern "C" BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
|
|
|
79
246
|
buniteSetLogLevel(static_cast<BuniteLogLevel>(level));
|
|
80
247
|
}
|
|
81
248
|
|
|
249
|
+
// See webview2_runtime.cpp `reapChildrenOnExit`. Same rationale for process_helper.
|
|
250
|
+
static void reapChildrenOnExit() {
|
|
251
|
+
HANDLE job = CreateJobObjectW(nullptr, nullptr);
|
|
252
|
+
if (!job) return;
|
|
253
|
+
JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{};
|
|
254
|
+
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
255
|
+
if (!SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info, sizeof(info)) ||
|
|
256
|
+
!AssignProcessToJobObject(job, GetCurrentProcess())) {
|
|
257
|
+
CloseHandle(job);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
82
261
|
extern "C" BUNITE_EXPORT bool bunite_init(
|
|
83
262
|
const char* cef_dir,
|
|
84
263
|
bool hide_console,
|
|
85
264
|
bool popup_blocking,
|
|
86
265
|
const char* engine_config_json
|
|
87
266
|
) {
|
|
267
|
+
buniteApplyEnvLogLevel();
|
|
268
|
+
reapChildrenOnExit();
|
|
88
269
|
{
|
|
89
270
|
std::lock_guard<std::mutex> lock(g_runtime.lifecycle_mutex);
|
|
90
271
|
if (g_runtime.initialized) {
|
|
@@ -857,6 +1038,1018 @@ extern "C" BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) {
|
|
|
857
1038
|
bunite_win::postCefUiTask([view_id]() { bunite_win::closeViewHost(bunite_win::getViewHostById(view_id)); });
|
|
858
1039
|
}
|
|
859
1040
|
|
|
1041
|
+
// Input dispatch — native CEF API. Real OS-input path → MouseEvent.isTrusted = true.
|
|
1042
|
+
namespace {
|
|
1043
|
+
|
|
1044
|
+
// Local names — `MOD_ALT`/`MOD_SHIFT` are Win32 RegisterHotKey macros.
|
|
1045
|
+
constexpr uint32_t kBmodAlt = 1, kBmodCtrl = 2, kBmodMeta = 4, kBmodShift = 8;
|
|
1046
|
+
|
|
1047
|
+
uint32_t cefModifiers(uint32_t bits) {
|
|
1048
|
+
uint32_t flags = 0;
|
|
1049
|
+
if (bits & kBmodShift) flags |= EVENTFLAG_SHIFT_DOWN;
|
|
1050
|
+
if (bits & kBmodCtrl) flags |= EVENTFLAG_CONTROL_DOWN;
|
|
1051
|
+
if (bits & kBmodAlt) flags |= EVENTFLAG_ALT_DOWN;
|
|
1052
|
+
if (bits & kBmodMeta) flags |= EVENTFLAG_COMMAND_DOWN;
|
|
1053
|
+
return flags;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
cef_mouse_button_type_t cefButton(int32_t b) {
|
|
1057
|
+
switch (b) { case 1: return MBT_MIDDLE; case 2: return MBT_RIGHT; default: return MBT_LEFT; }
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
} // namespace
|
|
1061
|
+
|
|
1062
|
+
extern "C" BUNITE_EXPORT void bunite_view_click(uint32_t view_id, double x, double y,
|
|
1063
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1064
|
+
if (click_count < 1) click_count = 1;
|
|
1065
|
+
bunite_win::postCefUiTask([view_id, x, y, button, click_count, modifiers]() {
|
|
1066
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1067
|
+
if (!view || !view->browser) return;
|
|
1068
|
+
auto host = view->browser->GetHost();
|
|
1069
|
+
if (!host) return;
|
|
1070
|
+
CefMouseEvent ev{};
|
|
1071
|
+
ev.x = static_cast<int>(x);
|
|
1072
|
+
ev.y = static_cast<int>(y);
|
|
1073
|
+
ev.modifiers = cefModifiers(modifiers);
|
|
1074
|
+
// Multi-click → repeated pairs with increasing clickCount so the page sees dblclick.
|
|
1075
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
1076
|
+
host->SendMouseClickEvent(ev, cefButton(button), /*mouseUp=*/false, i);
|
|
1077
|
+
host->SendMouseClickEvent(ev, cefButton(button), /*mouseUp=*/true, i);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
extern "C" BUNITE_EXPORT void bunite_view_type(uint32_t view_id, const char* text) {
|
|
1083
|
+
std::string s = text ? text : "";
|
|
1084
|
+
bunite_win::postCefUiTask([view_id, s]() {
|
|
1085
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1086
|
+
if (!view || !view->browser) return;
|
|
1087
|
+
auto host = view->browser->GetHost();
|
|
1088
|
+
if (!host) return;
|
|
1089
|
+
std::wstring wide(s.size(), 0);
|
|
1090
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()),
|
|
1091
|
+
wide.data(), static_cast<int>(wide.size()));
|
|
1092
|
+
wide.resize(n);
|
|
1093
|
+
// Per character: RAWKEYDOWN + CHAR + KEYUP. CHAR-only doesn't trigger the
|
|
1094
|
+
// DOM `input` event on text fields — Chromium expects a paired key cycle.
|
|
1095
|
+
// BMP only (surrogate pairs would mis-fire `keypress` twice).
|
|
1096
|
+
for (size_t i = 0; i < wide.size(); ++i) {
|
|
1097
|
+
wchar_t ch = wide[i];
|
|
1098
|
+
if (ch >= 0xD800 && ch <= 0xDBFF) {
|
|
1099
|
+
static bool warned = false;
|
|
1100
|
+
if (!warned) { warned = true; BUNITE_WARN("cef type: supplementary-plane codepoint skipped"); }
|
|
1101
|
+
if (i + 1 < wide.size()) ++i;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const int vk = (ch >= 'a' && ch <= 'z') ? (ch - ('a' - 'A')) // letters map to upper VK
|
|
1105
|
+
: (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') ? ch
|
|
1106
|
+
: 0;
|
|
1107
|
+
CefKeyEvent down{};
|
|
1108
|
+
down.type = KEYEVENT_RAWKEYDOWN;
|
|
1109
|
+
down.windows_key_code = vk ? vk : ch;
|
|
1110
|
+
down.character = ch;
|
|
1111
|
+
down.unmodified_character = ch;
|
|
1112
|
+
host->SendKeyEvent(down);
|
|
1113
|
+
CefKeyEvent c{};
|
|
1114
|
+
c.type = KEYEVENT_CHAR;
|
|
1115
|
+
c.windows_key_code = ch;
|
|
1116
|
+
c.character = ch;
|
|
1117
|
+
c.unmodified_character = ch;
|
|
1118
|
+
host->SendKeyEvent(c);
|
|
1119
|
+
CefKeyEvent up{};
|
|
1120
|
+
up.type = KEYEVENT_KEYUP;
|
|
1121
|
+
up.windows_key_code = vk ? vk : ch;
|
|
1122
|
+
up.character = ch;
|
|
1123
|
+
up.unmodified_character = ch;
|
|
1124
|
+
host->SendKeyEvent(up);
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t view_id, int32_t windows_vk_code,
|
|
1130
|
+
int32_t /*mac_key_code*/,
|
|
1131
|
+
const char* /*key*/, const char* /*code*/,
|
|
1132
|
+
const char* character, uint32_t modifiers,
|
|
1133
|
+
int32_t action, bool extended, int32_t /*location*/) {
|
|
1134
|
+
std::string char_str = character ? character : "";
|
|
1135
|
+
bunite_win::postCefUiTask([view_id, windows_vk_code, char_str, modifiers, action, extended]() {
|
|
1136
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1137
|
+
if (!view || !view->browser) return;
|
|
1138
|
+
auto host = view->browser->GetHost();
|
|
1139
|
+
if (!host) return;
|
|
1140
|
+
uint32_t mod = cefModifiers(modifiers);
|
|
1141
|
+
// Chromium's KeycodeConverter::NativeKeycodeToDomCode expects raw scancode
|
|
1142
|
+
// with 0xE0 prefix when extended (see chromium ui/events dom_code_data.inc).
|
|
1143
|
+
// Not LPARAM — Chromium's Win backend keys off scancode|(extended ? 0xE000 : 0).
|
|
1144
|
+
UINT scancode = windows_vk_code ? MapVirtualKeyW(static_cast<UINT>(windows_vk_code), MAPVK_VK_TO_VSC) : 0;
|
|
1145
|
+
int32_t native = static_cast<int32_t>(extended ? (0xE000u | scancode) : scancode);
|
|
1146
|
+
|
|
1147
|
+
if (action != 1 && windows_vk_code != 0) {
|
|
1148
|
+
CefKeyEvent down{};
|
|
1149
|
+
down.type = KEYEVENT_RAWKEYDOWN;
|
|
1150
|
+
down.windows_key_code = windows_vk_code;
|
|
1151
|
+
down.native_key_code = native;
|
|
1152
|
+
down.modifiers = mod;
|
|
1153
|
+
host->SendKeyEvent(down);
|
|
1154
|
+
}
|
|
1155
|
+
// CHAR rides with the down half (Playwright convention) — emitted only
|
|
1156
|
+
// when we're sending the down (action=both or down).
|
|
1157
|
+
if (action != 1 && !char_str.empty()) {
|
|
1158
|
+
std::wstring wide(char_str.size(), 0);
|
|
1159
|
+
int n = MultiByteToWideChar(CP_UTF8, 0, char_str.c_str(), static_cast<int>(char_str.size()),
|
|
1160
|
+
wide.data(), static_cast<int>(wide.size()));
|
|
1161
|
+
wide.resize(n);
|
|
1162
|
+
for (size_t i = 0; i < wide.size(); ++i) {
|
|
1163
|
+
wchar_t ch = wide[i];
|
|
1164
|
+
if (ch >= 0xD800 && ch <= 0xDBFF) {
|
|
1165
|
+
if (i + 1 < wide.size()) ++i;
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
CefKeyEvent ce{};
|
|
1169
|
+
ce.type = KEYEVENT_CHAR;
|
|
1170
|
+
ce.character = ch;
|
|
1171
|
+
ce.unmodified_character = ch;
|
|
1172
|
+
ce.windows_key_code = ch;
|
|
1173
|
+
ce.modifiers = mod;
|
|
1174
|
+
host->SendKeyEvent(ce);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (action != 0 && windows_vk_code != 0) {
|
|
1178
|
+
CefKeyEvent up{};
|
|
1179
|
+
up.type = KEYEVENT_KEYUP;
|
|
1180
|
+
up.windows_key_code = windows_vk_code;
|
|
1181
|
+
up.native_key_code = native;
|
|
1182
|
+
up.modifiers = mod;
|
|
1183
|
+
host->SendKeyEvent(up);
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t view_id, double dx, double dy,
|
|
1189
|
+
double x, double y, uint32_t modifiers) {
|
|
1190
|
+
bunite_win::postCefUiTask([view_id, dx, dy, x, y, modifiers]() {
|
|
1191
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1192
|
+
if (!view) return;
|
|
1193
|
+
// CDP path — native SendMouseWheelEvent doesn't reach the page in
|
|
1194
|
+
// windowed CEF. deltaY is CSS pixels, matching WV2.
|
|
1195
|
+
std::string params =
|
|
1196
|
+
"{\"type\":\"mouseWheel\""
|
|
1197
|
+
",\"x\":" + std::to_string(x) +
|
|
1198
|
+
",\"y\":" + std::to_string(y) +
|
|
1199
|
+
",\"deltaX\":" + std::to_string(dx) +
|
|
1200
|
+
",\"deltaY\":" + std::to_string(dy) +
|
|
1201
|
+
",\"modifiers\":" + std::to_string(modifiers) + "}";
|
|
1202
|
+
cefCdpCall(view, "Input.dispatchMouseEvent", params);
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t view_id, int32_t action,
|
|
1207
|
+
double x, double y, int32_t button,
|
|
1208
|
+
uint32_t modifiers) {
|
|
1209
|
+
bunite_win::postCefUiTask([view_id, action, x, y, button, modifiers]() {
|
|
1210
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1211
|
+
if (!view || !view->browser) return;
|
|
1212
|
+
auto host = view->browser->GetHost();
|
|
1213
|
+
if (!host) return;
|
|
1214
|
+
CefMouseEvent ev{};
|
|
1215
|
+
ev.x = static_cast<int>(x);
|
|
1216
|
+
ev.y = static_cast<int>(y);
|
|
1217
|
+
ev.modifiers = cefModifiers(modifiers);
|
|
1218
|
+
if (action == 0) {
|
|
1219
|
+
// move
|
|
1220
|
+
host->SendMouseMoveEvent(ev, /*mouseLeave=*/false);
|
|
1221
|
+
} else {
|
|
1222
|
+
// down (1) / up (2)
|
|
1223
|
+
CefBrowserHost::MouseButtonType btn = (button == 2) ? MBT_RIGHT
|
|
1224
|
+
: (button == 1) ? MBT_MIDDLE : MBT_LEFT;
|
|
1225
|
+
host->SendMouseClickEvent(ev, btn, /*mouseUp=*/action == 2, /*clickCount=*/1);
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
|
|
1231
|
+
bool accept, const char* text) {
|
|
1232
|
+
std::string text_str = text ? text : "";
|
|
1233
|
+
bunite_win::postCefUiTask([view_id, request_id, accept, text_str]() {
|
|
1234
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1235
|
+
if (!view) return;
|
|
1236
|
+
bunite_win::respondToDialogRequest(view, request_id, accept, text_str);
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
namespace {
|
|
1241
|
+
|
|
1242
|
+
void emitScreenshotError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
|
|
1243
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1244
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
1245
|
+
"\"message\":\"" + bunite_win::escapeJsonString(msg) + "\"}";
|
|
1246
|
+
bunite_win::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
} // namespace
|
|
1250
|
+
|
|
1251
|
+
extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
1252
|
+
// CEF — click/type/press are native (isTrusted=true); scroll and
|
|
1253
|
+
// screenshot go via CDP (windowed SendMouseWheelEvent doesn't reach the
|
|
1254
|
+
// page, and Page.captureScreenshot is compositor-aware vs PrintWindow's
|
|
1255
|
+
// black-frame trap).
|
|
1256
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1257
|
+
if (!view) return 0;
|
|
1258
|
+
return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
|
|
1259
|
+
BUNITE_CAP_NATIVE_INPUT_TRUSTED |
|
|
1260
|
+
BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
|
|
1261
|
+
BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
1262
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
|
|
1263
|
+
BUNITE_CAP_AX | BUNITE_CAP_BOUNDING_RECT | BUNITE_CAP_FRAMES |
|
|
1264
|
+
BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
|
|
1265
|
+
BUNITE_CAP_RESOLVE_AND_CLICK;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
|
|
1269
|
+
bunite_win::postCefUiTask([view_id, policy, dir = std::string(download_dir ? download_dir : "")]() {
|
|
1270
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1271
|
+
if (!view) return;
|
|
1272
|
+
int32_t p = policy;
|
|
1273
|
+
if (p < 0 || p > 2) p = 2;
|
|
1274
|
+
view->download_policy.store(p);
|
|
1275
|
+
view->download_dir = dir;
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
namespace bunite_win {
|
|
1280
|
+
|
|
1281
|
+
void applyPopupAccept(ViewHost* view, uint32_t host_window_id, double x, double y, double w, double h) {
|
|
1282
|
+
if (!view || !view->browser) return;
|
|
1283
|
+
auto* host = getWindowHostById(host_window_id);
|
|
1284
|
+
if (!host || !host->hwnd) return;
|
|
1285
|
+
view->window = host;
|
|
1286
|
+
view->is_popup_pending = false;
|
|
1287
|
+
host->views.push_back(view);
|
|
1288
|
+
HWND browser_hwnd = view->browser->GetHost()->GetWindowHandle();
|
|
1289
|
+
if (browser_hwnd) {
|
|
1290
|
+
SetParent(browser_hwnd, host->hwnd);
|
|
1291
|
+
SetWindowPos(browser_hwnd, HWND_TOP,
|
|
1292
|
+
static_cast<int>(x), static_cast<int>(y),
|
|
1293
|
+
static_cast<int>(w), static_cast<int>(h),
|
|
1294
|
+
SWP_SHOWWINDOW | SWP_NOACTIVATE);
|
|
1295
|
+
view->bounds = RECT{
|
|
1296
|
+
static_cast<LONG>(x), static_cast<LONG>(y),
|
|
1297
|
+
static_cast<LONG>(x + w), static_cast<LONG>(y + h)
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
// Re-emit view-ready so the TS-side BrowserView.adopt resolves its
|
|
1301
|
+
// `_readyPromise` (the original view-ready from OnAfterCreated fired before
|
|
1302
|
+
// the TS resolver was registered).
|
|
1303
|
+
emitWebviewEvent(view->id, "view-ready", "");
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
} // namespace bunite_win
|
|
1307
|
+
|
|
1308
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
|
|
1309
|
+
double x, double y, double w, double h) {
|
|
1310
|
+
bunite_win::postCefUiTask([new_view_id, host_window_id, x, y, w, h]() {
|
|
1311
|
+
auto* view = bunite_win::getViewHostById(new_view_id);
|
|
1312
|
+
if (!view) return;
|
|
1313
|
+
if (!view->browser) {
|
|
1314
|
+
// OnAfterCreated hasn't fired yet; stash the accept and apply when it does.
|
|
1315
|
+
view->pending_popup_accept = ViewHost::PendingPopupAccept{host_window_id, x, y, w, h};
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
bunite_win::applyPopupAccept(view, host_window_id, x, y, w, h);
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
|
|
1323
|
+
bunite_win::postCefUiTask([new_view_id]() {
|
|
1324
|
+
auto* view = bunite_win::getViewHostById(new_view_id);
|
|
1325
|
+
if (!view) return;
|
|
1326
|
+
if (!view->is_popup_pending && view->window) return; // already adopted — caller responsibility, ignore.
|
|
1327
|
+
if (view->browser) {
|
|
1328
|
+
view->closing.store(true);
|
|
1329
|
+
view->browser->GetHost()->CloseBrowser(true);
|
|
1330
|
+
} else {
|
|
1331
|
+
// Browser not yet created — let OnAfterCreated handle dismissal.
|
|
1332
|
+
view->popup_dismiss_requested = true;
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
1338
|
+
const char* format, int32_t quality) {
|
|
1339
|
+
std::string fmt = format ? format : "png";
|
|
1340
|
+
bunite_win::postCefUiTask([view_id, request_id, fmt, quality]() {
|
|
1341
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1342
|
+
if (!view) {
|
|
1343
|
+
emitScreenshotError(view_id, request_id, "not_supported", "view not ready");
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const bool jpeg = (fmt == "jpeg" || fmt == "jpg");
|
|
1347
|
+
const std::string outFmt = jpeg ? "jpeg" : "png";
|
|
1348
|
+
const std::string mime = jpeg ? "image/jpeg" : "image/png";
|
|
1349
|
+
std::string params = "{\"format\":\"" + outFmt + "\"";
|
|
1350
|
+
if (jpeg && quality >= 0) params += ",\"quality\":" + std::to_string(quality);
|
|
1351
|
+
params += "}";
|
|
1352
|
+
cefCdpCall(view, "Page.captureScreenshot", params,
|
|
1353
|
+
[view_id, request_id, outFmt, mime](bool ok, std::string result) {
|
|
1354
|
+
if (!ok) {
|
|
1355
|
+
emitScreenshotError(view_id, request_id, "runtime_error",
|
|
1356
|
+
std::string("Page.captureScreenshot failed: ") + result);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
CefRefPtr<CefValue> val = CefParseJSON(result, JSON_PARSER_RFC);
|
|
1360
|
+
if (!val || val->GetType() != VTYPE_DICTIONARY) {
|
|
1361
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "captureScreenshot malformed result");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
CefString data = val->GetDictionary()->GetString("data");
|
|
1365
|
+
if (data.empty()) {
|
|
1366
|
+
emitScreenshotError(view_id, request_id, "runtime_error", "captureScreenshot missing data");
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
std::string b64 = data.ToString();
|
|
1370
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1371
|
+
",\"ok\":true,\"format\":\"" + outFmt +
|
|
1372
|
+
"\",\"mime\":\"" + mime +
|
|
1373
|
+
"\",\"dataBase64\":\"" + b64 + "\"}";
|
|
1374
|
+
bunite_win::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
static void emitAxError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
1380
|
+
std::string esc; esc.reserve(message.size());
|
|
1381
|
+
for (char c : message) {
|
|
1382
|
+
if (c == '"' || c == '\\') { esc.push_back('\\'); esc.push_back(c); }
|
|
1383
|
+
else if (c == '\n') esc += "\\n";
|
|
1384
|
+
else if (c == '\r') esc += "\\r";
|
|
1385
|
+
else if (c == '\t') esc += "\\t";
|
|
1386
|
+
else esc.push_back(c);
|
|
1387
|
+
}
|
|
1388
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1389
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
1390
|
+
"\",\"message\":\"" + esc + "\"}";
|
|
1391
|
+
bunite_win::emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
extern "C" BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
|
|
1395
|
+
int32_t /*interesting_only*/) {
|
|
1396
|
+
// CDP `Accessibility.getFullAXTree` takes `depth`/`frameId` only; the
|
|
1397
|
+
// interesting-only filter is applied TS-side on the flat node list.
|
|
1398
|
+
bunite_win::postCefUiTask([view_id, request_id]() {
|
|
1399
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1400
|
+
if (!view) { emitAxError(view_id, request_id, "not_supported", "view not ready"); return; }
|
|
1401
|
+
cefCdpCall(view, "Accessibility.getFullAXTree", "{}",
|
|
1402
|
+
[view_id, request_id](bool ok, std::string result) {
|
|
1403
|
+
if (!ok) { emitAxError(view_id, request_id, "runtime_error", "getFullAXTree failed: " + result); return; }
|
|
1404
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1405
|
+
",\"ok\":true,\"tree\":" + result + "}";
|
|
1406
|
+
bunite_win::emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
static void emitListFramesError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& message) {
|
|
1412
|
+
std::string esc; esc.reserve(message.size());
|
|
1413
|
+
for (char c : message) {
|
|
1414
|
+
if (c == '"' || c == '\\') { esc.push_back('\\'); esc.push_back(c); }
|
|
1415
|
+
else esc.push_back(c);
|
|
1416
|
+
}
|
|
1417
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1418
|
+
",\"ok\":false,\"code\":\"" + code +
|
|
1419
|
+
"\",\"message\":\"" + esc + "\"}";
|
|
1420
|
+
bunite_win::emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
extern "C" BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
|
|
1424
|
+
bunite_win::postCefUiTask([view_id, request_id]() {
|
|
1425
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1426
|
+
if (!view) { emitListFramesError(view_id, request_id, "not_supported", "view not ready"); return; }
|
|
1427
|
+
cefCdpCall(view, "Page.getFrameTree", "{}",
|
|
1428
|
+
[view_id, request_id](bool ok, std::string result) {
|
|
1429
|
+
if (!ok) { emitListFramesError(view_id, request_id, "runtime_error", "getFrameTree failed: " + result); return; }
|
|
1430
|
+
// Raw CDP `{frameTree:{frame,childFrames}}` — TS flattens.
|
|
1431
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1432
|
+
",\"ok\":true,\"raw\":" + result + "}";
|
|
1433
|
+
bunite_win::emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
extern "C" BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
|
|
1439
|
+
const char* script_c, const char* frame_id_c) {
|
|
1440
|
+
std::string script = script_c ? script_c : "";
|
|
1441
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
1442
|
+
if (frameId.empty()) {
|
|
1443
|
+
bunite_view_evaluate(view_id, request_id, script_c);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
bunite_win::postCefUiTask([view_id, request_id, script, frameId]() {
|
|
1447
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1448
|
+
if (!view) {
|
|
1449
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1450
|
+
",\"ok\":false,\"code\":\"not_supported\",\"message\":\"view not ready\"}";
|
|
1451
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
// Step 1: create an isolated world in the target frame.
|
|
1455
|
+
std::string isoParams = "{\"frameId\":\"" + bunite_win::escapeJsonString(frameId) + "\",\"worldName\":\"bunite-eval\"}";
|
|
1456
|
+
cefCdpCall(view, "Page.createIsolatedWorld", isoParams,
|
|
1457
|
+
[view_id, request_id, script](bool ok, std::string isoResult) {
|
|
1458
|
+
if (!ok) {
|
|
1459
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1460
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
1461
|
+
"\"message\":\"createIsolatedWorld failed\"}";
|
|
1462
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
CefRefPtr<CefValue> val = CefParseJSON(isoResult, JSON_PARSER_RFC);
|
|
1466
|
+
if (!val || val->GetType() != VTYPE_DICTIONARY) {
|
|
1467
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1468
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
1469
|
+
"\"message\":\"createIsolatedWorld malformed\"}";
|
|
1470
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
int contextId = val->GetDictionary()->GetInt("executionContextId");
|
|
1474
|
+
// Re-lookup view — async gap may have destroyed it.
|
|
1475
|
+
auto* view2 = bunite_win::getViewHostById(view_id);
|
|
1476
|
+
if (!view2) {
|
|
1477
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1478
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
1479
|
+
"\"message\":\"view destroyed\"}";
|
|
1480
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
// Step 2: Runtime.evaluate in that context.
|
|
1484
|
+
std::string escScript; escScript.reserve(script.size());
|
|
1485
|
+
for (char c : script) {
|
|
1486
|
+
if (c == '"' || c == '\\') { escScript.push_back('\\'); escScript.push_back(c); }
|
|
1487
|
+
else if (c == '\n') escScript += "\\n";
|
|
1488
|
+
else if (c == '\r') escScript += "\\r";
|
|
1489
|
+
else if (c == '\t') escScript += "\\t";
|
|
1490
|
+
else escScript.push_back(c);
|
|
1491
|
+
}
|
|
1492
|
+
std::string evalParams = "{\"expression\":\"" + escScript +
|
|
1493
|
+
"\",\"contextId\":" + std::to_string(contextId) +
|
|
1494
|
+
",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1495
|
+
cefCdpCall(view2, "Runtime.evaluate", evalParams,
|
|
1496
|
+
[view_id, request_id](bool ok2, std::string evalResult) {
|
|
1497
|
+
if (!ok2) {
|
|
1498
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1499
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
1500
|
+
"\"message\":\"Runtime.evaluate failed\"}";
|
|
1501
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
CefRefPtr<CefValue> ev = CefParseJSON(evalResult, JSON_PARSER_RFC);
|
|
1505
|
+
if (!ev || ev->GetType() != VTYPE_DICTIONARY) {
|
|
1506
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1507
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
1508
|
+
"\"message\":\"Runtime.evaluate malformed\"}";
|
|
1509
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
CefRefPtr<CefDictionaryValue> d = ev->GetDictionary();
|
|
1513
|
+
if (d->HasKey("exceptionDetails")) {
|
|
1514
|
+
CefRefPtr<CefDictionaryValue> ex = d->GetDictionary("exceptionDetails");
|
|
1515
|
+
std::string msg = ex && ex->HasKey("text") ? ex->GetString("text").ToString() : "runtime exception";
|
|
1516
|
+
std::string escMsg; escMsg.reserve(msg.size());
|
|
1517
|
+
for (char c : msg) {
|
|
1518
|
+
if (c == '"' || c == '\\') { escMsg.push_back('\\'); escMsg.push_back(c); }
|
|
1519
|
+
else escMsg.push_back(c);
|
|
1520
|
+
}
|
|
1521
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1522
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
1523
|
+
"\"message\":\"" + escMsg + "\"}";
|
|
1524
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
// result.value (JSON-serialized into "value") -> stringify.
|
|
1528
|
+
CefRefPtr<CefDictionaryValue> result = d->GetDictionary("result");
|
|
1529
|
+
std::string valueJson = "null";
|
|
1530
|
+
if (result && result->HasKey("value")) {
|
|
1531
|
+
CefRefPtr<CefValue> v = result->GetValue("value");
|
|
1532
|
+
if (v) valueJson = CefWriteJSON(v, JSON_WRITER_DEFAULT);
|
|
1533
|
+
}
|
|
1534
|
+
// The host expects "value" to be a JSON STRING (it re-parses).
|
|
1535
|
+
std::string escVal; escVal.reserve(valueJson.size());
|
|
1536
|
+
for (char c : valueJson) {
|
|
1537
|
+
if (c == '"' || c == '\\') { escVal.push_back('\\'); escVal.push_back(c); }
|
|
1538
|
+
else if (c == '\n') escVal += "\\n";
|
|
1539
|
+
else if (c == '\r') escVal += "\\r";
|
|
1540
|
+
else if (c == '\t') escVal += "\\t";
|
|
1541
|
+
else escVal.push_back(c);
|
|
1542
|
+
}
|
|
1543
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1544
|
+
",\"ok\":true,\"value\":\"" + escVal + "\"}";
|
|
1545
|
+
bunite_win::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
1546
|
+
});
|
|
1547
|
+
});
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
namespace {
|
|
1552
|
+
|
|
1553
|
+
void emitResolveAndClickError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
|
|
1554
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1555
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
1556
|
+
"\"message\":\"" + bunite_win::escapeJsonString(msg) + "\"}";
|
|
1557
|
+
bunite_win::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
const char* cdpButtonName(int32_t b) {
|
|
1561
|
+
switch (b) { case 1: return "middle"; case 2: return "right"; default: return "left"; }
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
std::string escapeForJsString(const std::string& s) {
|
|
1565
|
+
std::string out; out.reserve(s.size() + 2);
|
|
1566
|
+
for (char c : s) {
|
|
1567
|
+
if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
|
|
1568
|
+
else if (c == '\n') out += "\\n";
|
|
1569
|
+
else if (c == '\r') out += "\\r";
|
|
1570
|
+
else if (c == '\t') out += "\\t";
|
|
1571
|
+
else out.push_back(c);
|
|
1572
|
+
}
|
|
1573
|
+
return out;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
std::string escapeForJsonString(const std::string& s) {
|
|
1577
|
+
std::string out; out.reserve(s.size());
|
|
1578
|
+
for (char c : s) {
|
|
1579
|
+
if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
|
|
1580
|
+
else if (c == '\n') out += "\\n";
|
|
1581
|
+
else if (c == '\r') out += "\\r";
|
|
1582
|
+
else if (c == '\t') out += "\\t";
|
|
1583
|
+
else out.push_back(c);
|
|
1584
|
+
}
|
|
1585
|
+
return out;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
std::string buildResolveScript(const std::string& selector) {
|
|
1589
|
+
// Frame-local rect + innerWidth/innerHeight for bilinear mapping when the
|
|
1590
|
+
// frame is transformed (rotate/scale). Main frame uses iw/ih harmlessly.
|
|
1591
|
+
std::string sel_lit = "\"" + escapeForJsString(selector) + "\"";
|
|
1592
|
+
return
|
|
1593
|
+
"(function(){"
|
|
1594
|
+
"var el=document.querySelector(" + sel_lit + ");"
|
|
1595
|
+
"if(!el)return{ok:false,code:\"not_found\"};"
|
|
1596
|
+
"el.scrollIntoView({block:\"nearest\",inline:\"nearest\",behavior:\"instant\"});"
|
|
1597
|
+
"var r=el.getBoundingClientRect();"
|
|
1598
|
+
"var vis=r.width>0&&r.height>0&&r.bottom>0&&r.right>0"
|
|
1599
|
+
"&&r.top<innerHeight&&r.left<innerWidth;"
|
|
1600
|
+
"if(!vis)return{ok:false,code:\"not_visible\"};"
|
|
1601
|
+
"return{ok:true,x:r.x,y:r.y,w:r.width,h:r.height,"
|
|
1602
|
+
"cx:r.x+r.width/2,cy:r.y+r.height/2,"
|
|
1603
|
+
"iw:innerWidth,ih:innerHeight};"
|
|
1604
|
+
"})()";
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Extract the user-returned value dict from a Runtime.evaluate response.
|
|
1608
|
+
// Complex (dict/list) CefValue handles reference parent storage — round-trip
|
|
1609
|
+
// through JSON so the returned dict has independent lifetime.
|
|
1610
|
+
CefRefPtr<CefDictionaryValue> parseEvaluateValue(
|
|
1611
|
+
const std::string& evalResult,
|
|
1612
|
+
std::function<void(const char*, const std::string&)> onErr) {
|
|
1613
|
+
CefRefPtr<CefValue> ev = CefParseJSON(evalResult, JSON_PARSER_RFC);
|
|
1614
|
+
if (!ev || ev->GetType() != VTYPE_DICTIONARY) { onErr("runtime_error", "Runtime.evaluate malformed"); return nullptr; }
|
|
1615
|
+
CefRefPtr<CefDictionaryValue> d = ev->GetDictionary();
|
|
1616
|
+
if (d->HasKey("exceptionDetails")) {
|
|
1617
|
+
CefRefPtr<CefDictionaryValue> ex = d->GetDictionary("exceptionDetails");
|
|
1618
|
+
std::string msg = ex && ex->HasKey("text") ? ex->GetString("text").ToString() : "runtime exception";
|
|
1619
|
+
onErr("runtime_error", msg);
|
|
1620
|
+
return nullptr;
|
|
1621
|
+
}
|
|
1622
|
+
CefRefPtr<CefDictionaryValue> result = d->GetDictionary("result");
|
|
1623
|
+
if (!result || !result->HasKey("value")) { onErr("runtime_error", "evaluate returned no value"); return nullptr; }
|
|
1624
|
+
CefRefPtr<CefValue> v = result->GetValue("value");
|
|
1625
|
+
if (!v || v->GetType() != VTYPE_DICTIONARY) { onErr("runtime_error", "evaluate returned non-object"); return nullptr; }
|
|
1626
|
+
std::string userJson = CefWriteJSON(v, JSON_WRITER_DEFAULT);
|
|
1627
|
+
CefRefPtr<CefValue> independent = CefParseJSON(userJson, JSON_PARSER_RFC);
|
|
1628
|
+
if (!independent || independent->GetType() != VTYPE_DICTIONARY) {
|
|
1629
|
+
onErr("runtime_error", "evaluate value re-parse failed"); return nullptr;
|
|
1630
|
+
}
|
|
1631
|
+
return independent->GetDictionary();
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
void dispatchCdpClick(ViewHost* view, double cx, double cy,
|
|
1635
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1636
|
+
if (click_count < 1) click_count = 1;
|
|
1637
|
+
const char* btn = cdpButtonName(button);
|
|
1638
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
1639
|
+
std::string base = "\"x\":" + std::to_string(cx) + ",\"y\":" + std::to_string(cy) +
|
|
1640
|
+
",\"button\":\"" + btn + "\",\"clickCount\":" + std::to_string(i) +
|
|
1641
|
+
",\"modifiers\":" + std::to_string(modifiers);
|
|
1642
|
+
cefCdpCall(view, "Input.dispatchMouseEvent", "{\"type\":\"mousePressed\"," + base + "}");
|
|
1643
|
+
cefCdpCall(view, "Input.dispatchMouseEvent", "{\"type\":\"mouseReleased\"," + base + "}");
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
} // namespace
|
|
1648
|
+
|
|
1649
|
+
namespace {
|
|
1650
|
+
|
|
1651
|
+
// Dispatch native click at page coords + emit success envelope.
|
|
1652
|
+
void finishResolveAndClick(uint32_t view_id, uint32_t request_id, double x, double y,
|
|
1653
|
+
double w, double h, double cx, double cy,
|
|
1654
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1655
|
+
auto* v = bunite_win::getViewHostById(view_id);
|
|
1656
|
+
if (!v || !v->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1657
|
+
dispatchCdpClick(v, cx, cy, button, click_count, modifiers);
|
|
1658
|
+
// CEF CDP `Input.dispatchMouseEvent` produces DOM events with isTrusted=true
|
|
1659
|
+
// (empirical — `e.isTrusted` on page-side listener reports true).
|
|
1660
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
1661
|
+
",\"ok\":true,\"rect\":{\"x\":" + std::to_string(x) +
|
|
1662
|
+
",\"y\":" + std::to_string(y) +
|
|
1663
|
+
",\"width\":" + std::to_string(w) +
|
|
1664
|
+
",\"height\":" + std::to_string(h) + "},"
|
|
1665
|
+
"\"isTrustedEvent\":true}";
|
|
1666
|
+
bunite_win::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
struct FrameResolveOk { double x, y, w, h, cx, cy, iw, ih; };
|
|
1670
|
+
|
|
1671
|
+
// CefDictionaryValue::GetDouble returns 0 for VTYPE_INT — JSON.stringify
|
|
1672
|
+
// serializes integer-valued numbers without `.0` so values like rect.height==35
|
|
1673
|
+
// re-parse as VTYPE_INT. Coerce here.
|
|
1674
|
+
double dictNumber(CefRefPtr<CefDictionaryValue> d, const char* key) {
|
|
1675
|
+
if (!d || !d->HasKey(key)) return 0.0;
|
|
1676
|
+
switch (d->GetType(key)) {
|
|
1677
|
+
case VTYPE_INT: return static_cast<double>(d->GetInt(key));
|
|
1678
|
+
case VTYPE_DOUBLE: return d->GetDouble(key);
|
|
1679
|
+
default: return 0.0;
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// Parse a Runtime.evaluate response (either main-session or sessionId-routed)
|
|
1684
|
+
// and forward the script's frame-local fields to `onOk`. The script's failure
|
|
1685
|
+
// branch (`{ok:false,code:...}`) routes through `onErr`.
|
|
1686
|
+
void parseEvalAndContinue(uint32_t view_id, uint32_t request_id, bool ok, const std::string& evalResult,
|
|
1687
|
+
std::function<void(const FrameResolveOk&)> onOk) {
|
|
1688
|
+
auto onErr = [view_id, request_id](const char* code, const std::string& msg) {
|
|
1689
|
+
emitResolveAndClickError(view_id, request_id, code, msg);
|
|
1690
|
+
};
|
|
1691
|
+
if (!ok) {
|
|
1692
|
+
BUNITE_INFO("cef/eval: Runtime.evaluate failed view=%u request=%u body=%.300s%s",
|
|
1693
|
+
view_id, request_id, evalResult.c_str(),
|
|
1694
|
+
evalResult.size() > 300 ? "..." : "");
|
|
1695
|
+
onErr("runtime_error", "Runtime.evaluate failed"); return;
|
|
1696
|
+
}
|
|
1697
|
+
auto value = parseEvaluateValue(evalResult, onErr);
|
|
1698
|
+
if (!value) return;
|
|
1699
|
+
if (!value->HasKey("ok") || !value->GetBool("ok")) {
|
|
1700
|
+
std::string code = value->HasKey("code") ? value->GetString("code").ToString() : "runtime_error";
|
|
1701
|
+
onErr(code.c_str(), "");
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
onOk(FrameResolveOk{
|
|
1705
|
+
dictNumber(value, "x"), dictNumber(value, "y"),
|
|
1706
|
+
dictNumber(value, "w"), dictNumber(value, "h"),
|
|
1707
|
+
dictNumber(value, "cx"), dictNumber(value, "cy"),
|
|
1708
|
+
dictNumber(value, "iw"), dictNumber(value, "ih"),
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Issue Runtime.evaluate inside the target frame — sessionId-routed (OOPIF) or
|
|
1713
|
+
// isolated-world via main session (same-renderer cross-origin or same-origin).
|
|
1714
|
+
void evalInFrame(uint32_t view_id, uint32_t request_id, const std::string& frameId,
|
|
1715
|
+
const std::string& escScript,
|
|
1716
|
+
std::function<void(const FrameResolveOk&)> onOk) {
|
|
1717
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1718
|
+
if (!view || !view->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1719
|
+
// Look up auto-attached OOPIF session.
|
|
1720
|
+
std::string session_id;
|
|
1721
|
+
{
|
|
1722
|
+
std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
|
|
1723
|
+
auto it = view->oopif_sessions.find(frameId);
|
|
1724
|
+
if (it != view->oopif_sessions.end()) session_id = it->second;
|
|
1725
|
+
}
|
|
1726
|
+
if (!session_id.empty()) {
|
|
1727
|
+
int id = nextRawCdpId();
|
|
1728
|
+
std::string msg = "{\"id\":" + std::to_string(id) +
|
|
1729
|
+
",\"sessionId\":\"" + session_id +
|
|
1730
|
+
"\",\"method\":\"Runtime.evaluate\""
|
|
1731
|
+
",\"params\":{\"expression\":\"" + escScript +
|
|
1732
|
+
"\",\"returnByValue\":true,\"awaitPromise\":true}}";
|
|
1733
|
+
cefSendRaw(view, msg, id,
|
|
1734
|
+
[view_id, request_id, onOk](bool ok, std::string r) {
|
|
1735
|
+
parseEvalAndContinue(view_id, request_id, ok, r, onOk);
|
|
1736
|
+
});
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
// In-process: createIsolatedWorld + Runtime.evaluate via main session.
|
|
1740
|
+
std::string isoParams = "{\"frameId\":\"" + bunite_win::escapeJsonString(frameId) + "\",\"worldName\":\"bunite-rac\"}";
|
|
1741
|
+
cefCdpCall(view, "Page.createIsolatedWorld", isoParams,
|
|
1742
|
+
[view_id, request_id, escScript, onOk](bool ok, std::string isoResult) {
|
|
1743
|
+
if (!ok) { emitResolveAndClickError(view_id, request_id, "runtime_error", "createIsolatedWorld failed"); return; }
|
|
1744
|
+
CefRefPtr<CefValue> val = CefParseJSON(isoResult, JSON_PARSER_RFC);
|
|
1745
|
+
if (!val || val->GetType() != VTYPE_DICTIONARY) {
|
|
1746
|
+
emitResolveAndClickError(view_id, request_id, "runtime_error", "createIsolatedWorld malformed"); return;
|
|
1747
|
+
}
|
|
1748
|
+
int contextId = val->GetDictionary()->GetInt("executionContextId");
|
|
1749
|
+
auto* v2 = bunite_win::getViewHostById(view_id);
|
|
1750
|
+
if (!v2) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1751
|
+
std::string evalParams = "{\"expression\":\"" + escScript +
|
|
1752
|
+
"\",\"contextId\":" + std::to_string(contextId) +
|
|
1753
|
+
",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1754
|
+
cefCdpCall(v2, "Runtime.evaluate", evalParams,
|
|
1755
|
+
[view_id, request_id, onOk](bool ok2, std::string r) {
|
|
1756
|
+
parseEvalAndContinue(view_id, request_id, ok2, r, onOk);
|
|
1757
|
+
});
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// Bilinear: (fx, fy) ∈ [0, iw] × [0, ih] → page coord using clockwise quad
|
|
1762
|
+
// TL/TR/BR/BL. Handles axis-aligned, scaled, rotated, and skewed iframes.
|
|
1763
|
+
inline void bilinearMap(const std::array<double, 8>& q, double iw, double ih,
|
|
1764
|
+
double fx, double fy, double& px, double& py) {
|
|
1765
|
+
const double u = (iw > 0) ? (fx / iw) : 0.0;
|
|
1766
|
+
const double v = (ih > 0) ? (fy / ih) : 0.0;
|
|
1767
|
+
px = (1-u)*(1-v)*q[0] + u*(1-v)*q[2] + u*v*q[4] + (1-u)*v*q[6];
|
|
1768
|
+
py = (1-u)*(1-v)*q[1] + u*(1-v)*q[3] + u*v*q[5] + (1-u)*v*q[7];
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Recursive frame path lookup in Page.getFrameTree response.
|
|
1772
|
+
// Returns [outermost frame id, ..., target_frame_id] including main; empty if
|
|
1773
|
+
// target not in tree.
|
|
1774
|
+
std::vector<std::string> findFramePath(CefRefPtr<CefDictionaryValue> node, const std::string& target) {
|
|
1775
|
+
if (!node) return {};
|
|
1776
|
+
CefRefPtr<CefDictionaryValue> frame = node->HasKey("frame") ? node->GetDictionary("frame") : nullptr;
|
|
1777
|
+
if (!frame) return {};
|
|
1778
|
+
std::string this_id = frame->GetString("id").ToString();
|
|
1779
|
+
if (this_id == target) return {this_id};
|
|
1780
|
+
CefRefPtr<CefListValue> children = node->HasKey("childFrames") ? node->GetList("childFrames") : nullptr;
|
|
1781
|
+
if (!children) return {};
|
|
1782
|
+
for (size_t i = 0; i < children->GetSize(); ++i) {
|
|
1783
|
+
auto v = children->GetValue(i);
|
|
1784
|
+
if (!v || v->GetType() != VTYPE_DICTIONARY) continue;
|
|
1785
|
+
auto sub = findFramePath(v->GetDictionary(), target);
|
|
1786
|
+
if (!sub.empty()) { sub.insert(sub.begin(), this_id); return sub; }
|
|
1787
|
+
}
|
|
1788
|
+
return {};
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
bool parseQuadFromBoxModel(const std::string& result, std::array<double, 8>& out) {
|
|
1792
|
+
CefRefPtr<CefValue> bv = CefParseJSON(result, JSON_PARSER_RFC);
|
|
1793
|
+
if (!bv || bv->GetType() != VTYPE_DICTIONARY) return false;
|
|
1794
|
+
auto model = bv->GetDictionary()->HasKey("model") ? bv->GetDictionary()->GetDictionary("model") : nullptr;
|
|
1795
|
+
if (!model) return false;
|
|
1796
|
+
auto content = model->HasKey("content") ? model->GetList("content") : nullptr;
|
|
1797
|
+
if (!content || content->GetSize() < 8) return false;
|
|
1798
|
+
for (int i = 0; i < 8; ++i) {
|
|
1799
|
+
// CDP serializes integer pixel positions without `.0`; coerce from INT.
|
|
1800
|
+
switch (content->GetType(i)) {
|
|
1801
|
+
case VTYPE_INT: out[i] = static_cast<double>(content->GetInt(i)); break;
|
|
1802
|
+
case VTYPE_DOUBLE: out[i] = content->GetDouble(i); break;
|
|
1803
|
+
default: out[i] = 0.0;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Eval a script on a specific session (OOPIF) or main session (empty session_id).
|
|
1810
|
+
// Result delivered as raw JSON of `Runtime.evaluate` response.
|
|
1811
|
+
void evalRaw(ViewHost* view, const std::string& session_id, const std::string& escScript,
|
|
1812
|
+
std::function<void(bool, std::string)> cb) {
|
|
1813
|
+
if (session_id.empty()) {
|
|
1814
|
+
std::string params = "{\"expression\":\"" + escScript + "\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
1815
|
+
cefCdpCall(view, "Runtime.evaluate", params, std::move(cb));
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
int id = nextRawCdpId();
|
|
1819
|
+
std::string msg = "{\"id\":" + std::to_string(id) +
|
|
1820
|
+
",\"sessionId\":\"" + session_id +
|
|
1821
|
+
"\",\"method\":\"Runtime.evaluate\",\"params\":{\"expression\":\"" +
|
|
1822
|
+
escScript + "\",\"returnByValue\":true,\"awaitPromise\":true}}";
|
|
1823
|
+
cefSendRaw(view, msg, id, std::move(cb));
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// State threaded through chain-walk continuations.
|
|
1827
|
+
struct ChainState {
|
|
1828
|
+
uint32_t view_id;
|
|
1829
|
+
uint32_t request_id;
|
|
1830
|
+
std::string targetFrameId;
|
|
1831
|
+
std::string escScript;
|
|
1832
|
+
int32_t button, click_count;
|
|
1833
|
+
uint32_t modifiers;
|
|
1834
|
+
// chain[0] = main frameId, chain[1..N-1] = outermost..target. N >= 2.
|
|
1835
|
+
std::vector<std::string> chain;
|
|
1836
|
+
// For each link i (parent = chain[i], child = chain[i+1]): quad in parent's coord system.
|
|
1837
|
+
std::vector<std::array<double, 8>> link_quads;
|
|
1838
|
+
// For chain[i] (i in [1..N-2]): innerWidth/innerHeight of that ancestor frame.
|
|
1839
|
+
// Used when mapping FROM chain[i+1]'s coords up to chain[i]'s coords.
|
|
1840
|
+
// Indexed by ancestor's chain position; chain[N-1] (target) iw/ih comes from
|
|
1841
|
+
// the target eval, not stored here.
|
|
1842
|
+
std::vector<std::pair<double, double>> ancestor_inner;
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
void composeAndDispatch(std::shared_ptr<ChainState> s, const FrameResolveOk& fr);
|
|
1846
|
+
void fetchTargetEval(std::shared_ptr<ChainState> s);
|
|
1847
|
+
void fetchAncestorInner(std::shared_ptr<ChainState> s, size_t i);
|
|
1848
|
+
void fetchLink(std::shared_ptr<ChainState> s, size_t link_idx);
|
|
1849
|
+
|
|
1850
|
+
// Look up the session for an ancestor frame (chain[idx]). idx == 0 → main (empty).
|
|
1851
|
+
std::string sessionForChainIdx(uint32_t view_id, const std::vector<std::string>& chain, size_t idx) {
|
|
1852
|
+
if (idx == 0) return {};
|
|
1853
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1854
|
+
if (!view) return {};
|
|
1855
|
+
std::lock_guard<std::mutex> lk(view->oopif_sessions_mutex);
|
|
1856
|
+
auto it = view->oopif_sessions.find(chain[idx]);
|
|
1857
|
+
return (it != view->oopif_sessions.end()) ? it->second : std::string{};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
void fetchLink(std::shared_ptr<ChainState> s, size_t link_idx) {
|
|
1861
|
+
if (link_idx + 1 >= s->chain.size()) {
|
|
1862
|
+
// All links collected. Move to ancestor inner sizes.
|
|
1863
|
+
fetchAncestorInner(s, 1);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
const std::string parent_session = sessionForChainIdx(s->view_id, s->chain, link_idx);
|
|
1867
|
+
const std::string& child_frameId = s->chain[link_idx + 1];
|
|
1868
|
+
auto* view = bunite_win::getViewHostById(s->view_id);
|
|
1869
|
+
if (!view) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1870
|
+
// DOM.getFrameOwner on parent's session.
|
|
1871
|
+
std::string ownerParams = "{\"frameId\":\"" + bunite_win::escapeJsonString(child_frameId) + "\"}";
|
|
1872
|
+
auto onOwner = [s, link_idx, parent_session](bool ok, std::string r) {
|
|
1873
|
+
if (!ok) { emitResolveAndClickError(s->view_id, s->request_id, "not_found", "getFrameOwner failed"); return; }
|
|
1874
|
+
CefRefPtr<CefValue> val = CefParseJSON(r, JSON_PARSER_RFC);
|
|
1875
|
+
if (!val || val->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "getFrameOwner malformed"); return; }
|
|
1876
|
+
int backendNodeId = val->GetDictionary()->HasKey("backendNodeId") ? val->GetDictionary()->GetInt("backendNodeId") : 0;
|
|
1877
|
+
if (!backendNodeId) { emitResolveAndClickError(s->view_id, s->request_id, "not_found", "no backendNodeId"); return; }
|
|
1878
|
+
auto* v2 = bunite_win::getViewHostById(s->view_id);
|
|
1879
|
+
if (!v2) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1880
|
+
std::string boxParams = "{\"backendNodeId\":" + std::to_string(backendNodeId) + "}";
|
|
1881
|
+
auto onBox = [s, link_idx](bool ok2, std::string rb) {
|
|
1882
|
+
if (!ok2) { emitResolveAndClickError(s->view_id, s->request_id, "not_visible", "iframe has no box"); return; }
|
|
1883
|
+
std::array<double, 8> quad{};
|
|
1884
|
+
if (!parseQuadFromBoxModel(rb, quad)) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "bad quad"); return; }
|
|
1885
|
+
s->link_quads.push_back(quad);
|
|
1886
|
+
fetchLink(s, link_idx + 1);
|
|
1887
|
+
};
|
|
1888
|
+
if (parent_session.empty()) {
|
|
1889
|
+
cefCdpCall(v2, "DOM.getBoxModel", boxParams, onBox);
|
|
1890
|
+
} else {
|
|
1891
|
+
int id = nextRawCdpId();
|
|
1892
|
+
std::string msg = "{\"id\":" + std::to_string(id) +
|
|
1893
|
+
",\"sessionId\":\"" + parent_session +
|
|
1894
|
+
"\",\"method\":\"DOM.getBoxModel\",\"params\":" + boxParams + "}";
|
|
1895
|
+
cefSendRaw(v2, msg, id, onBox);
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
if (parent_session.empty()) {
|
|
1899
|
+
cefCdpCall(view, "DOM.getFrameOwner", ownerParams, onOwner);
|
|
1900
|
+
} else {
|
|
1901
|
+
int id = nextRawCdpId();
|
|
1902
|
+
std::string msg = "{\"id\":" + std::to_string(id) +
|
|
1903
|
+
",\"sessionId\":\"" + parent_session +
|
|
1904
|
+
"\",\"method\":\"DOM.getFrameOwner\",\"params\":" + ownerParams + "}";
|
|
1905
|
+
cefSendRaw(view, msg, id, onOwner);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
void fetchAncestorInner(std::shared_ptr<ChainState> s, size_t i) {
|
|
1910
|
+
// i ranges [1, N-2]. Skip N-1 (target — iw/ih from target eval).
|
|
1911
|
+
if (i + 1 >= s->chain.size()) {
|
|
1912
|
+
// Done with ancestors. Eval target script.
|
|
1913
|
+
fetchTargetEval(s);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
const std::string sid = sessionForChainIdx(s->view_id, s->chain, i);
|
|
1917
|
+
auto* view = bunite_win::getViewHostById(s->view_id);
|
|
1918
|
+
if (!view) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "view destroyed"); return; }
|
|
1919
|
+
const std::string& script = "JSON.stringify({iw:innerWidth,ih:innerHeight})";
|
|
1920
|
+
std::string escScript = escapeForJsonString(script);
|
|
1921
|
+
evalRaw(view, sid, escScript,
|
|
1922
|
+
[s, i](bool ok, std::string r) {
|
|
1923
|
+
if (!ok) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor eval failed"); return; }
|
|
1924
|
+
// Result envelope: {"result":{"type":"string","value":"<json string>"}}
|
|
1925
|
+
CefRefPtr<CefValue> v = CefParseJSON(r, JSON_PARSER_RFC);
|
|
1926
|
+
if (!v || v->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor eval malformed"); return; }
|
|
1927
|
+
auto result = v->GetDictionary()->GetDictionary("result");
|
|
1928
|
+
if (!result || !result->HasKey("value")) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor eval no value"); return; }
|
|
1929
|
+
std::string vs = result->GetString("value").ToString();
|
|
1930
|
+
CefRefPtr<CefValue> inner = CefParseJSON(vs, JSON_PARSER_RFC);
|
|
1931
|
+
if (!inner || inner->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(s->view_id, s->request_id, "runtime_error", "ancestor inner malformed"); return; }
|
|
1932
|
+
s->ancestor_inner.push_back({dictNumber(inner->GetDictionary(), "iw"), dictNumber(inner->GetDictionary(), "ih")});
|
|
1933
|
+
fetchAncestorInner(s, i + 1);
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
void fetchTargetEval(std::shared_ptr<ChainState> s) {
|
|
1938
|
+
evalInFrame(s->view_id, s->request_id, s->targetFrameId, s->escScript,
|
|
1939
|
+
[s](const FrameResolveOk& fr) { composeAndDispatch(s, fr); });
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
void composeAndDispatch(std::shared_ptr<ChainState> s, const FrameResolveOk& fr) {
|
|
1943
|
+
// Stack iw/ih per chain level chain[1..N-1] for the bilinear walk.
|
|
1944
|
+
// chain[N-1] = target → fr.iw, fr.ih.
|
|
1945
|
+
// chain[i] (1 <= i < N-1) → s->ancestor_inner[i-1].
|
|
1946
|
+
// link_quads[i] = quad of chain[i+1]'s iframe element, in chain[i]'s coords.
|
|
1947
|
+
// Map order: from target up to main, applying bilinear at each link.
|
|
1948
|
+
auto mapCorner = [&](double fx, double fy, double& px, double& py) {
|
|
1949
|
+
double cur_x = fx, cur_y = fy;
|
|
1950
|
+
double cur_iw = fr.iw, cur_ih = fr.ih;
|
|
1951
|
+
// link i (chain[i+1] in chain[i]'s coords) for i = N-2 down to 0.
|
|
1952
|
+
for (size_t i = s->link_quads.size(); i-- > 0; ) {
|
|
1953
|
+
double mapped_x, mapped_y;
|
|
1954
|
+
bilinearMap(s->link_quads[i], cur_iw, cur_ih, cur_x, cur_y, mapped_x, mapped_y);
|
|
1955
|
+
cur_x = mapped_x; cur_y = mapped_y;
|
|
1956
|
+
if (i == 0) break; // chain[i] is main-direct child handled; next would be main itself
|
|
1957
|
+
// Now in chain[i]'s coords; next iteration maps to chain[i-1]'s coords.
|
|
1958
|
+
cur_iw = s->ancestor_inner[i - 1].first;
|
|
1959
|
+
cur_ih = s->ancestor_inner[i - 1].second;
|
|
1960
|
+
}
|
|
1961
|
+
px = cur_x; py = cur_y;
|
|
1962
|
+
};
|
|
1963
|
+
double pcx, pcy; mapCorner(fr.cx, fr.cy, pcx, pcy);
|
|
1964
|
+
double cx0, cy0, cx1, cy1, cx2, cy2, cx3, cy3;
|
|
1965
|
+
mapCorner(fr.x, fr.y, cx0, cy0);
|
|
1966
|
+
mapCorner(fr.x + fr.w, fr.y, cx1, cy1);
|
|
1967
|
+
mapCorner(fr.x + fr.w, fr.y + fr.h, cx2, cy2);
|
|
1968
|
+
mapCorner(fr.x, fr.y + fr.h, cx3, cy3);
|
|
1969
|
+
const double min_x = std::min(std::min(cx0, cx1), std::min(cx2, cx3));
|
|
1970
|
+
const double max_x = std::max(std::max(cx0, cx1), std::max(cx2, cx3));
|
|
1971
|
+
const double min_y = std::min(std::min(cy0, cy1), std::min(cy2, cy3));
|
|
1972
|
+
const double max_y = std::max(std::max(cy0, cy1), std::max(cy2, cy3));
|
|
1973
|
+
finishResolveAndClick(s->view_id, s->request_id,
|
|
1974
|
+
min_x, min_y, max_x - min_x, max_y - min_y, pcx, pcy,
|
|
1975
|
+
s->button, s->click_count, s->modifiers);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// `frameId` non-empty: walk ancestor chain via Page.getFrameTree, compose
|
|
1979
|
+
// bilinear transforms across nested OOPIF/same-origin frames, dispatch click
|
|
1980
|
+
// in main-page coords.
|
|
1981
|
+
void runFrameTargeted(uint32_t view_id, uint32_t request_id, const std::string& frameId,
|
|
1982
|
+
const std::string& escScript,
|
|
1983
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
1984
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
1985
|
+
if (!view || !view->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view destroyed"); return; }
|
|
1986
|
+
cefCdpCall(view, "Page.getFrameTree", "{}",
|
|
1987
|
+
[view_id, request_id, frameId, escScript, button, click_count, modifiers](bool ok, std::string r) {
|
|
1988
|
+
if (!ok) { emitResolveAndClickError(view_id, request_id, "runtime_error", "getFrameTree failed"); return; }
|
|
1989
|
+
CefRefPtr<CefValue> val = CefParseJSON(r, JSON_PARSER_RFC);
|
|
1990
|
+
if (!val || val->GetType() != VTYPE_DICTIONARY) { emitResolveAndClickError(view_id, request_id, "runtime_error", "getFrameTree malformed"); return; }
|
|
1991
|
+
auto root = val->GetDictionary()->GetDictionary("frameTree");
|
|
1992
|
+
std::vector<std::string> chain = findFramePath(root, frameId);
|
|
1993
|
+
if (chain.size() < 2) { emitResolveAndClickError(view_id, request_id, "not_found", "frame not in tree"); return; }
|
|
1994
|
+
auto s = std::make_shared<ChainState>();
|
|
1995
|
+
s->view_id = view_id;
|
|
1996
|
+
s->request_id = request_id;
|
|
1997
|
+
s->targetFrameId = frameId;
|
|
1998
|
+
s->escScript = escScript;
|
|
1999
|
+
s->button = button;
|
|
2000
|
+
s->click_count = click_count;
|
|
2001
|
+
s->modifiers = modifiers;
|
|
2002
|
+
s->chain = std::move(chain); // chain[0] = main, chain[N-1] = target
|
|
2003
|
+
fetchLink(s, 0);
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
} // namespace
|
|
2008
|
+
|
|
2009
|
+
extern "C" BUNITE_EXPORT void bunite_view_resolve_and_click(
|
|
2010
|
+
uint32_t view_id, uint32_t request_id,
|
|
2011
|
+
const char* selector_c, const char* frame_id_c,
|
|
2012
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
2013
|
+
std::string selector = selector_c ? selector_c : "";
|
|
2014
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
2015
|
+
bunite_win::postCefUiTask([view_id, request_id, selector, frameId, button, click_count, modifiers]() {
|
|
2016
|
+
auto* view = bunite_win::getViewHostById(view_id);
|
|
2017
|
+
if (!view || !view->browser) { emitResolveAndClickError(view_id, request_id, "runtime_error", "view not ready"); return; }
|
|
2018
|
+
|
|
2019
|
+
std::string script = buildResolveScript(selector);
|
|
2020
|
+
std::string escScript = escapeForJsonString(script);
|
|
2021
|
+
|
|
2022
|
+
if (frameId.empty()) {
|
|
2023
|
+
// Main frame — fr.x/y/w/h are already page-viewport coords (iw/ih unused).
|
|
2024
|
+
std::string evalParams = "{\"expression\":\"" + escScript + "\",\"returnByValue\":true,\"awaitPromise\":true}";
|
|
2025
|
+
cefCdpCall(view, "Runtime.evaluate", evalParams,
|
|
2026
|
+
[view_id, request_id, button, click_count, modifiers](bool ok, std::string r) {
|
|
2027
|
+
parseEvalAndContinue(view_id, request_id, ok, r,
|
|
2028
|
+
[view_id, request_id, button, click_count, modifiers](const FrameResolveOk& fr) {
|
|
2029
|
+
finishResolveAndClick(view_id, request_id,
|
|
2030
|
+
fr.x, fr.y, fr.w, fr.h, fr.cx, fr.cy,
|
|
2031
|
+
button, click_count, modifiers);
|
|
2032
|
+
});
|
|
2033
|
+
});
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Frame-targeted: lazy Target.setAutoAttach so OOPIF frames get sessionIds
|
|
2038
|
+
// populated into view->oopif_sessions via OnDevToolsEvent. Wait for response
|
|
2039
|
+
// so attachedToTarget events fire before we proceed.
|
|
2040
|
+
if (!view->oopif_autoattach_armed.exchange(true)) {
|
|
2041
|
+
cefCdpCall(view, "Target.setAutoAttach",
|
|
2042
|
+
"{\"autoAttach\":true,\"flatten\":true,\"waitForDebuggerOnStart\":false}",
|
|
2043
|
+
[view_id, request_id, frameId, escScript, button, click_count, modifiers](bool ok, std::string) {
|
|
2044
|
+
if (!ok) { emitResolveAndClickError(view_id, request_id, "runtime_error", "setAutoAttach failed"); return; }
|
|
2045
|
+
runFrameTargeted(view_id, request_id, frameId, escScript, button, click_count, modifiers);
|
|
2046
|
+
});
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
runFrameTargeted(view_id, request_id, frameId, escScript, button, click_count, modifiers);
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
860
2053
|
extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
861
2054
|
bunite_win::postCefUiTask([view_id]() { bunite_win::openDevToolsForView(bunite_win::getViewHostById(view_id)); });
|
|
862
2055
|
}
|