@livx.cc/appwrap 0.55.0 → 0.57.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.
Files changed (43) hide show
  1. package/package.json +2 -3
  2. package/runtime/app/shell/capabilities.manifest.ts +3 -3
  3. package/runtime/tests/bridge-response-delivery.test.ts +10 -1
  4. package/runtime/tests/env-scheme.test.ts +6 -0
  5. package/scripts/stage-assets.mjs +1 -1
  6. package/src/cli.ts +40 -570
  7. package/src/config.ts +1 -1
  8. package/src/handlers.ts +1 -1
  9. package/src/packs.ts +1 -1
  10. package/templates/ci/fastlane/Fastfile +47 -6
  11. package/runtime-desktop/autotest/index.html +0 -77
  12. package/runtime-desktop/bridge-shim/build.sh +0 -9
  13. package/runtime-desktop/bridge-shim/content-entry.ts +0 -87
  14. package/runtime-desktop/bridge-shim/shim.ts +0 -132
  15. package/runtime-desktop/chrome/appwrap-browser-chrome.html +0 -185
  16. package/runtime-desktop/crates/webview-control/Cargo.lock +0 -330
  17. package/runtime-desktop/crates/webview-control/Cargo.toml +0 -28
  18. package/runtime-desktop/crates/webview-control/examples/host_harness.rs +0 -362
  19. package/runtime-desktop/crates/webview-control/src/embedded.rs +0 -138
  20. package/runtime-desktop/crates/webview-control/src/lib.rs +0 -100
  21. package/runtime-desktop/crates/webview-control/src/popup.rs +0 -522
  22. package/runtime-desktop/crates/webview-control/src/profiles.rs +0 -57
  23. package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +0 -558
  24. package/runtime-desktop/src-tauri/Cargo.lock +0 -5474
  25. package/runtime-desktop/src-tauri/Cargo.toml +0 -45
  26. package/runtime-desktop/src-tauri/build.rs +0 -3
  27. package/runtime-desktop/src-tauri/capabilities/default.json +0 -6
  28. package/runtime-desktop/src-tauri/icons/icon.png +0 -0
  29. package/runtime-desktop/src-tauri/shell_config.json +0 -10
  30. package/runtime-desktop/src-tauri/src/biometrics_mac.rs +0 -138
  31. package/runtime-desktop/src-tauri/src/bridge_mac.rs +0 -424
  32. package/runtime-desktop/src-tauri/src/browser_tab.rs +0 -804
  33. package/runtime-desktop/src-tauri/src/main.rs +0 -1228
  34. package/runtime-desktop/src-tauri/src/notifications_mac.rs +0 -202
  35. package/runtime-desktop/src-tauri/src/oauth_mac.rs +0 -139
  36. package/runtime-desktop/src-tauri/src/plugin_host.rs +0 -522
  37. package/runtime-desktop/src-tauri/src/popup_mac.rs +0 -5
  38. package/runtime-desktop/src-tauri/src/push_mac.rs +0 -193
  39. package/runtime-desktop/src-tauri/src/server.rs +0 -156
  40. package/runtime-desktop/src-tauri/src/sidecar.rs +0 -398
  41. package/runtime-desktop/src-tauri/tauri.conf.json +0 -14
  42. package/src/desktop.ts +0 -300
  43. package/src/plugin/host.ts +0 -242
@@ -1,558 +0,0 @@
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
- /// True when webviews should be Safari-inspectable (Develop ▸ <app> ▸ Web Inspector). Debug builds
32
- /// are ALWAYS inspectable; a release build is inspectable ONLY when `APPWRAP_DEVTOOLS` is set to
33
- /// `1`/`true` — so production apps aren't inspectable by default. Self-contained in this crate (no
34
- /// shell-config dependency) so every webview creation site can gate uniformly.
35
- pub fn devtools_enabled() -> bool {
36
- cfg!(debug_assertions)
37
- || matches!(std::env::var("APPWRAP_DEVTOOLS").ok().as_deref(), Some("1") | Some("true"))
38
- }
39
-
40
- /// Flip a WKWebView's `inspectable` property ON when [`devtools_enabled`], making it attachable from
41
- /// Safari → Develop. The property lands in macOS 13.3+, so it's guarded by `respondsToSelector:` and
42
- /// no-ops on older systems. Set right after webview creation, on the main thread. `wk` must be a live
43
- /// `*mut WKWebView`.
44
- pub unsafe fn set_inspectable(wk: *mut WKWebView) {
45
- if wk.is_null() || !devtools_enabled() {
46
- return;
47
- }
48
- let wk = &*wk;
49
- let sel = objc2::sel!(setInspectable:);
50
- let responds: bool = msg_send![wk, respondsToSelector: sel];
51
- if responds {
52
- let _: () = msg_send![wk, setInspectable: true];
53
- }
54
- }
55
-
56
- /// Autoresizing mask that pins a mounted webview to fill its superview on resize — the same
57
- /// width+height-sizable mask popup windows use, applied on every (re)mount.
58
- const FILL_MASK: NSAutoresizingMaskOptions =
59
- NSAutoresizingMaskOptions(NSAutoresizingMaskOptions::ViewWidthSizable.0 | NSAutoresizingMaskOptions::ViewHeightSizable.0);
60
-
61
- /// Raw page->host message sink. Receives the UNWRAPPED WKScriptMessage body string (the page's
62
- /// `webkit.messageHandlers.bridgeShim.postMessage(...)` payload). The adapter wraps it into its
63
- /// transport envelope — the primitive stays framing-agnostic. `Send` (it crosses onto the main
64
- /// thread with the create op) but not `Sync` — the handler is MainThreadOnly and only ever invoked
65
- /// on the main thread, mirroring the original bare `Sender`.
66
- pub type MessageSink = Box<dyn Fn(String) + Send>;
67
-
68
- /// Result of creating a profile window — the store facts the harness asserts. The adapter builds
69
- /// the wire JSON from this; the primitive returns plain data.
70
- pub struct CreateResult {
71
- pub persistent: bool,
72
- pub store_uuid: String,
73
- }
74
-
75
- /// One profile window's public identity for `list_webviews`.
76
- pub struct WebviewInfo {
77
- pub label: String,
78
- pub profile_id: String,
79
- }
80
-
81
- /// Spec for creating a webview via the embedded facade (Phase 2). Mirrors the control-channel
82
- /// `createWindow` op fields plus the `mount` target (the adapter always builds `Mount::TopLevel`).
83
- pub struct CreateSpec {
84
- pub label: String,
85
- pub url: String,
86
- pub profile_id: String,
87
- pub init_js: String,
88
- pub mount: Mount,
89
- }
90
-
91
- // ---------------------------------------------------------------------------
92
- // Page -> host transport: a native WKScriptMessageHandler ("bridgeShim").
93
- // WebKit blocks ws://127.0.0.1 (and http beacons) from https pages as mixed content — NO loopback
94
- // exemption like Chrome — so the content layer posts results via
95
- // webkit.messageHandlers.bridgeShim.postMessage(json) instead. We hand the RAW payload to the
96
- // on_message sink; the adapter does the framing.
97
- // ---------------------------------------------------------------------------
98
- pub struct MsgHandlerIvars {
99
- on_message: MessageSink,
100
- }
101
-
102
- define_class!(
103
- #[unsafe(super(NSObject))]
104
- #[thread_kind = MainThreadOnly]
105
- #[name = "AppwrapBridgeMsgHandler"]
106
- #[ivars = MsgHandlerIvars]
107
- struct MsgHandler;
108
-
109
- unsafe impl NSObjectProtocol for MsgHandler {}
110
-
111
- unsafe impl WKScriptMessageHandler for MsgHandler {
112
- #[unsafe(method(userContentController:didReceiveScriptMessage:))]
113
- fn did_receive(&self, _ucc: &WKUserContentController, message: &WKScriptMessage) {
114
- let body = unsafe { message.body() };
115
- if let Ok(s) = body.downcast::<NSString>() {
116
- (self.ivars().on_message)(s.to_string());
117
- }
118
- }
119
- }
120
- );
121
-
122
- /// Install the bridgeShim WKScriptMessageHandler on an arbitrary WKWebView (used by the adapter for
123
- /// the legacy tauri-window path — the profile-window path installs it on the config's UCC before
124
- /// creation). The UCC retains the handler, so no external retention is needed here. Main-thread only.
125
- pub unsafe fn install_message_handler(wk: *mut WKWebView, mtm: MainThreadMarker, on_message: MessageSink) {
126
- install_named_message_handler(wk, mtm, "bridgeShim", on_message);
127
- }
128
-
129
- /// Install a WKScriptMessageHandler under `name` on a WKWebView, so page JS reaches native via
130
- /// `webkit.messageHandlers.<name>.postMessage(str)`. Origin-agnostic and NOT command-ACL-gated —
131
- /// the ONLY reliable page→native path for a REMOTE-served page (Tauri command IPC is ACL-blocked
132
- /// for remote origins; ws/http/custom-scheme are mixed-content-blocked from https). Main-thread only.
133
- pub unsafe fn install_named_message_handler(
134
- wk: *mut WKWebView,
135
- mtm: MainThreadMarker,
136
- name: &str,
137
- on_message: MessageSink,
138
- ) {
139
- let wk = &*wk;
140
- let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { on_message });
141
- let handler: Retained<MsgHandler> = msg_send![super(handler), init];
142
- let ucc = wk.configuration().userContentController();
143
- // Idempotent per UCC: addScriptMessageHandler throws NSInvalidArgumentException if `name` is
144
- // already registered; remove-then-add makes re-install a no-op instead of a crash.
145
- let ns_name = NSString::from_str(name);
146
- ucc.removeScriptMessageHandlerForName(&ns_name);
147
- ucc.addScriptMessageHandler_name(ProtocolObject::from_ref(&*handler), &ns_name);
148
- }
149
-
150
- /// A manually-created profile window. Strong refs kept alive until close (or windowWillClose).
151
- /// Only touched on the main thread; the unsafe Send exists solely so the registry can be a static
152
- /// Mutex (same pattern as popup_mac).
153
- struct ProfileWindow {
154
- label: String,
155
- profile_id: String,
156
- window: Retained<NSWindow>,
157
- webview: Retained<WKWebView>,
158
- /// Set true by the window-close delegate on a USER (red-button) close.
159
- closed: AtomicBool,
160
- _handler: Retained<MsgHandler>,
161
- /// Per-window SSO-popup delegate (weak uiDelegate → held here). Opaque; lifetime only.
162
- _popup_delegate: Retained<AnyObject>,
163
- /// NSWindowDelegate that flags `closed` on a user close (weak setDelegate → held here).
164
- _win_delegate: Retained<ProfileWinDelegate>,
165
- }
166
- unsafe impl Send for ProfileWindow {}
167
- static PROFILE_WINDOWS: Mutex<Vec<ProfileWindow>> = Mutex::new(Vec::new());
168
-
169
- // NSWindowDelegate for profile windows — flags the matching registry entry `closed` when the user
170
- // closes the window (red button), so it drops out of list_webviews. Matches by NSWindow identity
171
- // (the notification's object). Main-thread only.
172
- define_class!(
173
- #[unsafe(super(NSObject))]
174
- #[thread_kind = MainThreadOnly]
175
- #[name = "AppwrapProfileWinDelegate"]
176
- struct ProfileWinDelegate;
177
-
178
- unsafe impl NSObjectProtocol for ProfileWinDelegate {}
179
-
180
- unsafe impl NSWindowDelegate for ProfileWinDelegate {
181
- #[unsafe(method(windowWillClose:))]
182
- fn window_will_close(&self, notification: &NSNotification) {
183
- let Some(obj) = notification.object() else { return };
184
- let key = Retained::as_ptr(&obj) as usize;
185
- let v = PROFILE_WINDOWS.lock().unwrap();
186
- if let Some(p) = v.iter().find(|p| Retained::as_ptr(&p.window) as usize == key) {
187
- eprintln!("[bridge-shim] profile window {} user-closed", p.label);
188
- // Detach from the superview so a ChildOf-mounted webview isn't left as a zombie in
189
- // the host pane once `reap_closed` drops the registry `Retained` (see `close`).
190
- p.webview.removeFromSuperview();
191
- p.closed.store(true, Ordering::Relaxed);
192
- }
193
- }
194
- }
195
- );
196
-
197
- /// Retained clone of a profile window's WKWebView (op routing), or None if the label is unknown /
198
- /// already closed. The adapter uses this to decide whether an op targets a profile window (else it
199
- /// falls back to its legacy tauri path).
200
- pub fn profile_webview(label: &str) -> Option<Retained<WKWebView>> {
201
- PROFILE_WINDOWS
202
- .lock()
203
- .unwrap()
204
- .iter()
205
- .find(|p| p.label == label && !p.closed.load(Ordering::Relaxed))
206
- .map(|p| p.webview.clone())
207
- }
208
-
209
- /// Public identities of the live (non-closed) profile windows.
210
- pub fn list_webviews() -> Vec<WebviewInfo> {
211
- PROFILE_WINDOWS
212
- .lock()
213
- .unwrap()
214
- .iter()
215
- .filter(|p| !p.closed.load(Ordering::Relaxed))
216
- .map(|p| WebviewInfo { label: p.label.clone(), profile_id: p.profile_id.clone() })
217
- .collect()
218
- }
219
-
220
- /// Drop entries flagged `closed` (user-closed windows). Called at safe points only — never from
221
- /// within a delegate callback, so the delegate isn't released mid-call.
222
- fn reap_closed() {
223
- let mut v = PROFILE_WINDOWS.lock().unwrap();
224
- v.retain(|p| !p.closed.load(Ordering::Relaxed));
225
- }
226
-
227
- /// Close + drop a profile window (releases its NSWindow + WKWebView). Main-thread only. Returns
228
- /// false if no such profile window (caller falls back to tauri). The removed entry is held on the
229
- /// stack across window.close() so its window delegate stays alive while AppKit fires windowWillClose.
230
- pub fn close(label: &str) -> bool {
231
- let entry = {
232
- let mut v = PROFILE_WINDOWS.lock().unwrap();
233
- v.iter().position(|p| p.label == label).map(|i| v.remove(i))
234
- };
235
- match entry {
236
- Some(p) => {
237
- // Detach the WKWebView from its superview BEFORE dropping the registry `Retained`. In
238
- // `Mount::ChildOf` (embedded) mode the webview is a subview of the HOST pane (retained
239
- // by the host's subview array), NOT the home window's contentView — so `window.close()`
240
- // alone leaves it alive and VISIBLE in the host pane (a zombie). Harmless for TopLevel
241
- // (the webview IS the home window's contentView, released with the window anyway).
242
- p.webview.removeFromSuperview();
243
- p.window.close();
244
- true
245
- }
246
- None => false,
247
- }
248
- }
249
-
250
- /// Drive NSWindow performClose: (the red-button path) WITHOUT touching the registry, so the
251
- /// windowWillClose delegate is the sole remover — proves user-close hygiene headlessly. Returns
252
- /// false if no such profile window. Main-thread only.
253
- pub fn user_close(label: &str) -> bool {
254
- let win = PROFILE_WINDOWS
255
- .lock()
256
- .unwrap()
257
- .iter()
258
- .find(|p| p.label == label)
259
- .map(|p| p.window.clone());
260
- match win {
261
- Some(w) => {
262
- w.performClose(None);
263
- true
264
- }
265
- None => false,
266
- }
267
- }
268
-
269
- /// Create a top-level profile window (the standalone/MCP shape — `Mount::TopLevel`). Thin wrapper
270
- /// over [`create_webview`]; kept as-is so the control-channel adapter (bridge_mac.rs) is unchanged
271
- /// and its behavior byte-identical. Main-thread only.
272
- pub fn create_profile_window(
273
- mtm: MainThreadMarker,
274
- label: &str,
275
- url: &str,
276
- profile_id: &str,
277
- init_js: &str,
278
- on_message: MessageSink,
279
- ) -> Result<CreateResult, String> {
280
- create_webview(
281
- mtm,
282
- CreateSpec {
283
- label: label.to_string(),
284
- url: url.to_string(),
285
- profile_id: profile_id.to_string(),
286
- init_js: init_js.to_string(),
287
- mount: Mount::TopLevel,
288
- },
289
- on_message,
290
- )
291
- }
292
-
293
- /// Create a manual NSWindow + WKWebView bound to `profile_id`'s persistent data store, inject the
294
- /// bridge content bundle + bridgeShim handler, mount it per `spec.mount`, and register it. Runs on
295
- /// the main thread (the adapter/host marshals it there). Returns the store facts.
296
- ///
297
- /// The webview ALWAYS owns a hidden home `NSWindow` (the `TopLevel` target), created here with
298
- /// `setReleasedWhenClosed(false)` so a host-window close never frees the view. For `TopLevel` the
299
- /// window is shown and holds the webview as its content view (identical to the pre-Phase-2 path);
300
- /// for `ChildOf(handle)` the window stays unshown and the webview is added as a subview of the
301
- /// host-owned pane. `set_mount` later moves the same live `WKWebView` between the two.
302
- pub fn create_webview(
303
- mtm: MainThreadMarker,
304
- spec: CreateSpec,
305
- on_message: MessageSink,
306
- ) -> Result<CreateResult, String> {
307
- let CreateSpec { label, url, profile_id, init_js, mount } = spec;
308
- let (label, url, profile_id, init_js) = (label.as_str(), url.as_str(), profile_id.as_str(), init_js.as_str());
309
- let uuid_str = profiles::uuid_for(profile_id);
310
- let nsuuid = NSUUID::initWithUUIDString(NSUUID::alloc(), &NSString::from_str(&uuid_str))
311
- .ok_or_else(|| format!("bad uuid for profile {profile_id}: {uuid_str}"))?;
312
- let store = unsafe { WKWebsiteDataStore::dataStoreForIdentifier(&nsuuid, mtm) };
313
- let persistent = unsafe { store.isPersistent() };
314
-
315
- let cfg = unsafe { WKWebViewConfiguration::new(mtm) };
316
- unsafe { cfg.setWebsiteDataStore(&store) };
317
- // Allow programmatic window.open (the createWebViewWithConfiguration UI-delegate below hosts the
318
- // child popup). Without this WKWebView blocks any window.open NOT fired synchronously in a click —
319
- // which silently kills OAuth "Sign in with Google/…" popups that open AFTER an async round-trip.
320
- unsafe { cfg.preferences().setJavaScriptCanOpenWindowsAutomatically(true) };
321
-
322
- // bridgeShim message handler + content bundle (documentStart) on this cfg's UCC.
323
- let ucc = unsafe { cfg.userContentController() };
324
- let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { on_message });
325
- let handler: Retained<MsgHandler> = unsafe { msg_send![super(handler), init] };
326
- unsafe {
327
- ucc.addScriptMessageHandler_name(
328
- ProtocolObject::from_ref(&*handler),
329
- &NSString::from_str("bridgeShim"),
330
- );
331
- let script = WKUserScript::initWithSource_injectionTime_forMainFrameOnly(
332
- WKUserScript::alloc(mtm),
333
- &NSString::from_str(init_js),
334
- WKUserScriptInjectionTime::AtDocumentStart,
335
- true,
336
- );
337
- ucc.addUserScript(&script);
338
- }
339
-
340
- let rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(1100.0, 800.0));
341
- let style = NSWindowStyleMask::Titled
342
- | NSWindowStyleMask::Closable
343
- | NSWindowStyleMask::Miniaturizable
344
- | NSWindowStyleMask::Resizable;
345
- let window = unsafe {
346
- NSWindow::initWithContentRect_styleMask_backing_defer(
347
- mtm.alloc::<NSWindow>(),
348
- rect,
349
- style,
350
- NSBackingStoreType::Buffered,
351
- false,
352
- )
353
- };
354
- unsafe { window.setReleasedWhenClosed(false) };
355
-
356
- let webview = unsafe {
357
- WKWebView::initWithFrame_configuration(mtm.alloc::<WKWebView>(), window.frame(), &cfg)
358
- };
359
- unsafe {
360
- webview.setCustomUserAgent(Some(&NSString::from_str(popup::SAFARI_UA)));
361
- window.setTitle(&NSString::from_str(label));
362
- // Safari Web Inspector (debug always; release only under APPWRAP_DEVTOOLS) for every
363
- // bridge/profile/sub-window webview created through this primitive.
364
- set_inspectable(Retained::as_ptr(&webview) as *mut WKWebView);
365
- }
366
-
367
- // Per-window SSO-popup delegate + window-close delegate. Both are weak refs on AppKit/WebKit →
368
- // retained in the registry entry below.
369
- let popup_delegate: Retained<AnyObject> =
370
- popup::install_for_profile(Retained::as_ptr(&webview) as *mut std::ffi::c_void, mtm);
371
- let win_delegate = ProfileWinDelegate::alloc(mtm).set_ivars(());
372
- let win_delegate: Retained<ProfileWinDelegate> = unsafe { msg_send![super(win_delegate), init] };
373
- window.setDelegate(Some(ProtocolObject::from_ref(&*win_delegate)));
374
- window.center();
375
-
376
- // Initial mount: TopLevel shows the home window with the webview as content view (the
377
- // pre-Phase-2 behavior, byte-identical); ChildOf leaves the home window unshown and adds the
378
- // webview into the host pane. Same reparent primitive as set_mount, applied at birth.
379
- unsafe { mount_view(mtm, &webview, &window, mount) };
380
-
381
- let nsurl = NSURL::URLWithString(&NSString::from_str(url))
382
- .ok_or_else(|| format!("bad url: {url}"))?;
383
- let req = NSURLRequest::requestWithURL(&nsurl);
384
- let _ = unsafe { webview.loadRequest(&req) };
385
-
386
- reap_closed();
387
- PROFILE_WINDOWS.lock().unwrap().push(ProfileWindow {
388
- label: label.to_string(),
389
- profile_id: profile_id.to_string(),
390
- window,
391
- webview,
392
- closed: AtomicBool::new(false),
393
- _handler: handler,
394
- _popup_delegate: popup_delegate,
395
- _win_delegate: win_delegate,
396
- });
397
- eprintln!(
398
- "[bridge-shim] profile window {label} profile={profile_id} store={uuid_str} persistent={persistent}"
399
- );
400
- Ok(CreateResult { persistent, store_uuid: uuid_str })
401
- }
402
-
403
- // ---------------------------------------------------------------------------
404
- // Phase 2: NSView-reparent (tear-off / dock). The SAME live WKWebView object is moved between its
405
- // home NSWindow (TopLevel) and a host-owned pane NSView (ChildOf) — page + WKWebsiteDataStore
406
- // session + WKScriptMessageHandler bridge + SSO uiDelegate all survive because it is never
407
- // re-created. Productionized from the Phase-0 spike. Main-thread only (WKWebView/NSView are not
408
- // thread-safe); zero-frame subviews render blank, so we always set frame = target bounds + a
409
- // fill autoresizing mask.
410
- // ---------------------------------------------------------------------------
411
-
412
- /// Reparent `webview` to `mount`, using `home_window` as the TopLevel target. Caller holds the
413
- /// main thread. `ChildOf` dereferences the host `NSView*` per the [`crate::ViewHandle`] contract.
414
- unsafe fn mount_view(
415
- _mtm: MainThreadMarker,
416
- webview: &WKWebView,
417
- home_window: &NSWindow,
418
- mount: Mount,
419
- ) {
420
- webview.removeFromSuperview();
421
- match mount {
422
- Mount::TopLevel => {
423
- let size = home_window.frame().size;
424
- webview.setFrame(NSRect::new(NSPoint::new(0.0, 0.0), size));
425
- webview.setAutoresizingMask(FILL_MASK);
426
- home_window.setContentView(Some(webview));
427
- home_window.makeKeyAndOrderFront(None);
428
- }
429
- Mount::ChildOf(handle) => {
430
- // The host owns this NSView; we only add a subview link (contract: valid, main-thread,
431
- // host keeps it alive while mounted). Order the home window out so it isn't a stray.
432
- let host = &*(handle.as_ptr() as *mut NSView);
433
- let bounds = host.bounds();
434
- webview.setFrame(bounds);
435
- webview.setAutoresizingMask(FILL_MASK);
436
- host.addSubview(webview);
437
- home_window.orderOut(None);
438
- }
439
- }
440
- }
441
-
442
- /// Move a live profile webview between top-level and child-of-a-host-pane (tear-off / dock).
443
- /// Returns false if `label` is unknown or already closed. Main-thread only.
444
- pub fn set_mount(mtm: MainThreadMarker, label: &str, mount: Mount) -> bool {
445
- let parts = PROFILE_WINDOWS
446
- .lock()
447
- .unwrap()
448
- .iter()
449
- .find(|p| p.label == label && !p.closed.load(Ordering::Relaxed))
450
- .map(|p| (p.webview.clone(), p.window.clone()));
451
- match parts {
452
- Some((webview, window)) => {
453
- unsafe { mount_view(mtm, &webview, &window, mount) };
454
- true
455
- }
456
- None => false,
457
- }
458
- }
459
-
460
- /// Navigate any WKWebView (profile or tauri) to `url`. Main-thread only.
461
- pub unsafe fn navigate(wk: *mut WKWebView, url: &str) -> Result<(), String> {
462
- let wk = &*wk;
463
- match NSURL::URLWithString(&NSString::from_str(url)) {
464
- Some(nsurl) => {
465
- let req = NSURLRequest::requestWithURL(&nsurl);
466
- let _ = wk.loadRequest(&req);
467
- Ok(())
468
- }
469
- None => Err(format!("bad url: {url}")),
470
- }
471
- }
472
-
473
- /// Navigate back in the WKWebView's session history (no-op if none). Main-thread only.
474
- pub unsafe fn go_back(wk: *mut WKWebView) {
475
- let _ = (&*wk).goBack();
476
- }
477
-
478
- /// Navigate forward in the WKWebView's session history (no-op if none). Main-thread only.
479
- pub unsafe fn go_forward(wk: *mut WKWebView) {
480
- let _ = (&*wk).goForward();
481
- }
482
-
483
- /// Reload the current page. Main-thread only.
484
- pub unsafe fn reload(wk: *mut WKWebView) {
485
- let _ = (&*wk).reload();
486
- }
487
-
488
- /// Fire-and-forget JS injection. Main-thread only.
489
- pub unsafe fn inject_script(wk: *mut WKWebView, js: &str) {
490
- let wk = &*wk;
491
- wk.evaluateJavaScript_completionHandler(&NSString::from_str(js), None);
492
- }
493
-
494
- /// callAsyncJavaScript on a raw WKWebView ptr — CSP-immune eval with an awaited return value.
495
- /// Delivers the RAW stringified JS return value ("null" or the NSString) via `on_done`; the adapter
496
- /// wraps it into the wire envelope. Main-thread only (the completion block also lands there).
497
- pub unsafe fn eval_async(
498
- wk: *mut WKWebView,
499
- js: String,
500
- on_done: impl FnOnce(Result<String, String>) + 'static,
501
- ) {
502
- let wk = &*wk;
503
- let mtm = MainThreadMarker::new().expect("eval_async runs on main");
504
- let on_done = RefCell::new(Some(on_done));
505
- let block = block2::RcBlock::new(move |result: *mut AnyObject, error: *mut NSError| {
506
- let Some(cb) = on_done.borrow_mut().take() else { return };
507
- if !error.is_null() {
508
- cb(Err((*error).localizedDescription().to_string()));
509
- return;
510
- }
511
- let out = if result.is_null() {
512
- "null".to_string()
513
- } else if let Ok(ns) = (&*result).downcast_ref::<NSString>().ok_or(()) {
514
- ns.to_string()
515
- } else {
516
- "null".to_string()
517
- };
518
- cb(Ok(out));
519
- });
520
- wk.callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler(
521
- &NSString::from_str(&js),
522
- None,
523
- None,
524
- &WKContentWorld::pageWorld(mtm),
525
- Some(&block),
526
- );
527
- }
528
-
529
- /// WKWebView takeSnapshot -> NSImage -> PNG bytes, delivered via `on_done`. The adapter base64s +
530
- /// frames it. Main-thread only (the completion block also lands there).
531
- pub unsafe fn snapshot(wk: *mut WKWebView, on_done: impl FnOnce(Result<Vec<u8>, String>) + 'static) {
532
- let wk = &*wk;
533
- let on_done = RefCell::new(Some(on_done));
534
- let block = block2::RcBlock::new(move |image: *mut NSImage, error: *mut NSError| {
535
- let Some(cb) = on_done.borrow_mut().take() else { return };
536
- if image.is_null() {
537
- let msg = if error.is_null() { "snapshot failed".to_string() } else { (*error).localizedDescription().to_string() };
538
- cb(Err(msg));
539
- return;
540
- }
541
- let img = &*image;
542
- let Some(tiff) = img.TIFFRepresentation() else {
543
- cb(Err("no TIFF representation".into()));
544
- return;
545
- };
546
- let Some(rep) = NSBitmapImageRep::initWithData(NSBitmapImageRep::alloc(), &tiff) else {
547
- cb(Err("bitmap rep failed".into()));
548
- return;
549
- };
550
- let props = NSDictionary::new();
551
- let Some(png) = rep.representationUsingType_properties(NSBitmapImageFileType::PNG, &props) else {
552
- cb(Err("png encode failed".into()));
553
- return;
554
- };
555
- cb(Ok(png.to_vec()));
556
- });
557
- wk.takeSnapshotWithConfiguration_completionHandler(None, &block);
558
- }