bunite-core 0.5.0 → 0.8.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 -2
- package/src/bun/core/App.ts +35 -34
- package/src/bun/core/BrowserView.ts +3 -9
- package/src/bun/core/singleInstanceLock.ts +91 -0
- package/src/bun/index.ts +5 -6
- package/src/bun/preload/inline.ts +1 -2
- package/src/bun/proc/native.ts +54 -78
- package/src/native/linux/bunite_linux_appres.cpp +173 -0
- package/src/native/linux/bunite_linux_ffi.cpp +263 -0
- package/src/native/linux/bunite_linux_internal.h +148 -0
- package/src/native/linux/bunite_linux_preload.cpp +1 -0
- package/src/native/linux/bunite_linux_runtime.cpp +120 -0
- package/src/native/linux/bunite_linux_utils.cpp +114 -0
- package/src/native/linux/bunite_linux_view.cpp +244 -0
- package/src/native/linux/bunite_linux_window.cpp +101 -0
- package/src/native/mac/bunite_mac_appres.mm +163 -0
- package/src/native/mac/bunite_mac_ffi.mm +470 -0
- package/src/native/mac/bunite_mac_internal.h +151 -0
- package/src/native/mac/bunite_mac_runtime.mm +15 -0
- package/src/native/mac/bunite_mac_utils.mm +121 -0
- package/src/native/mac/bunite_mac_view.mm +279 -0
- package/src/native/mac/bunite_mac_window.mm +187 -0
- package/src/native/shared/ffi_exports.h +13 -13
- package/src/native/shared/permissions.h +14 -0
- package/src/native/shared/webview_storage.h +6 -3
- package/src/native/win/native_host_cef.cpp +4 -8
- package/src/native/win/native_host_ffi.cpp +76 -123
- package/src/native/win/native_host_internal.h +5 -3
- package/src/native/win/native_host_runtime.cpp +2 -6
- package/src/native/win/native_host_utils.cpp +23 -52
- package/src/native/win/process_helper_win.cpp +1 -3
- package/src/preload/runtime.ts +1 -3
- package/src/preload/tsconfig.tsbuildinfo +1 -1
- package/src/preload/webviewElement.ts +3 -8
- package/src/shared/paths.ts +35 -44
- package/src/shared/platform.ts +1 -2
- package/src/shared/rpc.ts +4 -13
- package/src/shared/rpcDemux.ts +47 -2
- package/src/shared/webRpcHandler.ts +1 -3
- package/src/shared/webviewPolyfill.ts +64 -6
- package/src/bun/core/Utils.ts +0 -301
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// appres://app.internal/* WKURLSchemeHandler.
|
|
2
|
+
// Precedence: stored HTML > dynamic route > static file under appres_root.
|
|
3
|
+
|
|
4
|
+
#import "bunite_mac_internal.h"
|
|
5
|
+
|
|
6
|
+
#include "webview_storage.h"
|
|
7
|
+
|
|
8
|
+
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
9
|
+
|
|
10
|
+
using bunite_mac::g_runtime;
|
|
11
|
+
using bunite_mac::findView;
|
|
12
|
+
using bunite_mac::viewIdForWebView;
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
NSString* mimeFor(NSString* path) {
|
|
17
|
+
NSString* ext = path.pathExtension.lowercaseString;
|
|
18
|
+
if (ext.length == 0) return @"text/html";
|
|
19
|
+
// UTType lacks application/wasm on older macOS — hard-map.
|
|
20
|
+
if ([ext isEqualToString:@"wasm"]) return @"application/wasm";
|
|
21
|
+
UTType* type = [UTType typeWithFilenameExtension:ext];
|
|
22
|
+
return type.preferredMIMEType ?: @"application/octet-stream";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Reject unsafe segments early — dynamic-route handlers (JS) must never see `..` paths.
|
|
26
|
+
BOOL hasUnsafeSegment(NSString* rel) {
|
|
27
|
+
for (NSString* seg in [rel componentsSeparatedByString:@"/"]) {
|
|
28
|
+
if (seg.length == 0) continue;
|
|
29
|
+
if ([seg isEqualToString:@"."]) return YES;
|
|
30
|
+
if ([seg isEqualToString:@".."]) return YES;
|
|
31
|
+
if ([seg containsString:@"\\"]) return YES;
|
|
32
|
+
if ([seg containsString:@"\0"]) return YES;
|
|
33
|
+
}
|
|
34
|
+
return NO;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
BOOL pathUnderRoot(NSString* root, NSString* path) {
|
|
38
|
+
NSString* resolved = [[path stringByStandardizingPath] stringByResolvingSymlinksInPath];
|
|
39
|
+
return [resolved hasPrefix:[root stringByAppendingString:@"/"]] || [resolved isEqualToString:root];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
NSString* resolveUnderRoot(NSString* root, NSString* rel) {
|
|
43
|
+
NSString* candidate = [[[root stringByAppendingPathComponent:rel]
|
|
44
|
+
stringByStandardizingPath] stringByResolvingSymlinksInPath];
|
|
45
|
+
return pathUnderRoot(root, candidate) ? candidate : nil;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Re-validate after appending index.html/.html — a symlink in the suffix could escape root.
|
|
49
|
+
NSString* withFallback(NSString* root, NSString* path) {
|
|
50
|
+
NSFileManager* fm = NSFileManager.defaultManager;
|
|
51
|
+
BOOL isDir = NO;
|
|
52
|
+
if ([fm fileExistsAtPath:path isDirectory:&isDir]) {
|
|
53
|
+
if (!isDir) return path;
|
|
54
|
+
NSString* idx = [path stringByAppendingPathComponent:@"index.html"];
|
|
55
|
+
if ([fm fileExistsAtPath:idx] && pathUnderRoot(root, idx)) return idx;
|
|
56
|
+
}
|
|
57
|
+
NSString* withExt = [path stringByAppendingPathExtension:@"html"];
|
|
58
|
+
if ([fm fileExistsAtPath:withExt] && pathUnderRoot(root, withExt)) return withExt;
|
|
59
|
+
return nil;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
} // namespace
|
|
63
|
+
|
|
64
|
+
@interface BuniteAppresSchemeHandler : NSObject <WKURLSchemeHandler>
|
|
65
|
+
@end
|
|
66
|
+
|
|
67
|
+
@implementation BuniteAppresSchemeHandler
|
|
68
|
+
|
|
69
|
+
- (void)webView:(WKWebView*)wv startURLSchemeTask:(id<WKURLSchemeTask>)task {
|
|
70
|
+
uint32_t view_id = viewIdForWebView(wv);
|
|
71
|
+
NSURL* url = task.request.URL;
|
|
72
|
+
|
|
73
|
+
if (![url.host isEqualToString:@"app.internal"]) {
|
|
74
|
+
[task didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil]];
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
NSString* rel = url.path;
|
|
79
|
+
while ([rel hasPrefix:@"/"]) rel = [rel substringFromIndex:1];
|
|
80
|
+
while ([rel hasSuffix:@"/"]) rel = [rel substringToIndex:rel.length - 1];
|
|
81
|
+
if (rel.length == 0) rel = @"index.html";
|
|
82
|
+
|
|
83
|
+
if (hasUnsafeSegment(rel)) {
|
|
84
|
+
[task didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadURL userInfo:nil]];
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
std::string rel_utf8 = rel.UTF8String;
|
|
89
|
+
|
|
90
|
+
// `has` (not `.empty()`) so an explicit load_html("") still intercepts.
|
|
91
|
+
if ([rel isEqualToString:@"internal/index.html"] &&
|
|
92
|
+
bunite::WebviewContentStorage::instance().has(view_id)) {
|
|
93
|
+
std::string stored = bunite::WebviewContentStorage::instance().get(view_id);
|
|
94
|
+
NSData* data = [NSData dataWithBytes:stored.data() length:stored.size()];
|
|
95
|
+
NSURLResponse* response = [[NSURLResponse alloc]
|
|
96
|
+
initWithURL:url MIMEType:@"text/html"
|
|
97
|
+
expectedContentLength:data.length textEncodingName:nil];
|
|
98
|
+
[task didReceiveResponse:response];
|
|
99
|
+
[task didReceiveData:data];
|
|
100
|
+
[task didFinish];
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. Dynamic route — async; JS replies via bunite_complete_route_request.
|
|
105
|
+
if (bunite::AppResRouteStorage::instance().hasRoute(rel_utf8)) {
|
|
106
|
+
if (!g_runtime.pending_route_tasks) {
|
|
107
|
+
g_runtime.pending_route_tasks = [NSMutableDictionary dictionary];
|
|
108
|
+
}
|
|
109
|
+
uint32_t request_id = g_runtime.next_route_request_id++;
|
|
110
|
+
BunitePendingRoute* entry = [[BunitePendingRoute alloc] init];
|
|
111
|
+
entry.viewId = view_id;
|
|
112
|
+
entry.task = task;
|
|
113
|
+
g_runtime.pending_route_tasks[@(request_id)] = entry;
|
|
114
|
+
std::string payload = "{\"requestId\":" + std::to_string(request_id) +
|
|
115
|
+
",\"path\":\"" + bunite_mac::escapeJsonString(rel_utf8) + "\"}";
|
|
116
|
+
bunite_mac::emitWebviewEvent(view_id, "route-request", payload);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 3. Static file under appres_root.
|
|
121
|
+
bunite_mac::ViewState* state = findView(view_id);
|
|
122
|
+
if (!state || state->appres_root.empty()) {
|
|
123
|
+
[task didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]];
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
NSString* rawRoot = [NSString stringWithUTF8String:state->appres_root.c_str()] ?: @"";
|
|
127
|
+
NSString* root = [[rawRoot stringByStandardizingPath] stringByResolvingSymlinksInPath];
|
|
128
|
+
NSString* candidate = resolveUnderRoot(root, rel);
|
|
129
|
+
NSString* resolved = candidate ? withFallback(root, candidate) : nil;
|
|
130
|
+
NSData* data = resolved ? [NSData dataWithContentsOfFile:resolved] : nil;
|
|
131
|
+
if (!data) {
|
|
132
|
+
[task didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]];
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
NSURLResponse* response = [[NSURLResponse alloc]
|
|
136
|
+
initWithURL:url MIMEType:mimeFor(resolved)
|
|
137
|
+
expectedContentLength:data.length textEncodingName:nil];
|
|
138
|
+
[task didReceiveResponse:response];
|
|
139
|
+
[task didReceiveData:data];
|
|
140
|
+
[task didFinish];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
- (void)webView:(WKWebView*)wv stopURLSchemeTask:(id<WKURLSchemeTask>)task {
|
|
144
|
+
(void)wv;
|
|
145
|
+
// Drop pending entry — complete_route_request on a stopped task raises NSInternalInconsistencyException.
|
|
146
|
+
if (g_runtime.pending_route_tasks.count == 0) return;
|
|
147
|
+
NSNumber* hit = nil;
|
|
148
|
+
for (NSNumber* key in g_runtime.pending_route_tasks) {
|
|
149
|
+
if (g_runtime.pending_route_tasks[key].task == task) { hit = key; break; }
|
|
150
|
+
}
|
|
151
|
+
if (hit) [g_runtime.pending_route_tasks removeObjectForKey:hit];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@end
|
|
155
|
+
|
|
156
|
+
namespace bunite_mac {
|
|
157
|
+
|
|
158
|
+
id<WKURLSchemeHandler> sharedAppresSchemeHandler() {
|
|
159
|
+
static __strong BuniteAppresSchemeHandler* handler = [[BuniteAppresSchemeHandler alloc] init];
|
|
160
|
+
return handler;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
} // namespace bunite_mac
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// FFI entry points for the macOS adapter.
|
|
2
|
+
|
|
3
|
+
#import "bunite_mac_internal.h"
|
|
4
|
+
|
|
5
|
+
#import <QuartzCore/QuartzCore.h>
|
|
6
|
+
|
|
7
|
+
#include "webview_storage.h"
|
|
8
|
+
|
|
9
|
+
#include <CoreFoundation/CoreFoundation.h>
|
|
10
|
+
|
|
11
|
+
#include <cstdlib>
|
|
12
|
+
#include <cstring>
|
|
13
|
+
#include <mutex>
|
|
14
|
+
#include <string>
|
|
15
|
+
#include <vector>
|
|
16
|
+
|
|
17
|
+
using bunite_mac::g_runtime;
|
|
18
|
+
using bunite_mac::isOnMainThread;
|
|
19
|
+
using bunite_mac::runOnUiThreadSync;
|
|
20
|
+
|
|
21
|
+
namespace {
|
|
22
|
+
|
|
23
|
+
constexpr int32_t kBuniteAbiVersion = 4;
|
|
24
|
+
|
|
25
|
+
// warn-once — avoid log spam from tight JS call loops.
|
|
26
|
+
#define BUNITE_MAC_TODO(name) \
|
|
27
|
+
do { \
|
|
28
|
+
static std::once_flag once; \
|
|
29
|
+
std::call_once(once, []() { \
|
|
30
|
+
BUNITE_WARN("%s not implemented on macOS.", (name)); \
|
|
31
|
+
}); \
|
|
32
|
+
} while (0)
|
|
33
|
+
|
|
34
|
+
} // namespace
|
|
35
|
+
|
|
36
|
+
extern "C" BUNITE_EXPORT int32_t bunite_abi_version(void) {
|
|
37
|
+
return kBuniteAbiVersion;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
extern "C" BUNITE_EXPORT const char* bunite_engine_name(void) {
|
|
41
|
+
return "wkwebview";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
extern "C" BUNITE_EXPORT const char* bunite_engine_version(void) {
|
|
45
|
+
// WebKit loads lazily on first WKWebView — re-check until cached value is non-OS fallback.
|
|
46
|
+
static std::string cached;
|
|
47
|
+
if (!cached.empty() && cached.compare(0, 9, "wkwebview") == 0) return cached.c_str();
|
|
48
|
+
NSBundle* webkit = [NSBundle bundleWithIdentifier:@"com.apple.WebKit"];
|
|
49
|
+
NSString* version = [webkit objectForInfoDictionaryKey:@"CFBundleVersion"];
|
|
50
|
+
if (version.length > 0) {
|
|
51
|
+
cached = std::string("wkwebview ") + [version UTF8String];
|
|
52
|
+
} else if (cached.empty()) {
|
|
53
|
+
NSString* os = [NSProcessInfo processInfo].operatingSystemVersionString;
|
|
54
|
+
cached = std::string("macOS ") + (os.length > 0 ? [os UTF8String] : "unknown");
|
|
55
|
+
}
|
|
56
|
+
return cached.c_str();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
extern "C" BUNITE_EXPORT void bunite_set_log_level(int32_t level) {
|
|
60
|
+
buniteSetLogLevel(static_cast<BuniteLogLevel>(level));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
extern "C" BUNITE_EXPORT bool bunite_init(
|
|
64
|
+
const char* engine_dir, // ignored — WKWebView is a system framework
|
|
65
|
+
bool hide_console, // ignored — no console concept on macOS
|
|
66
|
+
bool popup_blocking,
|
|
67
|
+
const char* engine_config_json // reserved
|
|
68
|
+
) {
|
|
69
|
+
(void)engine_dir;
|
|
70
|
+
(void)hide_console;
|
|
71
|
+
(void)engine_config_json;
|
|
72
|
+
|
|
73
|
+
if (![NSThread isMainThread]) {
|
|
74
|
+
BUNITE_ERROR("bunite_init must be called from the main thread on macOS.");
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (g_runtime.initialized) return true;
|
|
79
|
+
|
|
80
|
+
[NSApplication sharedApplication];
|
|
81
|
+
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
|
82
|
+
// Without finishLaunching, WKWebView defers all navigation since we don't call [NSApp run].
|
|
83
|
+
[NSApp finishLaunching];
|
|
84
|
+
|
|
85
|
+
g_runtime.popup_blocking = popup_blocking;
|
|
86
|
+
g_runtime.initialized = true;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
extern "C" BUNITE_EXPORT void bunite_run_loop(void) {
|
|
91
|
+
// No-op — JS drives bunite_pump_once via setImmediate. Kept for ABI symmetry.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
extern "C" BUNITE_EXPORT void bunite_pump_once(void) {
|
|
95
|
+
if (!isOnMainThread()) {
|
|
96
|
+
BUNITE_WARN("bunite_pump_once called off the main thread; ignoring.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Per-iter timeout lets WKWebView IPC deliver (0 polls too tight); wall cap keeps libuv lively.
|
|
100
|
+
static constexpr CFAbsoluteTime kCap = 0.005; // 5ms
|
|
101
|
+
CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + kCap;
|
|
102
|
+
do {
|
|
103
|
+
CFRunLoopRunResult r = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0005, true);
|
|
104
|
+
NSEvent* e = [NSApp nextEventMatchingMask:NSEventMaskAny
|
|
105
|
+
untilDate:[NSDate distantPast]
|
|
106
|
+
inMode:NSDefaultRunLoopMode
|
|
107
|
+
dequeue:YES];
|
|
108
|
+
if (e) [NSApp sendEvent:e];
|
|
109
|
+
if (r != kCFRunLoopRunHandledSource && !e) break; // empty
|
|
110
|
+
} while (CFAbsoluteTimeGetCurrent() < deadline);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
extern "C" BUNITE_EXPORT void bunite_quit(void) {
|
|
114
|
+
if (g_runtime.shutting_down.exchange(true)) return;
|
|
115
|
+
|
|
116
|
+
runOnUiThreadSync([]() {
|
|
117
|
+
// WindowState is non-copyable (atomic field) — snapshot the NSWindow* refs only.
|
|
118
|
+
std::vector<NSWindow*> windows;
|
|
119
|
+
{
|
|
120
|
+
std::lock_guard<std::mutex> lock(g_runtime.object_mutex);
|
|
121
|
+
windows.reserve(g_runtime.windows.size());
|
|
122
|
+
for (auto& [_, st] : g_runtime.windows) {
|
|
123
|
+
if (st.window) windows.push_back(st.window);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (NSWindow* w : windows) {
|
|
127
|
+
[w close];
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
extern "C" BUNITE_EXPORT void bunite_free_cstring(const char* value) {
|
|
133
|
+
std::free(const_cast<char*>(value));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
extern "C" BUNITE_EXPORT void bunite_set_webview_event_handler(BuniteWebviewEventHandler handler) {
|
|
137
|
+
g_runtime.webview_event_handler = handler;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
extern "C" BUNITE_EXPORT void bunite_set_window_event_handler(BuniteWindowEventHandler handler) {
|
|
141
|
+
g_runtime.window_event_handler = handler;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Window FFI — real impls (delegate body lives in bunite_mac_window.mm).
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
extern "C" BUNITE_EXPORT bool bunite_window_create(
|
|
149
|
+
uint32_t window_id,
|
|
150
|
+
double x, double y, double width, double height,
|
|
151
|
+
const char* title, const char* title_bar_style,
|
|
152
|
+
bool transparent, bool hidden, bool minimized, bool maximized
|
|
153
|
+
) {
|
|
154
|
+
NSString* t = bunite_mac::utf8ToNSString(title);
|
|
155
|
+
NSString* tbs = bunite_mac::utf8ToNSString(title_bar_style);
|
|
156
|
+
return runOnUiThreadSync([=]() -> bool {
|
|
157
|
+
return bunite_mac::createWindow(window_id, x, y, width, height,
|
|
158
|
+
t, tbs, transparent, hidden, minimized, maximized);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
extern "C" BUNITE_EXPORT void bunite_window_destroy(uint32_t window_id) {
|
|
163
|
+
runOnUiThreadSync([=]() { bunite_mac::destroyWindow(window_id); });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
extern "C" BUNITE_EXPORT void bunite_window_reset_close_pending(uint32_t window_id) {
|
|
167
|
+
runOnUiThreadSync([=]() {
|
|
168
|
+
if (auto* s = bunite_mac::findWindow(window_id)) s->close_pending.store(false);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
extern "C" BUNITE_EXPORT void bunite_window_show(uint32_t window_id) {
|
|
173
|
+
runOnUiThreadSync([=]() {
|
|
174
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
175
|
+
if (!s) return;
|
|
176
|
+
[s->window makeKeyAndOrderFront:nil];
|
|
177
|
+
[NSApp activateIgnoringOtherApps:YES];
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
extern "C" BUNITE_EXPORT void bunite_window_close(uint32_t window_id) {
|
|
182
|
+
runOnUiThreadSync([=]() {
|
|
183
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
184
|
+
if (s) [s->window performClose:nil];
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
extern "C" BUNITE_EXPORT void bunite_window_set_title(uint32_t window_id, const char* title) {
|
|
189
|
+
NSString* t = bunite_mac::utf8ToNSString(title);
|
|
190
|
+
runOnUiThreadSync([=]() {
|
|
191
|
+
if (auto* s = bunite_mac::findWindow(window_id)) s->window.title = t;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
extern "C" BUNITE_EXPORT void bunite_window_minimize(uint32_t window_id) {
|
|
196
|
+
runOnUiThreadSync([=]() {
|
|
197
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
198
|
+
if (s && !s->window.miniaturized) [s->window miniaturize:nil];
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
extern "C" BUNITE_EXPORT void bunite_window_unminimize(uint32_t window_id) {
|
|
203
|
+
runOnUiThreadSync([=]() {
|
|
204
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
205
|
+
if (s && s->window.miniaturized) [s->window deminiaturize:nil];
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
extern "C" BUNITE_EXPORT bool bunite_window_is_minimized(uint32_t window_id) {
|
|
210
|
+
return runOnUiThreadSync([=]() -> bool {
|
|
211
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
212
|
+
return s ? (bool)s->window.miniaturized : false;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
extern "C" BUNITE_EXPORT void bunite_window_maximize(uint32_t window_id) {
|
|
217
|
+
runOnUiThreadSync([=]() {
|
|
218
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
219
|
+
if (s && !s->window.zoomed) [s->window zoom:nil]; // idempotent — zoom toggles
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
extern "C" BUNITE_EXPORT void bunite_window_unmaximize(uint32_t window_id) {
|
|
224
|
+
runOnUiThreadSync([=]() {
|
|
225
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
226
|
+
if (s && s->window.zoomed) [s->window zoom:nil];
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
extern "C" BUNITE_EXPORT bool bunite_window_is_maximized(uint32_t window_id) {
|
|
231
|
+
return runOnUiThreadSync([=]() -> bool {
|
|
232
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
233
|
+
return s ? (bool)s->window.zoomed : false;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
extern "C" BUNITE_EXPORT void bunite_window_set_frame(
|
|
238
|
+
uint32_t window_id, double x, double y, double width, double height
|
|
239
|
+
) {
|
|
240
|
+
runOnUiThreadSync([=]() {
|
|
241
|
+
auto* s = bunite_mac::findWindow(window_id);
|
|
242
|
+
if (s) [s->window setFrame:bunite_mac::topLeftToBottomLeft(x, y, width, height) display:YES];
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// View FFI.
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
extern "C" BUNITE_EXPORT bool bunite_view_create(
|
|
251
|
+
uint32_t view_id, uint32_t window_id,
|
|
252
|
+
const char* url, const char* html, const char* preload,
|
|
253
|
+
const char* appres_root, const char* navigation_rules_json,
|
|
254
|
+
double x, double y, double width, double height,
|
|
255
|
+
bool auto_resize, bool sandbox, const char* preload_origins_json
|
|
256
|
+
) {
|
|
257
|
+
(void)sandbox;
|
|
258
|
+
NSString* u = bunite_mac::utf8ToNSString(url);
|
|
259
|
+
NSString* h = bunite_mac::utf8ToNSString(html);
|
|
260
|
+
NSString* p = bunite_mac::utf8ToNSString(preload);
|
|
261
|
+
NSString* ar = bunite_mac::utf8ToNSString(appres_root);
|
|
262
|
+
NSString* nav = bunite_mac::utf8ToNSString(navigation_rules_json);
|
|
263
|
+
NSString* origins = bunite_mac::utf8ToNSString(preload_origins_json);
|
|
264
|
+
return runOnUiThreadSync([=]() -> bool {
|
|
265
|
+
return bunite_mac::createView(view_id, window_id, u, h, p, ar, nav, origins, x, y, width, height, auto_resize);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
extern "C" BUNITE_EXPORT void bunite_view_execute_javascript(uint32_t view_id, const char* script) {
|
|
270
|
+
NSString* s = bunite_mac::utf8ToNSString(script);
|
|
271
|
+
runOnUiThreadSync([=]() {
|
|
272
|
+
if (auto* v = bunite_mac::findView(view_id)) [v->webview evaluateJavaScript:s completionHandler:nil];
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
extern "C" BUNITE_EXPORT void bunite_view_load_url(uint32_t view_id, const char* url) {
|
|
277
|
+
// Drop stored HTML so a later nav to internal/index.html doesn't resurrect it.
|
|
278
|
+
bunite::WebviewContentStorage::instance().remove(view_id);
|
|
279
|
+
NSString* s = bunite_mac::utf8ToNSString(url);
|
|
280
|
+
runOnUiThreadSync([=]() {
|
|
281
|
+
auto* v = bunite_mac::findView(view_id);
|
|
282
|
+
if (!v) return;
|
|
283
|
+
NSURL* u = [NSURL URLWithString:s];
|
|
284
|
+
if (u) [v->webview loadRequest:[NSURLRequest requestWithURL:u]];
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
extern "C" BUNITE_EXPORT void bunite_view_load_html(uint32_t view_id, const char* html) {
|
|
289
|
+
// Nav to internal/index.html so origin = appres://app.internal — preload/RPC/CSP/CORS match static pages.
|
|
290
|
+
std::string content = html ? html : "";
|
|
291
|
+
bunite::WebviewContentStorage::instance().set(view_id, content);
|
|
292
|
+
runOnUiThreadSync([=]() {
|
|
293
|
+
auto* v = bunite_mac::findView(view_id);
|
|
294
|
+
if (!v) return;
|
|
295
|
+
NSURL* u = [NSURL URLWithString:@"appres://app.internal/internal/index.html"];
|
|
296
|
+
[v->webview loadRequest:[NSURLRequest requestWithURL:u]];
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
extern "C" BUNITE_EXPORT void bunite_register_appres_route(const char* path) {
|
|
301
|
+
bunite::AppResRouteStorage::instance().registerRoute(path ? path : "");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
extern "C" BUNITE_EXPORT void bunite_unregister_appres_route(const char* path) {
|
|
305
|
+
bunite::AppResRouteStorage::instance().unregisterRoute(path ? path : "");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
extern "C" BUNITE_EXPORT void bunite_complete_route_request(uint32_t request_id, const char* html) {
|
|
309
|
+
std::string body = html ? html : "";
|
|
310
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
311
|
+
BunitePendingRoute* entry = bunite_mac::g_runtime.pending_route_tasks[@(request_id)];
|
|
312
|
+
if (!entry) return; // already stopped (view destroyed) or unknown id
|
|
313
|
+
[bunite_mac::g_runtime.pending_route_tasks removeObjectForKey:@(request_id)];
|
|
314
|
+
id<WKURLSchemeTask> task = entry.task;
|
|
315
|
+
NSData* data = [NSData dataWithBytes:body.data() length:body.size()];
|
|
316
|
+
NSURLResponse* response = [[NSURLResponse alloc]
|
|
317
|
+
initWithURL:task.request.URL MIMEType:@"text/html"
|
|
318
|
+
expectedContentLength:data.length textEncodingName:nil];
|
|
319
|
+
// Race: WebKit can stop the task between lookup and didFinish — swallow.
|
|
320
|
+
@try {
|
|
321
|
+
[task didReceiveResponse:response];
|
|
322
|
+
[task didReceiveData:data];
|
|
323
|
+
[task didFinish];
|
|
324
|
+
} @catch (NSException* e) {
|
|
325
|
+
// task stopped — silent
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_visible(uint32_t view_id, bool visible) {
|
|
331
|
+
runOnUiThreadSync([=]() {
|
|
332
|
+
if (auto* v = bunite_mac::findView(view_id)) v->container.hidden = !visible;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_input_passthrough(uint32_t view_id, bool passthrough) {
|
|
337
|
+
runOnUiThreadSync([=]() {
|
|
338
|
+
if (auto* v = bunite_mac::findView(view_id)) v->container.passthrough = passthrough;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_mask_region(uint32_t view_id, const double* rects, uint32_t count) {
|
|
343
|
+
std::vector<NSRect> physical(count);
|
|
344
|
+
for (uint32_t i = 0; i < count; i++) {
|
|
345
|
+
const double* r = rects + i * 4;
|
|
346
|
+
physical[i] = NSMakeRect(r[0], r[1], r[2], r[3]);
|
|
347
|
+
}
|
|
348
|
+
runOnUiThreadSync([view_id, physical = std::move(physical)]() {
|
|
349
|
+
auto* v = bunite_mac::findView(view_id);
|
|
350
|
+
if (!v || !v->container) return;
|
|
351
|
+
BunitePassthroughContainer* container = v->container;
|
|
352
|
+
if (physical.empty()) {
|
|
353
|
+
container.layer.mask = nil;
|
|
354
|
+
container.maskHoles = nil;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
CGFloat dpr = container.window.backingScaleFactor ?: 1.0;
|
|
358
|
+
NSPoint origin = container.frame.origin; // window-local, points
|
|
359
|
+
CGMutablePathRef path = CGPathCreateMutable();
|
|
360
|
+
CGPathAddRect(path, NULL, container.bounds); // outer
|
|
361
|
+
NSMutableArray<NSValue*>* holes = [NSMutableArray arrayWithCapacity:physical.size()];
|
|
362
|
+
for (const NSRect& rp : physical) {
|
|
363
|
+
// physical pixels in window coords → points → container-local.
|
|
364
|
+
NSRect local = NSMakeRect(
|
|
365
|
+
rp.origin.x / dpr - origin.x,
|
|
366
|
+
rp.origin.y / dpr - origin.y,
|
|
367
|
+
rp.size.width / dpr,
|
|
368
|
+
rp.size.height / dpr);
|
|
369
|
+
CGPathAddRect(path, NULL, local);
|
|
370
|
+
[holes addObject:[NSValue valueWithRect:local]];
|
|
371
|
+
}
|
|
372
|
+
// kCAFillRuleEvenOdd: overlapping holes XOR (unlike win RGN_DIFF). Hit-test uses raw rects.
|
|
373
|
+
CAShapeLayer* mask = [CAShapeLayer layer];
|
|
374
|
+
mask.path = path;
|
|
375
|
+
mask.fillRule = kCAFillRuleEvenOdd;
|
|
376
|
+
container.layer.mask = mask;
|
|
377
|
+
container.maskHoles = holes;
|
|
378
|
+
CGPathRelease(path);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
extern "C" BUNITE_EXPORT void bunite_view_bring_to_front(uint32_t view_id) {
|
|
383
|
+
runOnUiThreadSync([=]() {
|
|
384
|
+
auto* v = bunite_mac::findView(view_id);
|
|
385
|
+
if (!v || !v->container) return;
|
|
386
|
+
NSView* parent = v->container.superview;
|
|
387
|
+
if (!parent) return;
|
|
388
|
+
[v->container removeFromSuperview];
|
|
389
|
+
[parent addSubview:v->container]; // last subview = top of z-order
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// set_bounds: physical pixels (JS pre-multiplies DPR). createView: logical points (main view only, never re-bounded).
|
|
394
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_bounds(
|
|
395
|
+
uint32_t view_id, double x, double y, double width, double height
|
|
396
|
+
) {
|
|
397
|
+
runOnUiThreadSync([=]() {
|
|
398
|
+
auto* v = bunite_mac::findView(view_id);
|
|
399
|
+
if (!v || !v->container) return;
|
|
400
|
+
CGFloat dpr = v->container.window.backingScaleFactor ?: 1.0;
|
|
401
|
+
v->container.frame = NSMakeRect(x / dpr, y / dpr, width / dpr, height / dpr);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_bounds_async(
|
|
406
|
+
uint32_t view_id, double x, double y, double width, double height
|
|
407
|
+
) {
|
|
408
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
409
|
+
auto* v = bunite_mac::findView(view_id);
|
|
410
|
+
if (!v || !v->container) return;
|
|
411
|
+
CGFloat dpr = v->container.window.backingScaleFactor ?: 1.0;
|
|
412
|
+
v->container.frame = NSMakeRect(x / dpr, y / dpr, width / dpr, height / dpr);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
extern "C" BUNITE_EXPORT void bunite_view_set_anchor(uint32_t view_id, int mode, double inset) {
|
|
417
|
+
(void)view_id; (void)mode; (void)inset;
|
|
418
|
+
BUNITE_MAC_TODO("bunite_view_set_anchor");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
extern "C" BUNITE_EXPORT void bunite_view_go_back(uint32_t view_id) {
|
|
422
|
+
runOnUiThreadSync([=]() {
|
|
423
|
+
if (auto* v = bunite_mac::findView(view_id)) [v->webview goBack];
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
extern "C" BUNITE_EXPORT void bunite_view_reload(uint32_t view_id) {
|
|
428
|
+
runOnUiThreadSync([=]() {
|
|
429
|
+
if (auto* v = bunite_mac::findView(view_id)) [v->webview reload];
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
extern "C" BUNITE_EXPORT void bunite_view_remove(uint32_t view_id) {
|
|
434
|
+
runOnUiThreadSync([=]() { bunite_mac::removeView(view_id); });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
extern "C" BUNITE_EXPORT void bunite_view_open_devtools(uint32_t view_id) {
|
|
438
|
+
runOnUiThreadSync([=]() {
|
|
439
|
+
if (@available(macOS 13.3, *)) {
|
|
440
|
+
if (auto* v = bunite_mac::findView(view_id)) v->webview.inspectable = YES;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
extern "C" BUNITE_EXPORT void bunite_view_close_devtools(uint32_t view_id) {
|
|
446
|
+
runOnUiThreadSync([=]() {
|
|
447
|
+
if (@available(macOS 13.3, *)) {
|
|
448
|
+
if (auto* v = bunite_mac::findView(view_id)) v->webview.inspectable = NO;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
extern "C" BUNITE_EXPORT void bunite_view_toggle_devtools(uint32_t view_id) {
|
|
454
|
+
runOnUiThreadSync([=]() {
|
|
455
|
+
if (@available(macOS 13.3, *)) {
|
|
456
|
+
if (auto* v = bunite_mac::findView(view_id)) v->webview.inspectable = !v->webview.inspectable;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
extern "C" BUNITE_EXPORT void bunite_complete_permission_request(uint32_t request_id, uint32_t state) {
|
|
462
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
463
|
+
BunitePendingPermission* entry = bunite_mac::g_runtime.pending_permissions[@(request_id)];
|
|
464
|
+
if (!entry) return; // unknown id, or already resolved (e.g. view destroyed)
|
|
465
|
+
[bunite_mac::g_runtime.pending_permissions removeObjectForKey:@(request_id)];
|
|
466
|
+
if (@available(macOS 12.0, *)) {
|
|
467
|
+
entry.handler(state == 0 ? WKPermissionDecisionDeny : WKPermissionDecisionGrant);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|