bunite-core 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/host/core/App.ts +2 -1
- package/src/host/core/BrowserView.ts +345 -24
- package/src/host/core/SurfaceBrowserIPC.ts +10 -1
- package/src/host/core/SurfaceManager.ts +357 -16
- package/src/host/events/webviewEvents.ts +18 -1
- package/src/host/log.ts +6 -1
- package/src/host/native.ts +140 -1
- package/src/host/preloadBundle.ts +7 -2
- package/src/native/linux/bunite_linux_ffi.cpp +205 -1
- package/src/native/linux/bunite_linux_internal.h +12 -0
- package/src/native/linux/bunite_linux_runtime.cpp +6 -1
- package/src/native/linux/bunite_linux_view.cpp +211 -5
- package/src/native/mac/bunite_mac_ffi.mm +278 -4
- package/src/native/mac/bunite_mac_internal.h +13 -0
- package/src/native/mac/bunite_mac_view.mm +227 -7
- package/src/native/shared/ffi_exports.h +93 -30
- package/src/native/win/native_host_cef.cpp +102 -10
- package/src/native/win/native_host_ffi.cpp +818 -2
- package/src/native/win/native_host_internal.h +22 -0
- package/src/native/win-webview2/bunite_webview2_ffi.cpp +788 -4
- package/src/native/win-webview2/webview2_internal.h +14 -0
- package/src/native/win-webview2/webview2_runtime.cpp +276 -23
- package/src/preload/runtime.built.js +1 -1
- package/src/rpc/framework.ts +174 -11
- package/src/rpc/index.ts +11 -0
- package/src/webview/native.ts +142 -32
- package/src/webview/polyfill.ts +91 -14
|
@@ -10,6 +10,10 @@ namespace {
|
|
|
10
10
|
|
|
11
11
|
constexpr const char* kViewIdKey = "bunite-view-id";
|
|
12
12
|
|
|
13
|
+
// Forward decls — on_create references these to wire popup-minted views.
|
|
14
|
+
gboolean on_decide_policy(WebKitWebView*, WebKitPolicyDecision*, WebKitPolicyDecisionType, gpointer);
|
|
15
|
+
void on_title_changed(GObject*, GParamSpec*, gpointer);
|
|
16
|
+
|
|
13
17
|
void emit_url(uint32_t view_id, const char* name, WebKitWebView* wv) {
|
|
14
18
|
const char* uri = webkit_web_view_get_uri(wv);
|
|
15
19
|
emitWebviewEvent(view_id, name, uri ? std::string(uri) : std::string{});
|
|
@@ -84,13 +88,49 @@ gboolean on_script_dialog(WebKitWebView* /*wv*/, WebKitScriptDialog* dialog, gpo
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
GtkWidget* on_create(WebKitWebView* wv, WebKitNavigationAction* action, gpointer user_data) {
|
|
87
|
-
const uint32_t
|
|
91
|
+
const uint32_t opener_view_id = GPOINTER_TO_UINT(user_data);
|
|
88
92
|
WebKitURIRequest* req = webkit_navigation_action_get_request(action);
|
|
89
93
|
const char* uri = webkit_uri_request_get_uri(req);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
if (g_runtime.popup_blocking) {
|
|
95
|
+
std::string payload = "{\"url\":\"" + escapeJsonString(uri ? uri : "") + "\"}";
|
|
96
|
+
emitWebviewEvent(opener_view_id, "new-window-open", payload);
|
|
97
|
+
return nullptr;
|
|
98
|
+
}
|
|
99
|
+
static std::atomic<uint32_t> g_popup_seq{0x80000000u};
|
|
100
|
+
const uint32_t new_view_id = g_popup_seq.fetch_add(1);
|
|
101
|
+
// Share network-session + user-content-manager so cookies/preload-injection
|
|
102
|
+
// carry across the opener boundary.
|
|
103
|
+
WebKitWebView* popup = WEBKIT_WEB_VIEW(g_object_new(
|
|
104
|
+
WEBKIT_TYPE_WEB_VIEW,
|
|
105
|
+
"network-session", webkit_web_view_get_network_session(wv),
|
|
106
|
+
"user-content-manager", webkit_web_view_get_user_content_manager(wv),
|
|
107
|
+
nullptr));
|
|
108
|
+
g_object_ref_sink(popup);
|
|
109
|
+
g_object_set_data(G_OBJECT(popup), kViewIdKey, GUINT_TO_POINTER(new_view_id));
|
|
110
|
+
g_signal_connect(popup, "load-changed", G_CALLBACK(on_load_changed), GUINT_TO_POINTER(new_view_id));
|
|
111
|
+
g_signal_connect(popup, "load-failed", G_CALLBACK(on_load_failed), GUINT_TO_POINTER(new_view_id));
|
|
112
|
+
g_signal_connect(popup, "load-failed-with-tls-errors", G_CALLBACK(on_load_failed_tls), GUINT_TO_POINTER(new_view_id));
|
|
113
|
+
g_signal_connect(popup, "decide-policy", G_CALLBACK(on_decide_policy), GUINT_TO_POINTER(new_view_id));
|
|
114
|
+
g_signal_connect(popup, "create", G_CALLBACK(on_create), GUINT_TO_POINTER(new_view_id));
|
|
115
|
+
g_signal_connect(popup, "notify::title", G_CALLBACK(on_title_changed), GUINT_TO_POINTER(new_view_id));
|
|
116
|
+
g_signal_connect(popup, "script-dialog", G_CALLBACK(on_script_dialog), GUINT_TO_POINTER(new_view_id));
|
|
117
|
+
{
|
|
118
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
119
|
+
g_runtime.parked_popups[new_view_id] = popup;
|
|
120
|
+
auto& st = g_runtime.views[new_view_id];
|
|
121
|
+
st.webview = popup;
|
|
122
|
+
st.window_id = 0; // bound on adoption
|
|
123
|
+
st.container = GTK_WIDGET(popup);
|
|
124
|
+
}
|
|
125
|
+
if (g_runtime.popup_parent) {
|
|
126
|
+
GtkWidget* box = gtk_window_get_child(g_runtime.popup_parent);
|
|
127
|
+
if (box) gtk_box_append(GTK_BOX(box), GTK_WIDGET(popup));
|
|
128
|
+
}
|
|
129
|
+
std::string payload = "{\"newSurfaceId\":" + std::to_string(new_view_id) +
|
|
130
|
+
",\"url\":\"" + escapeJsonString(uri ? uri : "") +
|
|
131
|
+
"\",\"disposition\":\"popup\"}";
|
|
132
|
+
emitWebviewEvent(opener_view_id, "popup-requested", payload);
|
|
133
|
+
return GTK_WIDGET(popup);
|
|
94
134
|
}
|
|
95
135
|
|
|
96
136
|
void on_title_changed(GObject* source, GParamSpec* /*pspec*/, gpointer user_data) {
|
|
@@ -120,8 +160,126 @@ gboolean on_decide_policy(WebKitWebView* wv, WebKitPolicyDecision* decision,
|
|
|
120
160
|
return TRUE;
|
|
121
161
|
}
|
|
122
162
|
|
|
163
|
+
constexpr const char* kDownloadIdKey = "bunite-download-id";
|
|
164
|
+
std::atomic<uint64_t> g_download_seq{1};
|
|
165
|
+
|
|
166
|
+
uint32_t viewIdForDownload(WebKitDownload* download) {
|
|
167
|
+
WebKitWebView* wv = webkit_download_get_web_view(download);
|
|
168
|
+
if (!wv) return 0;
|
|
169
|
+
return GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(wv), kViewIdKey));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
void on_received_data(WebKitDownload* download, guint64 /*data_length*/, gpointer /*user_data*/) {
|
|
173
|
+
const uint32_t view_id = viewIdForDownload(download);
|
|
174
|
+
if (!view_id) return;
|
|
175
|
+
const uint64_t id = GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(download), kDownloadIdKey));
|
|
176
|
+
const guint64 received = webkit_download_get_received_data_length(download);
|
|
177
|
+
WebKitURIResponse* resp = webkit_download_get_response(download);
|
|
178
|
+
const guint64 total = resp ? webkit_uri_response_get_content_length(resp) : 0;
|
|
179
|
+
std::string payload = "{\"kind\":\"progress\",\"id\":\"linux-" + std::to_string(id) +
|
|
180
|
+
"\",\"receivedBytes\":" + std::to_string(received);
|
|
181
|
+
if (total > 0) payload += ",\"totalBytes\":" + std::to_string(total);
|
|
182
|
+
payload += "}";
|
|
183
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
void on_download_finished(WebKitDownload* download, gpointer /*user_data*/) {
|
|
187
|
+
const uint32_t view_id = viewIdForDownload(download);
|
|
188
|
+
if (!view_id) return;
|
|
189
|
+
const uint64_t id = GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(download), kDownloadIdKey));
|
|
190
|
+
const char* dest_uri = webkit_download_get_destination(download);
|
|
191
|
+
std::string dest = dest_uri ? dest_uri : "";
|
|
192
|
+
if (dest.rfind("file://", 0) == 0) dest = dest.substr(7);
|
|
193
|
+
std::string payload = "{\"kind\":\"completed\",\"id\":\"linux-" + std::to_string(id) +
|
|
194
|
+
"\",\"localPath\":\"" + escapeJsonString(dest) + "\"}";
|
|
195
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
void on_download_failed(WebKitDownload* download, GError* error, gpointer /*user_data*/) {
|
|
199
|
+
const uint32_t view_id = viewIdForDownload(download);
|
|
200
|
+
if (!view_id) return;
|
|
201
|
+
const uint64_t id = GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(download), kDownloadIdKey));
|
|
202
|
+
std::string reason = error && error->message ? error->message : "unknown";
|
|
203
|
+
std::string payload = "{\"kind\":\"failed\",\"id\":\"linux-" + std::to_string(id) +
|
|
204
|
+
"\",\"reason\":\"" + escapeJsonString(reason) + "\"}";
|
|
205
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// `decide-destination` is the gate: emits `started` / `blocked` and sets path.
|
|
209
|
+
// Returning TRUE tells WebKit we resolved (or cancelled) destination.
|
|
210
|
+
gboolean on_decide_destination(WebKitDownload* download, const gchar* suggested, gpointer /*user_data*/) {
|
|
211
|
+
const uint32_t view_id = viewIdForDownload(download);
|
|
212
|
+
if (!view_id) return FALSE;
|
|
213
|
+
ViewState* st = nullptr;
|
|
214
|
+
{
|
|
215
|
+
std::lock_guard<std::mutex> lk(g_runtime.object_mutex);
|
|
216
|
+
auto it = g_runtime.views.find(view_id);
|
|
217
|
+
if (it != g_runtime.views.end()) st = &it->second;
|
|
218
|
+
}
|
|
219
|
+
if (!st) return FALSE;
|
|
220
|
+
const uint64_t id = GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(download), kDownloadIdKey));
|
|
221
|
+
WebKitURIRequest* req = webkit_download_get_request(download);
|
|
222
|
+
const std::string url = (req && webkit_uri_request_get_uri(req)) ? webkit_uri_request_get_uri(req) : "";
|
|
223
|
+
|
|
224
|
+
const int32_t policy = st->download_policy.load();
|
|
225
|
+
if (policy != 0) {
|
|
226
|
+
const char* reason = (policy == 1) ? "ask-not-implemented" : "host-policy";
|
|
227
|
+
std::string payload = "{\"kind\":\"blocked\",\"id\":\"linux-" + std::to_string(id) +
|
|
228
|
+
"\",\"url\":\"" + escapeJsonString(url) +
|
|
229
|
+
"\",\"reason\":\"" + reason + "\"}";
|
|
230
|
+
emitWebviewEvent(view_id, "download-event", payload);
|
|
231
|
+
webkit_download_cancel(download);
|
|
232
|
+
return TRUE;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const std::string sug = suggested ? suggested : "download";
|
|
236
|
+
WebKitURIResponse* resp = webkit_download_get_response(download);
|
|
237
|
+
const std::string mime = (resp && webkit_uri_response_get_mime_type(resp)) ? webkit_uri_response_get_mime_type(resp) : "";
|
|
238
|
+
const guint64 total = resp ? webkit_uri_response_get_content_length(resp) : 0;
|
|
239
|
+
std::string started = "{\"kind\":\"started\",\"id\":\"linux-" + std::to_string(id) +
|
|
240
|
+
"\",\"url\":\"" + escapeJsonString(url) +
|
|
241
|
+
"\",\"suggestedFilename\":\"" + escapeJsonString(sug) +
|
|
242
|
+
"\",\"mimeType\":\"" + escapeJsonString(mime) + "\"";
|
|
243
|
+
if (total > 0) started += ",\"sizeBytes\":" + std::to_string(total);
|
|
244
|
+
started += "}";
|
|
245
|
+
emitWebviewEvent(view_id, "download-event", started);
|
|
246
|
+
|
|
247
|
+
std::string dir = st->download_dir;
|
|
248
|
+
if (dir.empty()) {
|
|
249
|
+
const char* d = g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD);
|
|
250
|
+
dir = d ? d : "/tmp";
|
|
251
|
+
}
|
|
252
|
+
std::string path = dir + "/" + sug;
|
|
253
|
+
std::string uri = "file://" + path;
|
|
254
|
+
webkit_download_set_destination(download, uri.c_str());
|
|
255
|
+
return TRUE;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
void on_download_started(WebKitNetworkSession* /*session*/, WebKitDownload* download, gpointer /*user_data*/) {
|
|
259
|
+
const uint32_t view_id = viewIdForDownload(download);
|
|
260
|
+
if (!view_id) {
|
|
261
|
+
webkit_download_cancel(download);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const uint64_t id = g_download_seq.fetch_add(1);
|
|
265
|
+
g_object_set_data(G_OBJECT(download), kDownloadIdKey, GSIZE_TO_POINTER(id));
|
|
266
|
+
g_signal_connect(download, "decide-destination", G_CALLBACK(on_decide_destination), nullptr);
|
|
267
|
+
g_signal_connect(download, "received-data", G_CALLBACK(on_received_data), nullptr);
|
|
268
|
+
g_signal_connect(download, "finished", G_CALLBACK(on_download_finished), nullptr);
|
|
269
|
+
g_signal_connect(download, "failed", G_CALLBACK(on_download_failed), nullptr);
|
|
270
|
+
}
|
|
271
|
+
|
|
123
272
|
} // namespace
|
|
124
273
|
|
|
274
|
+
void wireDownloadHandlers() {
|
|
275
|
+
static bool wired = false;
|
|
276
|
+
if (wired) return;
|
|
277
|
+
WebKitNetworkSession* session = webkit_network_session_get_default();
|
|
278
|
+
if (!session) return;
|
|
279
|
+
g_signal_connect(session, "download-started", G_CALLBACK(on_download_started), nullptr);
|
|
280
|
+
wired = true;
|
|
281
|
+
}
|
|
282
|
+
|
|
125
283
|
ViewState* findView(uint32_t view_id) {
|
|
126
284
|
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
127
285
|
auto it = g_runtime.views.find(view_id);
|
|
@@ -167,6 +325,8 @@ bool createView(uint32_t view_id, uint32_t window_id,
|
|
|
167
325
|
return false;
|
|
168
326
|
}
|
|
169
327
|
|
|
328
|
+
wireDownloadHandlers();
|
|
329
|
+
|
|
170
330
|
WebKitUserContentManager* ucm = webkit_user_content_manager_new();
|
|
171
331
|
|
|
172
332
|
// Origin-gate the preload so http(s) navigations don't inherit the RPC bridge.
|
|
@@ -291,6 +451,52 @@ void applyViewBounds(uint32_t view_id, double x, double y, double w, double h) {
|
|
|
291
451
|
queueViewRedraw(v->webview);
|
|
292
452
|
}
|
|
293
453
|
|
|
454
|
+
bool acceptParkedPopup(uint32_t new_view_id, uint32_t host_window_id, double x, double y, double w, double h) {
|
|
455
|
+
WebKitWebView* popup = nullptr;
|
|
456
|
+
{
|
|
457
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
458
|
+
auto it = g_runtime.parked_popups.find(new_view_id);
|
|
459
|
+
if (it == g_runtime.parked_popups.end()) return false;
|
|
460
|
+
popup = it->second;
|
|
461
|
+
g_runtime.parked_popups.erase(it);
|
|
462
|
+
}
|
|
463
|
+
auto* host = findWindow(host_window_id);
|
|
464
|
+
if (!host || !host->host) return false;
|
|
465
|
+
gtk_widget_unparent(GTK_WIDGET(popup));
|
|
466
|
+
GtkWidget* container = gtk_fixed_new();
|
|
467
|
+
gtk_widget_set_halign(container, GTK_ALIGN_START);
|
|
468
|
+
gtk_widget_set_valign(container, GTK_ALIGN_START);
|
|
469
|
+
gtk_widget_set_overflow(container, GTK_OVERFLOW_HIDDEN);
|
|
470
|
+
gtk_fixed_put(GTK_FIXED(container), GTK_WIDGET(popup), 0, 0);
|
|
471
|
+
gtk_overlay_add_overlay(host->host, container);
|
|
472
|
+
{
|
|
473
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
474
|
+
auto& st = g_runtime.views[new_view_id];
|
|
475
|
+
st.window_id = host_window_id;
|
|
476
|
+
st.container = container;
|
|
477
|
+
st.webview = popup;
|
|
478
|
+
}
|
|
479
|
+
applyViewBounds(new_view_id, x, y, w, h);
|
|
480
|
+
// Re-emit view-ready so TS BrowserView.adopt resolves its waiter — the
|
|
481
|
+
// initial `did-navigate` fired before the adopter registered.
|
|
482
|
+
emitWebviewEvent(new_view_id, "view-ready", std::string{});
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
void dismissParkedPopup(uint32_t new_view_id) {
|
|
487
|
+
WebKitWebView* popup = nullptr;
|
|
488
|
+
{
|
|
489
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
490
|
+
auto it = g_runtime.parked_popups.find(new_view_id);
|
|
491
|
+
if (it == g_runtime.parked_popups.end()) return;
|
|
492
|
+
popup = it->second;
|
|
493
|
+
g_runtime.parked_popups.erase(it);
|
|
494
|
+
g_runtime.views.erase(new_view_id);
|
|
495
|
+
}
|
|
496
|
+
gtk_widget_unparent(GTK_WIDGET(popup));
|
|
497
|
+
g_object_unref(popup);
|
|
498
|
+
}
|
|
499
|
+
|
|
294
500
|
void detachViewSideState(uint32_t view_id) {
|
|
295
501
|
bunite::WebviewContentStorage::instance().remove(view_id);
|
|
296
502
|
std::vector<uint32_t> request_ids;
|
|
@@ -20,7 +20,7 @@ using bunite_mac::runOnUiThreadSync;
|
|
|
20
20
|
|
|
21
21
|
namespace {
|
|
22
22
|
|
|
23
|
-
constexpr int32_t kBuniteAbiVersion =
|
|
23
|
+
constexpr int32_t kBuniteAbiVersion = 11;
|
|
24
24
|
|
|
25
25
|
// warn-once — avoid log spam from tight JS call loops.
|
|
26
26
|
#define BUNITE_MAC_TODO(name) \
|
|
@@ -513,8 +513,9 @@ extern "C" BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) {
|
|
|
513
513
|
runOnUiThreadSync([=]() { bunite_mac::removeView(view_id); });
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
// Input dispatch — synthesized NSEvent
|
|
517
|
-
// `
|
|
516
|
+
// Input dispatch — synthesized NSEvent directly to the WKWebView (not via
|
|
517
|
+
// `[NSApp sendEvent:]` — see ctrl-strip note in `bunite_view_click`). WebKit
|
|
518
|
+
// marks these events `isTrusted=true` on the page; capabilities flag matches.
|
|
518
519
|
namespace {
|
|
519
520
|
|
|
520
521
|
NSEventModifierFlags macModifiers(uint32_t bits) {
|
|
@@ -743,7 +744,280 @@ extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
|
743
744
|
BUNITE_CAP_NATIVE_INPUT_TRUSTED |
|
|
744
745
|
BUNITE_CAP_CLICK | BUNITE_CAP_TYPE | BUNITE_CAP_PRESS | BUNITE_CAP_SCROLL |
|
|
745
746
|
BUNITE_CAP_MOUSE | BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
746
|
-
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG
|
|
747
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG |
|
|
748
|
+
BUNITE_CAP_BOUNDING_RECT |
|
|
749
|
+
BUNITE_CAP_RESOLVE_AND_CLICK | BUNITE_CAP_DOWNLOADS | BUNITE_CAP_POPUPS |
|
|
750
|
+
BUNITE_CAP_AX | BUNITE_CAP_FRAMES;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
namespace {
|
|
754
|
+
|
|
755
|
+
// JS-bridge ax tree (mirrors linux backend) — WKWebView has no public host-side
|
|
756
|
+
// ax API. Walks DOM + ARIA attrs, emits CDP-shaped flat list so TS-side
|
|
757
|
+
// convertAxTree works unchanged. `ignored` always false (interestingOnly no-op).
|
|
758
|
+
NSString* const kAxScript = @"(function(){"
|
|
759
|
+
"var nodes=[];"
|
|
760
|
+
"function add(el,parentId){"
|
|
761
|
+
"if(!el||el.nodeType!==1)return null;"
|
|
762
|
+
"var id=String(nodes.length+1);"
|
|
763
|
+
"var node={nodeId:id,parentId:parentId,"
|
|
764
|
+
"role:{value:el.getAttribute('role')||el.tagName.toLowerCase()},"
|
|
765
|
+
"name:{value:el.getAttribute('aria-label')||"
|
|
766
|
+
"(el.tagName==='INPUT'||el.tagName==='TEXTAREA'?'':"
|
|
767
|
+
"(el.firstChild&&el.firstChild.nodeType===3?el.firstChild.textContent.trim().slice(0,100):''))},"
|
|
768
|
+
"properties:[],childIds:[],ignored:false};"
|
|
769
|
+
"var d=el.getAttribute('aria-description');if(d)node.description={value:d};"
|
|
770
|
+
"if(el.tagName==='INPUT'||el.tagName==='TEXTAREA'){if(el.value)node.value={value:el.value};}"
|
|
771
|
+
"if(el.getAttribute('aria-disabled')==='true'||el.disabled)node.properties.push({name:'disabled',value:{value:true}});"
|
|
772
|
+
"var ck=el.getAttribute('aria-checked');"
|
|
773
|
+
"if(ck==='true')node.properties.push({name:'checked',value:{value:true}});"
|
|
774
|
+
"else if(ck==='false')node.properties.push({name:'checked',value:{value:false}});"
|
|
775
|
+
"else if(ck==='mixed')node.properties.push({name:'checked',value:{value:'mixed'}});"
|
|
776
|
+
"var pr=el.getAttribute('aria-pressed');"
|
|
777
|
+
"if(pr==='true')node.properties.push({name:'pressed',value:{value:true}});"
|
|
778
|
+
"else if(pr==='false')node.properties.push({name:'pressed',value:{value:false}});"
|
|
779
|
+
"else if(pr==='mixed')node.properties.push({name:'pressed',value:{value:'mixed'}});"
|
|
780
|
+
"if(el.getAttribute('aria-expanded')==='true')node.properties.push({name:'expanded',value:{value:true}});"
|
|
781
|
+
"if(el.getAttribute('aria-selected')==='true')node.properties.push({name:'selected',value:{value:true}});"
|
|
782
|
+
"if(el.getAttribute('aria-required')==='true')node.properties.push({name:'required',value:{value:true}});"
|
|
783
|
+
"if(el.getAttribute('aria-invalid')==='true')node.properties.push({name:'invalid',value:{value:true}});"
|
|
784
|
+
"var lv=el.getAttribute('aria-level');if(lv)node.properties.push({name:'level',value:{value:parseInt(lv,10)}});"
|
|
785
|
+
"if(document.activeElement===el)node.properties.push({name:'focused',value:{value:true}});"
|
|
786
|
+
"nodes.push(node);"
|
|
787
|
+
"for(var i=0;i<el.children.length;i++){var cid=add(el.children[i],id);if(cid)node.childIds.push(cid);}"
|
|
788
|
+
"return id;"
|
|
789
|
+
"}"
|
|
790
|
+
"add(document.documentElement,null);"
|
|
791
|
+
"return JSON.stringify({nodes:nodes});"
|
|
792
|
+
"})()";
|
|
793
|
+
|
|
794
|
+
// JS-bridge frame tree — walks window.frames into CDP `Page.getFrameTree` shape.
|
|
795
|
+
NSString* const kFramesScript = @"(function(){"
|
|
796
|
+
"var id=0;"
|
|
797
|
+
"function walk(win){"
|
|
798
|
+
"var fid=String(++id);"
|
|
799
|
+
"var frame={id:fid,securityOrigin:'',url:''};"
|
|
800
|
+
"try{frame.url=win.location.href;frame.securityOrigin=win.location.origin;"
|
|
801
|
+
"if(win.frameElement&&win.frameElement.name)frame.name=win.frameElement.name;}catch(e){}"
|
|
802
|
+
"var children=[];"
|
|
803
|
+
"try{for(var i=0;i<win.frames.length;i++)children.push(walk(win.frames[i]));}catch(e){}"
|
|
804
|
+
"return {frame:frame,childFrames:children};"
|
|
805
|
+
"}"
|
|
806
|
+
"return JSON.stringify({frameTree:walk(window)});"
|
|
807
|
+
"})()";
|
|
808
|
+
|
|
809
|
+
} // namespace
|
|
810
|
+
|
|
811
|
+
extern "C" BUNITE_EXPORT void bunite_view_accessibility_snapshot(uint32_t view_id, uint32_t request_id,
|
|
812
|
+
int32_t /*interesting_only*/) {
|
|
813
|
+
runOnUiThreadSync([=]() {
|
|
814
|
+
auto* v = bunite_mac::findView(view_id);
|
|
815
|
+
if (!v || !v->webview) {
|
|
816
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
817
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
818
|
+
"\"message\":\"view not ready\"}";
|
|
819
|
+
bunite_mac::emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
[v->webview evaluateJavaScript:kAxScript completionHandler:^(id result, NSError* error) {
|
|
823
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id);
|
|
824
|
+
if (error || ![result isKindOfClass:[NSString class]]) {
|
|
825
|
+
std::string msg = error ? std::string(error.localizedDescription.UTF8String ?: "evaluate failed")
|
|
826
|
+
: std::string("non-string ax result");
|
|
827
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
828
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
829
|
+
} else {
|
|
830
|
+
std::string tree_json = ((NSString*)result).UTF8String ?: "{}";
|
|
831
|
+
payload += ",\"ok\":true,\"tree\":" + tree_json + "}";
|
|
832
|
+
}
|
|
833
|
+
bunite_mac::emitWebviewEvent(view_id, "accessibility-result", payload);
|
|
834
|
+
}];
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
extern "C" BUNITE_EXPORT void bunite_view_list_frames(uint32_t view_id, uint32_t request_id) {
|
|
839
|
+
runOnUiThreadSync([=]() {
|
|
840
|
+
auto* v = bunite_mac::findView(view_id);
|
|
841
|
+
if (!v || !v->webview) {
|
|
842
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
843
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
844
|
+
"\"message\":\"view not ready\"}";
|
|
845
|
+
bunite_mac::emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
[v->webview evaluateJavaScript:kFramesScript completionHandler:^(id result, NSError* error) {
|
|
849
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id);
|
|
850
|
+
if (error || ![result isKindOfClass:[NSString class]]) {
|
|
851
|
+
std::string msg = error ? std::string(error.localizedDescription.UTF8String ?: "evaluate failed")
|
|
852
|
+
: std::string("non-string frames result");
|
|
853
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
854
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
855
|
+
} else {
|
|
856
|
+
std::string tree_json = ((NSString*)result).UTF8String ?: "{}";
|
|
857
|
+
payload += ",\"ok\":true,\"raw\":" + tree_json + "}";
|
|
858
|
+
}
|
|
859
|
+
bunite_mac::emitWebviewEvent(view_id, "list-frames-result", payload);
|
|
860
|
+
}];
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
extern "C" BUNITE_EXPORT void bunite_view_evaluate_in_frame(uint32_t view_id, uint32_t request_id,
|
|
865
|
+
const char* script_c, const char* frame_id_c) {
|
|
866
|
+
std::string script = script_c ? script_c : "";
|
|
867
|
+
std::string frame_id = frame_id_c ? frame_id_c : "";
|
|
868
|
+
if (frame_id.empty()) {
|
|
869
|
+
bunite_view_evaluate(view_id, request_id, script_c);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
std::string js_target = bunite_mac::escapeJsonString(frame_id);
|
|
873
|
+
std::string js_script = bunite_mac::escapeJsonString(script);
|
|
874
|
+
std::string inner =
|
|
875
|
+
"(function(){var target=\"" + js_target + "\";var id=0;var found=null;"
|
|
876
|
+
"function walk(win){var fid=String(++id);if(fid===target){found=win;return;}"
|
|
877
|
+
"try{for(var i=0;i<win.frames.length;i++){walk(win.frames[i]);if(found)return;}}catch(e){}}"
|
|
878
|
+
"walk(window);"
|
|
879
|
+
"if(!found)throw new Error('frame not found');"
|
|
880
|
+
"return found.eval(\"(\"+\"" + js_script + "\"+\")\");"
|
|
881
|
+
"})()";
|
|
882
|
+
bunite_view_evaluate(view_id, request_id, inner.c_str());
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
namespace {
|
|
886
|
+
|
|
887
|
+
void emitResolveAndClickErrorMac(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
|
|
888
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
889
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
890
|
+
"\"message\":\"" + bunite_mac::escapeJsonString(msg) + "\"}";
|
|
891
|
+
bunite_mac::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
std::string escapeForJsStringMac(const std::string& s) {
|
|
895
|
+
std::string out; out.reserve(s.size() + 2);
|
|
896
|
+
for (char c : s) {
|
|
897
|
+
if (c == '"' || c == '\\') { out.push_back('\\'); out.push_back(c); }
|
|
898
|
+
else if (c == '\n') out += "\\n";
|
|
899
|
+
else if (c == '\r') out += "\\r";
|
|
900
|
+
else if (c == '\t') out += "\\t";
|
|
901
|
+
else out.push_back(c);
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
} // namespace
|
|
907
|
+
|
|
908
|
+
extern "C" BUNITE_EXPORT void bunite_view_resolve_and_click(
|
|
909
|
+
uint32_t view_id, uint32_t request_id,
|
|
910
|
+
const char* selector_c, const char* frame_id_c,
|
|
911
|
+
int32_t button, int32_t click_count, uint32_t modifiers) {
|
|
912
|
+
std::string selector = selector_c ? selector_c : "";
|
|
913
|
+
std::string frameId = frame_id_c ? frame_id_c : "";
|
|
914
|
+
if (!frameId.empty()) {
|
|
915
|
+
// No frame addressing on WKWebView — sync reject before UI hop.
|
|
916
|
+
emitResolveAndClickErrorMac(view_id, request_id, "not_supported",
|
|
917
|
+
"WKWebView has no frame addressing API");
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (click_count < 1) click_count = 1;
|
|
921
|
+
std::string sel_lit = "\"" + escapeForJsStringMac(selector) + "\"";
|
|
922
|
+
std::string script =
|
|
923
|
+
"(function(){"
|
|
924
|
+
"var el=document.querySelector(" + sel_lit + ");"
|
|
925
|
+
"if(!el)return{ok:false,code:\"not_found\"};"
|
|
926
|
+
"el.scrollIntoView({block:\"nearest\",inline:\"nearest\",behavior:\"instant\"});"
|
|
927
|
+
"var r=el.getBoundingClientRect();"
|
|
928
|
+
"var vis=r.width>0&&r.height>0&&r.bottom>0&&r.right>0"
|
|
929
|
+
"&&r.top<innerHeight&&r.left<innerWidth;"
|
|
930
|
+
"if(!vis)return{ok:false,code:\"not_visible\"};"
|
|
931
|
+
"return{ok:true,x:r.x,y:r.y,w:r.width,h:r.height,"
|
|
932
|
+
"cx:r.x+r.width/2,cy:r.y+r.height/2};"
|
|
933
|
+
"})()";
|
|
934
|
+
NSString* nsScript = [NSString stringWithUTF8String:script.c_str()];
|
|
935
|
+
runOnUiThreadSync([=]() {
|
|
936
|
+
auto* v = bunite_mac::findView(view_id);
|
|
937
|
+
if (!v || !v->webview || !v->webview.window) {
|
|
938
|
+
emitResolveAndClickErrorMac(view_id, request_id, "runtime_error", "view not ready");
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
[v->webview evaluateJavaScript:nsScript completionHandler:^(id result, NSError* error) {
|
|
942
|
+
if (error || ![result isKindOfClass:[NSDictionary class]]) {
|
|
943
|
+
std::string msg = error ? std::string(error.localizedDescription.UTF8String ?: "evaluate failed")
|
|
944
|
+
: std::string("evaluate returned non-object");
|
|
945
|
+
emitResolveAndClickErrorMac(view_id, request_id, "runtime_error", msg);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
NSDictionary* d = (NSDictionary*)result;
|
|
949
|
+
id okVal = d[@"ok"];
|
|
950
|
+
const bool ok = [okVal respondsToSelector:@selector(boolValue)] && [okVal boolValue];
|
|
951
|
+
if (!ok) {
|
|
952
|
+
id codeVal = d[@"code"];
|
|
953
|
+
std::string code = [codeVal isKindOfClass:[NSString class]]
|
|
954
|
+
? std::string([(NSString*)codeVal UTF8String] ?: "runtime_error")
|
|
955
|
+
: std::string("runtime_error");
|
|
956
|
+
emitResolveAndClickErrorMac(view_id, request_id, code.c_str(), "");
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
auto pickD = [&](NSString* k)->double {
|
|
960
|
+
id x = d[k];
|
|
961
|
+
return [x respondsToSelector:@selector(doubleValue)] ? [x doubleValue] : 0.0;
|
|
962
|
+
};
|
|
963
|
+
double x = pickD(@"x"), y = pickD(@"y"), w = pickD(@"w"), h = pickD(@"h");
|
|
964
|
+
double cx = pickD(@"cx"), cy = pickD(@"cy");
|
|
965
|
+
auto* v2 = bunite_mac::findView(view_id);
|
|
966
|
+
if (!v2 || !v2->webview || !v2->webview.window) {
|
|
967
|
+
emitResolveAndClickErrorMac(view_id, request_id, "runtime_error", "view destroyed");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
NSWindow* win = v2->webview.window;
|
|
971
|
+
NSPoint loc = viewPointToWindow(v2->webview, cx, cy);
|
|
972
|
+
NSEventModifierFlags flags = macModifiers(modifiers) & ~NSEventModifierFlagControl;
|
|
973
|
+
for (int i = 1; i <= click_count; ++i) {
|
|
974
|
+
NSEvent* down = [NSEvent mouseEventWithType:macMouseDownType(button)
|
|
975
|
+
location:loc modifierFlags:flags
|
|
976
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
977
|
+
windowNumber:win.windowNumber context:nil
|
|
978
|
+
eventNumber:0 clickCount:i pressure:1.0];
|
|
979
|
+
NSEvent* up = [NSEvent mouseEventWithType:macMouseUpType(button)
|
|
980
|
+
location:loc modifierFlags:flags
|
|
981
|
+
timestamp:[[NSProcessInfo processInfo] systemUptime]
|
|
982
|
+
windowNumber:win.windowNumber context:nil
|
|
983
|
+
eventNumber:0 clickCount:i pressure:0.0];
|
|
984
|
+
if (down) {
|
|
985
|
+
if (button == 0) [v2->webview mouseDown:down];
|
|
986
|
+
else if (button == 2) [v2->webview rightMouseDown:down];
|
|
987
|
+
else [v2->webview otherMouseDown:down];
|
|
988
|
+
}
|
|
989
|
+
if (up) {
|
|
990
|
+
if (button == 0) [v2->webview mouseUp:up];
|
|
991
|
+
else if (button == 2) [v2->webview rightMouseUp:up];
|
|
992
|
+
else [v2->webview otherMouseUp:up];
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
996
|
+
",\"ok\":true,\"rect\":{\"x\":" + std::to_string(x) +
|
|
997
|
+
",\"y\":" + std::to_string(y) +
|
|
998
|
+
",\"width\":" + std::to_string(w) +
|
|
999
|
+
",\"height\":" + std::to_string(h) + "},"
|
|
1000
|
+
"\"isTrustedEvent\":true}";
|
|
1001
|
+
bunite_mac::emitWebviewEvent(view_id, "resolve-and-click-result", payload);
|
|
1002
|
+
}];
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_download_policy(uint32_t view_id, int32_t policy, const char* download_dir) {
|
|
1007
|
+
auto* st = bunite_mac::findView(view_id);
|
|
1008
|
+
if (!st) return;
|
|
1009
|
+
if (policy < 0 || policy > 2) policy = 2;
|
|
1010
|
+
st->download_policy.store(policy);
|
|
1011
|
+
st->download_dir = download_dir ? download_dir : "";
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_accept(uint32_t new_view_id, uint32_t host_window_id,
|
|
1015
|
+
double x, double y, double w, double h) {
|
|
1016
|
+
runOnUiThreadSync([=]() { bunite_mac::acceptParkedPopup(new_view_id, host_window_id, x, y, w, h); });
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
extern "C" BUNITE_EXPORT void bunite_view_popup_dismiss(uint32_t new_view_id) {
|
|
1020
|
+
runOnUiThreadSync([=]() { bunite_mac::dismissParkedPopup(new_view_id); });
|
|
747
1021
|
}
|
|
748
1022
|
|
|
749
1023
|
extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
@@ -67,6 +67,10 @@ struct ViewState {
|
|
|
67
67
|
// respondToDialog invokes them; the page execution is paused meanwhile.
|
|
68
68
|
std::unordered_map<uint32_t, void(^)(bool /*accept*/, const std::string& /*text*/)> pending_dialogs;
|
|
69
69
|
uint32_t next_dialog_request_id = 1;
|
|
70
|
+
|
|
71
|
+
// Download policy: 0=auto, 1=ask (treated as block), 2=block (default).
|
|
72
|
+
std::atomic<int32_t> download_policy{2};
|
|
73
|
+
std::string download_dir;
|
|
70
74
|
};
|
|
71
75
|
|
|
72
76
|
// ---------------------------------------------------------------------------
|
|
@@ -92,6 +96,11 @@ struct RuntimeState {
|
|
|
92
96
|
|
|
93
97
|
BuniteWebviewEventHandler webview_event_handler = nullptr;
|
|
94
98
|
BuniteWindowEventHandler window_event_handler = nullptr;
|
|
99
|
+
|
|
100
|
+
// Hidden NSWindow — popup-minted WKWebViews park here until adoption.
|
|
101
|
+
__strong NSWindow* popup_parent = nil;
|
|
102
|
+
// Parked popup webviews awaiting acceptPopup/dismissPopup. Key = popup view_id (>= 0x80000000).
|
|
103
|
+
__strong NSMutableDictionary<NSNumber*, WKWebView*>* parked_popups = nil;
|
|
95
104
|
};
|
|
96
105
|
|
|
97
106
|
extern RuntimeState g_runtime;
|
|
@@ -139,6 +148,10 @@ void destroyWindow(uint32_t window_id);
|
|
|
139
148
|
// Defined in bunite_mac_view.mm.
|
|
140
149
|
ViewState* findView(uint32_t view_id);
|
|
141
150
|
uint32_t viewIdForWebView(WKWebView* wv); // returns 0 if not tracked
|
|
151
|
+
void registerWebViewId(WKWebView* wv, uint32_t view_id);
|
|
152
|
+
void unregisterWebViewId(WKWebView* wv);
|
|
153
|
+
bool acceptParkedPopup(uint32_t new_view_id, uint32_t host_window_id, double x, double y, double w, double h);
|
|
154
|
+
void dismissParkedPopup(uint32_t new_view_id);
|
|
142
155
|
bool createView(uint32_t view_id, uint32_t window_id,
|
|
143
156
|
NSString* url, NSString* html, NSString* preload, NSString* appres_root,
|
|
144
157
|
NSString* navigation_rules_json, NSString* preload_origins_json,
|