@livx.cc/appwrap 0.46.1 → 0.46.2
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 +1 -1
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +1 -0
- package/runtime-desktop/bridge-shim/shim.ts +12 -197
- package/runtime-desktop/crates/webview-control/Cargo.lock +330 -0
- package/runtime-desktop/crates/webview-control/Cargo.toml +28 -0
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +245 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +100 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +95 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +519 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +45 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +497 -0
- package/runtime-desktop/src-tauri/Cargo.lock +13 -0
- package/runtime-desktop/src-tauri/Cargo.toml +3 -0
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +83 -441
- package/runtime-desktop/src-tauri/src/browser_tab.rs +193 -0
- package/runtime-desktop/src-tauri/src/main.rs +8 -1
- package/runtime-desktop/src-tauri/src/popup_mac.rs +5 -519
- package/runtime-desktop/src-tauri/tauri.conf.json +1 -0
- package/src/cli.ts +16 -0
- package/src/config.ts +10 -1
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// WKWebView adapter for the webview-control primitive (macOS). Lifted from bridge_mac.rs
|
|
2
|
+
// (Phase 1 extraction) with the transport/protocol stripped OUT: this module emits RAW page
|
|
3
|
+
// payloads via an `on_message` callback and RAW eval/snapshot results via per-call `on_done`
|
|
4
|
+
// callbacks. It knows nothing of the bod-browser wire format, the TCP Sender, or the
|
|
5
|
+
// {type:"pageMsg"} / {id,result} framing — those live in the control-channel adapter (bridge_mac.rs).
|
|
6
|
+
use std::cell::RefCell;
|
|
7
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
8
|
+
use std::sync::Mutex;
|
|
9
|
+
|
|
10
|
+
use objc2::rc::Retained;
|
|
11
|
+
use objc2::runtime::AnyObject;
|
|
12
|
+
use objc2::runtime::ProtocolObject;
|
|
13
|
+
use objc2::{define_class, msg_send, AnyThread, DefinedClass, MainThreadMarker, MainThreadOnly};
|
|
14
|
+
use objc2_app_kit::{
|
|
15
|
+
NSAutoresizingMaskOptions, NSBackingStoreType, NSBitmapImageFileType, NSBitmapImageRep, NSImage,
|
|
16
|
+
NSView, NSWindow, NSWindowDelegate, NSWindowStyleMask,
|
|
17
|
+
};
|
|
18
|
+
use objc2_foundation::{
|
|
19
|
+
NSDictionary, NSError, NSNotification, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize,
|
|
20
|
+
NSString, NSURL, NSURLRequest, NSUUID,
|
|
21
|
+
};
|
|
22
|
+
use objc2_web_kit::{
|
|
23
|
+
WKContentWorld, WKScriptMessage, WKScriptMessageHandler, WKUserContentController, WKUserScript,
|
|
24
|
+
WKUserScriptInjectionTime, WKWebView, WKWebViewConfiguration, WKWebsiteDataStore,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
use crate::popup;
|
|
28
|
+
use crate::profiles;
|
|
29
|
+
use crate::Mount;
|
|
30
|
+
|
|
31
|
+
/// Autoresizing mask that pins a mounted webview to fill its superview on resize — the same
|
|
32
|
+
/// width+height-sizable mask popup windows use, applied on every (re)mount.
|
|
33
|
+
const FILL_MASK: NSAutoresizingMaskOptions =
|
|
34
|
+
NSAutoresizingMaskOptions(NSAutoresizingMaskOptions::ViewWidthSizable.0 | NSAutoresizingMaskOptions::ViewHeightSizable.0);
|
|
35
|
+
|
|
36
|
+
/// Raw page->host message sink. Receives the UNWRAPPED WKScriptMessage body string (the page's
|
|
37
|
+
/// `webkit.messageHandlers.bridgeShim.postMessage(...)` payload). The adapter wraps it into its
|
|
38
|
+
/// transport envelope — the primitive stays framing-agnostic. `Send` (it crosses onto the main
|
|
39
|
+
/// thread with the create op) but not `Sync` — the handler is MainThreadOnly and only ever invoked
|
|
40
|
+
/// on the main thread, mirroring the original bare `Sender`.
|
|
41
|
+
pub type MessageSink = Box<dyn Fn(String) + Send>;
|
|
42
|
+
|
|
43
|
+
/// Result of creating a profile window — the store facts the harness asserts. The adapter builds
|
|
44
|
+
/// the wire JSON from this; the primitive returns plain data.
|
|
45
|
+
pub struct CreateResult {
|
|
46
|
+
pub persistent: bool,
|
|
47
|
+
pub store_uuid: String,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// One profile window's public identity for `list_webviews`.
|
|
51
|
+
pub struct WebviewInfo {
|
|
52
|
+
pub label: String,
|
|
53
|
+
pub profile_id: String,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Spec for creating a webview via the embedded facade (Phase 2). Mirrors the control-channel
|
|
57
|
+
/// `createWindow` op fields plus the `mount` target (the adapter always builds `Mount::TopLevel`).
|
|
58
|
+
pub struct CreateSpec {
|
|
59
|
+
pub label: String,
|
|
60
|
+
pub url: String,
|
|
61
|
+
pub profile_id: String,
|
|
62
|
+
pub init_js: String,
|
|
63
|
+
pub mount: Mount,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Page -> host transport: a native WKScriptMessageHandler ("bridgeShim").
|
|
68
|
+
// WebKit blocks ws://127.0.0.1 (and http beacons) from https pages as mixed content — NO loopback
|
|
69
|
+
// exemption like Chrome — so the content layer posts results via
|
|
70
|
+
// webkit.messageHandlers.bridgeShim.postMessage(json) instead. We hand the RAW payload to the
|
|
71
|
+
// on_message sink; the adapter does the framing.
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
pub struct MsgHandlerIvars {
|
|
74
|
+
on_message: MessageSink,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
define_class!(
|
|
78
|
+
#[unsafe(super(NSObject))]
|
|
79
|
+
#[thread_kind = MainThreadOnly]
|
|
80
|
+
#[name = "AppwrapBridgeMsgHandler"]
|
|
81
|
+
#[ivars = MsgHandlerIvars]
|
|
82
|
+
struct MsgHandler;
|
|
83
|
+
|
|
84
|
+
unsafe impl NSObjectProtocol for MsgHandler {}
|
|
85
|
+
|
|
86
|
+
unsafe impl WKScriptMessageHandler for MsgHandler {
|
|
87
|
+
#[unsafe(method(userContentController:didReceiveScriptMessage:))]
|
|
88
|
+
fn did_receive(&self, _ucc: &WKUserContentController, message: &WKScriptMessage) {
|
|
89
|
+
let body = unsafe { message.body() };
|
|
90
|
+
if let Ok(s) = body.downcast::<NSString>() {
|
|
91
|
+
(self.ivars().on_message)(s.to_string());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/// Install the bridgeShim WKScriptMessageHandler on an arbitrary WKWebView (used by the adapter for
|
|
98
|
+
/// the legacy tauri-window path — the profile-window path installs it on the config's UCC before
|
|
99
|
+
/// creation). The UCC retains the handler, so no external retention is needed here. Main-thread only.
|
|
100
|
+
pub unsafe fn install_message_handler(wk: *mut WKWebView, mtm: MainThreadMarker, on_message: MessageSink) {
|
|
101
|
+
let wk = &*wk;
|
|
102
|
+
let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { on_message });
|
|
103
|
+
let handler: Retained<MsgHandler> = msg_send![super(handler), init];
|
|
104
|
+
let ucc = wk.configuration().userContentController();
|
|
105
|
+
ucc.addScriptMessageHandler_name(
|
|
106
|
+
ProtocolObject::from_ref(&*handler),
|
|
107
|
+
&NSString::from_str("bridgeShim"),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// A manually-created profile window. Strong refs kept alive until close (or windowWillClose).
|
|
112
|
+
/// Only touched on the main thread; the unsafe Send exists solely so the registry can be a static
|
|
113
|
+
/// Mutex (same pattern as popup_mac).
|
|
114
|
+
struct ProfileWindow {
|
|
115
|
+
label: String,
|
|
116
|
+
profile_id: String,
|
|
117
|
+
window: Retained<NSWindow>,
|
|
118
|
+
webview: Retained<WKWebView>,
|
|
119
|
+
/// Set true by the window-close delegate on a USER (red-button) close.
|
|
120
|
+
closed: AtomicBool,
|
|
121
|
+
_handler: Retained<MsgHandler>,
|
|
122
|
+
/// Per-window SSO-popup delegate (weak uiDelegate → held here). Opaque; lifetime only.
|
|
123
|
+
_popup_delegate: Retained<AnyObject>,
|
|
124
|
+
/// NSWindowDelegate that flags `closed` on a user close (weak setDelegate → held here).
|
|
125
|
+
_win_delegate: Retained<ProfileWinDelegate>,
|
|
126
|
+
}
|
|
127
|
+
unsafe impl Send for ProfileWindow {}
|
|
128
|
+
static PROFILE_WINDOWS: Mutex<Vec<ProfileWindow>> = Mutex::new(Vec::new());
|
|
129
|
+
|
|
130
|
+
// NSWindowDelegate for profile windows — flags the matching registry entry `closed` when the user
|
|
131
|
+
// closes the window (red button), so it drops out of list_webviews. Matches by NSWindow identity
|
|
132
|
+
// (the notification's object). Main-thread only.
|
|
133
|
+
define_class!(
|
|
134
|
+
#[unsafe(super(NSObject))]
|
|
135
|
+
#[thread_kind = MainThreadOnly]
|
|
136
|
+
#[name = "AppwrapProfileWinDelegate"]
|
|
137
|
+
struct ProfileWinDelegate;
|
|
138
|
+
|
|
139
|
+
unsafe impl NSObjectProtocol for ProfileWinDelegate {}
|
|
140
|
+
|
|
141
|
+
unsafe impl NSWindowDelegate for ProfileWinDelegate {
|
|
142
|
+
#[unsafe(method(windowWillClose:))]
|
|
143
|
+
fn window_will_close(&self, notification: &NSNotification) {
|
|
144
|
+
let Some(obj) = notification.object() else { return };
|
|
145
|
+
let key = Retained::as_ptr(&obj) as usize;
|
|
146
|
+
let v = PROFILE_WINDOWS.lock().unwrap();
|
|
147
|
+
if let Some(p) = v.iter().find(|p| Retained::as_ptr(&p.window) as usize == key) {
|
|
148
|
+
eprintln!("[bridge-shim] profile window {} user-closed", p.label);
|
|
149
|
+
// Detach from the superview so a ChildOf-mounted webview isn't left as a zombie in
|
|
150
|
+
// the host pane once `reap_closed` drops the registry `Retained` (see `close`).
|
|
151
|
+
p.webview.removeFromSuperview();
|
|
152
|
+
p.closed.store(true, Ordering::Relaxed);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
/// Retained clone of a profile window's WKWebView (op routing), or None if the label is unknown /
|
|
159
|
+
/// already closed. The adapter uses this to decide whether an op targets a profile window (else it
|
|
160
|
+
/// falls back to its legacy tauri path).
|
|
161
|
+
pub fn profile_webview(label: &str) -> Option<Retained<WKWebView>> {
|
|
162
|
+
PROFILE_WINDOWS
|
|
163
|
+
.lock()
|
|
164
|
+
.unwrap()
|
|
165
|
+
.iter()
|
|
166
|
+
.find(|p| p.label == label && !p.closed.load(Ordering::Relaxed))
|
|
167
|
+
.map(|p| p.webview.clone())
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Public identities of the live (non-closed) profile windows.
|
|
171
|
+
pub fn list_webviews() -> Vec<WebviewInfo> {
|
|
172
|
+
PROFILE_WINDOWS
|
|
173
|
+
.lock()
|
|
174
|
+
.unwrap()
|
|
175
|
+
.iter()
|
|
176
|
+
.filter(|p| !p.closed.load(Ordering::Relaxed))
|
|
177
|
+
.map(|p| WebviewInfo { label: p.label.clone(), profile_id: p.profile_id.clone() })
|
|
178
|
+
.collect()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Drop entries flagged `closed` (user-closed windows). Called at safe points only — never from
|
|
182
|
+
/// within a delegate callback, so the delegate isn't released mid-call.
|
|
183
|
+
fn reap_closed() {
|
|
184
|
+
let mut v = PROFILE_WINDOWS.lock().unwrap();
|
|
185
|
+
v.retain(|p| !p.closed.load(Ordering::Relaxed));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Close + drop a profile window (releases its NSWindow + WKWebView). Main-thread only. Returns
|
|
189
|
+
/// false if no such profile window (caller falls back to tauri). The removed entry is held on the
|
|
190
|
+
/// stack across window.close() so its window delegate stays alive while AppKit fires windowWillClose.
|
|
191
|
+
pub fn close(label: &str) -> bool {
|
|
192
|
+
let entry = {
|
|
193
|
+
let mut v = PROFILE_WINDOWS.lock().unwrap();
|
|
194
|
+
v.iter().position(|p| p.label == label).map(|i| v.remove(i))
|
|
195
|
+
};
|
|
196
|
+
match entry {
|
|
197
|
+
Some(p) => {
|
|
198
|
+
// Detach the WKWebView from its superview BEFORE dropping the registry `Retained`. In
|
|
199
|
+
// `Mount::ChildOf` (embedded) mode the webview is a subview of the HOST pane (retained
|
|
200
|
+
// by the host's subview array), NOT the home window's contentView — so `window.close()`
|
|
201
|
+
// alone leaves it alive and VISIBLE in the host pane (a zombie). Harmless for TopLevel
|
|
202
|
+
// (the webview IS the home window's contentView, released with the window anyway).
|
|
203
|
+
p.webview.removeFromSuperview();
|
|
204
|
+
p.window.close();
|
|
205
|
+
true
|
|
206
|
+
}
|
|
207
|
+
None => false,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Drive NSWindow performClose: (the red-button path) WITHOUT touching the registry, so the
|
|
212
|
+
/// windowWillClose delegate is the sole remover — proves user-close hygiene headlessly. Returns
|
|
213
|
+
/// false if no such profile window. Main-thread only.
|
|
214
|
+
pub fn user_close(label: &str) -> bool {
|
|
215
|
+
let win = PROFILE_WINDOWS
|
|
216
|
+
.lock()
|
|
217
|
+
.unwrap()
|
|
218
|
+
.iter()
|
|
219
|
+
.find(|p| p.label == label)
|
|
220
|
+
.map(|p| p.window.clone());
|
|
221
|
+
match win {
|
|
222
|
+
Some(w) => {
|
|
223
|
+
w.performClose(None);
|
|
224
|
+
true
|
|
225
|
+
}
|
|
226
|
+
None => false,
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// Create a top-level profile window (the standalone/MCP shape — `Mount::TopLevel`). Thin wrapper
|
|
231
|
+
/// over [`create_webview`]; kept as-is so the control-channel adapter (bridge_mac.rs) is unchanged
|
|
232
|
+
/// and its behavior byte-identical. Main-thread only.
|
|
233
|
+
pub fn create_profile_window(
|
|
234
|
+
mtm: MainThreadMarker,
|
|
235
|
+
label: &str,
|
|
236
|
+
url: &str,
|
|
237
|
+
profile_id: &str,
|
|
238
|
+
init_js: &str,
|
|
239
|
+
on_message: MessageSink,
|
|
240
|
+
) -> Result<CreateResult, String> {
|
|
241
|
+
create_webview(
|
|
242
|
+
mtm,
|
|
243
|
+
CreateSpec {
|
|
244
|
+
label: label.to_string(),
|
|
245
|
+
url: url.to_string(),
|
|
246
|
+
profile_id: profile_id.to_string(),
|
|
247
|
+
init_js: init_js.to_string(),
|
|
248
|
+
mount: Mount::TopLevel,
|
|
249
|
+
},
|
|
250
|
+
on_message,
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Create a manual NSWindow + WKWebView bound to `profile_id`'s persistent data store, inject the
|
|
255
|
+
/// bridge content bundle + bridgeShim handler, mount it per `spec.mount`, and register it. Runs on
|
|
256
|
+
/// the main thread (the adapter/host marshals it there). Returns the store facts.
|
|
257
|
+
///
|
|
258
|
+
/// The webview ALWAYS owns a hidden home `NSWindow` (the `TopLevel` target), created here with
|
|
259
|
+
/// `setReleasedWhenClosed(false)` so a host-window close never frees the view. For `TopLevel` the
|
|
260
|
+
/// window is shown and holds the webview as its content view (identical to the pre-Phase-2 path);
|
|
261
|
+
/// for `ChildOf(handle)` the window stays unshown and the webview is added as a subview of the
|
|
262
|
+
/// host-owned pane. `set_mount` later moves the same live `WKWebView` between the two.
|
|
263
|
+
pub fn create_webview(
|
|
264
|
+
mtm: MainThreadMarker,
|
|
265
|
+
spec: CreateSpec,
|
|
266
|
+
on_message: MessageSink,
|
|
267
|
+
) -> Result<CreateResult, String> {
|
|
268
|
+
let CreateSpec { label, url, profile_id, init_js, mount } = spec;
|
|
269
|
+
let (label, url, profile_id, init_js) = (label.as_str(), url.as_str(), profile_id.as_str(), init_js.as_str());
|
|
270
|
+
let uuid_str = profiles::uuid_for(profile_id);
|
|
271
|
+
let nsuuid = NSUUID::initWithUUIDString(NSUUID::alloc(), &NSString::from_str(&uuid_str))
|
|
272
|
+
.ok_or_else(|| format!("bad uuid for profile {profile_id}: {uuid_str}"))?;
|
|
273
|
+
let store = unsafe { WKWebsiteDataStore::dataStoreForIdentifier(&nsuuid, mtm) };
|
|
274
|
+
let persistent = unsafe { store.isPersistent() };
|
|
275
|
+
|
|
276
|
+
let cfg = unsafe { WKWebViewConfiguration::new(mtm) };
|
|
277
|
+
unsafe { cfg.setWebsiteDataStore(&store) };
|
|
278
|
+
|
|
279
|
+
// bridgeShim message handler + content bundle (documentStart) on this cfg's UCC.
|
|
280
|
+
let ucc = unsafe { cfg.userContentController() };
|
|
281
|
+
let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { on_message });
|
|
282
|
+
let handler: Retained<MsgHandler> = unsafe { msg_send![super(handler), init] };
|
|
283
|
+
unsafe {
|
|
284
|
+
ucc.addScriptMessageHandler_name(
|
|
285
|
+
ProtocolObject::from_ref(&*handler),
|
|
286
|
+
&NSString::from_str("bridgeShim"),
|
|
287
|
+
);
|
|
288
|
+
let script = WKUserScript::initWithSource_injectionTime_forMainFrameOnly(
|
|
289
|
+
WKUserScript::alloc(mtm),
|
|
290
|
+
&NSString::from_str(init_js),
|
|
291
|
+
WKUserScriptInjectionTime::AtDocumentStart,
|
|
292
|
+
true,
|
|
293
|
+
);
|
|
294
|
+
ucc.addUserScript(&script);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(1100.0, 800.0));
|
|
298
|
+
let style = NSWindowStyleMask::Titled
|
|
299
|
+
| NSWindowStyleMask::Closable
|
|
300
|
+
| NSWindowStyleMask::Miniaturizable
|
|
301
|
+
| NSWindowStyleMask::Resizable;
|
|
302
|
+
let window = unsafe {
|
|
303
|
+
NSWindow::initWithContentRect_styleMask_backing_defer(
|
|
304
|
+
mtm.alloc::<NSWindow>(),
|
|
305
|
+
rect,
|
|
306
|
+
style,
|
|
307
|
+
NSBackingStoreType::Buffered,
|
|
308
|
+
false,
|
|
309
|
+
)
|
|
310
|
+
};
|
|
311
|
+
unsafe { window.setReleasedWhenClosed(false) };
|
|
312
|
+
|
|
313
|
+
let webview = unsafe {
|
|
314
|
+
WKWebView::initWithFrame_configuration(mtm.alloc::<WKWebView>(), window.frame(), &cfg)
|
|
315
|
+
};
|
|
316
|
+
unsafe {
|
|
317
|
+
webview.setCustomUserAgent(Some(&NSString::from_str(popup::SAFARI_UA)));
|
|
318
|
+
window.setTitle(&NSString::from_str(label));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Per-window SSO-popup delegate + window-close delegate. Both are weak refs on AppKit/WebKit →
|
|
322
|
+
// retained in the registry entry below.
|
|
323
|
+
let popup_delegate: Retained<AnyObject> =
|
|
324
|
+
popup::install_for_profile(Retained::as_ptr(&webview) as *mut std::ffi::c_void, mtm);
|
|
325
|
+
let win_delegate = ProfileWinDelegate::alloc(mtm).set_ivars(());
|
|
326
|
+
let win_delegate: Retained<ProfileWinDelegate> = unsafe { msg_send![super(win_delegate), init] };
|
|
327
|
+
window.setDelegate(Some(ProtocolObject::from_ref(&*win_delegate)));
|
|
328
|
+
window.center();
|
|
329
|
+
|
|
330
|
+
// Initial mount: TopLevel shows the home window with the webview as content view (the
|
|
331
|
+
// pre-Phase-2 behavior, byte-identical); ChildOf leaves the home window unshown and adds the
|
|
332
|
+
// webview into the host pane. Same reparent primitive as set_mount, applied at birth.
|
|
333
|
+
unsafe { mount_view(mtm, &webview, &window, mount) };
|
|
334
|
+
|
|
335
|
+
let nsurl = NSURL::URLWithString(&NSString::from_str(url))
|
|
336
|
+
.ok_or_else(|| format!("bad url: {url}"))?;
|
|
337
|
+
let req = NSURLRequest::requestWithURL(&nsurl);
|
|
338
|
+
let _ = unsafe { webview.loadRequest(&req) };
|
|
339
|
+
|
|
340
|
+
reap_closed();
|
|
341
|
+
PROFILE_WINDOWS.lock().unwrap().push(ProfileWindow {
|
|
342
|
+
label: label.to_string(),
|
|
343
|
+
profile_id: profile_id.to_string(),
|
|
344
|
+
window,
|
|
345
|
+
webview,
|
|
346
|
+
closed: AtomicBool::new(false),
|
|
347
|
+
_handler: handler,
|
|
348
|
+
_popup_delegate: popup_delegate,
|
|
349
|
+
_win_delegate: win_delegate,
|
|
350
|
+
});
|
|
351
|
+
eprintln!(
|
|
352
|
+
"[bridge-shim] profile window {label} profile={profile_id} store={uuid_str} persistent={persistent}"
|
|
353
|
+
);
|
|
354
|
+
Ok(CreateResult { persistent, store_uuid: uuid_str })
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Phase 2: NSView-reparent (tear-off / dock). The SAME live WKWebView object is moved between its
|
|
359
|
+
// home NSWindow (TopLevel) and a host-owned pane NSView (ChildOf) — page + WKWebsiteDataStore
|
|
360
|
+
// session + WKScriptMessageHandler bridge + SSO uiDelegate all survive because it is never
|
|
361
|
+
// re-created. Productionized from the Phase-0 spike. Main-thread only (WKWebView/NSView are not
|
|
362
|
+
// thread-safe); zero-frame subviews render blank, so we always set frame = target bounds + a
|
|
363
|
+
// fill autoresizing mask.
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
/// Reparent `webview` to `mount`, using `home_window` as the TopLevel target. Caller holds the
|
|
367
|
+
/// main thread. `ChildOf` dereferences the host `NSView*` per the [`crate::ViewHandle`] contract.
|
|
368
|
+
unsafe fn mount_view(
|
|
369
|
+
_mtm: MainThreadMarker,
|
|
370
|
+
webview: &WKWebView,
|
|
371
|
+
home_window: &NSWindow,
|
|
372
|
+
mount: Mount,
|
|
373
|
+
) {
|
|
374
|
+
webview.removeFromSuperview();
|
|
375
|
+
match mount {
|
|
376
|
+
Mount::TopLevel => {
|
|
377
|
+
let size = home_window.frame().size;
|
|
378
|
+
webview.setFrame(NSRect::new(NSPoint::new(0.0, 0.0), size));
|
|
379
|
+
webview.setAutoresizingMask(FILL_MASK);
|
|
380
|
+
home_window.setContentView(Some(webview));
|
|
381
|
+
home_window.makeKeyAndOrderFront(None);
|
|
382
|
+
}
|
|
383
|
+
Mount::ChildOf(handle) => {
|
|
384
|
+
// The host owns this NSView; we only add a subview link (contract: valid, main-thread,
|
|
385
|
+
// host keeps it alive while mounted). Order the home window out so it isn't a stray.
|
|
386
|
+
let host = &*(handle.as_ptr() as *mut NSView);
|
|
387
|
+
let bounds = host.bounds();
|
|
388
|
+
webview.setFrame(bounds);
|
|
389
|
+
webview.setAutoresizingMask(FILL_MASK);
|
|
390
|
+
host.addSubview(webview);
|
|
391
|
+
home_window.orderOut(None);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// Move a live profile webview between top-level and child-of-a-host-pane (tear-off / dock).
|
|
397
|
+
/// Returns false if `label` is unknown or already closed. Main-thread only.
|
|
398
|
+
pub fn set_mount(mtm: MainThreadMarker, label: &str, mount: Mount) -> bool {
|
|
399
|
+
let parts = PROFILE_WINDOWS
|
|
400
|
+
.lock()
|
|
401
|
+
.unwrap()
|
|
402
|
+
.iter()
|
|
403
|
+
.find(|p| p.label == label && !p.closed.load(Ordering::Relaxed))
|
|
404
|
+
.map(|p| (p.webview.clone(), p.window.clone()));
|
|
405
|
+
match parts {
|
|
406
|
+
Some((webview, window)) => {
|
|
407
|
+
unsafe { mount_view(mtm, &webview, &window, mount) };
|
|
408
|
+
true
|
|
409
|
+
}
|
|
410
|
+
None => false,
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/// Navigate any WKWebView (profile or tauri) to `url`. Main-thread only.
|
|
415
|
+
pub unsafe fn navigate(wk: *mut WKWebView, url: &str) -> Result<(), String> {
|
|
416
|
+
let wk = &*wk;
|
|
417
|
+
match NSURL::URLWithString(&NSString::from_str(url)) {
|
|
418
|
+
Some(nsurl) => {
|
|
419
|
+
let req = NSURLRequest::requestWithURL(&nsurl);
|
|
420
|
+
let _ = wk.loadRequest(&req);
|
|
421
|
+
Ok(())
|
|
422
|
+
}
|
|
423
|
+
None => Err(format!("bad url: {url}")),
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// Fire-and-forget JS injection. Main-thread only.
|
|
428
|
+
pub unsafe fn inject_script(wk: *mut WKWebView, js: &str) {
|
|
429
|
+
let wk = &*wk;
|
|
430
|
+
wk.evaluateJavaScript_completionHandler(&NSString::from_str(js), None);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/// callAsyncJavaScript on a raw WKWebView ptr — CSP-immune eval with an awaited return value.
|
|
434
|
+
/// Delivers the RAW stringified JS return value ("null" or the NSString) via `on_done`; the adapter
|
|
435
|
+
/// wraps it into the wire envelope. Main-thread only (the completion block also lands there).
|
|
436
|
+
pub unsafe fn eval_async(
|
|
437
|
+
wk: *mut WKWebView,
|
|
438
|
+
js: String,
|
|
439
|
+
on_done: impl FnOnce(Result<String, String>) + 'static,
|
|
440
|
+
) {
|
|
441
|
+
let wk = &*wk;
|
|
442
|
+
let mtm = MainThreadMarker::new().expect("eval_async runs on main");
|
|
443
|
+
let on_done = RefCell::new(Some(on_done));
|
|
444
|
+
let block = block2::RcBlock::new(move |result: *mut AnyObject, error: *mut NSError| {
|
|
445
|
+
let Some(cb) = on_done.borrow_mut().take() else { return };
|
|
446
|
+
if !error.is_null() {
|
|
447
|
+
cb(Err((*error).localizedDescription().to_string()));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
let out = if result.is_null() {
|
|
451
|
+
"null".to_string()
|
|
452
|
+
} else if let Ok(ns) = (&*result).downcast_ref::<NSString>().ok_or(()) {
|
|
453
|
+
ns.to_string()
|
|
454
|
+
} else {
|
|
455
|
+
"null".to_string()
|
|
456
|
+
};
|
|
457
|
+
cb(Ok(out));
|
|
458
|
+
});
|
|
459
|
+
wk.callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler(
|
|
460
|
+
&NSString::from_str(&js),
|
|
461
|
+
None,
|
|
462
|
+
None,
|
|
463
|
+
&WKContentWorld::pageWorld(mtm),
|
|
464
|
+
Some(&block),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/// WKWebView takeSnapshot -> NSImage -> PNG bytes, delivered via `on_done`. The adapter base64s +
|
|
469
|
+
/// frames it. Main-thread only (the completion block also lands there).
|
|
470
|
+
pub unsafe fn snapshot(wk: *mut WKWebView, on_done: impl FnOnce(Result<Vec<u8>, String>) + 'static) {
|
|
471
|
+
let wk = &*wk;
|
|
472
|
+
let on_done = RefCell::new(Some(on_done));
|
|
473
|
+
let block = block2::RcBlock::new(move |image: *mut NSImage, error: *mut NSError| {
|
|
474
|
+
let Some(cb) = on_done.borrow_mut().take() else { return };
|
|
475
|
+
if image.is_null() {
|
|
476
|
+
let msg = if error.is_null() { "snapshot failed".to_string() } else { (*error).localizedDescription().to_string() };
|
|
477
|
+
cb(Err(msg));
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
let img = &*image;
|
|
481
|
+
let Some(tiff) = img.TIFFRepresentation() else {
|
|
482
|
+
cb(Err("no TIFF representation".into()));
|
|
483
|
+
return;
|
|
484
|
+
};
|
|
485
|
+
let Some(rep) = NSBitmapImageRep::initWithData(NSBitmapImageRep::alloc(), &tiff) else {
|
|
486
|
+
cb(Err("bitmap rep failed".into()));
|
|
487
|
+
return;
|
|
488
|
+
};
|
|
489
|
+
let props = NSDictionary::new();
|
|
490
|
+
let Some(png) = rep.representationUsingType_properties(NSBitmapImageFileType::PNG, &props) else {
|
|
491
|
+
cb(Err("png encode failed".into()));
|
|
492
|
+
return;
|
|
493
|
+
};
|
|
494
|
+
cb(Ok(png.to_vec()));
|
|
495
|
+
});
|
|
496
|
+
wk.takeSnapshotWithConfiguration_completionHandler(None, &block);
|
|
497
|
+
}
|
|
@@ -70,6 +70,7 @@ dependencies = [
|
|
|
70
70
|
"tauri-plugin-deep-link",
|
|
71
71
|
"tauri-plugin-notification",
|
|
72
72
|
"tauri-plugin-window-state",
|
|
73
|
+
"webview-control",
|
|
73
74
|
]
|
|
74
75
|
|
|
75
76
|
[[package]]
|
|
@@ -4626,6 +4627,18 @@ dependencies = [
|
|
|
4626
4627
|
"system-deps",
|
|
4627
4628
|
]
|
|
4628
4629
|
|
|
4630
|
+
[[package]]
|
|
4631
|
+
name = "webview-control"
|
|
4632
|
+
version = "0.1.0"
|
|
4633
|
+
dependencies = [
|
|
4634
|
+
"block2",
|
|
4635
|
+
"objc2",
|
|
4636
|
+
"objc2-app-kit",
|
|
4637
|
+
"objc2-foundation",
|
|
4638
|
+
"objc2-web-kit",
|
|
4639
|
+
"serde_json",
|
|
4640
|
+
]
|
|
4641
|
+
|
|
4629
4642
|
[[package]]
|
|
4630
4643
|
name = "webview2-com"
|
|
4631
4644
|
version = "0.38.2"
|
|
@@ -20,6 +20,9 @@ libc = "0.2" # desktop.server: setsid the local-server child + SIGTERM its whol
|
|
|
20
20
|
# macOS oauth.authorize — ASWebAuthenticationSession (system-browser auth sheet, exact parity with the
|
|
21
21
|
# iOS shell path in runtime/app/shell/handlers-oauth.ts: auto-close on redirect, user-cancel signal).
|
|
22
22
|
[target.'cfg(target_os = "macos")'.dependencies]
|
|
23
|
+
# The generalized native webview primitive (per-profile store, eval, snapshot, popup, bridgeShim).
|
|
24
|
+
# objc2* versions MUST stay identical to those below — the WKWebView pointer crosses the boundary.
|
|
25
|
+
webview-control = { path = "../crates/webview-control" }
|
|
23
26
|
base64 = "0.22" # SPIKE: bridge_mac screenshot PNG encoding
|
|
24
27
|
objc2 = "0.6"
|
|
25
28
|
objc2-foundation = "0.3"
|