bunite-core 0.12.0 → 0.14.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 +17 -1
- package/src/host/core/BrowserView.ts +197 -28
- package/src/host/core/SurfaceBrowserIPC.ts +44 -3
- package/src/host/core/SurfaceManager.ts +260 -28
- package/src/host/core/SurfaceRegistry.ts +9 -1
- package/src/host/core/inputDispatch.ts +147 -0
- package/src/host/events/webviewEvents.ts +8 -1
- package/src/host/native.ts +124 -1
- package/src/native/linux/bunite_linux_ffi.cpp +223 -6
- package/src/native/linux/bunite_linux_internal.h +6 -0
- package/src/native/linux/bunite_linux_runtime.cpp +1 -1
- package/src/native/linux/bunite_linux_utils.cpp +2 -2
- package/src/native/linux/bunite_linux_view.cpp +85 -0
- package/src/native/mac/bunite_mac_ffi.mm +356 -8
- package/src/native/mac/bunite_mac_internal.h +6 -0
- package/src/native/mac/bunite_mac_utils.mm +2 -2
- package/src/native/mac/bunite_mac_view.mm +144 -2
- package/src/native/shared/ffi_exports.h +135 -0
- package/src/native/win/native_host_cef.cpp +86 -3
- package/src/native/win/native_host_ffi.cpp +378 -1
- package/src/native/win/native_host_internal.h +13 -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 +303 -9
- package/src/native/win-webview2/webview2_internal.h +11 -0
- package/src/native/win-webview2/webview2_runtime.cpp +128 -12
- 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 +173 -4
- package/src/rpc/index.ts +21 -0
- package/src/webview/native.ts +126 -25
- package/src/webview/polyfill.ts +196 -12
package/src/host/native.ts
CHANGED
|
@@ -102,6 +102,31 @@ type NativeSymbols = {
|
|
|
102
102
|
bunite_view_reload: (viewId: number) => void;
|
|
103
103
|
bunite_view_execute_javascript: (viewId: number, script: CStringPointer) => void;
|
|
104
104
|
bunite_view_evaluate: (viewId: number, requestId: number, script: CStringPointer) => void;
|
|
105
|
+
bunite_view_click: (
|
|
106
|
+
viewId: number, x: number, y: number,
|
|
107
|
+
button: number, clickCount: number, modifiers: number
|
|
108
|
+
) => void;
|
|
109
|
+
bunite_view_type: (viewId: number, text: CStringPointer) => void;
|
|
110
|
+
bunite_view_press: (
|
|
111
|
+
viewId: number, windowsVkCode: number, macKeyCode: number,
|
|
112
|
+
key: CStringPointer, code: CStringPointer, character: CStringPointer,
|
|
113
|
+
modifiers: number, action: number, extended: boolean, location: number
|
|
114
|
+
) => void;
|
|
115
|
+
bunite_view_scroll: (
|
|
116
|
+
viewId: number, dx: number, dy: number,
|
|
117
|
+
x: number, y: number, modifiers: number
|
|
118
|
+
) => void;
|
|
119
|
+
bunite_view_mouse: (
|
|
120
|
+
viewId: number, action: number, x: number, y: number,
|
|
121
|
+
button: number, modifiers: number
|
|
122
|
+
) => void;
|
|
123
|
+
bunite_view_respond_dialog: (
|
|
124
|
+
viewId: number, requestId: number, accept: boolean, text: CStringPointer
|
|
125
|
+
) => void;
|
|
126
|
+
bunite_view_screenshot: (
|
|
127
|
+
viewId: number, requestId: number, format: CStringPointer, quality: number
|
|
128
|
+
) => void;
|
|
129
|
+
bunite_view_capabilities: (viewId: number) => number;
|
|
105
130
|
bunite_view_load_url: (viewId: number, url: CStringPointer) => void;
|
|
106
131
|
bunite_view_load_html: (viewId: number, html: CStringPointer) => void;
|
|
107
132
|
bunite_view_remove: (viewId: number) => void;
|
|
@@ -289,6 +314,38 @@ const nativeSymbolDefinitions = {
|
|
|
289
314
|
args: [FFIType.u32, FFIType.u32, FFIType.cstring],
|
|
290
315
|
returns: FFIType.void
|
|
291
316
|
},
|
|
317
|
+
bunite_view_click: {
|
|
318
|
+
args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.i32, FFIType.i32, FFIType.u32],
|
|
319
|
+
returns: FFIType.void
|
|
320
|
+
},
|
|
321
|
+
bunite_view_type: {
|
|
322
|
+
args: [FFIType.u32, FFIType.cstring],
|
|
323
|
+
returns: FFIType.void
|
|
324
|
+
},
|
|
325
|
+
bunite_view_press: {
|
|
326
|
+
args: [FFIType.u32, FFIType.i32, FFIType.i32, FFIType.cstring, FFIType.cstring, FFIType.cstring, FFIType.u32, FFIType.i32, FFIType.bool, FFIType.i32],
|
|
327
|
+
returns: FFIType.void
|
|
328
|
+
},
|
|
329
|
+
bunite_view_scroll: {
|
|
330
|
+
args: [FFIType.u32, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.f64, FFIType.u32],
|
|
331
|
+
returns: FFIType.void
|
|
332
|
+
},
|
|
333
|
+
bunite_view_mouse: {
|
|
334
|
+
args: [FFIType.u32, FFIType.i32, FFIType.f64, FFIType.f64, FFIType.i32, FFIType.u32],
|
|
335
|
+
returns: FFIType.void
|
|
336
|
+
},
|
|
337
|
+
bunite_view_respond_dialog: {
|
|
338
|
+
args: [FFIType.u32, FFIType.u32, FFIType.bool, FFIType.cstring],
|
|
339
|
+
returns: FFIType.void
|
|
340
|
+
},
|
|
341
|
+
bunite_view_screenshot: {
|
|
342
|
+
args: [FFIType.u32, FFIType.u32, FFIType.cstring, FFIType.i32],
|
|
343
|
+
returns: FFIType.void
|
|
344
|
+
},
|
|
345
|
+
bunite_view_capabilities: {
|
|
346
|
+
args: [FFIType.u32],
|
|
347
|
+
returns: FFIType.u32
|
|
348
|
+
},
|
|
292
349
|
bunite_view_load_url: {
|
|
293
350
|
args: [FFIType.u32, FFIType.cstring],
|
|
294
351
|
returns: FFIType.void
|
|
@@ -350,6 +407,20 @@ export function setEvaluateResultHandler(handler: (viewId: number, result: Nativ
|
|
|
350
407
|
evaluateResultHandler = handler;
|
|
351
408
|
}
|
|
352
409
|
|
|
410
|
+
export type NativeScreenshotResult = {
|
|
411
|
+
requestId: number;
|
|
412
|
+
ok: boolean;
|
|
413
|
+
format?: "png" | "jpeg";
|
|
414
|
+
mime?: string;
|
|
415
|
+
dataBase64?: string;
|
|
416
|
+
code?: string;
|
|
417
|
+
message?: string;
|
|
418
|
+
};
|
|
419
|
+
let screenshotResultHandler: ((viewId: number, result: NativeScreenshotResult) => void) | null = null;
|
|
420
|
+
export function setScreenshotResultHandler(handler: (viewId: number, result: NativeScreenshotResult) => void) {
|
|
421
|
+
screenshotResultHandler = handler;
|
|
422
|
+
}
|
|
423
|
+
|
|
353
424
|
// Per-view deferred resolvers for "view-ready" (OnAfterCreated).
|
|
354
425
|
const viewReadyResolvers = new Map<number, () => void>();
|
|
355
426
|
|
|
@@ -499,6 +570,11 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
|
|
|
499
570
|
evaluateResultHandler?.(viewId, parsed);
|
|
500
571
|
break;
|
|
501
572
|
}
|
|
573
|
+
case "screenshot-result": {
|
|
574
|
+
const parsed = maybeParsePayload(payload) as NativeScreenshotResult;
|
|
575
|
+
screenshotResultHandler?.(viewId, parsed);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
502
578
|
case "title-changed": {
|
|
503
579
|
const parsed = maybeParsePayload(payload) as { title: string };
|
|
504
580
|
buniteEventEmitter.emitEvent(
|
|
@@ -507,6 +583,53 @@ function registerNativeCallbacks(library: LoadedNativeLibrary) {
|
|
|
507
583
|
);
|
|
508
584
|
break;
|
|
509
585
|
}
|
|
586
|
+
case "load-start":
|
|
587
|
+
buniteEventEmitter.emitEvent(
|
|
588
|
+
buniteEventEmitter.events.webview.loadStart({ detail: payload }),
|
|
589
|
+
viewId
|
|
590
|
+
);
|
|
591
|
+
break;
|
|
592
|
+
case "load-finish":
|
|
593
|
+
buniteEventEmitter.emitEvent(
|
|
594
|
+
buniteEventEmitter.events.webview.loadFinish({ detail: payload }),
|
|
595
|
+
viewId
|
|
596
|
+
);
|
|
597
|
+
break;
|
|
598
|
+
case "load-fail": {
|
|
599
|
+
const parsed = maybeParsePayload(payload) as { url?: string; reason?: string };
|
|
600
|
+
buniteEventEmitter.emitEvent(
|
|
601
|
+
buniteEventEmitter.events.webview.loadFail({
|
|
602
|
+
url: parsed.url ?? "", reason: parsed.reason,
|
|
603
|
+
}),
|
|
604
|
+
viewId
|
|
605
|
+
);
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case "dialog": {
|
|
609
|
+
const parsed = maybeParsePayload(payload) as {
|
|
610
|
+
requestId: number;
|
|
611
|
+
kind: "alert" | "confirm" | "prompt" | "beforeunload";
|
|
612
|
+
message: string;
|
|
613
|
+
defaultPrompt?: string;
|
|
614
|
+
};
|
|
615
|
+
buniteEventEmitter.emitEvent(
|
|
616
|
+
buniteEventEmitter.events.webview.dialog(parsed),
|
|
617
|
+
viewId
|
|
618
|
+
);
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
case "console-message": {
|
|
622
|
+
const parsed = maybeParsePayload(payload) as {
|
|
623
|
+
level: "log" | "warn" | "error" | "info" | "debug";
|
|
624
|
+
args: string[];
|
|
625
|
+
ts: number;
|
|
626
|
+
};
|
|
627
|
+
buniteEventEmitter.emitEvent(
|
|
628
|
+
buniteEventEmitter.events.webview.consoleMessage(parsed),
|
|
629
|
+
viewId
|
|
630
|
+
);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
510
633
|
}
|
|
511
634
|
},
|
|
512
635
|
{
|
|
@@ -656,7 +779,7 @@ export async function initNativeRuntime(
|
|
|
656
779
|
throw new Error(`bunite: failed to load native library at ${artifacts.nativeLibPath}.`);
|
|
657
780
|
}
|
|
658
781
|
|
|
659
|
-
const EXPECTED_ABI =
|
|
782
|
+
const EXPECTED_ABI = 9;
|
|
660
783
|
const nativeAbi = nativeLibrary.symbols.bunite_abi_version();
|
|
661
784
|
if (nativeAbi !== EXPECTED_ABI) {
|
|
662
785
|
throw new Error(
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#include "bunite_linux_internal.h"
|
|
2
2
|
#include "webview_storage.h"
|
|
3
3
|
|
|
4
|
+
#include <cairo.h>
|
|
5
|
+
|
|
4
6
|
#include <cstdlib>
|
|
5
7
|
#include <cstring>
|
|
6
8
|
#include <mutex>
|
|
7
9
|
#include <string>
|
|
10
|
+
#include <vector>
|
|
8
11
|
|
|
9
12
|
using bunite_linux::g_runtime;
|
|
10
13
|
using bunite_linux::runOnUiThreadSync;
|
|
@@ -133,12 +136,103 @@ extern "C" BUNITE_EXPORT void bunite_view_execute_javascript(uint32_t view_id, c
|
|
|
133
136
|
});
|
|
134
137
|
}
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
namespace {
|
|
140
|
+
|
|
141
|
+
struct EvaluateCtx {
|
|
142
|
+
uint32_t view_id;
|
|
143
|
+
uint32_t request_id;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
void on_evaluate_done(GObject* source, GAsyncResult* res, gpointer user_data) {
|
|
147
|
+
auto* ctx = static_cast<EvaluateCtx*>(user_data);
|
|
148
|
+
WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
|
|
149
|
+
GError* err = nullptr;
|
|
150
|
+
JSCValue* value = webkit_web_view_evaluate_javascript_finish(wv, res, &err);
|
|
151
|
+
|
|
152
|
+
std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id);
|
|
153
|
+
if (err || !value) {
|
|
154
|
+
std::string msg = err ? err->message : "evaluate failed";
|
|
155
|
+
if (err) g_error_free(err);
|
|
156
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
157
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
158
|
+
} else if (!jsc_value_is_string(value)) {
|
|
159
|
+
payload += ",\"ok\":false,\"code\":\"runtime_error\","
|
|
160
|
+
"\"message\":\"wrapper returned non-string\"}";
|
|
161
|
+
} else {
|
|
162
|
+
char* raw = jsc_value_to_string(value);
|
|
163
|
+
std::string inner = raw ? raw : "";
|
|
164
|
+
if (raw) g_free(raw);
|
|
165
|
+
if (inner.find("\"__bunite_ok\":true") != std::string::npos) {
|
|
166
|
+
static const std::string prefix = "{\"__bunite_ok\":true,\"value\":";
|
|
167
|
+
std::string value_json = "null";
|
|
168
|
+
if (inner.compare(0, prefix.size(), prefix) == 0 &&
|
|
169
|
+
inner.size() > prefix.size() + 1) {
|
|
170
|
+
value_json = inner.substr(prefix.size(), inner.size() - prefix.size() - 1);
|
|
171
|
+
}
|
|
172
|
+
payload += ",\"ok\":true,\"value\":\"" + bunite_linux::escapeJsonString(value_json) + "\"}";
|
|
173
|
+
} else {
|
|
174
|
+
static const std::string codePrefix = "{\"__bunite_ok\":false,\"code\":\"";
|
|
175
|
+
std::string code = "runtime_error";
|
|
176
|
+
std::string msg = "script threw";
|
|
177
|
+
if (inner.compare(0, codePrefix.size(), codePrefix) == 0) {
|
|
178
|
+
size_t start = codePrefix.size();
|
|
179
|
+
size_t end = start;
|
|
180
|
+
while (end < inner.size() && inner[end] != '"') ++end;
|
|
181
|
+
if (end > start) code = inner.substr(start, end - start);
|
|
182
|
+
static const std::string msgKey = "\",\"message\":\"";
|
|
183
|
+
if (end + msgKey.size() <= inner.size() &&
|
|
184
|
+
inner.compare(end, msgKey.size(), msgKey) == 0) {
|
|
185
|
+
size_t mstart = end + msgKey.size();
|
|
186
|
+
size_t mend = mstart;
|
|
187
|
+
while (mend < inner.size()) {
|
|
188
|
+
if (inner[mend] == '"' && (mend == mstart || inner[mend - 1] != '\\')) break;
|
|
189
|
+
++mend;
|
|
190
|
+
}
|
|
191
|
+
if (mend > mstart) msg = inner.substr(mstart, mend - mstart);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
payload += ",\"ok\":false,\"code\":\"" + bunite_linux::escapeJsonString(code) + "\","
|
|
195
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (value) g_object_unref(value);
|
|
199
|
+
bunite_linux::emitWebviewEvent(ctx->view_id, "evaluate-result", payload);
|
|
200
|
+
delete ctx;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
} // namespace
|
|
204
|
+
|
|
205
|
+
extern "C" BUNITE_EXPORT void bunite_view_evaluate(uint32_t view_id, uint32_t request_id, const char* script) {
|
|
206
|
+
// Wrapper matches WebView2/CEF: try/catch returns JSON envelope string.
|
|
207
|
+
// jsc_value_to_string delivers the wrapper's return value directly.
|
|
208
|
+
if (!script) {
|
|
209
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
210
|
+
",\"ok\":false,\"code\":\"runtime_error\","
|
|
211
|
+
"\"message\":\"null script\"}";
|
|
212
|
+
bunite_linux::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
std::string wrapped =
|
|
216
|
+
"(function(){try{return JSON.stringify({__bunite_ok:true,value:("
|
|
217
|
+
+ std::string(script) +
|
|
218
|
+
")})}catch(e){var c=(e&&e.name===\"SecurityError\")?\"cross_origin\":\"runtime_error\";"
|
|
219
|
+
"return JSON.stringify({__bunite_ok:false,code:c,"
|
|
220
|
+
"message:(e&&e.message)?e.message:String(e),"
|
|
221
|
+
"name:(e&&e.name)||\"\"})}})()";
|
|
222
|
+
runOnUiThreadSync([=]() {
|
|
223
|
+
auto* v = bunite_linux::findView(view_id);
|
|
224
|
+
if (!v || !v->webview) {
|
|
225
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
226
|
+
",\"ok\":false,\"code\":\"not_supported\","
|
|
227
|
+
"\"message\":\"view not ready\"}";
|
|
228
|
+
bunite_linux::emitWebviewEvent(view_id, "evaluate-result", payload);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
auto* ctx = new EvaluateCtx{view_id, request_id};
|
|
232
|
+
webkit_web_view_evaluate_javascript(
|
|
233
|
+
v->webview, wrapped.c_str(), -1, nullptr, nullptr, nullptr,
|
|
234
|
+
on_evaluate_done, ctx);
|
|
235
|
+
});
|
|
142
236
|
}
|
|
143
237
|
|
|
144
238
|
extern "C" BUNITE_EXPORT void bunite_view_load_url(uint32_t view_id, const char* url) {
|
|
@@ -254,6 +348,129 @@ extern "C" BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) {
|
|
|
254
348
|
runOnUiThreadSync([=]() { bunite_linux::removeView(view_id); });
|
|
255
349
|
}
|
|
256
350
|
|
|
351
|
+
// Input dispatch — no-op on GTK4 + Wayland (no portable synthetic-input primitive).
|
|
352
|
+
// Capability `click/type/press/scroll: false` is honest; calls are silent no-ops.
|
|
353
|
+
extern "C" BUNITE_EXPORT void bunite_view_click(uint32_t, double, double, int32_t, int32_t, uint32_t) {}
|
|
354
|
+
extern "C" BUNITE_EXPORT void bunite_view_type(uint32_t, const char*) {}
|
|
355
|
+
extern "C" BUNITE_EXPORT void bunite_view_press(uint32_t, int32_t, int32_t, const char*, const char*, const char*, uint32_t, int32_t, bool, int32_t) {}
|
|
356
|
+
extern "C" BUNITE_EXPORT void bunite_view_scroll(uint32_t, double, double, double, double, uint32_t) {}
|
|
357
|
+
extern "C" BUNITE_EXPORT void bunite_view_mouse(uint32_t, int32_t, double, double, int32_t, uint32_t) {}
|
|
358
|
+
|
|
359
|
+
extern "C" BUNITE_EXPORT void bunite_view_respond_dialog(uint32_t view_id, uint32_t request_id,
|
|
360
|
+
bool accept, const char* text) {
|
|
361
|
+
bunite_linux::respondToDialogRequest(view_id, request_id, accept, text ? text : "");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Screenshot — webkit_web_view_get_snapshot → cairo_surface_t → PNG bytes via
|
|
365
|
+
// cairo_surface_write_to_png_stream → g_base64_encode. JPEG path uses GdkPixbuf
|
|
366
|
+
// for `pixbuf_save_to_buffer(... "jpeg" ...)`.
|
|
367
|
+
namespace {
|
|
368
|
+
|
|
369
|
+
struct LinuxShotCtx {
|
|
370
|
+
uint32_t view_id;
|
|
371
|
+
uint32_t request_id;
|
|
372
|
+
std::string format; // "png" | "jpeg"
|
|
373
|
+
int32_t quality;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
void emitLinuxShotError(uint32_t view_id, uint32_t request_id, const char* code, const std::string& msg) {
|
|
377
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
378
|
+
",\"ok\":false,\"code\":\"" + code + "\","
|
|
379
|
+
"\"message\":\"" + bunite_linux::escapeJsonString(msg) + "\"}";
|
|
380
|
+
bunite_linux::emitWebviewEvent(view_id, "screenshot-result", payload);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// WebKitGTK 2.52+ snapshot returns GdkTexture (was cairo_surface_t pre-2.52).
|
|
384
|
+
// PNG: gdk_texture_save_to_png_bytes — built-in. JPEG: bridge through GdkPixbuf
|
|
385
|
+
// via gdk_pixbuf_get_from_texture (non-deprecated GTK4 path).
|
|
386
|
+
void on_snapshot_done(GObject* source, GAsyncResult* res, gpointer user_data) {
|
|
387
|
+
auto* ctx = static_cast<LinuxShotCtx*>(user_data);
|
|
388
|
+
WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
|
|
389
|
+
GError* err = nullptr;
|
|
390
|
+
GdkTexture* texture = webkit_web_view_get_snapshot_finish(wv, res, &err);
|
|
391
|
+
if (!texture) {
|
|
392
|
+
emitLinuxShotError(ctx->view_id, ctx->request_id, "runtime_error",
|
|
393
|
+
err ? err->message : "snapshot returned nil");
|
|
394
|
+
if (err) g_error_free(err);
|
|
395
|
+
delete ctx;
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
std::vector<unsigned char> bytes;
|
|
400
|
+
std::string mime;
|
|
401
|
+
const bool jpeg = (ctx->format == "jpeg" || ctx->format == "jpg");
|
|
402
|
+
if (jpeg) {
|
|
403
|
+
GdkPixbuf* pix = gdk_pixbuf_get_from_texture(texture);
|
|
404
|
+
if (pix) {
|
|
405
|
+
gchar* raw = nullptr; gsize raw_len = 0;
|
|
406
|
+
char qbuf[8]; snprintf(qbuf, sizeof(qbuf), "%d",
|
|
407
|
+
ctx->quality < 0 ? 90 : (ctx->quality > 100 ? 100 : ctx->quality));
|
|
408
|
+
GError* perr = nullptr;
|
|
409
|
+
if (gdk_pixbuf_save_to_buffer(pix, &raw, &raw_len, "jpeg", &perr, "quality", qbuf, nullptr)) {
|
|
410
|
+
bytes.assign(raw, raw + raw_len);
|
|
411
|
+
g_free(raw);
|
|
412
|
+
} else if (perr) {
|
|
413
|
+
g_error_free(perr);
|
|
414
|
+
}
|
|
415
|
+
g_object_unref(pix);
|
|
416
|
+
}
|
|
417
|
+
mime = "image/jpeg";
|
|
418
|
+
ctx->format = "jpeg";
|
|
419
|
+
} else {
|
|
420
|
+
GBytes* png = gdk_texture_save_to_png_bytes(texture);
|
|
421
|
+
if (png) {
|
|
422
|
+
gsize n = 0;
|
|
423
|
+
const auto* p = static_cast<const unsigned char*>(g_bytes_get_data(png, &n));
|
|
424
|
+
bytes.assign(p, p + n);
|
|
425
|
+
g_bytes_unref(png);
|
|
426
|
+
}
|
|
427
|
+
mime = "image/png";
|
|
428
|
+
ctx->format = "png";
|
|
429
|
+
}
|
|
430
|
+
g_object_unref(texture);
|
|
431
|
+
|
|
432
|
+
if (bytes.empty()) {
|
|
433
|
+
emitLinuxShotError(ctx->view_id, ctx->request_id, "runtime_error", "encode failed");
|
|
434
|
+
delete ctx;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
gchar* b64 = g_base64_encode(bytes.data(), bytes.size());
|
|
438
|
+
std::string payload = "{\"requestId\":" + std::to_string(ctx->request_id) +
|
|
439
|
+
",\"ok\":true,\"format\":\"" + ctx->format +
|
|
440
|
+
"\",\"mime\":\"" + mime +
|
|
441
|
+
"\",\"dataBase64\":\"" + (b64 ? b64 : "") + "\"}";
|
|
442
|
+
if (b64) g_free(b64);
|
|
443
|
+
bunite_linux::emitWebviewEvent(ctx->view_id, "screenshot-result", payload);
|
|
444
|
+
delete ctx;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
} // namespace
|
|
448
|
+
|
|
449
|
+
extern "C" BUNITE_EXPORT uint32_t bunite_view_capabilities(uint32_t view_id) {
|
|
450
|
+
// WebKitGTK — input dispatch impossible on GTK4+Wayland; screenshot via cairo.
|
|
451
|
+
auto* v = bunite_linux::findView(view_id);
|
|
452
|
+
if (!v) return 0;
|
|
453
|
+
return BUNITE_CAP_EVALUATE | BUNITE_CAP_SURFACE_EVENTS |
|
|
454
|
+
BUNITE_CAP_DIALOGS | BUNITE_CAP_CONSOLE |
|
|
455
|
+
BUNITE_CAP_SCREENSHOT | BUNITE_CAP_FORMAT_PNG | BUNITE_CAP_FORMAT_JPEG;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
extern "C" BUNITE_EXPORT void bunite_view_screenshot(uint32_t view_id, uint32_t request_id,
|
|
459
|
+
const char* format, int32_t quality) {
|
|
460
|
+
std::string fmt = format ? format : "png";
|
|
461
|
+
runOnUiThreadSync([=]() {
|
|
462
|
+
auto* v = bunite_linux::findView(view_id);
|
|
463
|
+
if (!v || !v->webview) {
|
|
464
|
+
emitLinuxShotError(view_id, request_id, "not_supported", "view not ready");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
auto* ctx = new LinuxShotCtx{view_id, request_id, fmt, quality};
|
|
468
|
+
webkit_web_view_get_snapshot(v->webview, WEBKIT_SNAPSHOT_REGION_VISIBLE,
|
|
469
|
+
WEBKIT_SNAPSHOT_OPTIONS_NONE, nullptr,
|
|
470
|
+
on_snapshot_done, ctx);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
257
474
|
extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
258
475
|
(void)view_id; BUNITE_LINUX_TODO("bunite_view_open_devtools");
|
|
259
476
|
}
|
|
@@ -38,6 +38,11 @@ struct ViewState {
|
|
|
38
38
|
std::string preload_script;
|
|
39
39
|
std::string stored_html;
|
|
40
40
|
std::vector<std::string> navigation_rules;
|
|
41
|
+
|
|
42
|
+
// Page-initiated dialogs awaiting host response. WebKitScriptDialog is
|
|
43
|
+
// held until we mark confirmed/text + emit signal completion.
|
|
44
|
+
std::unordered_map<uint32_t, WebKitScriptDialog*> pending_dialogs;
|
|
45
|
+
uint32_t next_dialog_request_id = 1;
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
struct RuntimeState {
|
|
@@ -134,6 +139,7 @@ void destroyWindow(uint32_t window_id);
|
|
|
134
139
|
|
|
135
140
|
ViewState* findView(uint32_t view_id);
|
|
136
141
|
uint32_t viewIdForWebView(WebKitWebView* wv);
|
|
142
|
+
void respondToDialogRequest(uint32_t view_id, uint32_t request_id, bool accept, const std::string& text);
|
|
137
143
|
bool createView(uint32_t view_id, uint32_t window_id,
|
|
138
144
|
const char* url, const char* html, const char* preload, const char* appres_root,
|
|
139
145
|
const char* navigation_rules_json, const char* preload_origins_json,
|
|
@@ -93,8 +93,8 @@ std::vector<std::string> parseNavigationRulesJson(const std::string& json) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
bool shouldAlwaysAllowNavigationUrl(const std::string& url) {
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
// Exact-match — prefix would let `../../evil` style paths bypass scrutiny.
|
|
97
|
+
return url == "about:blank" || url == "appres://app.internal/internal/index.html";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
bool shouldAllowNavigation(const ViewState* view, const std::string& url) {
|
|
@@ -18,11 +18,15 @@ void emit_url(uint32_t view_id, const char* name, WebKitWebView* wv) {
|
|
|
18
18
|
void on_load_changed(WebKitWebView* wv, WebKitLoadEvent event, gpointer user_data) {
|
|
19
19
|
const uint32_t view_id = GPOINTER_TO_UINT(user_data);
|
|
20
20
|
switch (event) {
|
|
21
|
+
case WEBKIT_LOAD_STARTED:
|
|
22
|
+
emit_url(view_id, "load-start", wv);
|
|
23
|
+
break;
|
|
21
24
|
case WEBKIT_LOAD_COMMITTED:
|
|
22
25
|
emit_url(view_id, "did-navigate", wv);
|
|
23
26
|
queueViewRedraw(wv);
|
|
24
27
|
break;
|
|
25
28
|
case WEBKIT_LOAD_FINISHED:
|
|
29
|
+
emit_url(view_id, "load-finish", wv);
|
|
26
30
|
emit_url(view_id, "dom-ready", wv);
|
|
27
31
|
queueViewRedraw(wv);
|
|
28
32
|
break;
|
|
@@ -30,6 +34,55 @@ void on_load_changed(WebKitWebView* wv, WebKitLoadEvent event, gpointer user_dat
|
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
gboolean on_load_failed(WebKitWebView* /*wv*/, WebKitLoadEvent /*ev*/, const char* failing_uri,
|
|
38
|
+
GError* error, gpointer user_data) {
|
|
39
|
+
const uint32_t view_id = GPOINTER_TO_UINT(user_data);
|
|
40
|
+
std::string payload = "{\"url\":\"" + escapeJsonString(failing_uri ? failing_uri : "") +
|
|
41
|
+
"\",\"reason\":\"" + escapeJsonString(error && error->message ? error->message : "") + "\"}";
|
|
42
|
+
emitWebviewEvent(view_id, "load-fail", payload);
|
|
43
|
+
return FALSE; // let WebKit show its default error page
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
gboolean on_load_failed_tls(WebKitWebView* /*wv*/, const char* failing_uri,
|
|
47
|
+
GTlsCertificate* /*cert*/, GTlsCertificateFlags /*errors*/,
|
|
48
|
+
gpointer user_data) {
|
|
49
|
+
const uint32_t view_id = GPOINTER_TO_UINT(user_data);
|
|
50
|
+
std::string payload = "{\"url\":\"" + escapeJsonString(failing_uri ? failing_uri : "") +
|
|
51
|
+
"\",\"reason\":\"tls-certificate-error\"}";
|
|
52
|
+
emitWebviewEvent(view_id, "load-fail", payload);
|
|
53
|
+
return FALSE; // do not override default certificate failure behavior
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
gboolean on_script_dialog(WebKitWebView* /*wv*/, WebKitScriptDialog* dialog, gpointer user_data) {
|
|
57
|
+
const uint32_t view_id = GPOINTER_TO_UINT(user_data);
|
|
58
|
+
auto* v = findView(view_id);
|
|
59
|
+
if (!v) return FALSE;
|
|
60
|
+
WebKitScriptDialogType type = webkit_script_dialog_get_dialog_type(dialog);
|
|
61
|
+
const char* kind = nullptr;
|
|
62
|
+
switch (type) {
|
|
63
|
+
case WEBKIT_SCRIPT_DIALOG_ALERT: kind = "alert"; break;
|
|
64
|
+
case WEBKIT_SCRIPT_DIALOG_CONFIRM: kind = "confirm"; break;
|
|
65
|
+
case WEBKIT_SCRIPT_DIALOG_PROMPT: kind = "prompt"; break;
|
|
66
|
+
case WEBKIT_SCRIPT_DIALOG_BEFORE_UNLOAD_CONFIRM: kind = "beforeunload"; break;
|
|
67
|
+
default: return FALSE;
|
|
68
|
+
}
|
|
69
|
+
// Defer the dialog so the page execution stays paused until host responds.
|
|
70
|
+
webkit_script_dialog_ref(dialog);
|
|
71
|
+
const uint32_t rid = v->next_dialog_request_id++;
|
|
72
|
+
v->pending_dialogs[rid] = dialog;
|
|
73
|
+
const char* message = webkit_script_dialog_get_message(dialog);
|
|
74
|
+
std::string payload = "{\"requestId\":" + std::to_string(rid) +
|
|
75
|
+
",\"kind\":\"" + kind +
|
|
76
|
+
"\",\"message\":\"" + escapeJsonString(message ? message : "") + "\"";
|
|
77
|
+
if (type == WEBKIT_SCRIPT_DIALOG_PROMPT) {
|
|
78
|
+
const char* def = webkit_script_dialog_prompt_get_default_text(dialog);
|
|
79
|
+
payload += ",\"defaultPrompt\":\"" + escapeJsonString(def ? def : "") + "\"";
|
|
80
|
+
}
|
|
81
|
+
payload += "}";
|
|
82
|
+
emitWebviewEvent(view_id, "dialog", payload);
|
|
83
|
+
return TRUE; // we handled it
|
|
84
|
+
}
|
|
85
|
+
|
|
33
86
|
GtkWidget* on_create(WebKitWebView* wv, WebKitNavigationAction* action, gpointer user_data) {
|
|
34
87
|
const uint32_t view_id = GPOINTER_TO_UINT(user_data);
|
|
35
88
|
WebKitURIRequest* req = webkit_navigation_action_get_request(action);
|
|
@@ -40,6 +93,14 @@ GtkWidget* on_create(WebKitWebView* wv, WebKitNavigationAction* action, gpointer
|
|
|
40
93
|
return nullptr; // cancel; JS opens via load_url if desired
|
|
41
94
|
}
|
|
42
95
|
|
|
96
|
+
void on_title_changed(GObject* source, GParamSpec* /*pspec*/, gpointer user_data) {
|
|
97
|
+
const uint32_t view_id = GPOINTER_TO_UINT(user_data);
|
|
98
|
+
WebKitWebView* wv = WEBKIT_WEB_VIEW(source);
|
|
99
|
+
const char* title = webkit_web_view_get_title(wv);
|
|
100
|
+
std::string payload = "{\"title\":\"" + escapeJsonString(title ? title : "") + "\"}";
|
|
101
|
+
emitWebviewEvent(view_id, "title-changed", payload);
|
|
102
|
+
}
|
|
103
|
+
|
|
43
104
|
// WebKitGTK fires nav-action for sub-frames with no main-frame discriminator — iframes hit nav rules.
|
|
44
105
|
gboolean on_decide_policy(WebKitWebView* wv, WebKitPolicyDecision* decision,
|
|
45
106
|
WebKitPolicyDecisionType type, gpointer user_data) {
|
|
@@ -72,6 +133,26 @@ uint32_t viewIdForWebView(WebKitWebView* wv) {
|
|
|
72
133
|
return GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(wv), kViewIdKey));
|
|
73
134
|
}
|
|
74
135
|
|
|
136
|
+
void respondToDialogRequest(uint32_t view_id, uint32_t request_id, bool accept,
|
|
137
|
+
const std::string& text) {
|
|
138
|
+
auto* v = findView(view_id);
|
|
139
|
+
if (!v) return;
|
|
140
|
+
auto it = v->pending_dialogs.find(request_id);
|
|
141
|
+
if (it == v->pending_dialogs.end()) return;
|
|
142
|
+
WebKitScriptDialog* dialog = it->second;
|
|
143
|
+
v->pending_dialogs.erase(it);
|
|
144
|
+
if (!dialog) return;
|
|
145
|
+
WebKitScriptDialogType type = webkit_script_dialog_get_dialog_type(dialog);
|
|
146
|
+
if (type == WEBKIT_SCRIPT_DIALOG_CONFIRM || type == WEBKIT_SCRIPT_DIALOG_BEFORE_UNLOAD_CONFIRM) {
|
|
147
|
+
webkit_script_dialog_confirm_set_confirmed(dialog, accept ? TRUE : FALSE);
|
|
148
|
+
} else if (type == WEBKIT_SCRIPT_DIALOG_PROMPT) {
|
|
149
|
+
if (accept) webkit_script_dialog_prompt_set_text(dialog, text.c_str());
|
|
150
|
+
else webkit_script_dialog_prompt_set_text(dialog, nullptr);
|
|
151
|
+
}
|
|
152
|
+
webkit_script_dialog_close(dialog);
|
|
153
|
+
webkit_script_dialog_unref(dialog);
|
|
154
|
+
}
|
|
155
|
+
|
|
75
156
|
bool createView(uint32_t view_id, uint32_t window_id,
|
|
76
157
|
const char* url, const char* html, const char* preload, const char* appres_root,
|
|
77
158
|
const char* navigation_rules_json, const char* preload_origins_json,
|
|
@@ -122,8 +203,12 @@ bool createView(uint32_t view_id, uint32_t window_id,
|
|
|
122
203
|
registerAppresScheme(webkit_web_view_get_context(wv));
|
|
123
204
|
|
|
124
205
|
g_signal_connect(wv, "load-changed", G_CALLBACK(on_load_changed), GUINT_TO_POINTER(view_id));
|
|
206
|
+
g_signal_connect(wv, "load-failed", G_CALLBACK(on_load_failed), GUINT_TO_POINTER(view_id));
|
|
207
|
+
g_signal_connect(wv, "load-failed-with-tls-errors", G_CALLBACK(on_load_failed_tls), GUINT_TO_POINTER(view_id));
|
|
125
208
|
g_signal_connect(wv, "decide-policy", G_CALLBACK(on_decide_policy), GUINT_TO_POINTER(view_id));
|
|
126
209
|
g_signal_connect(wv, "create", G_CALLBACK(on_create), GUINT_TO_POINTER(view_id));
|
|
210
|
+
g_signal_connect(wv, "notify::title", G_CALLBACK(on_title_changed), GUINT_TO_POINTER(view_id));
|
|
211
|
+
g_signal_connect(wv, "script-dialog", G_CALLBACK(on_script_dialog), GUINT_TO_POINTER(view_id));
|
|
127
212
|
|
|
128
213
|
GtkWidget* container = GTK_WIDGET(wv);
|
|
129
214
|
if (auto_resize) {
|