diff --git a/.changes/objc2.md b/.changes/objc2.md new file mode 100644 index 000000000..57397a964 --- /dev/null +++ b/.changes/objc2.md @@ -0,0 +1,5 @@ +--- +"wry": minor +--- + +Migrate to obj2. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index ff81aa2d2..3a80812ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ -workspace = { } +workspace = {} [package] name = "wry" @@ -10,31 +10,31 @@ description = "Cross-platform WebView rendering library" readme = "README.md" repository = "https://github.com/tauri-apps/wry" documentation = "https://docs.rs/wry" -categories = [ "gui" ] -exclude = [ "/.changes", "/.github", "/audits", "/wry-logo.svg" ] +categories = ["gui"] +exclude = ["/.changes", "/.github", "/audits", "/wry-logo.svg"] [package.metadata.docs.rs] no-default-features = true -features = [ "drag-drop", "protocol", "os-webview" ] +features = ["drag-drop", "protocol", "os-webview"] targets = [ "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc", - "x86_64-apple-darwin" + "x86_64-apple-darwin", ] -rustc-args = [ "--cfg", "docsrs" ] -rustdoc-args = [ "--cfg", "docsrs" ] +rustc-args = ["--cfg", "docsrs"] +rustdoc-args = ["--cfg", "docsrs"] [features] -default = [ "drag-drop", "objc-exception", "protocol", "os-webview" ] -serde = [ "dpi/serde" ] -objc-exception = [ "objc/exception" ] -drag-drop = [ ] -protocol = [ ] -devtools = [ ] -transparent = [ ] -fullscreen = [ ] -linux-body = [ "webkit2gtk/v2_40", "os-webview" ] -mac-proxy = [ ] +default = ["drag-drop", "objc-exception", "protocol", "os-webview"] +serde = ["dpi/serde"] +objc-exception = ["objc2/catch-all"] +drag-drop = [] +protocol = [] +devtools = [] +transparent = [] +fullscreen = [] +linux-body = ["webkit2gtk/v2_40", "os-webview"] +mac-proxy = [] os-webview = [ "javascriptcore-rs", "webkit2gtk", @@ -42,21 +42,23 @@ os-webview = [ "dep:gtk", "soup3", "x11-dl", - "gdkx11" + "gdkx11", ] -tracing = [ "dep:tracing" ] +tracing = ["dep:tracing"] [dependencies] tracing = { version = "0.1", optional = true } once_cell = "1" thiserror = "1.0" http = "1.1" -raw-window-handle = { version = "0.6", features = [ "std" ] } +raw-window-handle = { version = "0.6", features = ["std"] } dpi = "0.1" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] -javascriptcore-rs = { version = "=1.1.2", features = [ "v2_28" ], optional = true } -webkit2gtk = { version = "=2.0.1", features = [ "v2_38" ], optional = true } +javascriptcore-rs = { version = "=1.1.2", features = [ + "v2_28", +], optional = true } +webkit2gtk = { version = "=2.0.1", features = ["v2_38"], optional = true } webkit2gtk-sys = { version = "=2.0.1", optional = true } gtk = { version = "0.18", optional = true } soup3 = { version = "0.5", optional = true } @@ -87,15 +89,79 @@ features = [ "Win32_Globalization", "Win32_UI_HiDpi", "Win32_UI_Input", - "Win32_UI_Input_KeyboardAndMouse" + "Win32_UI_Input_KeyboardAndMouse", ] [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies] -block = "0.1" -cocoa = "0.26" -core-graphics = "0.24" -objc = "0.2" -objc_id = "0.1" +block2 = "0.5" +objc2 = "0.5" +objc2-web-kit = { version = "0.2.0", features = [ + "objc2-app-kit", + "block2", + "WKWebView", + "WKWebViewConfiguration", + "WKWebsiteDataStore", + "WKDownload", + "WKDownloadDelegate", + "WKNavigation", + "WKNavigationDelegate", + "WKUserContentController", + "WKURLSchemeHandler", + "WKPreferences", + "WKURLSchemeTask", + "WKScriptMessageHandler", + "WKUIDelegate", + "WKOpenPanelParameters", + "WKFrameInfo", + "WKSecurityOrigin", + "WKScriptMessage", + "WKNavigationAction", + "WKWebpagePreferences", + "WKNavigationResponse", + "WKUserScript", +] } +objc2-foundation = { version = "0.2.0", features = [ + "NSURLRequest", + "NSURL", + "NSString", + "NSKeyValueCoding", + "NSStream", + "NSDictionary", + "NSObject", + "NSData", + "NSKeyValueObserving", + "NSThread", + "NSJSONSerialization", + "NSDate", + "NSBundle", + "NSProcessInfo", + "NSValue", + "NSRange", +] } + +[target."cfg(target_os = \"ios\")".dependencies] +objc2-ui-kit = { version = "0.2.2", features = [ + "UIResponder", + "UIScrollView", + "UIView", + "UIWindow", + "UIApplication", + "UIEvent", +] } + +[target."cfg(target_os = \"macos\")".dependencies] +objc2-app-kit = { version = "0.2.0", features = [ + "NSApplication", + "NSEvent", + "NSWindow", + "NSView", + "NSPasteboard", + "NSPanel", + "NSResponder", + "NSOpenPanel", + "NSSavePanel", + "NSMenu", +] } [target."cfg(target_os = \"android\")".dependencies] crossbeam-channel = "0.5" @@ -119,4 +185,4 @@ percent-encoding = "2.3" [lints.rust.unexpected_cfgs] level = "warn" -check-cfg = [ "cfg(linux)", "cfg(gtk)" ] +check-cfg = ["cfg(linux)", "cfg(gtk)"] diff --git a/examples/reparent.rs b/examples/reparent.rs index 7d682a428..1b5476508 100644 --- a/examples/reparent.rs +++ b/examples/reparent.rs @@ -11,7 +11,7 @@ use tao::{ use wry::WebViewBuilder; #[cfg(target_os = "macos")] -use {tao::platform::macos::WindowExtMacOS, wry::WebViewExtMacOS}; +use {objc2_app_kit::NSWindow, tao::platform::macos::WindowExtMacOS, wry::WebViewExtMacOS}; #[cfg(target_os = "windows")] use {tao::platform::windows::WindowExtWindows, wry::WebViewExtWindows}; @@ -91,7 +91,7 @@ fn main() -> wry::Result<()> { #[cfg(target_os = "macos")] webview - .reparent(new_parent.ns_window() as cocoa::base::id) + .reparent(new_parent.ns_window() as *mut NSWindow) .unwrap(); #[cfg(not(any( target_os = "windows", diff --git a/src/error.rs b/src/error.rs index e74fb283d..e3b026117 100644 --- a/src/error.rs +++ b/src/error.rs @@ -55,6 +55,8 @@ pub enum Error { #[cfg(target_os = "android")] #[error(transparent)] CrossBeamRecvError(#[from] crossbeam_channel::RecvError), + #[error("not on the main thread")] + NotMainThread, #[error("Custom protocol task is invalid.")] CustomProtocolTaskInvalid, #[error("Failed to register URL scheme: {0}, could be due to invalid URL scheme or the scheme is already registered.")] diff --git a/src/lib.rs b/src/lib.rs index ccced7c19..dbd40d009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,9 +190,9 @@ #![allow(clippy::type_complexity)] #![cfg_attr(docsrs, feature(doc_cfg))] -#[cfg(any(target_os = "macos", target_os = "ios"))] -#[macro_use] -extern crate objc; +// #[cfg(any(target_os = "macos", target_os = "ios"))] +// #[macro_use] +// extern crate objc; mod error; mod proxy; @@ -222,12 +222,18 @@ use raw_window_handle::HasWindowHandle; #[cfg(gtk)] use webkitgtk::*; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use objc2::rc::Retained; +#[cfg(target_os = "macos")] +use objc2_app_kit::NSWindow; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use objc2_web_kit::WKUserContentController; #[cfg(any(target_os = "macos", target_os = "ios"))] pub(crate) mod wkwebview; #[cfg(any(target_os = "macos", target_os = "ios"))] use wkwebview::*; #[cfg(any(target_os = "macos", target_os = "ios"))] -pub use wkwebview::{PrintMargin, PrintOptions}; +pub use wkwebview::{PrintMargin, PrintOptions, WryWebView}; #[cfg(target_os = "windows")] pub(crate) mod webview2; @@ -416,7 +422,7 @@ pub struct WebViewAttributes<'a> { /// second is a mutable `PathBuf` reference that (possibly) represents where the file will be downloaded to. The latter /// parameter can be used to set the download location by assigning a new path to it, the assigned path _must_ be /// absolute. The closure returns a `bool` to allow or deny the download. - pub download_started_handler: Option bool>>, + pub download_started_handler: Option bool + 'static>>, /// A download completion handler to manage downloads that have finished. /// @@ -1146,20 +1152,11 @@ impl<'a> WebViewBuilder<'a> { } #[cfg(any(target_os = "macos", target_os = "ios",))] -#[derive(Clone)] +#[derive(Clone, Default)] pub(crate) struct PlatformSpecificWebViewAttributes { data_store_identifier: Option<[u8; 16]>, } -#[cfg(any(target_os = "macos", target_os = "ios",))] -impl Default for PlatformSpecificWebViewAttributes { - fn default() -> Self { - Self { - data_store_identifier: None, - } - } -} - #[cfg(any(target_os = "macos", target_os = "ios",))] pub trait WebViewBuilderExtDarwin { /// Initialize the WebView with a custom data store identifier. @@ -1767,35 +1764,32 @@ impl WebViewExtUnix for WebView { #[cfg(target_os = "macos")] pub trait WebViewExtMacOS { /// Returns WKWebView handle - fn webview(&self) -> cocoa::base::id; + fn webview(&self) -> Retained; /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle - fn manager(&self) -> cocoa::base::id; + fn manager(&self) -> Retained; /// Returns NSWindow associated with the WKWebView webview - fn ns_window(&self) -> cocoa::base::id; + fn ns_window(&self) -> Retained; /// Attaches this webview to the given NSWindow and removes it from the current one. - fn reparent(&self, window: cocoa::base::id) -> Result<()>; + fn reparent(&self, window: *mut NSWindow) -> Result<()>; // Prints with extra options fn print_with_options(&self, options: &PrintOptions) -> Result<()>; } #[cfg(target_os = "macos")] impl WebViewExtMacOS for WebView { - fn webview(&self) -> cocoa::base::id { - self.webview.webview + fn webview(&self) -> Retained { + self.webview.webview.clone() } - fn manager(&self) -> cocoa::base::id { - self.webview.manager + fn manager(&self) -> Retained { + self.webview.manager.clone() } - fn ns_window(&self) -> cocoa::base::id { - unsafe { - let ns_window: cocoa::base::id = msg_send![self.webview.webview, window]; - ns_window - } + fn ns_window(&self) -> Retained { + self.webview.webview.window().unwrap().clone() } - fn reparent(&self, window: cocoa::base::id) -> Result<()> { + fn reparent(&self, window: *mut NSWindow) -> Result<()> { self.webview.reparent(window) } @@ -1808,19 +1802,19 @@ impl WebViewExtMacOS for WebView { #[cfg(target_os = "ios")] pub trait WebViewExtIOS { /// Returns WKWebView handle - fn webview(&self) -> cocoa::base::id; + fn webview(&self) -> Retained; /// Returns WKWebView manager [(userContentController)](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler/1396222-usercontentcontroller) handle - fn manager(&self) -> cocoa::base::id; + fn manager(&self) -> Retained; } #[cfg(target_os = "ios")] impl WebViewExtIOS for WebView { - fn webview(&self) -> cocoa::base::id { - self.webview.webview + fn webview(&self) -> Retained { + self.webview.webview.clone() } - fn manager(&self) -> cocoa::base::id { - self.webview.manager + fn manager(&self) -> Retained { + self.webview.manager.clone() } } diff --git a/src/wkwebview/class/document_title_changed_observer.rs b/src/wkwebview/class/document_title_changed_observer.rs new file mode 100644 index 000000000..90c6cdbfd --- /dev/null +++ b/src/wkwebview/class/document_title_changed_observer.rs @@ -0,0 +1,98 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ffi::c_void, ptr::null_mut}; + +use objc2::{ + declare_class, msg_send, msg_send_id, + mutability::InteriorMutable, + rc::Retained, + runtime::{AnyObject, NSObject}, + ClassType, DeclaredClass, +}; +use objc2_foundation::{ + NSDictionary, NSKeyValueChangeKey, NSKeyValueObservingOptions, + NSObjectNSKeyValueObserverRegistration, NSObjectProtocol, NSString, +}; + +use crate::WryWebView; +pub struct DocumentTitleChangedObserverIvars { + pub object: Retained, + pub handler: Box, +} + +declare_class!( + pub struct DocumentTitleChangedObserver; + + unsafe impl ClassType for DocumentTitleChangedObserver { + type Super = NSObject; + type Mutability = InteriorMutable; + const NAME: &'static str = "DocumentTitleChangedObserver"; + } + + impl DeclaredClass for DocumentTitleChangedObserver { + type Ivars = DocumentTitleChangedObserverIvars; + } + + unsafe impl DocumentTitleChangedObserver { + #[method(observeValueForKeyPath:ofObject:change:context:)] + fn observe_value_for_key_path( + &self, + key_path: Option<&NSString>, + of_object: Option<&AnyObject>, + _change: Option<&NSDictionary>, + _context: *mut c_void, + ) { + if let (Some(key_path), Some(object)) = (key_path, of_object) { + if key_path.to_string() == "title" { + unsafe { + let handler = &self.ivars().handler; + // if !handler.is_null() { + let title: *const NSString = msg_send![object, title]; + handler((*title).to_string()); + // } + } + } + } + } + } + + unsafe impl NSObjectProtocol for DocumentTitleChangedObserver {} +); + +impl DocumentTitleChangedObserver { + pub fn new(webview: Retained, handler: Box) -> Retained { + let observer = Self::alloc().set_ivars(DocumentTitleChangedObserverIvars { + object: webview, + handler, + }); + + let observer: Retained = unsafe { msg_send_id![super(observer), init] }; + + unsafe { + observer + .ivars() + .object + .addObserver_forKeyPath_options_context( + &observer, + &NSString::from_str("title"), + NSKeyValueObservingOptions::NSKeyValueObservingOptionNew, + null_mut(), + ); + } + + observer + } +} + +impl Drop for DocumentTitleChangedObserver { + fn drop(&mut self) { + unsafe { + self + .ivars() + .object + .removeObserver_forKeyPath(self, &NSString::from_str("title")); + } + } +} diff --git a/src/wkwebview/class/mod.rs b/src/wkwebview/class/mod.rs new file mode 100644 index 000000000..22bd404ef --- /dev/null +++ b/src/wkwebview/class/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +pub mod document_title_changed_observer; +pub mod url_scheme_handler; +pub mod wry_download_delegate; +pub mod wry_navigation_delegate; +pub mod wry_web_view; +pub mod wry_web_view_delegate; +pub mod wry_web_view_parent; +pub mod wry_web_view_ui_delegate; diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs new file mode 100644 index 000000000..b0aaf8937 --- /dev/null +++ b/src/wkwebview/class/url_scheme_handler.rs @@ -0,0 +1,304 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{ + borrow::Cow, + ffi::{c_char, c_void, CStr}, + panic::AssertUnwindSafe, + ptr::NonNull, + slice, +}; + +use http::{ + header::{CONTENT_LENGTH, CONTENT_TYPE}, + Request, Response as HttpResponse, StatusCode, Version, +}; +use objc2::{ + rc::Retained, + runtime::{AnyClass, AnyObject, ClassBuilder, ProtocolObject}, + ClassType, +}; +use objc2_foundation::{ + NSData, NSHTTPURLResponse, NSMutableDictionary, NSObject, NSObjectProtocol, NSString, NSURL, + NSUUID, +}; +use objc2_web_kit::{WKURLSchemeHandler, WKURLSchemeTask}; + +use crate::{wkwebview::WEBVIEW_IDS, RequestAsyncResponder, WryWebView}; + +pub fn create(name: &str) -> &AnyClass { + unsafe { + let scheme_name = format!("{}URLSchemeHandler", name); + let cls = ClassBuilder::new(&scheme_name, NSObject::class()); + match cls { + Some(mut cls) => { + cls.add_ivar::<*mut c_void>("function"); + cls.add_ivar::<*mut c_char>("webview_id"); + cls.add_method( + objc2::sel!(webView:startURLSchemeTask:), + start_task as extern "C" fn(_, _, _, _), + ); + cls.add_method( + objc2::sel!(webView:stopURLSchemeTask:), + stop_task as extern "C" fn(_, _, _, _), + ); + cls.register() + } + None => AnyClass::get(&scheme_name).expect("Failed to get the class definition"), + } + } +} + +// Task handler for custom protocol +extern "C" fn start_task( + this: &AnyObject, + _sel: objc2::runtime::Sel, + webview: &'static mut WryWebView, + task: &'static ProtocolObject, +) { + unsafe { + #[cfg(feature = "tracing")] + let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty) + .entered(); + + let task_key = task.hash(); // hash by task object address + let task_uuid = webview.add_custom_task_key(task_key); + + let ivar = this.class().instance_variable("webview_id").unwrap(); + let webview_id_ptr: *mut c_char = *ivar.load(this); + let webview_id = CStr::from_ptr(webview_id_ptr) + .to_str() + .ok() + .unwrap_or_default(); + + let ivar = this.class().instance_variable("function").unwrap(); + let function: &*mut c_void = ivar.load(this); + if !function.is_null() { + let function = &mut *(*function + as *mut Box>, RequestAsyncResponder)>); + + // Get url request + let request = task.request(); + let url = request.URL().unwrap(); + + let uri = url.absoluteString().unwrap().to_string(); + + #[cfg(feature = "tracing")] + span.record("uri", uri.clone()); + + // Get request method (GET, POST, PUT etc...) + let method = request.HTTPMethod().unwrap().to_string(); + + // Prepare our HttpRequest + let mut http_request = Request::builder().uri(uri).method(method.as_str()); + + // Get body + let mut sent_form_body = Vec::new(); + let body = request.HTTPBody(); + let body_stream = request.HTTPBodyStream(); + if let Some(body) = body { + let length = body.length(); + let data_bytes = body.bytes(); + sent_form_body = slice::from_raw_parts(data_bytes.as_ptr(), length).to_vec(); + } else if let Some(body_stream) = body_stream { + body_stream.open(); + + while body_stream.hasBytesAvailable() { + sent_form_body.reserve(128); + let p = sent_form_body.as_mut_ptr().add(sent_form_body.len()); + let read_length = sent_form_body.capacity() - sent_form_body.len(); + let count = body_stream.read_maxLength(NonNull::new(p).unwrap(), read_length); + sent_form_body.set_len(sent_form_body.len() + count as usize); + } + + body_stream.close(); + } + + // Extract all headers fields + let all_headers = request.allHTTPHeaderFields(); + + // get all our headers values and inject them in our request + if let Some(all_headers) = all_headers { + for current_header in all_headers.allKeys().to_vec() { + let header_value = all_headers.valueForKey(current_header).unwrap(); + + // inject the header into the request + http_request = http_request.header(current_header.to_string(), header_value.to_string()); + } + } + + let respond_with_404 = || { + let urlresponse = NSHTTPURLResponse::alloc(); + let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields( + urlresponse, + &url, + StatusCode::NOT_FOUND.as_u16().try_into().unwrap(), + Some(&NSString::from_str( + format!("{:#?}", Version::HTTP_11).as_str(), + )), + None, + ) + .unwrap(); + task.didReceiveResponse(&response); + // Finish + task.didFinish(); + }; + + // send response + match http_request.body(sent_form_body) { + Ok(final_request) => { + let responder: Box>)> = + Box::new(move |sent_response| { + fn check_webview_id_valid(webview_id: &str) -> crate::Result<()> { + if !WEBVIEW_IDS.lock().unwrap().contains(webview_id) { + return Err(crate::Error::CustomProtocolTaskInvalid); + } + Ok(()) + } + /// Task may not live longer than async custom protocol handler. + /// + /// There are roughly 2 ways to cause segfault: + /// 1. Task has stopped. pointer of the task not valid anymore. + /// 2. Task had stopped, but the pointer of the task has allocated to a new task. + /// Outdated custom handler may call to the new task instance and cause segfault. + fn check_task_is_valid( + webview: &WryWebView, + task_key: usize, + current_uuid: Retained, + ) -> crate::Result<()> { + let latest_task_uuid = webview.get_custom_task_uuid(task_key); + if let Some(latest_uuid) = latest_task_uuid { + if latest_uuid != current_uuid { + return Err(crate::Error::CustomProtocolTaskInvalid); + } + } else { + return Err(crate::Error::CustomProtocolTaskInvalid); + } + Ok(()) + } + + unsafe fn response( + // FIXME: though we give it a static lifetime, it's not guaranteed to be valid. + task: &'static ProtocolObject, + // FIXME: though we give it a static lifetime, it's not guaranteed to be valid. + webview: &'static mut WryWebView, + task_key: usize, + task_uuid: Retained, + webview_id: &str, + url: Retained, + sent_response: HttpResponse>, + ) -> crate::Result<()> { + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + + let content = sent_response.body(); + // default: application/octet-stream, but should be provided by the client + let wanted_mime = sent_response.headers().get(CONTENT_TYPE); + // default to 200 + let wanted_status_code = sent_response.status().as_u16() as i32; + // default to HTTP/1.1 + let wanted_version = format!("{:#?}", sent_response.version()); + + let mut headers = NSMutableDictionary::new(); + + if let Some(mime) = wanted_mime { + headers.insert_id( + NSString::from_str(mime.to_str().unwrap()).as_ref(), + NSString::from_str(CONTENT_TYPE.as_str()), + ); + } + headers.insert_id( + NSString::from_str(&content.len().to_string()).as_ref(), + NSString::from_str(CONTENT_LENGTH.as_str()), + ); + + // add headers + for (name, value) in sent_response.headers().iter() { + if let Ok(value) = value.to_str() { + headers.insert_id( + NSString::from_str(name.as_str()).as_ref(), + NSString::from_str(value), + ); + } + } + + let urlresponse = NSHTTPURLResponse::alloc(); + let response = NSHTTPURLResponse::initWithURL_statusCode_HTTPVersion_headerFields( + urlresponse, + &url, + wanted_status_code.try_into().unwrap(), + Some(&NSString::from_str(&wanted_version)), + Some(&headers), + ) + .unwrap(); + + check_webview_id_valid(webview_id)?; + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + + objc2::exception::catch(AssertUnwindSafe(|| { + task.didReceiveResponse(&response); + })) + .unwrap(); + + // Send data + let bytes = content.as_ptr() as *mut c_void; + let data = NSData::alloc(); + // MIGRATE NOTE: we copied the content to the NSData because content will be freed + // when out of scope but NSData will also free the content when it's done and cause doube free. + let data = NSData::initWithBytes_length(data, bytes, content.len()); + check_webview_id_valid(webview_id)?; + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + objc2::exception::catch(AssertUnwindSafe(|| { + task.didReceiveData(&data); + })) + .unwrap(); + + // Finish + check_webview_id_valid(webview_id)?; + check_task_is_valid(&*webview, task_key, task_uuid.clone())?; + objc2::exception::catch(AssertUnwindSafe(|| { + task.didFinish(); + })) + .unwrap(); + + webview.remove_custom_task_key(task_key); + Ok(()) + } + + let _ = response( + task, + webview, + task_key, + task_uuid, + webview_id, + url.clone(), + sent_response, + ); + }); + + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); + function( + webview_id, + final_request, + RequestAsyncResponder { responder }, + ); + } + Err(_) => respond_with_404(), + }; + } else { + #[cfg(feature = "tracing")] + tracing::warn!( + "Either WebView or WebContext instance is dropped! This handler shouldn't be called." + ); + } + } +} +extern "C" fn stop_task( + _this: &ProtocolObject, + _sel: objc2::runtime::Sel, + webview: &mut WryWebView, + task: &ProtocolObject, +) { + webview.remove_custom_task_key(task.hash()); +} diff --git a/src/wkwebview/class/wry_download_delegate.rs b/src/wkwebview/class/wry_download_delegate.rs new file mode 100644 index 000000000..5521b6509 --- /dev/null +++ b/src/wkwebview/class/wry_download_delegate.rs @@ -0,0 +1,82 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject, + ClassType, DeclaredClass, +}; +use objc2_foundation::{ + MainThreadMarker, NSData, NSError, NSObjectProtocol, NSString, NSURLResponse, NSURL, +}; +use objc2_web_kit::{WKDownload, WKDownloadDelegate}; + +use crate::wkwebview::download::{download_did_fail, download_did_finish, download_policy}; + +pub struct WryDownloadDelegateIvars { + pub started: Option bool + 'static>>>, + pub completed: Option, bool) + 'static>>, +} + +declare_class!( + pub struct WryDownloadDelegate; + + unsafe impl ClassType for WryDownloadDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryDownloadDelegate"; + } + + impl DeclaredClass for WryDownloadDelegate { + type Ivars = WryDownloadDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryDownloadDelegate {} + + unsafe impl WKDownloadDelegate for WryDownloadDelegate { + #[method(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:)] + fn download_policy( + &self, + download: &WKDownload, + response: &NSURLResponse, + suggested_path: &NSString, + handler: &block2::Block, + ) { + download_policy(self, download, response, suggested_path, handler); + } + + #[method(downloadDidFinish:)] + fn download_did_finish(&self, download: &WKDownload) { + download_did_finish(self, download); + } + + #[method(download:didFailWithError:resumeData:)] + fn download_did_fail( + &self, + download: &WKDownload, + error: &NSError, + resume_data: &NSData, + ) { + download_did_fail(self, download, error, resume_data); + } + } +); + +impl WryDownloadDelegate { + pub fn new( + download_started_handler: Option bool + 'static>>, + download_completed_handler: Option, bool) + 'static>>, + mtm: MainThreadMarker, + ) -> Retained { + let delegate = mtm + .alloc::() + .set_ivars(WryDownloadDelegateIvars { + started: download_started_handler.map(|handler| RefCell::new(handler)), + completed: download_completed_handler, + }); + + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/src/wkwebview/class/wry_navigation_delegate.rs b/src/wkwebview/class/wry_navigation_delegate.rs new file mode 100644 index 000000000..82cc67a74 --- /dev/null +++ b/src/wkwebview/class/wry_navigation_delegate.rs @@ -0,0 +1,164 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::sync::{Arc, Mutex}; + +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject, + ClassType, DeclaredClass, +}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; +use objc2_web_kit::{ + WKDownload, WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationDelegate, + WKNavigationResponse, WKNavigationResponsePolicy, +}; + +#[cfg(target_os = "ios")] +use crate::wkwebview::ios::WKWebView::WKWebView; +#[cfg(target_os = "macos")] +use objc2_web_kit::WKWebView; + +use crate::{ + url_from_webview, + wkwebview::{ + download::{navigation_download_action, navigation_download_response}, + navigation::{ + did_commit_navigation, did_finish_navigation, navigation_policy, navigation_policy_response, + }, + }, + PageLoadEvent, WryWebView, +}; + +use super::wry_download_delegate::WryDownloadDelegate; + +pub struct WryNavigationDelegateIvars { + pub pending_scripts: Arc>>>, + pub has_download_handler: bool, + pub navigation_policy_function: Box bool>, + pub download_delegate: Option>, + pub on_page_load_handler: Option>, +} + +declare_class!( + pub struct WryNavigationDelegate; + + unsafe impl ClassType for WryNavigationDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryNavigationDelegate"; + } + + impl DeclaredClass for WryNavigationDelegate { + type Ivars = WryNavigationDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryNavigationDelegate {} + + unsafe impl WKNavigationDelegate for WryNavigationDelegate { + #[method(webView:decidePolicyForNavigationAction:decisionHandler:)] + fn navigation_policy( + &self, + webview: &WKWebView, + action: &WKNavigationAction, + handler: &block2::Block, + ) { + navigation_policy(self, webview, action, handler); + } + + #[method(webView:decidePolicyForNavigationResponse:decisionHandler:)] + fn navigation_policy_response( + &self, + webview: &WKWebView, + response: &WKNavigationResponse, + handler: &block2::Block, + ) { + navigation_policy_response(self, webview, response, handler); + } + + #[method(webView:didFinishNavigation:)] + fn did_finish_navigation( + &self, + webview: &WKWebView, + navigation: &WKNavigation, + ) { + did_finish_navigation(self, webview, navigation); + } + + #[method(webView:didCommitNavigation:)] + fn did_commit_navigation( + &self, + webview: &WKWebView, + navigation: &WKNavigation, + ) { + did_commit_navigation(self, webview, navigation); + } + + #[method(webView:navigationAction:didBecomeDownload:)] + fn navigation_download_action( + &self, + webview: &WKWebView, + action: &WKNavigationAction, + download: &WKDownload, + ) { + navigation_download_action(self, webview, action, download); + } + + #[method(webView:navigationResponse:didBecomeDownload:)] + fn navigation_download_response( + &self, + webview: &WKWebView, + response: &WKNavigationResponse, + download: &WKDownload, + ) { + navigation_download_response(self, webview, response, download); + } + } +); + +impl WryNavigationDelegate { + #[allow(clippy::too_many_arguments)] + pub fn new( + webview: Retained, + pending_scripts: Arc>>>, + has_download_handler: bool, + navigation_handler: Option bool>>, + new_window_req_handler: Option bool>>, + download_delegate: Option>, + on_page_load_handler: Option>, + mtm: MainThreadMarker, + ) -> Retained { + let navigation_policy_function = Box::new(move |url: String, is_main_frame: bool| -> bool { + if is_main_frame { + navigation_handler + .as_ref() + .map_or(true, |navigation_handler| (navigation_handler)(url)) + } else { + new_window_req_handler + .as_ref() + .map_or(true, |new_window_req_handler| (new_window_req_handler)(url)) + } + }); + + let on_page_load_handler = if let Some(handler) = on_page_load_handler { + let custom_handler = Box::new(move |event| { + handler(event, url_from_webview(&webview).unwrap_or_default()); + }) as Box; + Some(custom_handler) + } else { + None + }; + + let delegate = mtm + .alloc::() + .set_ivars(WryNavigationDelegateIvars { + pending_scripts, + navigation_policy_function, + has_download_handler, + download_delegate, + on_page_load_handler, + }); + + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/src/wkwebview/class/wry_web_view.rs b/src/wkwebview/class/wry_web_view.rs new file mode 100644 index 000000000..0ebd7e762 --- /dev/null +++ b/src/wkwebview/class/wry_web_view.rs @@ -0,0 +1,154 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; + +#[cfg(target_os = "macos")] +use objc2::runtime::ProtocolObject; +use objc2::{ + declare_class, mutability::MainThreadOnly, rc::Retained, runtime::Bool, ClassType, DeclaredClass, +}; +#[cfg(target_os = "macos")] +use objc2_app_kit::{NSDraggingDestination, NSEvent}; +use objc2_foundation::{NSObjectProtocol, NSUUID}; + +#[cfg(target_os = "ios")] +use crate::wkwebview::ios::WKWebView::WKWebView; +#[cfg(target_os = "macos")] +use crate::{ + wkwebview::{drag_drop, synthetic_mouse_events}, + DragDropEvent, +}; +#[cfg(target_os = "ios")] +use objc2_ui_kit::UIEvent as NSEvent; +#[cfg(target_os = "macos")] +use objc2_web_kit::WKWebView; + +pub struct WryWebViewIvars { + pub(crate) is_child: bool, + #[cfg(target_os = "macos")] + pub(crate) drag_drop_handler: Box bool>, + #[cfg(target_os = "macos")] + pub(crate) accept_first_mouse: objc2::runtime::Bool, + pub(crate) custom_protocol_task_ids: HashMap>, +} + +declare_class!( + pub struct WryWebView; + + unsafe impl ClassType for WryWebView { + type Super = WKWebView; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebView"; + } + + impl DeclaredClass for WryWebView { + type Ivars = WryWebViewIvars; + } + + unsafe impl WryWebView { + #[method(performKeyEquivalent:)] + fn perform_key_equivalent( + &self, + event: &NSEvent, + ) -> Bool { + // This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426 + // FIXME: When the webview is a child webview, performKeyEquivalent always return YES + // and stop propagating the event to the window, hence the menu shortcut won't be + // triggered. However, overriding this method also means the cmd+key event won't be + // handled in webview, which means the key cannot be listened by JavaScript. + if self.ivars().is_child { + Bool::NO + } else { + unsafe { + objc2::msg_send![super(self), performKeyEquivalent: event] + } + } + } + + #[cfg(target_os = "macos")] + #[method(acceptsFirstMouse:)] + fn accept_first_mouse( + &self, + _event: &NSEvent, + ) -> Bool { + self.ivars().accept_first_mouse + } + } + unsafe impl NSObjectProtocol for WryWebView {} + + // Drag & Drop + #[cfg(target_os = "macos")] + unsafe impl NSDraggingDestination for WryWebView { + #[method(draggingEntered:)] + fn dragging_entered( + &self, + drag_info: &ProtocolObject, + ) -> objc2_app_kit::NSDragOperation { + drag_drop::dragging_entered(self, drag_info) + } + + #[method(draggingUpdated:)] + fn dragging_updated( + &self, + drag_info: &ProtocolObject, + ) -> objc2_app_kit::NSDragOperation { + drag_drop::dragging_updated(self, drag_info) + } + + #[method(performDragOperation:)] + fn perform_drag_operation( + &self, + drag_info: &ProtocolObject, + ) -> Bool { + drag_drop::perform_drag_operation(self, drag_info) + } + + #[method(draggingExited:)] + fn dragging_exited( + &self, + drag_info: &ProtocolObject, + ) { + drag_drop::dragging_exited(self, drag_info) + } + } + + // Synthetic mouse events + #[cfg(target_os = "macos")] + unsafe impl WryWebView { + #[method(otherMouseDown:)] + fn other_mouse_down( + &self, + event: &NSEvent, + ) { + synthetic_mouse_events::other_mouse_down(self, event) + } + + #[method(otherMouseUp:)] + fn other_mouse_up( + &self, + event: &NSEvent, + ) { + synthetic_mouse_events::other_mouse_up(self, event) + } + } +); + +// Custom Protocol Task Checker +impl WryWebView { + pub(crate) fn add_custom_task_key(&mut self, task_id: usize) -> Retained { + let task_uuid = NSUUID::new(); + self + .ivars_mut() + .custom_protocol_task_ids + .insert(task_id, task_uuid.clone()); + task_uuid + } + pub(crate) fn remove_custom_task_key(&mut self, task_id: usize) { + self.ivars_mut().custom_protocol_task_ids.remove(&task_id); + } + pub(crate) fn get_custom_task_uuid(&self, task_id: usize) -> Option> { + self.ivars().custom_protocol_task_ids.get(&task_id).cloned() + } +} diff --git a/src/wkwebview/class/wry_web_view_delegate.rs b/src/wkwebview/class/wry_web_view_delegate.rs new file mode 100644 index 000000000..d49fe00e5 --- /dev/null +++ b/src/wkwebview/class/wry_web_view_delegate.rs @@ -0,0 +1,108 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::ffi::CStr; + +use http::Request; +use objc2::{ + declare_class, msg_send_id, + mutability::MainThreadOnly, + rc::Retained, + runtime::{NSObject, ProtocolObject}, + ClassType, DeclaredClass, +}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol, NSString}; +use objc2_web_kit::{WKScriptMessage, WKScriptMessageHandler, WKUserContentController}; + +pub const IPC_MESSAGE_HANDLER_NAME: &str = "ipc"; + +pub struct WryWebViewDelegateIvars { + pub controller: Retained, + pub ipc_handler: Box)>, +} + +declare_class!( + pub struct WryWebViewDelegate; + + unsafe impl ClassType for WryWebViewDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebViewDelegate"; + } + + impl DeclaredClass for WryWebViewDelegate { + type Ivars = WryWebViewDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryWebViewDelegate {} + + unsafe impl WKScriptMessageHandler for WryWebViewDelegate { + // Function for ipc handler + #[method(userContentController:didReceiveScriptMessage:)] + fn did_receive( + this: &WryWebViewDelegate, + _controller: &WKUserContentController, + msg: &WKScriptMessage, + ) { + // Safety: objc runtime calls are unsafe + unsafe { + #[cfg(feature = "tracing")] + let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered(); + + let ipc_handler = &this.ivars().ipc_handler; + let body = msg.body(); + let is_string = Retained::cast::(body.clone()).isKindOfClass(NSString::class()); + if is_string { + let body = Retained::cast::(body); + let js_utf8 = body.UTF8String(); + + let frame_info = msg.frameInfo(); + let request = frame_info.request(); + let url = request.URL().unwrap(); + let absolute_url = url.absoluteString().unwrap(); + let url_utf8 = absolute_url.UTF8String(); + + if let (Ok(url), Ok(js)) = ( + CStr::from_ptr(url_utf8).to_str(), + CStr::from_ptr(js_utf8).to_str(), + ) { + ipc_handler(Request::builder().uri(url).body(js.to_string()).unwrap()); + return; + } + } + + #[cfg(feature = "tracing")] + tracing::warn!("WebView received invalid IPC call."); + } + } + } +); + +impl WryWebViewDelegate { + pub fn new( + controller: Retained, + ipc_handler: Box)>, + mtm: MainThreadMarker, + ) -> Retained { + let delegate = mtm + .alloc::() + .set_ivars(WryWebViewDelegateIvars { + ipc_handler, + controller, + }); + + let delegate: Retained = unsafe { msg_send_id![super(delegate), init] }; + + let proto_delegate = ProtocolObject::from_ref(delegate.as_ref()); + unsafe { + // this will increate the retain count of the delegate + delegate.ivars().controller.addScriptMessageHandler_name( + proto_delegate, + &NSString::from_str(IPC_MESSAGE_HANDLER_NAME), + ); + } + + delegate + } +} diff --git a/src/wkwebview/class/wry_web_view_parent.rs b/src/wkwebview/class/wry_web_view_parent.rs new file mode 100644 index 000000000..95ef05c50 --- /dev/null +++ b/src/wkwebview/class/wry_web_view_parent.rs @@ -0,0 +1,55 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, ClassType, DeclaredClass, +}; +#[cfg(target_os = "macos")] +use objc2_app_kit::{NSApplication, NSEvent, NSView}; +use objc2_foundation::MainThreadMarker; +#[cfg(target_os = "ios")] +use objc2_ui_kit::UIView as NSView; + +pub struct WryWebViewParentIvars {} + +declare_class!( + pub struct WryWebViewParent; + + unsafe impl ClassType for WryWebViewParent { + type Super = NSView; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebViewParent"; + } + + impl DeclaredClass for WryWebViewParent { + type Ivars = WryWebViewParentIvars; + } + + unsafe impl WryWebViewParent { + #[cfg(target_os = "macos")] + #[method(keyDown:)] + fn key_down( + &self, + event: &NSEvent, + ) { + let mtm = MainThreadMarker::new().unwrap(); + let app = NSApplication::sharedApplication(mtm); + unsafe { + if let Some(menu) = app.mainMenu() { + menu.performKeyEquivalent(event); + } + } + } + } +); + +impl WryWebViewParent { + #[allow(dead_code)] + pub fn new(mtm: MainThreadMarker) -> Retained { + let delegate = mtm + .alloc::() + .set_ivars(WryWebViewParentIvars {}); + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/src/wkwebview/class/wry_web_view_ui_delegate.rs b/src/wkwebview/class/wry_web_view_ui_delegate.rs new file mode 100644 index 000000000..d436a29a4 --- /dev/null +++ b/src/wkwebview/class/wry_web_view_ui_delegate.rs @@ -0,0 +1,95 @@ +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +#[cfg(target_os = "macos")] +use std::ptr::null_mut; + +use block2::Block; +use objc2::{ + declare_class, msg_send_id, mutability::MainThreadOnly, rc::Retained, runtime::NSObject, + ClassType, DeclaredClass, +}; +#[cfg(target_os = "macos")] +use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSOpenPanel}; +use objc2_foundation::{MainThreadMarker, NSObjectProtocol}; +#[cfg(target_os = "macos")] +use objc2_foundation::{NSArray, NSURL}; + +#[cfg(target_os = "macos")] +use objc2_web_kit::WKOpenPanelParameters; +use objc2_web_kit::{ + WKFrameInfo, WKMediaCaptureType, WKPermissionDecision, WKSecurityOrigin, WKUIDelegate, +}; + +use crate::WryWebView; + +pub struct WryWebViewUIDelegateIvars {} + +declare_class!( + pub struct WryWebViewUIDelegate; + + unsafe impl ClassType for WryWebViewUIDelegate { + type Super = NSObject; + type Mutability = MainThreadOnly; + const NAME: &'static str = "WryWebViewUIDelegate"; + } + + impl DeclaredClass for WryWebViewUIDelegate { + type Ivars = WryWebViewUIDelegateIvars; + } + + unsafe impl NSObjectProtocol for WryWebViewUIDelegate {} + + unsafe impl WKUIDelegate for WryWebViewUIDelegate { + #[cfg(target_os = "macos")] + #[method(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:)] + fn run_file_upload_panel( + &self, + _webview: &WryWebView, + open_panel_params: &WKOpenPanelParameters, + _frame: &WKFrameInfo, + handler: &block2::Block)> + ) { + unsafe { + if let Some(mtm) = MainThreadMarker::new() { + let open_panel = NSOpenPanel::openPanel(mtm); + open_panel.setCanChooseFiles(true); + let allow_multi = open_panel_params.allowsMultipleSelection(); + open_panel.setAllowsMultipleSelection(allow_multi); + let allow_dir = open_panel_params.allowsDirectories(); + open_panel.setCanChooseDirectories(allow_dir); + let ok: NSModalResponse = open_panel.runModal(); + if ok == NSModalResponseOK { + let url = open_panel.URLs(); + (*handler).call((Retained::as_ptr(&url),)); + } else { + (*handler).call((null_mut(),)); + } + } + } + } + + #[method(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:)] + fn request_media_capture_permission( + &self, + _webview: &WryWebView, + _origin: &WKSecurityOrigin, + _frame: &WKFrameInfo, + _capture_type: WKMediaCaptureType, + decision_handler: &Block + ) { + //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc + (*decision_handler).call((WKPermissionDecision::Grant,)); + } + } +); + +impl WryWebViewUIDelegate { + pub fn new(mtm: MainThreadMarker) -> Retained { + let delegate = mtm + .alloc::() + .set_ivars(WryWebViewUIDelegateIvars {}); + unsafe { msg_send_id![super(delegate), init] } + } +} diff --git a/src/wkwebview/download.rs b/src/wkwebview/download.rs index 4752e0b05..0f6724a62 100644 --- a/src/wkwebview/download.rs +++ b/src/wkwebview/download.rs @@ -1,123 +1,106 @@ -use std::{path::PathBuf, ptr::null_mut, rc::Rc}; +use std::{path::PathBuf, ptr::null_mut}; -use cocoa::base::id; -use objc::{ - declare::ClassDecl, - runtime::{Object, Sel}, -}; -use std::ffi::c_void; - -use super::NSString; +use objc2::{rc::Retained, runtime::ProtocolObject, DeclaredClass}; +use objc2_foundation::{NSData, NSError, NSString, NSURLResponse, NSURL}; +use objc2_web_kit::{WKDownload, WKNavigationAction, WKNavigationResponse}; -pub(crate) unsafe fn set_download_delegate(webview: *mut Object, download_delegate: *mut Object) { - (*webview).set_ivar( - "DownloadDelegate", - download_delegate as *mut _ as *mut c_void, - ); -} +#[cfg(target_os = "ios")] +use crate::wkwebview::ios::WKWebView::WKWebView; +#[cfg(target_os = "macos")] +use objc2_web_kit::WKWebView; -unsafe fn get_download_delegate(this: &mut Object) -> *mut objc::runtime::Object { - let delegate: *mut c_void = *this.get_ivar("DownloadDelegate"); - delegate as *mut Object -} +use super::class::{ + wry_download_delegate::WryDownloadDelegate, wry_navigation_delegate::WryNavigationDelegate, +}; // Download action handler -extern "C" fn navigation_download_action(this: &mut Object, _: Sel, _: id, _: id, download: id) { +pub(crate) fn navigation_download_action( + this: &WryNavigationDelegate, + _webview: &WKWebView, + _action: &WKNavigationAction, + download: &WKDownload, +) { unsafe { - let delegate = get_download_delegate(this); - let _: () = msg_send![download, setDelegate: delegate]; + if let Some(delegate) = &this.ivars().download_delegate { + let proto_delegate = ProtocolObject::from_ref(delegate.as_ref()); + download.setDelegate(Some(proto_delegate)); + } } } // Download response handler -extern "C" fn navigation_download_response(this: &mut Object, _: Sel, _: id, _: id, download: id) { +pub(crate) fn navigation_download_response( + this: &WryNavigationDelegate, + _webview: &WKWebView, + _response: &WKNavigationResponse, + download: &WKDownload, +) { unsafe { - let delegate = get_download_delegate(this); - let _: () = msg_send![download, setDelegate: delegate]; + if let Some(delegate) = &this.ivars().download_delegate { + let proto_delegate = ProtocolObject::from_ref(delegate.as_ref()); + download.setDelegate(Some(proto_delegate)); + } } } -pub(crate) unsafe fn add_download_methods(decl: &mut ClassDecl) { - decl.add_ivar::<*mut c_void>("DownloadDelegate"); - - decl.add_method( - sel!(webView:navigationAction:didBecomeDownload:), - navigation_download_action as extern "C" fn(&mut Object, Sel, id, id, id), - ); - - decl.add_method( - sel!(webView:navigationResponse:didBecomeDownload:), - navigation_download_response as extern "C" fn(&mut Object, Sel, id, id, id), - ); -} - -pub extern "C" fn download_policy( - this: &Object, - _: Sel, - download: id, - _: id, - suggested_path: id, - handler: id, +pub(crate) fn download_policy( + this: &WryDownloadDelegate, + download: &WKDownload, + _response: &NSURLResponse, + suggested_path: &NSString, + completion_handler: &block2::Block, ) { unsafe { - let request: id = msg_send![download, originalRequest]; - let url: id = msg_send![request, URL]; - let url: id = msg_send![url, absoluteString]; - let url = NSString(url); - let path = NSString(suggested_path); - let mut path = PathBuf::from(path.to_str()); - let handler = handler as *mut block::Block<(id,), c_void>; + let request = download.originalRequest().unwrap(); + let url = request.URL().unwrap().absoluteString().unwrap(); + let mut path = PathBuf::from(suggested_path.to_string()); - let function = this.get_ivar::<*mut c_void>("started"); - if !function.is_null() { - let function = &mut *(*function as *mut Box FnMut(String, &mut PathBuf) -> bool>); - match (function)(url.to_str().to_string(), &mut path) { + let started_fn = &this.ivars().started; + if let Some(started_fn) = started_fn { + let mut started_fn = started_fn.borrow_mut(); + match started_fn(url.to_string().to_string(), &mut path) { true => { - let nsurl: id = msg_send![class!(NSURL), fileURLWithPath: NSString::new(&path.display().to_string()) isDirectory: false]; - (*handler).call((nsurl,)) + let path = NSString::from_str(&path.display().to_string()); + let ns_url = NSURL::fileURLWithPath_isDirectory(&path, false); + (*completion_handler).call((Retained::as_ptr(&ns_url),)) } - false => (*handler).call((null_mut(),)), + false => (*completion_handler).call((null_mut(),)), }; } else { #[cfg(feature = "tracing")] tracing::warn!("WebView instance is dropped! This navigation handler shouldn't be called."); - (*handler).call((null_mut(),)); + (*completion_handler).call((null_mut(),)); } } } -pub extern "C" fn download_did_finish(this: &Object, _: Sel, download: id) { +pub(crate) fn download_did_finish(this: &WryDownloadDelegate, download: &WKDownload) { unsafe { - let function = this.get_ivar::<*mut c_void>("completed"); - let original_request: id = msg_send![download, originalRequest]; - let url: id = msg_send![original_request, URL]; - let url: id = msg_send![url, absoluteString]; - let url = NSString(url).to_str().to_string(); - if !function.is_null() { - let function = &mut *(*function as *mut Rc Fn(String, Option, bool)>); - function(url, None, true); + let original_request = download.originalRequest().unwrap(); + let url = original_request.URL().unwrap().absoluteString().unwrap(); + if let Some(completed_fn) = this.ivars().completed.clone() { + completed_fn(url.to_string(), None, true); } } } -pub extern "C" fn download_did_fail(this: &Object, _: Sel, download: id, _error: id, _: id) { +pub(crate) fn download_did_fail( + this: &WryDownloadDelegate, + download: &WKDownload, + error: &NSError, + _resume_data: &NSData, +) { unsafe { #[cfg(debug_assertions)] { - let description: id = msg_send![_error, localizedDescription]; - let description = NSString(description).to_str().to_string(); + let description = error.localizedDescription().to_string(); eprintln!("Download failed with error: {}", description); } - let original_request: id = msg_send![download, originalRequest]; - let url: id = msg_send![original_request, URL]; - let url: id = msg_send![url, absoluteString]; - let url = NSString(url).to_str().to_string(); - - let function = this.get_ivar::<*mut c_void>("completed"); - if !function.is_null() { - let function = &mut *(*function as *mut Rc Fn(String, Option, bool)>); - function(url, None, false); + let original_request = download.originalRequest().unwrap(); + let url = original_request.URL().unwrap().absoluteString().unwrap(); + if let Some(completed_fn) = this.ivars().completed.clone() { + completed_fn(url.to_string(), None, false); } } } diff --git a/src/wkwebview/drag_drop.rs b/src/wkwebview/drag_drop.rs index 948d83df7..18ddaa3b1 100644 --- a/src/wkwebview/drag_drop.rs +++ b/src/wkwebview/drag_drop.rs @@ -2,179 +2,103 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{ - ffi::{c_void, CStr}, - path::PathBuf, -}; +use std::{ffi::CStr, path::PathBuf}; -use cocoa::{ - base::{id, BOOL, YES}, - foundation::{NSPoint, NSRect}, -}; -use objc::{ - declare::ClassDecl, - runtime::{class_getInstanceMethod, method_getImplementation, Object, Sel}, +use objc2::{ + rc::Id, + runtime::{AnyObject, Bool, ProtocolObject}, + DeclaredClass, }; -use once_cell::sync::Lazy; +use objc2_app_kit::{NSDragOperation, NSDraggingInfo, NSFilenamesPboardType}; +use objc2_foundation::{NSArray, NSPoint, NSRect, NSString}; use crate::DragDropEvent; -pub(crate) type NSDragOperation = cocoa::foundation::NSUInteger; - -#[allow(non_upper_case_globals)] -const NSDragOperationCopy: NSDragOperation = 1; - -const DRAG_DROP_HANDLER_IVAR: &str = "DragDropHandler"; - -static OBJC_DRAGGING_ENTERED: Lazy NSDragOperation> = - Lazy::new(|| unsafe { - std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(draggingEntered:), - ))) - }); - -static OBJC_DRAGGING_EXITED: Lazy = Lazy::new(|| unsafe { - std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(draggingExited:), - ))) -}); - -static OBJC_PERFORM_DRAG_OPERATION: Lazy BOOL> = - Lazy::new(|| unsafe { - std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(performDragOperation:), - ))) - }); - -static OBJC_DRAGGING_UPDATED: Lazy NSDragOperation> = - Lazy::new(|| unsafe { - std::mem::transmute(method_getImplementation(class_getInstanceMethod( - class!(WKWebView), - sel!(draggingUpdated:), - ))) - }); +use super::WryWebView; -// Safety: objc runtime calls are unsafe -pub(crate) unsafe fn set_drag_drop_handler( - webview: *mut Object, - handler: Box bool>, -) -> *mut Box bool> { - let listener = Box::into_raw(Box::new(handler)); - (*webview).set_ivar(DRAG_DROP_HANDLER_IVAR, listener as *mut _ as *mut c_void); - listener -} - -#[allow(clippy::mut_from_ref)] -unsafe fn get_handler(this: &Object) -> &mut Box bool> { - let delegate: *mut c_void = *this.get_ivar(DRAG_DROP_HANDLER_IVAR); - &mut *(delegate as *mut Box bool>) -} - -unsafe fn collect_paths(drag_info: id) -> Vec { - use cocoa::{ - appkit::{NSFilenamesPboardType, NSPasteboard}, - foundation::{NSFastEnumeration, NSString}, - }; - - let pb: id = msg_send![drag_info, draggingPasteboard]; +pub(crate) unsafe fn collect_paths(drag_info: &ProtocolObject) -> Vec { + let pb = drag_info.draggingPasteboard(); let mut drag_drop_paths = Vec::new(); - let types: id = msg_send![class!(NSArray), arrayWithObject: NSFilenamesPboardType]; - if !NSPasteboard::availableTypeFromArray(pb, types).is_null() { - for path in NSPasteboard::propertyListForType(pb, NSFilenamesPboardType).iter() { - drag_drop_paths.push(PathBuf::from( - CStr::from_ptr(NSString::UTF8String(path)) - .to_string_lossy() - .into_owned(), - )); + let types = NSArray::arrayWithObject(NSFilenamesPboardType); + + if pb.availableTypeFromArray(&types).is_some() { + let paths = pb.propertyListForType(NSFilenamesPboardType).unwrap(); + let paths: Id> = Id::::cast(paths.clone()); + for path in paths.to_vec() { + let path = CStr::from_ptr(path.UTF8String()).to_string_lossy(); + drag_drop_paths.push(PathBuf::from(path.into_owned())); } } drag_drop_paths } -extern "C" fn dragging_updated(this: &mut Object, sel: Sel, drag_info: id) -> NSDragOperation { - let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] }; - let frame: NSRect = unsafe { msg_send![this, frame] }; +pub(crate) fn dragging_entered( + this: &WryWebView, + drag_info: &ProtocolObject, +) -> NSDragOperation { + let paths = unsafe { collect_paths(drag_info) }; + let dl: NSPoint = unsafe { drag_info.draggingLocation() }; + let frame: NSRect = this.frame(); let position = (dl.x as i32, (frame.size.height - dl.y) as i32); - let listener = unsafe { get_handler(this) }; - if !listener(DragDropEvent::Over { position }) { - let os_operation = OBJC_DRAGGING_UPDATED(this, sel, drag_info); - if os_operation == 0 { - // 0 will be returned for a drop on any arbitrary location on the webview. - // We'll override that with NSDragOperationCopy. - NSDragOperationCopy - } else { - // A different NSDragOperation is returned when a file is hovered over something like - // a , so we'll make sure to preserve that behaviour. - os_operation - } + + let listener = &this.ivars().drag_drop_handler; + if !listener(DragDropEvent::Enter { paths, position }) { + // Reject the Wry file drop (invoke the OS default behaviour) + unsafe { objc2::msg_send![super(this), draggingEntered: drag_info] } } else { - NSDragOperationCopy + NSDragOperation::Copy } } -extern "C" fn dragging_entered(this: &mut Object, sel: Sel, drag_info: id) -> NSDragOperation { - let listener = unsafe { get_handler(this) }; - let paths = unsafe { collect_paths(drag_info) }; - - let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] }; - let frame: NSRect = unsafe { msg_send![this, frame] }; +pub(crate) fn dragging_updated( + this: &WryWebView, + drag_info: &ProtocolObject, +) -> NSDragOperation { + let dl: NSPoint = unsafe { drag_info.draggingLocation() }; + let frame: NSRect = this.frame(); let position = (dl.x as i32, (frame.size.height - dl.y) as i32); - if !listener(DragDropEvent::Enter { paths, position }) { - // Reject the Wry file drop (invoke the OS default behaviour) - OBJC_DRAGGING_ENTERED(this, sel, drag_info) + let listener = &this.ivars().drag_drop_handler; + if !listener(DragDropEvent::Over { position }) { + unsafe { + let os_operation = objc2::msg_send![super(this), draggingUpdated: drag_info]; + if os_operation == NSDragOperation::None { + // 0 will be returned for a drop on any arbitrary location on the webview. + // We'll override that with NSDragOperationCopy. + NSDragOperation::Copy + } else { + // A different NSDragOperation is returned when a file is hovered over something like + // a , so we'll make sure to preserve that behaviour. + os_operation + } + } } else { - NSDragOperationCopy + NSDragOperation::Copy } } -extern "C" fn perform_drag_operation(this: &mut Object, sel: Sel, drag_info: id) -> BOOL { - let listener = unsafe { get_handler(this) }; +pub(crate) fn perform_drag_operation( + this: &WryWebView, + drag_info: &ProtocolObject, +) -> Bool { let paths = unsafe { collect_paths(drag_info) }; - - let dl: NSPoint = unsafe { msg_send![drag_info, draggingLocation] }; - let frame: NSRect = unsafe { msg_send![this, frame] }; + let dl: NSPoint = unsafe { drag_info.draggingLocation() }; + let frame: NSRect = this.frame(); let position = (dl.x as i32, (frame.size.height - dl.y) as i32); + let listener = &this.ivars().drag_drop_handler; if !listener(DragDropEvent::Drop { paths, position }) { // Reject the Wry drop (invoke the OS default behaviour) - OBJC_PERFORM_DRAG_OPERATION(this, sel, drag_info) + unsafe { objc2::msg_send![super(this), performDragOperation: drag_info] } } else { - YES + Bool::YES } } -extern "C" fn dragging_exited(this: &mut Object, sel: Sel, drag_info: id) { - let listener = unsafe { get_handler(this) }; +pub(crate) fn dragging_exited(this: &WryWebView, drag_info: &ProtocolObject) { + let listener = &this.ivars().drag_drop_handler; if !listener(DragDropEvent::Leave) { // Reject the Wry drop (invoke the OS default behaviour) - OBJC_DRAGGING_EXITED(this, sel, drag_info); + unsafe { objc2::msg_send![super(this), draggingExited: drag_info] } } } - -pub(crate) unsafe fn add_drag_drop_methods(decl: &mut ClassDecl) { - decl.add_ivar::<*mut c_void>(DRAG_DROP_HANDLER_IVAR); - - decl.add_method( - sel!(draggingEntered:), - dragging_entered as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation, - ); - - decl.add_method( - sel!(draggingUpdated:), - dragging_updated as extern "C" fn(&mut Object, Sel, id) -> NSDragOperation, - ); - - decl.add_method( - sel!(performDragOperation:), - perform_drag_operation as extern "C" fn(&mut Object, Sel, id) -> BOOL, - ); - - decl.add_method( - sel!(draggingExited:), - dragging_exited as extern "C" fn(&mut Object, Sel, id), - ); -} diff --git a/src/wkwebview/ios/WKWebView.rs b/src/wkwebview/ios/WKWebView.rs new file mode 100644 index 000000000..72462cda6 --- /dev/null +++ b/src/wkwebview/ios/WKWebView.rs @@ -0,0 +1,621 @@ +#![allow(warnings)] +#![allow(clippy::all)] + +//! This file has been automatically generated by `objc2`'s `header-translator`. +//! DO NOT EDIT +use objc2::__framework_prelude::*; +use objc2_foundation::*; +use objc2_ui_kit::*; +use objc2_web_kit::*; + +use crate::*; + +// NS_ENUM +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct WKMediaPlaybackState(pub NSInteger); +impl WKMediaPlaybackState { + #[doc(alias = "WKMediaPlaybackStateNone")] + pub const None: Self = Self(0); + #[doc(alias = "WKMediaPlaybackStatePlaying")] + pub const Playing: Self = Self(1); + #[doc(alias = "WKMediaPlaybackStatePaused")] + pub const Paused: Self = Self(2); + #[doc(alias = "WKMediaPlaybackStateSuspended")] + pub const Suspended: Self = Self(3); +} + +unsafe impl Encode for WKMediaPlaybackState { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +unsafe impl RefEncode for WKMediaPlaybackState { + const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING); +} + +// NS_ENUM +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct WKMediaCaptureState(pub NSInteger); +impl WKMediaCaptureState { + #[doc(alias = "WKMediaCaptureStateNone")] + pub const None: Self = Self(0); + #[doc(alias = "WKMediaCaptureStateActive")] + pub const Active: Self = Self(1); + #[doc(alias = "WKMediaCaptureStateMuted")] + pub const Muted: Self = Self(2); +} + +unsafe impl Encode for WKMediaCaptureState { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +unsafe impl RefEncode for WKMediaCaptureState { + const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING); +} + +// NS_ENUM +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct WKFullscreenState(pub NSInteger); +impl WKFullscreenState { + #[doc(alias = "WKFullscreenStateNotInFullscreen")] + pub const NotInFullscreen: Self = Self(0); + #[doc(alias = "WKFullscreenStateEnteringFullscreen")] + pub const EnteringFullscreen: Self = Self(1); + #[doc(alias = "WKFullscreenStateInFullscreen")] + pub const InFullscreen: Self = Self(2); + #[doc(alias = "WKFullscreenStateExitingFullscreen")] + pub const ExitingFullscreen: Self = Self(3); +} + +unsafe impl Encode for WKFullscreenState { + const ENCODING: Encoding = NSInteger::ENCODING; +} + +unsafe impl RefEncode for WKFullscreenState { + const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING); +} + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct WKWebView; + + unsafe impl ClassType for WKWebView { + #[inherits(UIResponder, NSObject)] + type Super = UIView; + type Mutability = MainThreadOnly; + } +); + +#[cfg(target_os = "macos")] +unsafe impl NSAccessibility for WKWebView {} + +#[cfg(target_os = "macos")] +unsafe impl NSAccessibilityElementProtocol for WKWebView {} + +#[cfg(target_os = "macos")] +unsafe impl NSAnimatablePropertyContainer for WKWebView {} + +#[cfg(target_os = "macos")] +unsafe impl NSAppearanceCustomization for WKWebView {} + +unsafe impl NSCoding for WKWebView {} + +#[cfg(target_os = "macos")] +unsafe impl NSDraggingDestination for WKWebView {} + +unsafe impl NSObjectProtocol for WKWebView {} + +#[cfg(target_os = "macos")] +unsafe impl NSUserInterfaceItemIdentification for WKWebView {} + +extern_methods!( + unsafe impl WKWebView { + // #[cfg(feature = "WKWebViewConfiguration")] + #[method_id(@__retain_semantics Other configuration)] + pub unsafe fn configuration(&self) -> Retained; + + // #[cfg(feature = "WKNavigationDelegate")] + #[method_id(@__retain_semantics Other navigationDelegate)] + pub unsafe fn navigationDelegate( + &self, + ) -> Option>>; + + // #[cfg(feature = "WKNavigationDelegate")] + #[method(setNavigationDelegate:)] + pub unsafe fn setNavigationDelegate( + &self, + navigation_delegate: Option<&ProtocolObject>, + ); + + // #[cfg(feature = "WKUIDelegate")] + #[method_id(@__retain_semantics Other UIDelegate)] + pub unsafe fn UIDelegate(&self) -> Option>>; + + // #[cfg(feature = "WKUIDelegate")] + #[method(setUIDelegate:)] + pub unsafe fn setUIDelegate(&self, ui_delegate: Option<&ProtocolObject>); + + #[cfg(target_os = "macos")] + // #[cfg(feature = "WKBackForwardList")] + #[method_id(@__retain_semantics Other backForwardList)] + pub unsafe fn backForwardList(&self) -> Retained; + + // #[cfg(feature = "WKWebViewConfiguration")] + #[method_id(@__retain_semantics Init initWithFrame:configuration:)] + pub unsafe fn initWithFrame_configuration( + this: Allocated, + frame: CGRect, + configuration: &WKWebViewConfiguration, + ) -> Retained; + + #[method_id(@__retain_semantics Init initWithCoder:)] + pub unsafe fn initWithCoder(this: Allocated, coder: &NSCoder) -> Option>; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other loadRequest:)] + pub unsafe fn loadRequest(&self, request: &NSURLRequest) -> Option>; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other loadFileURL:allowingReadAccessToURL:)] + pub unsafe fn loadFileURL_allowingReadAccessToURL( + &self, + url: &NSURL, + read_access_url: &NSURL, + ) -> Option>; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other loadHTMLString:baseURL:)] + pub unsafe fn loadHTMLString_baseURL( + &self, + string: &NSString, + base_url: Option<&NSURL>, + ) -> Option>; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other loadData:MIMEType:characterEncodingName:baseURL:)] + pub unsafe fn loadData_MIMEType_characterEncodingName_baseURL( + &self, + data: &NSData, + mime_type: &NSString, + character_encoding_name: &NSString, + base_url: &NSURL, + ) -> Option>; + + #[cfg(target_os = "macos")] + // #[cfg(all(feature = "WKBackForwardListItem", feature = "WKNavigation"))] + #[method_id(@__retain_semantics Other goToBackForwardListItem:)] + pub unsafe fn goToBackForwardListItem( + &self, + item: &WKBackForwardListItem, + ) -> Option>; + + #[method_id(@__retain_semantics Other title)] + pub unsafe fn title(&self) -> Option>; + + #[method_id(@__retain_semantics Other URL)] + pub unsafe fn URL(&self) -> Option>; + + #[method(isLoading)] + pub unsafe fn isLoading(&self) -> bool; + + #[method(estimatedProgress)] + pub unsafe fn estimatedProgress(&self) -> c_double; + + #[method(hasOnlySecureContent)] + pub unsafe fn hasOnlySecureContent(&self) -> bool; + + #[method(canGoBack)] + pub unsafe fn canGoBack(&self) -> bool; + + #[method(canGoForward)] + pub unsafe fn canGoForward(&self) -> bool; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other goBack)] + pub unsafe fn goBack(&self) -> Option>; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other goForward)] + pub unsafe fn goForward(&self) -> Option>; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other reload)] + pub unsafe fn reload(&self) -> Option>; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other reloadFromOrigin)] + pub unsafe fn reloadFromOrigin(&self) -> Option>; + + #[method(stopLoading)] + pub unsafe fn stopLoading(&self); + + // #[cfg(feature = "block2")] + #[method(evaluateJavaScript:completionHandler:)] + pub unsafe fn evaluateJavaScript_completionHandler( + &self, + java_script_string: &NSString, + completion_handler: Option<&block2::Block>, + ); + + #[cfg(target_os = "macos")] + // #[cfg(all( + // feature = "WKContentWorld", + // feature = "WKFrameInfo", + // feature = "block2" + // ))] + #[method(evaluateJavaScript:inFrame:inContentWorld:completionHandler:)] + pub unsafe fn evaluateJavaScript_inFrame_inContentWorld_completionHandler( + &self, + java_script_string: &NSString, + frame: Option<&WKFrameInfo>, + content_world: &WKContentWorld, + completion_handler: Option<&block2::Block>, + ); + + #[cfg(target_os = "macos")] + // #[cfg(all( + // feature = "WKContentWorld", + // feature = "WKFrameInfo", + // feature = "block2" + // ))] + #[method(callAsyncJavaScript:arguments:inFrame:inContentWorld:completionHandler:)] + pub unsafe fn callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler( + &self, + function_body: &NSString, + arguments: Option<&NSDictionary>, + frame: Option<&WKFrameInfo>, + content_world: &WKContentWorld, + completion_handler: Option<&block2::Block>, + ); + + // #[cfg(feature = "block2")] + #[method(closeAllMediaPresentationsWithCompletionHandler:)] + pub unsafe fn closeAllMediaPresentationsWithCompletionHandler( + &self, + completion_handler: Option<&block2::Block>, + ); + + #[deprecated] + #[method(closeAllMediaPresentations)] + pub unsafe fn closeAllMediaPresentations(&self); + + // #[cfg(feature = "block2")] + #[method(pauseAllMediaPlaybackWithCompletionHandler:)] + pub unsafe fn pauseAllMediaPlaybackWithCompletionHandler( + &self, + completion_handler: Option<&block2::Block>, + ); + + // #[cfg(feature = "block2")] + #[deprecated] + #[method(pauseAllMediaPlayback:)] + pub unsafe fn pauseAllMediaPlayback( + &self, + completion_handler: Option<&block2::Block>, + ); + + // #[cfg(feature = "block2")] + #[method(setAllMediaPlaybackSuspended:completionHandler:)] + pub unsafe fn setAllMediaPlaybackSuspended_completionHandler( + &self, + suspended: bool, + completion_handler: Option<&block2::Block>, + ); + + // #[cfg(feature = "block2")] + #[deprecated] + #[method(resumeAllMediaPlayback:)] + pub unsafe fn resumeAllMediaPlayback( + &self, + completion_handler: Option<&block2::Block>, + ); + + // #[cfg(feature = "block2")] + #[deprecated] + #[method(suspendAllMediaPlayback:)] + pub unsafe fn suspendAllMediaPlayback( + &self, + completion_handler: Option<&block2::Block>, + ); + + // #[cfg(feature = "block2")] + #[method(requestMediaPlaybackStateWithCompletionHandler:)] + pub unsafe fn requestMediaPlaybackStateWithCompletionHandler( + &self, + completion_handler: &block2::Block, + ); + + // #[cfg(feature = "block2")] + #[deprecated] + #[method(requestMediaPlaybackState:)] + pub unsafe fn requestMediaPlaybackState( + &self, + completion_handler: &block2::Block, + ); + + #[method(cameraCaptureState)] + pub unsafe fn cameraCaptureState(&self) -> WKMediaCaptureState; + + #[method(microphoneCaptureState)] + pub unsafe fn microphoneCaptureState(&self) -> WKMediaCaptureState; + + // #[cfg(feature = "block2")] + #[method(setCameraCaptureState:completionHandler:)] + pub unsafe fn setCameraCaptureState_completionHandler( + &self, + state: WKMediaCaptureState, + completion_handler: Option<&block2::Block>, + ); + + // #[cfg(feature = "block2")] + #[method(setMicrophoneCaptureState:completionHandler:)] + pub unsafe fn setMicrophoneCaptureState_completionHandler( + &self, + state: WKMediaCaptureState, + completion_handler: Option<&block2::Block>, + ); + + #[cfg(target_os = "macos")] + // #[cfg(all(feature = "WKSnapshotConfiguration", feature = "block2"))] + #[method(takeSnapshotWithConfiguration:completionHandler:)] + pub unsafe fn takeSnapshotWithConfiguration_completionHandler( + &self, + snapshot_configuration: Option<&WKSnapshotConfiguration>, + completion_handler: &block2::Block, + ); + + #[cfg(target_os = "macos")] + // #[cfg(all(feature = "WKPDFConfiguration", feature = "block2"))] + #[method(createPDFWithConfiguration:completionHandler:)] + pub unsafe fn createPDFWithConfiguration_completionHandler( + &self, + pdf_configuration: Option<&WKPDFConfiguration>, + completion_handler: &block2::Block, + ); + + // #[cfg(feature = "block2")] + #[method(createWebArchiveDataWithCompletionHandler:)] + pub unsafe fn createWebArchiveDataWithCompletionHandler( + &self, + completion_handler: &block2::Block, NonNull)>, + ); + + #[method(allowsBackForwardNavigationGestures)] + pub unsafe fn allowsBackForwardNavigationGestures(&self) -> bool; + + #[method(setAllowsBackForwardNavigationGestures:)] + pub unsafe fn setAllowsBackForwardNavigationGestures( + &self, + allows_back_forward_navigation_gestures: bool, + ); + + #[method_id(@__retain_semantics Other customUserAgent)] + pub unsafe fn customUserAgent(&self) -> Option>; + + #[method(setCustomUserAgent:)] + pub unsafe fn setCustomUserAgent(&self, custom_user_agent: Option<&NSString>); + + #[method(allowsLinkPreview)] + pub unsafe fn allowsLinkPreview(&self) -> bool; + + #[method(setAllowsLinkPreview:)] + pub unsafe fn setAllowsLinkPreview(&self, allows_link_preview: bool); + + #[method(allowsMagnification)] + pub unsafe fn allowsMagnification(&self) -> bool; + + #[method(setAllowsMagnification:)] + pub unsafe fn setAllowsMagnification(&self, allows_magnification: bool); + + #[method(magnification)] + pub unsafe fn magnification(&self) -> CGFloat; + + #[method(setMagnification:)] + pub unsafe fn setMagnification(&self, magnification: CGFloat); + + #[method(setMagnification:centeredAtPoint:)] + pub unsafe fn setMagnification_centeredAtPoint(&self, magnification: CGFloat, point: CGPoint); + + #[method(pageZoom)] + pub unsafe fn pageZoom(&self) -> CGFloat; + + #[method(setPageZoom:)] + pub unsafe fn setPageZoom(&self, page_zoom: CGFloat); + + #[cfg(target_os = "macos")] + // #[cfg(all( + // feature = "WKFindConfiguration", + // feature = "WKFindResult", + // feature = "block2" + // ))] + #[method(findString:withConfiguration:completionHandler:)] + pub unsafe fn findString_withConfiguration_completionHandler( + &self, + string: &NSString, + configuration: Option<&WKFindConfiguration>, + completion_handler: &block2::Block)>, + ); + + #[method(handlesURLScheme:)] + pub unsafe fn handlesURLScheme(url_scheme: &NSString, mtm: MainThreadMarker) -> bool; + + // #[cfg(all(feature = "WKDownload", feature = "block2"))] + #[method(startDownloadUsingRequest:completionHandler:)] + pub unsafe fn startDownloadUsingRequest_completionHandler( + &self, + request: &NSURLRequest, + completion_handler: &block2::Block)>, + ); + + // #[cfg(all(feature = "WKDownload", feature = "block2"))] + #[method(resumeDownloadFromResumeData:completionHandler:)] + pub unsafe fn resumeDownloadFromResumeData_completionHandler( + &self, + resume_data: &NSData, + completion_handler: &block2::Block)>, + ); + + #[method_id(@__retain_semantics Other mediaType)] + pub unsafe fn mediaType(&self) -> Option>; + + #[method(setMediaType:)] + pub unsafe fn setMediaType(&self, media_type: Option<&NSString>); + + #[method_id(@__retain_semantics Other interactionState)] + pub unsafe fn interactionState(&self) -> Option>; + + #[method(setInteractionState:)] + pub unsafe fn setInteractionState(&self, interaction_state: Option<&AnyObject>); + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other loadSimulatedRequest:response:responseData:)] + pub unsafe fn loadSimulatedRequest_response_responseData( + &self, + request: &NSURLRequest, + response: &NSURLResponse, + data: &NSData, + ) -> Retained; + + // #[cfg(feature = "WKNavigation")] + #[deprecated] + #[method_id(@__retain_semantics Other loadSimulatedRequest:withResponse:responseData:)] + pub unsafe fn loadSimulatedRequest_withResponse_responseData( + &self, + request: &NSURLRequest, + response: &NSURLResponse, + data: &NSData, + ) -> Retained; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other loadFileRequest:allowingReadAccessToURL:)] + pub unsafe fn loadFileRequest_allowingReadAccessToURL( + &self, + request: &NSURLRequest, + read_access_url: &NSURL, + ) -> Retained; + + // #[cfg(feature = "WKNavigation")] + #[method_id(@__retain_semantics Other loadSimulatedRequest:responseHTMLString:)] + pub unsafe fn loadSimulatedRequest_responseHTMLString( + &self, + request: &NSURLRequest, + string: &NSString, + ) -> Retained; + + // #[cfg(feature = "WKNavigation")] + #[deprecated] + #[method_id(@__retain_semantics Other loadSimulatedRequest:withResponseHTMLString:)] + pub unsafe fn loadSimulatedRequest_withResponseHTMLString( + &self, + request: &NSURLRequest, + string: &NSString, + ) -> Retained; + + #[cfg(target_os = "macos")] + #[method_id(@__retain_semantics Other printOperationWithPrintInfo:)] + pub unsafe fn printOperationWithPrintInfo( + &self, + print_info: &NSPrintInfo, + ) -> Retained; + + #[cfg(target_os = "macos")] + #[method_id(@__retain_semantics Other themeColor)] + pub unsafe fn themeColor(&self) -> Option>; + + #[cfg(target_os = "macos")] + #[method_id(@__retain_semantics Other underPageBackgroundColor)] + pub unsafe fn underPageBackgroundColor(&self) -> Retained; + + #[cfg(target_os = "macos")] + #[method(setUnderPageBackgroundColor:)] + pub unsafe fn setUnderPageBackgroundColor(&self, under_page_background_color: Option<&NSColor>); + + #[method(fullscreenState)] + pub unsafe fn fullscreenState(&self) -> WKFullscreenState; + + #[method(minimumViewportInset)] + pub unsafe fn minimumViewportInset(&self) -> NSEdgeInsets; + + #[method(maximumViewportInset)] + pub unsafe fn maximumViewportInset(&self) -> NSEdgeInsets; + + #[method(setMinimumViewportInset:maximumViewportInset:)] + pub unsafe fn setMinimumViewportInset_maximumViewportInset( + &self, + minimum_viewport_inset: NSEdgeInsets, + maximum_viewport_inset: NSEdgeInsets, + ); + + #[method(isInspectable)] + pub unsafe fn isInspectable(&self) -> bool; + + #[method(setInspectable:)] + pub unsafe fn setInspectable(&self, inspectable: bool); + } +); + +extern_methods!( + /// Methods declared on superclass `UIView` + unsafe impl WKWebView { + #[method_id(@__retain_semantics Init initWithFrame:)] + pub unsafe fn initWithFrame(this: Allocated, frame_rect: NSRect) -> Retained; + } +); + +extern_methods!( + /// Methods declared on superclass `UIResponder` + unsafe impl WKWebView { + #[method_id(@__retain_semantics Init init)] + pub unsafe fn init(this: Allocated) -> Retained; + } +); + +extern_methods!( + /// Methods declared on superclass `NSObject` + unsafe impl WKWebView { + #[method_id(@__retain_semantics New new)] + pub unsafe fn new(mtm: MainThreadMarker) -> Retained; + } +); + +extern_methods!( + /// WKIBActions + unsafe impl WKWebView { + #[method(goBack:)] + pub unsafe fn goBack_(&self, sender: Option<&AnyObject>); + + #[method(goForward:)] + pub unsafe fn goForward_(&self, sender: Option<&AnyObject>); + + #[method(reload:)] + pub unsafe fn reload_(&self, sender: Option<&AnyObject>); + + #[method(reloadFromOrigin:)] + pub unsafe fn reloadFromOrigin_(&self, sender: Option<&AnyObject>); + + #[method(stopLoading:)] + pub unsafe fn stopLoading_(&self, sender: Option<&AnyObject>); + } +); + +#[cfg(target_os = "macos")] +unsafe impl NSUserInterfaceValidations for WKWebView {} + +extern_methods!( + /// WKNSTextFinderClient + unsafe impl WKWebView {} +); + +#[cfg(target_os = "macos")] +unsafe impl NSTextFinderClient for WKWebView {} + +extern_methods!( + /// WKDeprecated + unsafe impl WKWebView { + #[deprecated] + #[method_id(@__retain_semantics Other certificateChain)] + pub unsafe fn certificateChain(&self) -> Retained; + } +); diff --git a/src/wkwebview/ios/mod.rs b/src/wkwebview/ios/mod.rs new file mode 100644 index 000000000..64ecbe707 --- /dev/null +++ b/src/wkwebview/ios/mod.rs @@ -0,0 +1 @@ +pub mod WKWebView; diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 1aadc0a5b..7669a76cb 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Tauri Programme within The Commons Conservancy +// Copyright 2020-2024 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT @@ -12,41 +12,72 @@ mod proxy; mod synthetic_mouse_events; mod util; +#[cfg(target_os = "ios")] +mod ios; + +mod class; +pub use class::wry_web_view::WryWebView; #[cfg(target_os = "macos")] -use cocoa::appkit::{NSView, NSViewHeightSizable, NSViewMinYMargin, NSViewWidthSizable}; -use cocoa::{ - base::{id, nil, NO, YES}, - foundation::{NSDictionary, NSFastEnumeration, NSInteger}, +use class::wry_web_view_parent::WryWebViewParent; +use class::{ + document_title_changed_observer::*, + url_scheme_handler, + wry_download_delegate::WryDownloadDelegate, + wry_navigation_delegate::WryNavigationDelegate, + wry_web_view::WryWebViewIvars, + wry_web_view_delegate::{WryWebViewDelegate, IPC_MESSAGE_HANDLER_NAME}, + wry_web_view_ui_delegate::WryWebViewUIDelegate, }; + use dpi::{LogicalPosition, LogicalSize}; +#[cfg(target_os = "macos")] +use objc2::runtime::Bool; +use objc2::{ + rc::Retained, + runtime::{AnyObject, NSObject, ProtocolObject}, + ClassType, DeclaredClass, +}; +#[cfg(target_os = "macos")] +use objc2_app_kit::{NSApplication, NSAutoresizingMaskOptions, NSTitlebarSeparatorStyle, NSView}; +#[cfg(target_os = "macos")] +use objc2_foundation::CGSize; +use objc2_foundation::{ + ns_string, CGPoint, CGRect, MainThreadMarker, NSBundle, NSDate, NSError, NSJSONSerialization, + NSMutableURLRequest, NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, NSString, + NSUTF8StringEncoding, NSURL, NSUUID, +}; +#[cfg(target_os = "ios")] +use objc2_ui_kit::{UIScrollView, UIViewAutoresizing}; + +#[cfg(target_os = "macos")] +use objc2_app_kit::NSWindow; +#[cfg(target_os = "ios")] +use objc2_ui_kit::UIView as NSView; +// #[cfg(target_os = "ios")] +// use objc2_ui_kit::UIWindow as NSWindow; + +#[cfg(target_os = "ios")] +use crate::wkwebview::ios::WKWebView::WKWebView; +#[cfg(target_os = "macos")] +use objc2_web_kit::WKWebView; + +use objc2_web_kit::{ + WKAudiovisualMediaTypes, WKURLSchemeHandler, WKUserContentController, WKUserScript, + WKUserScriptInjectionTime, WKWebViewConfiguration, WKWebsiteDataStore, +}; use once_cell::sync::Lazy; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use std::{ - borrow::Cow, - collections::HashSet, - ffi::{c_void, CStr, CString}, + collections::{HashMap, HashSet}, + ffi::{c_void, CString}, os::raw::c_char, - ptr::{null, null_mut}, - slice, str, + panic::AssertUnwindSafe, + ptr::null_mut, + str, sync::{Arc, Mutex}, }; -use core_graphics::{ - base::CGFloat, - geometry::{CGPoint, CGRect, CGSize}, -}; - -use objc::{ - declare::ClassDecl, - runtime::{Class, Object, Sel, BOOL}, - Message, -}; -use objc_id::Id; - -#[cfg(target_os = "macos")] -use drag_drop::{add_drag_drop_methods, set_drag_drop_handler}; - #[cfg(feature = "mac-proxy")] use crate::{ proxy::ProxyConfig, @@ -55,32 +86,12 @@ use crate::{ }, }; -use crate::{ - wkwebview::{ - download::{ - add_download_methods, download_did_fail, download_did_finish, download_policy, - set_download_delegate, - }, - navigation::{add_navigation_mathods, drop_navigation_methods, set_navigation_methods}, - }, - Error, PageLoadEvent, Rect, RequestAsyncResponder, Result, WebViewAttributes, RGBA, -}; +use crate::{Error, Rect, RequestAsyncResponder, Result, WebViewAttributes, RGBA}; -use http::{ - header::{CONTENT_LENGTH, CONTENT_TYPE}, - status::StatusCode, - version::Version, - Request, Response as HttpResponse, -}; +use http::Request; use crate::util::Counter; -const IPC_MESSAGE_HANDLER_NAME: &str = "ipc"; -#[cfg(target_os = "macos")] -const ACCEPT_FIRST_MOUSE: &str = "accept_first_mouse"; - -const NS_JSON_WRITING_FRAGMENTS_ALLOWED: u64 = 4; - static COUNTER: Counter = Counter::new(); static WEBVIEW_IDS: Lazy>> = Lazy::new(Default::default); @@ -99,19 +110,26 @@ pub struct PrintOptions { pub(crate) struct InnerWebView { id: String, - pub webview: id, - pub manager: id, + pub webview: Retained, + pub manager: Retained, + #[allow(dead_code)] is_child: bool, pending_scripts: Arc>>>, // Note that if following functions signatures are changed in the future, // all functions pointer declarations in objc callbacks below all need to get updated. - ipc_handler_ptr: *mut Box)>, - document_title_changed_handler: *mut Box, - navigation_decide_policy_ptr: *mut Box bool>, - page_load_handler: *mut Box, - #[cfg(target_os = "macos")] - drag_drop_ptr: *mut Box bool>, - download_delegate: id, + ipc_handler_delegate: Option>, + #[allow(dead_code)] + // We need this the keep the reference count + document_title_changed_observer: Option>, + #[allow(dead_code)] + // We need this the keep the reference count + navigation_policy_delegate: Retained, + #[allow(dead_code)] + // We need this the keep the reference count + download_delegate: Option>, + #[allow(dead_code)] + // We need this the keep the reference count + ui_delegate: Retained, protocol_ptrs: Vec<*mut Box>, RequestAsyncResponder)>>, } @@ -129,7 +147,7 @@ impl InnerWebView { _ => return Err(Error::UnsupportedWindowHandle), }; - Self::new_ns_view(ns_view as _, attributes, pl_attrs, false) + unsafe { Self::new_ns_view(&*(ns_view as *mut NSView), attributes, pl_attrs, false) } } pub fn new_as_child( @@ -145,229 +163,16 @@ impl InnerWebView { _ => return Err(Error::UnsupportedWindowHandle), }; - Self::new_ns_view(ns_view as _, attributes, pl_attrs, true) + unsafe { Self::new_ns_view(&*(ns_view as *mut NSView), attributes, pl_attrs, true) } } fn new_ns_view( - ns_view: id, + ns_view: &NSView, attributes: WebViewAttributes, pl_attrs: super::PlatformSpecificWebViewAttributes, is_child: bool, ) -> Result { - // Function for ipc handler - extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) { - // Safety: objc runtime calls are unsafe - unsafe { - #[cfg(feature = "tracing")] - let _span = tracing::info_span!(parent: None, "wry::ipc::handle").entered(); - - let function = this.get_ivar::<*mut c_void>("function"); - if !function.is_null() { - let function = &mut *(*function as *mut Box)>); - let body: id = msg_send![msg, body]; - let is_string: bool = msg_send![body, isKindOfClass: class!(NSString)]; - if is_string { - let js_utf8: *const c_char = msg_send![body, UTF8String]; - - let frame_info: id = msg_send![msg, frameInfo]; - let request: id = msg_send![frame_info, request]; - let url: id = msg_send![request, URL]; - let absolute_url: id = msg_send![url, absoluteString]; - let url_utf8: *const c_char = msg_send![absolute_url, UTF8String]; - - if let (Ok(url), Ok(js)) = ( - CStr::from_ptr(url_utf8).to_str(), - CStr::from_ptr(js_utf8).to_str(), - ) { - (function)(Request::builder().uri(url).body(js.to_string()).unwrap()); - return; - } - } - } - - #[cfg(feature = "tracing")] - tracing::warn!("WebView received invalid IPC call."); - } - } - - // Task handler for custom protocol - extern "C" fn start_task(this: &Object, _: Sel, _webview: id, task: *mut Object) { - unsafe { - #[cfg(feature = "tracing")] - let span = tracing::info_span!(parent: None, "wry::custom_protocol::handle", uri = tracing::field::Empty) - .entered(); - - let webview_id_ptr: *mut c_char = *this.get_ivar("id"); - let webview_id = CStr::from_ptr(webview_id_ptr) - .to_str() - .ok() - .unwrap_or_default(); - - let function = this.get_ivar::<*mut c_void>("function"); - if !function.is_null() { - let function = &mut *(*function - as *mut Box>, RequestAsyncResponder)>); - - // Get url request - let request: id = msg_send![task, request]; - let url: id = msg_send![request, URL]; - - let uri_nsstring = { - let s: id = msg_send![url, absoluteString]; - NSString(s) - }; - let uri = uri_nsstring.to_str(); - - #[cfg(feature = "tracing")] - span.record("uri", uri); - - // Get request method (GET, POST, PUT etc...) - let method = { - let s: id = msg_send![request, HTTPMethod]; - NSString(s) - }; - - // Prepare our HttpRequest - let mut http_request = Request::builder().uri(uri).method(method.to_str()); - - // Get body - let mut sent_form_body = Vec::new(); - let body: id = msg_send![request, HTTPBody]; - let body_stream: id = msg_send![request, HTTPBodyStream]; - if !body.is_null() { - let length = msg_send![body, length]; - let data_bytes: id = msg_send![body, bytes]; - if !data_bytes.is_null() { - sent_form_body = slice::from_raw_parts(data_bytes as *const u8, length).to_vec(); - } - } else if !body_stream.is_null() { - let _: () = msg_send![body_stream, open]; - - while msg_send![body_stream, hasBytesAvailable] { - sent_form_body.reserve(128); - let p = sent_form_body.as_mut_ptr().add(sent_form_body.len()); - let read_length = sent_form_body.capacity() - sent_form_body.len(); - let count: usize = msg_send![body_stream, read: p maxLength: read_length]; - sent_form_body.set_len(sent_form_body.len() + count); - } - - let _: () = msg_send![body_stream, close]; - } - - // Extract all headers fields - let all_headers: id = msg_send![request, allHTTPHeaderFields]; - - // get all our headers values and inject them in our request - for current_header_ptr in all_headers.iter() { - let header_field = NSString(current_header_ptr); - let header_value = NSString(all_headers.valueForKey_(current_header_ptr)); - - // inject the header into the request - http_request = http_request.header(header_field.to_str(), header_value.to_str()); - } - - let respond_with_404 = || { - let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc]; - let response: id = msg_send![urlresponse, initWithURL:url statusCode:StatusCode::NOT_FOUND HTTPVersion:NSString::new(format!("{:#?}", Version::HTTP_11).as_str()) headerFields:null::()]; - let () = msg_send![task, didReceiveResponse: response]; - // Finish - let () = msg_send![task, didFinish]; - }; - - // send response - match http_request.body(sent_form_body) { - Ok(final_request) => { - let () = msg_send![task, retain]; - - let responder: Box>)> = Box::new( - move |sent_response| { - fn check_webview_id_valid(webview_id: &str) -> crate::Result<()> { - match WEBVIEW_IDS.lock().unwrap().contains(webview_id) { - true => Ok(()), - false => Err(crate::Error::CustomProtocolTaskInvalid), - } - } - - unsafe fn response( - task: id, - webview_id: &str, - url: id, /* NSURL */ - sent_response: HttpResponse>, - ) -> crate::Result<()> { - let content = sent_response.body(); - // default: application/octet-stream, but should be provided by the client - let wanted_mime = sent_response.headers().get(CONTENT_TYPE); - // default to 200 - let wanted_status_code = sent_response.status().as_u16() as i32; - // default to HTTP/1.1 - let wanted_version = format!("{:#?}", sent_response.version()); - - let dictionary: id = msg_send![class!(NSMutableDictionary), alloc]; - let headers: id = msg_send![dictionary, initWithCapacity:1]; - if let Some(mime) = wanted_mime { - let () = msg_send![headers, setObject:NSString::new(mime.to_str().unwrap()) forKey: NSString::new(CONTENT_TYPE.as_str())]; - } - let () = msg_send![headers, setObject:NSString::new(&content.len().to_string()) forKey: NSString::new(CONTENT_LENGTH.as_str())]; - - // add headers - for (name, value) in sent_response.headers().iter() { - let header_key = name.as_str(); - if let Ok(value) = value.to_str() { - let () = msg_send![headers, setObject:NSString::new(value) forKey: NSString::new(header_key)]; - } - } - - let urlresponse: id = msg_send![class!(NSHTTPURLResponse), alloc]; - let response: id = msg_send![urlresponse, initWithURL:url statusCode: wanted_status_code HTTPVersion:NSString::new(&wanted_version) headerFields:headers]; - - check_webview_id_valid(&webview_id)?; - (*task) - .send_message::<(id,), ()>(sel!(didReceiveResponse:), (response,)) - .map_err(|_| crate::Error::CustomProtocolTaskInvalid)?; - - // Send data - let bytes = content.as_ptr() as *mut c_void; - let data: id = msg_send![class!(NSData), alloc]; - let data: id = msg_send![data, initWithBytesNoCopy:bytes length:content.len() freeWhenDone: if content.len() == 0 { NO } else { YES }]; - - check_webview_id_valid(&webview_id)?; - (*task) - .send_message::<(id,), ()>(sel!(didReceiveData:), (data,)) - .map_err(|_| crate::Error::CustomProtocolTaskInvalid)?; - - // Finish - check_webview_id_valid(&webview_id)?; - (*task) - .send_message::<(), ()>(sel!(didFinish), ()) - .map_err(|_| crate::Error::CustomProtocolTaskInvalid)?; - - Ok(()) - } - - let _ = response(task, &webview_id, url, sent_response); - let () = msg_send![task, release]; - }, - ); - - #[cfg(feature = "tracing")] - let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); - function( - webview_id, - final_request, - RequestAsyncResponder { responder }, - ); - } - Err(_) => respond_with_404(), - }; - } else { - #[cfg(feature = "tracing")] - tracing::warn!( - "Either WebView or WebContext instance is dropped! This handler shouldn't be called." - ); - } - } - } - extern "C" fn stop_task(_: &Object, _: Sel, _webview: id, _task: id) {} + let mtm = MainThreadMarker::new().ok_or(Error::NotMainThread)?; let webview_id = attributes .id @@ -380,127 +185,81 @@ impl InnerWebView { // Safety: objc runtime calls are unsafe unsafe { - // Config and custom protocol - let config: id = msg_send![class!(WKWebViewConfiguration), new]; - let mut protocol_ptrs = Vec::new(); + let config = WKWebViewConfiguration::new(); + // Incognito mode let os_version = util::operating_system_version(); - #[cfg(target_os = "macos")] let custom_data_store_available = os_version.0 >= 14; - #[cfg(target_os = "ios")] let custom_data_store_available = os_version.0 >= 17; - let data_store: id = match ( + let data_store = match ( attributes.incognito, custom_data_store_available, pl_attrs.data_store_identifier, ) { - // incognito has priority - (true, _, _) => msg_send![class!(WKWebsiteDataStore), nonPersistentDataStore], + (true, _, _) => WKWebsiteDataStore::nonPersistentDataStore(), // if data_store_identifier is given and custom data stores are available, use custom store (false, true, Some(data_store)) => { - let ns_uuid = NSUUID::new(&data_store); - msg_send![class!(WKWebsiteDataStore), dataStoreForIdentifier:ns_uuid.0] + let identifier = NSUUID::from_bytes(data_store); + WKWebsiteDataStore::dataStoreForIdentifier(&identifier) } // default data store - _ => msg_send![class!(WKWebsiteDataStore), defaultDataStore], + _ => WKWebsiteDataStore::defaultDataStore(), }; + // Register Custom Protocols + let mut protocol_ptrs = Vec::new(); for (name, function) in attributes.custom_protocols { - let scheme_name = format!("{}URLSchemeHandler", name); - let cls = ClassDecl::new(&scheme_name, class!(NSObject)); - let cls = match cls { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("function"); - cls.add_ivar::<*mut c_char>("id"); - cls.add_method( - sel!(webView:startURLSchemeTask:), - start_task as extern "C" fn(&Object, Sel, id, id), - ); - cls.add_method( - sel!(webView:stopURLSchemeTask:), - stop_task as extern "C" fn(&Object, Sel, id, id), - ); - cls.register() - } - None => Class::get(&scheme_name).expect("Failed to get the class definition"), - }; - let handler: id = msg_send![cls, new]; + let url_scheme_handler_cls = url_scheme_handler::create(&name); + let handler: *mut AnyObject = objc2::msg_send![url_scheme_handler_cls, new]; let function = Box::into_raw(Box::new(function)); protocol_ptrs.push(function); - (*handler).set_ivar("function", function as *mut _ as *mut c_void); - (*handler).set_ivar( - "id", - CString::new(webview_id.as_bytes()).unwrap().into_raw(), - ); + let ivar = (*handler).class().instance_variable("function").unwrap(); + let ivar_delegate = ivar.load_mut(&mut *handler); + *ivar_delegate = function as *mut _ as *mut c_void; - (*config) - .send_message::<(id, NSString), ()>( - sel!(setURLSchemeHandler:forURLScheme:), - (handler, NSString::new(&name)), - ) - .map_err(|_| crate::Error::UrlSchemeRegisterError(name))?; + let ivar = (*handler).class().instance_variable("webview_id").unwrap(); + let ivar_delegate: &mut *mut c_char = ivar.load_mut(&mut *handler); + *ivar_delegate = CString::new(webview_id.as_bytes()).unwrap().into_raw(); + + let set_result = objc2::exception::catch(AssertUnwindSafe(|| { + config.setURLSchemeHandler_forURLScheme( + Some(&*(handler.cast::>())), + &NSString::from_str(&name), + ); + })); + if set_result.is_err() { + return Err(Error::UrlSchemeRegisterError(name)); + } } // WebView and manager - let manager: id = msg_send![config, userContentController]; - let cls = match ClassDecl::new("WryWebView", class!(WKWebView)) { - #[allow(unused_mut)] - Some(mut decl) => { - #[cfg(target_os = "macos")] - { - add_drag_drop_methods(&mut decl); - synthetic_mouse_events::setup(&mut decl); - decl.add_ivar::(ACCEPT_FIRST_MOUSE); - decl.add_method( - sel!(acceptsFirstMouse:), - accept_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL, - ); - extern "C" fn accept_first_mouse(this: &Object, _sel: Sel, _event: id) -> BOOL { - unsafe { - let accept: bool = *this.get_ivar(ACCEPT_FIRST_MOUSE); - if accept { - YES - } else { - NO - } - } - } - - // This is a temporary workaround for https://github.com/tauri-apps/tauri/issues/9426 - // FIXME: When the webview is a child webview, performKeyEquivalent always return YES - // and stop propagating the event to the window, hence the menu shortcut won't be - // triggered. However, overriding this method also means the cmd+key event won't be - // handled in webview, which means the key cannot be listened by JavaScript. - if is_child { - decl.add_method( - sel!(performKeyEquivalent:), - key_equivalent as extern "C" fn(&mut Object, Sel, id) -> BOOL, - ); - extern "C" fn key_equivalent(_this: &mut Object, _sel: Sel, _event: id) -> BOOL { - NO - } - } - } - decl.register() - } - _ => class!(WryWebView), - }; - let webview: id = msg_send![cls, alloc]; + let manager = config.userContentController(); + let webview = mtm.alloc::().set_ivars(WryWebViewIvars { + is_child, + #[cfg(target_os = "macos")] + drag_drop_handler: match attributes.drag_drop_handler { + Some(handler) => handler, + None => Box::new(|_| false), + }, + #[cfg(target_os = "macos")] + accept_first_mouse: Bool::new(attributes.accept_first_mouse), + custom_protocol_task_ids: HashMap::new(), + }); - let () = msg_send![config, setWebsiteDataStore: data_store]; - let _preference: id = msg_send![config, preferences]; - let _yes: id = msg_send![class!(NSNumber), numberWithBool:1]; + config.setWebsiteDataStore(&data_store); + let _preference = config.preferences(); + let _yes = NSNumber::numberWithBool(true); #[cfg(feature = "mac-proxy")] if let Some(proxy_config) = attributes.proxy_config { let proxy_config = match proxy_config { ProxyConfig::Http(endpoint) => { let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap(); - nw_proxy_config_create_http_connect(nw_endpoint, nil) + nw_proxy_config_create_http_connect(nw_endpoint, null_mut()) } ProxyConfig::Socks5(endpoint) => { let nw_endpoint = nw_endpoint_t::try_from(endpoint).unwrap(); @@ -508,43 +267,40 @@ impl InnerWebView { } }; - let proxies: id = msg_send![class!(NSArray), arrayWithObject: proxy_config]; - let () = msg_send![data_store, setProxyConfigurations: proxies]; + let proxies: Retained> = + objc2_foundation::NSArray::arrayWithObject(&*proxy_config); + data_store.setValue_forKey(Some(&proxies), ns_string!("proxyConfigurations")); } - #[cfg(target_os = "macos")] - (*webview).set_ivar(ACCEPT_FIRST_MOUSE, attributes.accept_first_mouse); - - let _: id = msg_send![_preference, setValue:_yes forKey:NSString::new("allowsPictureInPictureMediaPlayback")]; + _preference.setValue_forKey( + Some(&_yes), + ns_string!("allowsPictureInPictureMediaPlayback"), + ); #[cfg(target_os = "ios")] - let _: id = msg_send![config, setAllowsInlineMediaPlayback: YES]; + config.setValue_forKey(Some(&_yes), ns_string!("allowsInlineMediaPlayback")); if attributes.autoplay { - let _: id = msg_send![config, setMediaTypesRequiringUserActionForPlayback:0]; + config.setMediaTypesRequiringUserActionForPlayback( + WKAudiovisualMediaTypes::WKAudiovisualMediaTypeNone, + ); } - #[cfg(target_os = "macos")] - let _: id = msg_send![_preference, setValue:_yes forKey:NSString::new("tabFocusesLinks")]; - #[cfg(feature = "transparent")] if attributes.transparent { - let no: id = msg_send![class!(NSNumber), numberWithBool:0]; + let no = NSNumber::numberWithBool(false); // Equivalent Obj-C: - // [config setValue:@NO forKey:@"drawsBackground"]; - let _: id = msg_send![config, setValue:no forKey:NSString::new("drawsBackground")]; + config.setValue_forKey(Some(&no), ns_string!("drawsBackground")); } #[cfg(feature = "fullscreen")] // Equivalent Obj-C: - // [preference setValue:@YES forKey:@"fullScreenEnabled"]; - let _: id = msg_send![_preference, setValue:_yes forKey:NSString::new("fullScreenEnabled")]; + _preference.setValue_forKey(Some(&_yes), ns_string!("fullScreenEnabled")); #[cfg(target_os = "macos")] - { - use cocoa::appkit::NSWindow; + let webview = { + let window = ns_view.window().unwrap(); - let window: id = msg_send![ns_view, window]; let scale_factor = window.backingScaleFactor(); let (x, y) = attributes .bounds @@ -569,427 +325,147 @@ impl InnerWebView { }); let frame = CGRect { - origin: window_position(if is_child { ns_view } else { webview }, x, y, h as f64), + origin: if is_child { + window_position(ns_view, x, y, h as f64) + } else { + CGPoint::new(x as f64, (0 - y - h as i32) as f64) + }, size: CGSize::new(w as f64, h as f64), }; + let webview: Retained = + objc2::msg_send_id![super(webview), initWithFrame:frame configuration:&**config]; + webview + }; + #[cfg(target_os = "ios")] + let webview = { + let frame = ns_view.frame(); + let webview: Retained = + objc2::msg_send_id![super(webview), initWithFrame:frame configuration:&**config]; + webview + }; - let _: () = msg_send![webview, initWithFrame:frame configuration:config]; + #[cfg(target_os = "macos")] + { if is_child { // fixed element - webview.setAutoresizingMask_(NSViewMinYMargin); + webview.setAutoresizingMask(NSAutoresizingMaskOptions::NSViewMinYMargin); } else { // Auto-resize - webview.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); + webview.setAutoresizingMask( + NSAutoresizingMaskOptions::NSViewHeightSizable + | NSAutoresizingMaskOptions::NSViewWidthSizable, + ); } - } + // allowsBackForwardNavigation + webview.setAllowsBackForwardNavigationGestures(attributes.back_forward_navigation_gestures); + + // tabFocusesLinks + _preference.setValue_forKey(Some(&_yes), ns_string!("tabFocusesLinks")); + } #[cfg(target_os = "ios")] { - let frame: CGRect = msg_send![ns_view, frame]; // set all autoresizingmasks - let () = msg_send![webview, setAutoresizingMask: 31]; - let _: () = msg_send![webview, initWithFrame:frame configuration:config]; + webview.setAutoresizingMask(UIViewAutoresizing::from_bits(31).unwrap()); + // let () = msg_send![webview, setAutoresizingMask: 31]; // disable scroll bounce by default - let scroll: id = msg_send![webview, scrollView]; - let _: () = msg_send![scroll, setBounces: NO]; + // https://developer.apple.com/documentation/webkit/wkwebview/1614784-scrollview?language=objc + // But not exist in objc2-web-kit + let scroll_view: Retained = objc2::msg_send_id![&webview, scrollView]; + // let scroll_view: Retained = webview.ivars().scrollView; // FIXME: not test yet + scroll_view.setBounces(false) } if !attributes.visible { - let () = msg_send![webview, setHidden: YES]; + webview.setHidden(true); } #[cfg(any(debug_assertions, feature = "devtools"))] if attributes.devtools { - let has_inspectable_property: BOOL = - msg_send![webview, respondsToSelector: sel!(setInspectable:)]; - if has_inspectable_property == YES { - let _: () = msg_send![webview, setInspectable: YES]; + let has_inspectable_property: bool = + NSObject::respondsToSelector(&webview, objc2::sel!(setInspectable:)); + if has_inspectable_property { + webview.setInspectable(true); } // this cannot be on an `else` statement, it does not work on macOS :( - let dev = NSString::new("developerExtrasEnabled"); - let _: id = msg_send![_preference, setValue:_yes forKey:dev]; - } - - // allowsBackForwardNavigation - #[cfg(target_os = "macos")] - { - let value = attributes.back_forward_navigation_gestures; - let _: () = msg_send![webview, setAllowsBackForwardNavigationGestures: value]; + let dev = NSString::from_str("developerExtrasEnabled"); + _preference.setValue_forKey(Some(&_yes), &dev); } // Message handler - let ipc_handler_ptr = if let Some(ipc_handler) = attributes.ipc_handler { - let cls = ClassDecl::new("WebViewDelegate", class!(NSObject)); - let cls = match cls { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("function"); - cls.add_method( - sel!(userContentController:didReceiveScriptMessage:), - did_receive as extern "C" fn(&Object, Sel, id, id), - ); - cls.register() - } - None => class!(WebViewDelegate), - }; - let handler: id = msg_send![cls, new]; - let ipc_handler_ptr = Box::into_raw(Box::new(ipc_handler)); - - (*handler).set_ivar("function", ipc_handler_ptr as *mut _ as *mut c_void); - let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME); - let _: () = msg_send![manager, addScriptMessageHandler:handler name:ipc]; - ipc_handler_ptr + let ipc_handler_delegate = if let Some(ipc_handler) = attributes.ipc_handler { + let delegate = WryWebViewDelegate::new(manager.clone(), ipc_handler, mtm); + Some(delegate) } else { - null_mut() + None }; // Document title changed handler - let document_title_changed_handler = if let Some(document_title_changed_handler) = - attributes.document_title_changed_handler - { - let cls = ClassDecl::new("DocumentTitleChangedDelegate", class!(NSObject)); - let cls = match cls { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("function"); - cls.add_method( - sel!(observeValueForKeyPath:ofObject:change:context:), - observe_value_for_key_path as extern "C" fn(&Object, Sel, id, id, id, id), - ); - extern "C" fn observe_value_for_key_path( - this: &Object, - _sel: Sel, - key_path: id, - of_object: id, - _change: id, - _context: id, - ) { - let key = NSString(key_path); - if key.to_str() == "title" { - unsafe { - let function = this.get_ivar::<*mut c_void>("function"); - if !function.is_null() { - let function = &mut *(*function as *mut Box); - let title: id = msg_send![of_object, title]; - (function)(NSString(title).to_str().to_string()); - } - } - } - } - cls.register() - } - None => class!(DocumentTitleChangedDelegate), + let document_title_changed_observer = + if let Some(handler) = attributes.document_title_changed_handler { + let delegate = DocumentTitleChangedObserver::new(webview.clone(), handler); + Some(delegate) + } else { + None }; - let handler: id = msg_send![cls, new]; - let document_title_changed_handler = - Box::into_raw(Box::new(document_title_changed_handler)); - - (*handler).set_ivar( - "function", - document_title_changed_handler as *mut _ as *mut c_void, - ); - - let _: () = msg_send![webview, addObserver:handler forKeyPath:NSString::new("title") options:0x01 context:nil ]; - - document_title_changed_handler - } else { - null_mut() - }; - - // Navigation handler - extern "C" fn navigation_policy(this: &Object, _: Sel, _: id, action: id, handler: id) { - unsafe { - // shouldPerformDownload is only available on macOS 11.3+ - let can_download: BOOL = - msg_send![action, respondsToSelector: sel!(shouldPerformDownload)]; - let should_download: BOOL = if can_download == YES { - msg_send![action, shouldPerformDownload] - } else { - NO - }; - let request: id = msg_send![action, request]; - let url: id = msg_send![request, URL]; - let url: id = msg_send![url, absoluteString]; - let url = NSString(url); - let target_frame: id = msg_send![action, targetFrame]; - let is_main_frame: bool = msg_send![target_frame, isMainFrame]; - - let handler = handler as *mut block::Block<(NSInteger,), c_void>; - - if should_download == YES { - let has_download_handler = this.get_ivar::<*mut c_void>("HasDownloadHandler"); - if !has_download_handler.is_null() { - let has_download_handler = &mut *(*has_download_handler as *mut Box); - if **has_download_handler { - (*handler).call((2,)); - } else { - (*handler).call((0,)); - } - } else { - (*handler).call((0,)); - } - } else { - let function = this.get_ivar::<*mut c_void>("navigation_policy_function"); - if !function.is_null() { - let function = &mut *(*function as *mut Box Fn(String, bool) -> bool>); - match (function)(url.to_str().to_string(), is_main_frame) { - true => (*handler).call((1,)), - false => (*handler).call((0,)), - }; - } else { - (*handler).call((1,)); - } - } - } - } - - // Navigation handler - extern "C" fn navigation_policy_response( - this: &Object, - _: Sel, - _: id, - response: id, - handler: id, - ) { - unsafe { - let handler = handler as *mut block::Block<(NSInteger,), c_void>; - let can_show_mime_type: bool = msg_send![response, canShowMIMEType]; - - if !can_show_mime_type { - let has_download_handler = this.get_ivar::<*mut c_void>("HasDownloadHandler"); - if !has_download_handler.is_null() { - let has_download_handler = &mut *(*has_download_handler as *mut Box); - if **has_download_handler { - (*handler).call((2,)); - return; - } - } - } - - (*handler).call((1,)); - } - } - let pending_scripts = Arc::new(Mutex::new(Some(Vec::new()))); - - let navigation_delegate_cls = match ClassDecl::new("WryNavigationDelegate", class!(NSObject)) + let has_download_handler = attributes.download_started_handler.is_some(); + // Download handler + let download_delegate = if attributes.download_started_handler.is_some() + || attributes.download_completed_handler.is_some() { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("pending_scripts"); - cls.add_ivar::<*mut c_void>("HasDownloadHandler"); - cls.add_method( - sel!(webView:decidePolicyForNavigationAction:decisionHandler:), - navigation_policy as extern "C" fn(&Object, Sel, id, id, id), - ); - cls.add_method( - sel!(webView:decidePolicyForNavigationResponse:decisionHandler:), - navigation_policy_response as extern "C" fn(&Object, Sel, id, id, id), - ); - add_download_methods(&mut cls); - add_navigation_mathods(&mut cls); - cls.register() - } - None => class!(WryNavigationDelegate), - }; - - let navigation_policy_handler: id = msg_send![navigation_delegate_cls, new]; - - (*navigation_policy_handler).set_ivar( - "pending_scripts", - Box::into_raw(Box::new(pending_scripts.clone())) as *mut c_void, - ); - - let (navigation_decide_policy_ptr, download_delegate) = if attributes - .navigation_handler - .is_some() - || attributes.new_window_req_handler.is_some() - || attributes.download_started_handler.is_some() - { - let function_ptr = { - let navigation_handler = attributes.navigation_handler; - let new_window_req_handler = attributes.new_window_req_handler; - Box::into_raw(Box::new( - Box::new(move |url: String, is_main_frame: bool| -> bool { - if is_main_frame { - navigation_handler - .as_ref() - .map_or(true, |navigation_handler| (navigation_handler)(url)) - } else { - new_window_req_handler - .as_ref() - .map_or(true, |new_window_req_handler| (new_window_req_handler)(url)) - } - }) as Box bool>, - )) - }; - (*navigation_policy_handler).set_ivar( - "navigation_policy_function", - function_ptr as *mut _ as *mut c_void, + let delegate = WryDownloadDelegate::new( + attributes.download_started_handler, + attributes.download_completed_handler, + mtm, ); - - let has_download_handler = Box::into_raw(Box::new(Box::new( - attributes.download_started_handler.is_some(), - ))); - (*navigation_policy_handler).set_ivar( - "HasDownloadHandler", - has_download_handler as *mut _ as *mut c_void, - ); - - // Download handler - let download_delegate = if attributes.download_started_handler.is_some() - || attributes.download_completed_handler.is_some() - { - let cls = match ClassDecl::new("WryDownloadDelegate", class!(NSObject)) { - Some(mut cls) => { - cls.add_ivar::<*mut c_void>("started"); - cls.add_ivar::<*mut c_void>("completed"); - cls.add_method( - sel!(download:decideDestinationUsingResponse:suggestedFilename:completionHandler:), - download_policy as extern "C" fn(&Object, Sel, id, id, id, id), - ); - cls.add_method( - sel!(downloadDidFinish:), - download_did_finish as extern "C" fn(&Object, Sel, id), - ); - cls.add_method( - sel!(download:didFailWithError:resumeData:), - download_did_fail as extern "C" fn(&Object, Sel, id, id, id), - ); - cls.register() - } - None => class!(WryDownloadDelegate), - }; - - let download_delegate: id = msg_send![cls, new]; - if let Some(download_started_handler) = attributes.download_started_handler { - let download_started_ptr = Box::into_raw(Box::new(download_started_handler)); - (*download_delegate).set_ivar("started", download_started_ptr as *mut _ as *mut c_void); - } - if let Some(download_completed_handler) = attributes.download_completed_handler { - let download_completed_ptr = Box::into_raw(Box::new(download_completed_handler)); - (*download_delegate) - .set_ivar("completed", download_completed_ptr as *mut _ as *mut c_void); - } - - set_download_delegate(navigation_policy_handler, download_delegate); - - navigation_policy_handler - } else { - null_mut() - }; - - (function_ptr, download_delegate) + Some(delegate) } else { - (null_mut(), null_mut()) + None }; - let page_load_handler = set_navigation_methods( - navigation_policy_handler, - webview, + let navigation_policy_delegate = WryNavigationDelegate::new( + webview.clone(), + pending_scripts.clone(), + has_download_handler, + attributes.navigation_handler, + attributes.new_window_req_handler, + download_delegate.clone(), attributes.on_page_load_handler, + mtm, ); - let _: () = msg_send![webview, setNavigationDelegate: navigation_policy_handler]; - - // File upload panel handler - extern "C" fn run_file_upload_panel( - _this: &Object, - _: Sel, - _webview: id, - open_panel_params: id, - _frame: id, - handler: id, - ) { - unsafe { - let handler = handler as *mut block::Block<(id,), c_void>; - let cls = class!(NSOpenPanel); - let open_panel: id = msg_send![cls, openPanel]; - let _: () = msg_send![open_panel, setCanChooseFiles: YES]; - let allow_multi: BOOL = msg_send![open_panel_params, allowsMultipleSelection]; - let _: () = msg_send![open_panel, setAllowsMultipleSelection: allow_multi]; - let allow_dir: BOOL = msg_send![open_panel_params, allowsDirectories]; - let _: () = msg_send![open_panel, setCanChooseDirectories: allow_dir]; - let ok: NSInteger = msg_send![open_panel, runModal]; - if ok == 1 { - let url: id = msg_send![open_panel, URLs]; - (*handler).call((url,)); - } else { - (*handler).call((nil,)); - } - } - } - - extern "C" fn request_media_capture_permission( - _this: &Object, - _: Sel, - _webview: id, - _origin: id, - _frame: id, - _type: id, - decision_handler: id, - ) { - unsafe { - let decision_handler = decision_handler as *mut block::Block<(NSInteger,), c_void>; - //https://developer.apple.com/documentation/webkit/wkpermissiondecision?language=objc - (*decision_handler).call((1,)); - } - } - - let ui_delegate = match ClassDecl::new("WebViewUIDelegate", class!(NSObject)) { - Some(mut ctl) => { - ctl.add_method( - sel!(webView:runOpenPanelWithParameters:initiatedByFrame:completionHandler:), - run_file_upload_panel as extern "C" fn(&Object, Sel, id, id, id, id), - ); - - ctl.add_method( - sel!(webView:requestMediaCapturePermissionForOrigin:initiatedByFrame:type:decisionHandler:), - request_media_capture_permission as extern "C" fn(&Object, Sel, id, id, id, id, id), - ); + let proto_navigation_policy_delegate = + ProtocolObject::from_ref(navigation_policy_delegate.as_ref()); + webview.setNavigationDelegate(Some(proto_navigation_policy_delegate)); - ctl.register() - } - None => class!(WebViewUIDelegate), - }; - let ui_delegate: id = msg_send![ui_delegate, new]; - let _: () = msg_send![webview, setUIDelegate: ui_delegate]; - - // File drop handling - #[cfg(target_os = "macos")] - let drag_drop_ptr = match attributes.drag_drop_handler { - // if we have a drag_drop_handler defined, use the defined handler - Some(drag_drop_handler) => set_drag_drop_handler(webview, drag_drop_handler), - // prevent panic by using a blank handler - None => set_drag_drop_handler(webview, Box::new(|_| false)), - }; + let ui_delegate: Retained = WryWebViewUIDelegate::new(mtm); + let proto_ui_delegate = ProtocolObject::from_ref(ui_delegate.as_ref()); + webview.setUIDelegate(Some(proto_ui_delegate)); // ns window is required for the print operation #[cfg(target_os = "macos")] { - let ns_window: id = msg_send![ns_view, window]; - - let can_set_titlebar_style: BOOL = msg_send![ - ns_window, - respondsToSelector: sel!(setTitlebarSeparatorStyle:) - ]; - if can_set_titlebar_style == YES { - // `1` means `none`, see https://developer.apple.com/documentation/appkit/nstitlebarseparatorstyle/none - let () = msg_send![ns_window, setTitlebarSeparatorStyle: 1]; + let ns_window = ns_view.window().unwrap(); + let can_set_titlebar_style = + ns_window.respondsToSelector(objc2::sel!(setTitlebarSeparatorStyle:)); + if can_set_titlebar_style { + ns_window.setTitlebarSeparatorStyle(NSTitlebarSeparatorStyle::None); } } let w = Self { id: webview_id, - webview, - manager, + webview: webview.clone(), + manager: manager.clone(), pending_scripts, - ipc_handler_ptr, - document_title_changed_handler, - navigation_decide_policy_ptr, - #[cfg(target_os = "macos")] - drag_drop_ptr, - page_load_handler, + ipc_handler_delegate, + document_title_changed_observer, + navigation_policy_delegate, download_delegate, + ui_delegate, protocol_ptrs, is_child, }; @@ -1020,50 +496,31 @@ r#"Object.defineProperty(window, 'ipc', { #[cfg(target_os = "macos")] { if is_child { - let _: () = msg_send![ns_view, addSubview: webview]; + ns_view.addSubview(&webview); } else { - let parent_view_cls = match ClassDecl::new("WryWebViewParent", class!(NSView)) { - Some(mut decl) => { - decl.add_method( - sel!(keyDown:), - key_down as extern "C" fn(&mut Object, Sel, id), - ); - - extern "C" fn key_down(_this: &mut Object, _sel: Sel, event: id) { - unsafe { - let app = cocoa::appkit::NSApp(); - let menu: id = msg_send![app, mainMenu]; - let () = msg_send![menu, performKeyEquivalent: event]; - } - } - - decl.register() - } - None => class!(NSView), - }; - - let parent_view: id = msg_send![parent_view_cls, alloc]; - let _: () = msg_send![parent_view, init]; - parent_view.setAutoresizingMask_(NSViewHeightSizable | NSViewWidthSizable); - let _: () = msg_send![parent_view, addSubview: webview]; + let parent_view = WryWebViewParent::new(mtm); + parent_view.setAutoresizingMask( + NSAutoresizingMaskOptions::NSViewHeightSizable + | NSAutoresizingMaskOptions::NSViewWidthSizable, + ); + parent_view.addSubview(&webview.clone()); // inject the webview into the window - let ns_window: id = msg_send![ns_view, window]; + let ns_window = ns_view.window().unwrap(); // Tell the webview receive keyboard events in the window. // See https://github.com/tauri-apps/wry/issues/739 - let _: () = msg_send![ns_window, setContentView: parent_view]; - let _: () = msg_send![ns_window, makeFirstResponder: webview]; + ns_window.setContentView(Some(&parent_view)); + ns_window.makeFirstResponder(Some(&webview)); } // make sure the window is always on top when we create a new webview - let app_class = class!(NSApplication); - let app: id = msg_send![app_class, sharedApplication]; - let _: () = msg_send![app, activateIgnoringOtherApps: YES]; + let app = NSApplication::sharedApplication(mtm); + NSApplication::activate(&app); } #[cfg(target_os = "ios")] { - let _: () = msg_send![ns_view, addSubview: webview]; + ns_view.addSubview(&webview); } Ok(w) @@ -1075,7 +532,7 @@ r#"Object.defineProperty(window, 'ipc', { } pub fn url(&self) -> crate::Result { - url_from_webview(self.webview) + url_from_webview(&self.webview) } pub fn eval(&self, js: &str, callback: Option) -> Result<()> { @@ -1089,36 +546,45 @@ r#"Object.defineProperty(window, 'ipc', { // we need to check if the callback exists outside the handler otherwise it's a segfault if let Some(callback) = callback { - let handler = block::ConcreteBlock::new(move |val: id, _err: id| { + let handler = block2::RcBlock::new(move |val: *mut AnyObject, _err: *mut NSError| { #[cfg(feature = "tracing")] span.lock().unwrap().take(); let mut result = String::new(); - if val != nil { - let serializer = class!(NSJSONSerialization); - let json_ns_data: NSData = msg_send![serializer, dataWithJSONObject:val options:NS_JSON_WRITING_FRAGMENTS_ALLOWED error:nil]; - let json_string = NSString::from(json_ns_data); - - result = json_string.to_str().to_string(); + if !val.is_null() { + let json_ns_data = NSJSONSerialization::dataWithJSONObject_options_error( + &*val, + objc2_foundation::NSJSONWritingOptions::NSJSONWritingFragmentsAllowed, + ) + .unwrap(); + let json_string = NSString::alloc(); + let json_string = + NSString::initWithData_encoding(json_string, &json_ns_data, NSUTF8StringEncoding) + .unwrap(); + + result = json_string.to_string(); } callback(result); - }).copy(); + }); - let _: () = - msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler]; + self + .webview + .evaluateJavaScript_completionHandler(&NSString::from_str(js), Some(&handler)); } else { #[cfg(feature = "tracing")] - let handler = block::ConcreteBlock::new(move |_val: id, _err: id| { - span.lock().unwrap().take(); - }) - .copy(); + let handler = Some(block2::RcBlock::new( + move |_val: *mut AnyObject, _err: *mut NSError| { + span.lock().unwrap().take(); + }, + )); #[cfg(not(feature = "tracing"))] - let handler = null::<*const c_void>(); + let handler: Option> = None; - let _: () = - msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler]; + self + .webview + .evaluateJavaScript_completionHandler(&NSString::from_str(js), handler.as_deref()); } } } @@ -1128,14 +594,16 @@ r#"Object.defineProperty(window, 'ipc', { fn init(&self, js: &str) { // Safety: objc runtime calls are unsafe - // Equivalent Obj-C: - // [manager addUserScript:[[WKUserScript alloc] initWithSource:[NSString stringWithUTF8String:js.c_str()] injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]] unsafe { - let userscript: id = msg_send![class!(WKUserScript), alloc]; - let script: id = + let userscript = WKUserScript::alloc(); // TODO: feature to allow injecting into subframes - msg_send![userscript, initWithSource:NSString::new(js) injectionTime:0 forMainFrameOnly:1]; - let _: () = msg_send![self.manager, addUserScript: script]; + let script = WKUserScript::initWithSource_injectionTime_forMainFrameOnly( + userscript, + &NSString::from_str(js), + WKUserScriptInjectionTime::AtDocumentStart, + true, + ); + self.manager.addUserScript(&script); } } @@ -1154,12 +622,12 @@ r#"Object.defineProperty(window, 'ipc', { pub fn clear_all_browsing_data(&self) -> Result<()> { unsafe { - let config: id = msg_send![self.webview, configuration]; - let store: id = msg_send![config, websiteDataStore]; - let all_data_types: id = msg_send![class!(WKWebsiteDataStore), allWebsiteDataTypes]; - let date: id = msg_send![class!(NSDate), dateWithTimeIntervalSince1970: 0.0]; - let handler = block::ConcreteBlock::new(|| {}).copy(); - let _: () = msg_send![store, removeDataOfTypes:all_data_types modifiedSince:date completionHandler:handler]; + let config = self.webview.configuration(); + let store = config.websiteDataStore(); + let all_data_types = WKWebsiteDataStore::allWebsiteDataTypes(); + let date = NSDate::dateWithTimeIntervalSince1970(0.0); + let handler = block2::RcBlock::new(|| {}); + store.removeDataOfTypes_modifiedSince_completionHandler(&all_data_types, &date, &handler); } Ok(()) } @@ -1167,16 +635,16 @@ r#"Object.defineProperty(window, 'ipc', { fn navigate_to_url(&self, url: &str, headers: Option) -> crate::Result<()> { // Safety: objc runtime calls are unsafe unsafe { - let url: id = msg_send![class!(NSURL), URLWithString: NSString::new(url)]; - let request: id = msg_send![class!(NSMutableURLRequest), requestWithURL: url]; + let url = NSURL::URLWithString(&NSString::from_str(url)).unwrap(); + let mut request = NSMutableURLRequest::requestWithURL(&url); if let Some(headers) = headers { for (name, value) in headers.iter() { - let key = NSString::new(name.as_str()); - let value = NSString::new(value.to_str().unwrap_or_default()); - let _: () = msg_send![request, addValue:value.as_ptr() forHTTPHeaderField:key.as_ptr()]; + let key = NSString::from_str(name.as_str()); + let value = NSString::from_str(value.to_str().unwrap_or_default()); + request.addValue_forHTTPHeaderField(&value, &key); } } - let () = msg_send![self.webview, loadRequest: request]; + self.webview.loadRequest(&request); } Ok(()) @@ -1185,13 +653,17 @@ r#"Object.defineProperty(window, 'ipc', { fn navigate_to_string(&self, html: &str) { // Safety: objc runtime calls are unsafe unsafe { - let () = msg_send![self.webview, loadHTMLString:NSString::new(html) baseURL:nil]; + self + .webview + .loadHTMLString_baseURL(&NSString::from_str(html), None); } } fn set_user_agent(&self, user_agent: &str) { unsafe { - let () = msg_send![self.webview, setCustomUserAgent: NSString::new(user_agent)]; + self + .webview + .setCustomUserAgent(Some(&NSString::from_str(user_agent))); } } @@ -1199,29 +671,36 @@ r#"Object.defineProperty(window, 'ipc', { self.print_with_options(&PrintOptions::default()) } - pub fn print_with_options(&self, options: &PrintOptions) -> crate::Result<()> { + pub fn print_with_options(&self, _options: &PrintOptions) -> crate::Result<()> { // Safety: objc runtime calls are unsafe #[cfg(target_os = "macos")] unsafe { - let can_print: BOOL = msg_send![ - self.webview, - respondsToSelector: sel!(printOperationWithPrintInfo:) - ]; - if can_print == YES { + let can_print = self + .webview + .respondsToSelector(objc2::sel!(printOperationWithPrintInfo:)); + if can_print { // Create a shared print info - let print_info: id = msg_send![class!(NSPrintInfo), sharedPrintInfo]; - let print_info: id = msg_send![print_info, init]; - let () = msg_send![print_info, setTopMargin:CGFloat::from(options.margins.top)]; - let () = msg_send![print_info, setRightMargin:CGFloat::from(options.margins.right)]; - let () = msg_send![print_info, setBottomMargin:CGFloat::from(options.margins.bottom)]; - let () = msg_send![print_info, setLeftMargin:CGFloat::from(options.margins.left)]; + let print_info = objc2_app_kit::NSPrintInfo::sharedPrintInfo(); + // let print_info: id = msg_send![print_info, init]; + print_info.setTopMargin(_options.margins.top.into()); + print_info.setRightMargin(_options.margins.right.into()); + print_info.setBottomMargin(_options.margins.bottom.into()); + print_info.setLeftMargin(_options.margins.left.into()); + // Create new print operation from the webview content - let print_operation: id = msg_send![self.webview, printOperationWithPrintInfo: print_info]; + let print_operation = self.webview.printOperationWithPrintInfo(&print_info); + // Allow the modal to detach from the current thread and be non-blocker - let () = msg_send![print_operation, setCanSpawnSeparateThread: YES]; + print_operation.setCanSpawnSeparateThread(true); + // Launch the modal - let window: id = msg_send![self.webview, window]; - let () = msg_send![print_operation, runOperationModalForWindow: window delegate: null::<*const c_void>() didRunSelector: null::<*const c_void>() contextInfo: null::<*const c_void>()]; + let window = self.webview.window().unwrap(); + print_operation.runOperationModalForWindow_delegate_didRunSelector_contextInfo( + &window, + None, + None, + null_mut(), + ) } } @@ -1233,8 +712,8 @@ r#"Object.defineProperty(window, 'ipc', { #[cfg(target_os = "macos")] unsafe { // taken from - let tool: id = msg_send![self.webview, _inspector]; - let _: id = msg_send![tool, show]; + let tool: Retained = objc2::msg_send_id![&self.webview, _inspector]; + let () = objc2::msg_send![&tool, show]; } } @@ -1243,8 +722,8 @@ r#"Object.defineProperty(window, 'ipc', { #[cfg(target_os = "macos")] unsafe { // taken from - let tool: id = msg_send![self.webview, _inspector]; - let _: id = msg_send![tool, close]; + let tool: Retained = objc2::msg_send_id![&self.webview, _inspector]; + let () = objc2::msg_send![&tool, close]; } } @@ -1253,9 +732,9 @@ r#"Object.defineProperty(window, 'ipc', { #[cfg(target_os = "macos")] unsafe { // taken from - let tool: id = msg_send![self.webview, _inspector]; - let is_visible: objc::runtime::BOOL = msg_send![tool, isVisible]; - is_visible == objc::runtime::YES + let tool: Retained = objc2::msg_send_id![&self.webview, _inspector]; + let is_visible: bool = objc2::msg_send![&tool, isVisible]; + is_visible } #[cfg(not(target_os = "macos"))] false @@ -1263,7 +742,7 @@ r#"Object.defineProperty(window, 'ipc', { pub fn zoom(&self, scale_factor: f64) -> crate::Result<()> { unsafe { - let _: () = msg_send![self.webview, setPageZoom: scale_factor]; + self.webview.setPageZoom(scale_factor); } Ok(()) @@ -1274,10 +753,11 @@ r#"Object.defineProperty(window, 'ipc', { } pub fn bounds(&self) -> crate::Result { + #[allow(unused_unsafe)] unsafe { - let parent: id = msg_send![self.webview, superview]; - let parent_frame: CGRect = msg_send![parent, frame]; - let webview_frame: CGRect = msg_send![self.webview, frame]; + let parent = self.webview.superview().unwrap(); + let parent_frame = parent.frame(); + let webview_frame = self.webview.frame(); Ok(Rect { position: LogicalPosition::new( @@ -1293,19 +773,18 @@ r#"Object.defineProperty(window, 'ipc', { pub fn set_bounds(&self, #[allow(unused)] bounds: Rect) -> crate::Result<()> { #[cfg(target_os = "macos")] if self.is_child { - use cocoa::appkit::NSWindow; - - let window: id = unsafe { msg_send![self.webview, window] }; - let scale_factor = unsafe { window.backingScaleFactor() }; + let window = self.webview.window().unwrap(); + let scale_factor = window.backingScaleFactor(); let (x, y) = bounds.position.to_logical::(scale_factor).into(); let (width, height) = bounds.size.to_logical::(scale_factor).into(); unsafe { + let parent_view = self.webview.superview().unwrap(); let frame = CGRect { - origin: window_position(msg_send![self.webview, superview], x, y, height), + origin: window_position(&parent_view, x, y, height), size: CGSize::new(width, height), }; - let () = msg_send![self.webview, setFrame: frame]; + self.webview.setFrame(frame); } } @@ -1313,44 +792,41 @@ r#"Object.defineProperty(window, 'ipc', { } pub fn set_visible(&self, visible: bool) -> Result<()> { - unsafe { - let () = msg_send![self.webview, setHidden: !visible]; - } - + self.webview.setHidden(!visible); Ok(()) } pub fn focus(&self) -> Result<()> { - unsafe { - let window: id = msg_send![self.webview, window]; - let _: () = msg_send![window, makeFirstResponder: self.webview]; + #[cfg(target_os = "macos")] + { + let window = self.webview.window().unwrap(); + window.makeFirstResponder(Some(&self.webview)); } - Ok(()) } #[cfg(target_os = "macos")] - pub(crate) fn reparent(&self, window: id) -> crate::Result<()> { + pub(crate) fn reparent(&self, window: *mut NSWindow) -> crate::Result<()> { unsafe { - let content_view: id = msg_send![window, contentView]; - let _: () = msg_send![content_view, addSubview: self.webview]; + let content_view = (*window).contentView().unwrap(); + content_view.addSubview(&self.webview); } Ok(()) } } -pub fn url_from_webview(webview: id) -> Result { - let url_obj: *mut Object = unsafe { msg_send![webview, URL] }; - let absolute_url: *mut Object = unsafe { msg_send![url_obj, absoluteString] }; +pub fn url_from_webview(webview: &WKWebView) -> Result { + let url_obj = unsafe { webview.URL().unwrap() }; + let absolute_url = unsafe { url_obj.absoluteString().unwrap() }; let bytes = { - let bytes: *const c_char = unsafe { msg_send![absolute_url, UTF8String] }; + let bytes: *const c_char = absolute_url.UTF8String(); bytes as *const u8 }; // 4 represents utf8 encoding - let len = unsafe { msg_send![absolute_url, lengthOfBytesUsingEncoding: 4] }; + let len = absolute_url.lengthOfBytesUsingEncoding(4); let bytes = unsafe { std::slice::from_raw_parts(bytes, len) }; std::str::from_utf8(bytes) @@ -1360,15 +836,15 @@ pub fn url_from_webview(webview: id) -> Result { pub fn platform_webview_version() -> Result { unsafe { - let bundle: id = - msg_send![class!(NSBundle), bundleWithIdentifier: NSString::new("com.apple.WebKit")]; - let dict: id = msg_send![bundle, infoDictionary]; - let webkit_version: id = msg_send![dict, objectForKey: NSString::new("CFBundleVersion")]; - - let nsstring = NSString(webkit_version); + let bundle = NSBundle::bundleWithIdentifier(&NSString::from_str("com.apple.WebKit")).unwrap(); + let dict = bundle.infoDictionary().unwrap(); + let webkit_version = dict + .objectForKey(&NSString::from_str("CFBundleVersion")) + .unwrap(); + let webkit_version = Retained::cast::(webkit_version); - let () = msg_send![bundle, unload]; - Ok(nsstring.to_str().to_string()) + bundle.unload(); + Ok(webkit_version.to_string()) } } @@ -1378,30 +854,13 @@ impl Drop for InnerWebView { // We need to drop handler closures here unsafe { - if !self.ipc_handler_ptr.is_null() { - drop(Box::from_raw(self.ipc_handler_ptr)); - - let ipc = NSString::new(IPC_MESSAGE_HANDLER_NAME); - let _: () = msg_send![self.manager, removeScriptMessageHandlerForName: ipc]; - } - - if !self.document_title_changed_handler.is_null() { - drop(Box::from_raw(self.document_title_changed_handler)); - } - - if !self.navigation_decide_policy_ptr.is_null() { - drop(Box::from_raw(self.navigation_decide_policy_ptr)); - } - - drop_navigation_methods(self); - - #[cfg(target_os = "macos")] - if !self.drag_drop_ptr.is_null() { - drop(Box::from_raw(self.drag_drop_ptr)); - } - - if !self.download_delegate.is_null() { - self.download_delegate.drop_in_place(); + if let Some(ipc_handler) = self.ipc_handler_delegate.take() { + let ipc = NSString::from_str(IPC_MESSAGE_HANDLER_NAME); + // this will decrease the retain count of the ipc handler and trigger the drop + ipc_handler + .ivars() + .controller + .removeScriptMessageHandlerForName(&ipc); } for ptr in self.protocol_ptrs.iter() { @@ -1411,91 +870,18 @@ impl Drop for InnerWebView { } // Remove webview from window's NSView before dropping. - let () = msg_send![self.webview, removeFromSuperview]; - let _: Id<_> = Id::from_retained_ptr(self.webview); - let _: Id<_> = Id::from_retained_ptr(self.manager); - } - } -} - -const UTF8_ENCODING: usize = 4; - -struct NSUUID(id); - -impl NSUUID { - fn new(data: &[u8; 16]) -> Self { - NSUUID(unsafe { - let ns_uuid: id = msg_send![class!(NSUUID), alloc]; - let ns_uuid: id = msg_send![ns_uuid, initWithUUIDBytes:data.as_ptr()]; - - let _: () = msg_send![ns_uuid, autorelease]; - - ns_uuid - }) - } -} - -struct NSString(id); - -impl NSString { - fn new(s: &str) -> Self { - // Safety: objc runtime calls are unsafe - NSString(unsafe { - let ns_string: id = msg_send![class!(NSString), alloc]; - let ns_string: id = msg_send![ns_string, - initWithBytes:s.as_ptr() - length:s.len() - encoding:UTF8_ENCODING]; - - // The thing is allocated in rust, the thing must be set to autorelease in rust to relinquish control - // or it can not be released correctly in OC runtime - let _: () = msg_send![ns_string, autorelease]; - - ns_string - }) - } - - fn to_str(&self) -> &str { - unsafe { - let bytes: *const c_char = msg_send![self.0, UTF8String]; - let len = msg_send![self.0, lengthOfBytesUsingEncoding: UTF8_ENCODING]; - let bytes = slice::from_raw_parts(bytes as *const u8, len); - str::from_utf8_unchecked(bytes) - } - } - - #[allow(dead_code)] // only used when `mac-proxy` feature is enabled - fn to_cstr(&self) -> *const c_char { - unsafe { - let utf_8_string = msg_send![self.0, UTF8String]; - utf_8_string + self.webview.removeFromSuperview(); + self.webview.retain(); + self.manager.retain(); } } - - fn as_ptr(&self) -> id { - self.0 - } } -impl From for NSString { - fn from(value: NSData) -> Self { - Self(unsafe { - let ns_string: id = msg_send![class!(NSString), alloc]; - let ns_string: id = msg_send![ns_string, initWithData:value encoding:UTF8_ENCODING]; - let _: () = msg_send![ns_string, autorelease]; - - ns_string - }) - } -} - -#[allow(dead_code)] // rustc complains `id` is unused, but it is actually used from Objective-C -struct NSData(id); - /// Converts from wry screen-coordinates to macOS screen-coordinates. /// wry: top-left is (0, 0) and y increasing downwards /// macOS: bottom-left is (0, 0) and y increasing upwards -unsafe fn window_position(view: id, x: i32, y: i32, height: f64) -> CGPoint { - let frame: CGRect = msg_send![view, frame]; +#[allow(dead_code)] +unsafe fn window_position(view: &NSView, x: i32, y: i32, height: f64) -> CGPoint { + let frame: CGRect = view.frame(); CGPoint::new(x as f64, frame.size.height - y as f64 - height) } diff --git a/src/wkwebview/navigation.rs b/src/wkwebview/navigation.rs index 567ce4ce7..9947412b4 100644 --- a/src/wkwebview/navigation.rs +++ b/src/wkwebview/navigation.rs @@ -1,86 +1,106 @@ -use std::{ - ffi::c_void, - ptr::{null, null_mut}, - sync::{Arc, Mutex}, +use objc2::DeclaredClass; +use objc2_foundation::{NSObjectProtocol, NSString}; +use objc2_web_kit::{ + WKNavigation, WKNavigationAction, WKNavigationActionPolicy, WKNavigationResponse, + WKNavigationResponsePolicy, }; -use cocoa::base::id; -use objc::{ - declare::ClassDecl, - runtime::{Object, Sel}, -}; +#[cfg(target_os = "ios")] +use crate::wkwebview::ios::WKWebView::WKWebView; +#[cfg(target_os = "macos")] +use objc2_web_kit::WKWebView; -use super::{url_from_webview, InnerWebView, NSString}; use crate::PageLoadEvent; -extern "C" fn did_commit_navigation(this: &Object, _: Sel, webview: id, _navigation: id) { +use super::class::wry_navigation_delegate::WryNavigationDelegate; + +pub(crate) fn did_commit_navigation( + this: &WryNavigationDelegate, + webview: &WKWebView, + _navigation: &WKNavigation, +) { unsafe { // Call on_load_handler - let on_page_load = this.get_ivar::<*mut c_void>("on_page_load_function"); - if !on_page_load.is_null() { - let on_page_load = &mut *(*on_page_load as *mut Box); + if let Some(on_page_load) = &this.ivars().on_page_load_handler { on_page_load(PageLoadEvent::Started); } // Inject scripts - let pending_scripts_ptr: *mut c_void = *this.get_ivar("pending_scripts"); - let pending_scripts = &(*(pending_scripts_ptr as *mut Arc>>>)); - let mut pending_scripts_ = pending_scripts.lock().unwrap(); - if let Some(pending_scripts) = &*pending_scripts_ { - for script in pending_scripts { - let _: id = msg_send![webview, evaluateJavaScript:NSString::new(script) completionHandler:null::<*const c_void>()]; + let mut pending_scripts = this.ivars().pending_scripts.lock().unwrap(); + if let Some(scripts) = &*pending_scripts { + for script in scripts { + webview.evaluateJavaScript_completionHandler(&NSString::from_str(script), None); } - *pending_scripts_ = None; + *pending_scripts = None; } } } -extern "C" fn did_finish_navigation(this: &Object, _: Sel, _webview: id, _navigation: id) { - unsafe { - // Call on_load_handler - let on_page_load = this.get_ivar::<*mut c_void>("on_page_load_function"); - if !on_page_load.is_null() { - let on_page_load = &mut *(*on_page_load as *mut Box); - on_page_load(PageLoadEvent::Finished); - } +pub(crate) fn did_finish_navigation( + this: &WryNavigationDelegate, + _webview: &WKWebView, + _navigation: &WKNavigation, +) { + if let Some(on_page_load) = &this.ivars().on_page_load_handler { + on_page_load(PageLoadEvent::Finished); } } -pub(crate) unsafe fn add_navigation_mathods(cls: &mut ClassDecl) { - cls.add_ivar::<*mut c_void>("navigation_policy_function"); - cls.add_ivar::<*mut c_void>("on_page_load_function"); - - cls.add_method( - sel!(webView:didFinishNavigation:), - did_finish_navigation as extern "C" fn(&Object, Sel, id, id), - ); - cls.add_method( - sel!(webView:didCommitNavigation:), - did_commit_navigation as extern "C" fn(&Object, Sel, id, id), - ); -} +// Navigation handler +pub(crate) fn navigation_policy( + this: &WryNavigationDelegate, + _webview: &WKWebView, + action: &WKNavigationAction, + handler: &block2::Block, +) { + unsafe { + // shouldPerformDownload is only available on macOS 11.3+ + let can_download = action.respondsToSelector(objc2::sel!(shouldPerformDownload)); + let should_download: bool = if can_download { + action.shouldPerformDownload() + } else { + false + }; + let request = action.request(); + let url = request.URL().unwrap().absoluteString().unwrap(); + let target_frame = action.targetFrame(); + let is_main_frame = target_frame.map_or(false, |frame| frame.isMainFrame()); -pub(crate) unsafe fn drop_navigation_methods(inner: &mut InnerWebView) { - if !inner.page_load_handler.is_null() { - drop(Box::from_raw(inner.page_load_handler)) + if should_download { + let has_download_handler = this.ivars().has_download_handler; + if has_download_handler { + (*handler).call((WKNavigationActionPolicy::Download,)); + } else { + (*handler).call((WKNavigationActionPolicy::Cancel,)); + } + } else { + let function = &this.ivars().navigation_policy_function; + match function(url.to_string(), is_main_frame) { + true => (*handler).call((WKNavigationActionPolicy::Allow,)), + false => (*handler).call((WKNavigationActionPolicy::Cancel,)), + }; + } } } -pub(crate) unsafe fn set_navigation_methods( - navigation_policy_handler: *mut Object, - webview: id, - on_page_load_handler: Option>, -) -> *mut Box { - if let Some(on_page_load_handler) = on_page_load_handler { - let on_page_load_handler = Box::into_raw(Box::new(Box::new(move |event| { - on_page_load_handler(event, url_from_webview(webview).unwrap_or_default()); - }) as Box)); - (*navigation_policy_handler).set_ivar( - "on_page_load_function", - on_page_load_handler as *mut _ as *mut c_void, - ); - on_page_load_handler - } else { - null_mut() +// Navigation handler +pub(crate) fn navigation_policy_response( + this: &WryNavigationDelegate, + _webview: &WKWebView, + response: &WKNavigationResponse, + handler: &block2::Block, +) { + unsafe { + let can_show_mime_type = response.canShowMIMEType(); + + if !can_show_mime_type { + let has_download_handler = this.ivars().has_download_handler; + if has_download_handler { + (*handler).call((WKNavigationResponsePolicy::Download,)); + return; + } + } + + (*handler).call((WKNavigationResponsePolicy::Allow,)); } } diff --git a/src/wkwebview/proxy.rs b/src/wkwebview/proxy.rs index 08290d167..b55ec6475 100644 --- a/src/wkwebview/proxy.rs +++ b/src/wkwebview/proxy.rs @@ -2,46 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use cocoa::base::nil; -use std::ffi::c_char; +use objc2_foundation::NSObject; +use std::ffi::{c_char, CString}; use crate::{proxy::ProxyEndpoint, Error}; -use super::NSString; - -#[allow(non_camel_case_types)] -pub type nw_endpoint_t = *mut objc::runtime::Object; #[allow(non_camel_case_types)] -pub type nw_relay_hop_t = *mut objc::runtime::Object; +pub type nw_endpoint_t = *mut NSObject; #[allow(non_camel_case_types)] -pub type nw_protocol_options_t = *mut objc::runtime::Object; +pub type nw_protocol_options_t = *mut NSObject; #[allow(non_camel_case_types)] -pub type nw_proxy_config_t = *mut objc::runtime::Object; +pub type nw_proxy_config_t = *mut NSObject; #[link(name = "Network", kind = "framework")] extern "C" { - #[allow(dead_code)] - fn nw_endpoint_create_url(url: *const c_char) -> nw_endpoint_t; - #[allow(dead_code)] - fn nw_endpoint_get_url(endpoint: nw_endpoint_t) -> *const c_char; fn nw_endpoint_create_host(host: *const c_char, port: *const c_char) -> nw_endpoint_t; - #[allow(dead_code)] - fn nw_proxy_config_set_username_and_password( - proxy_config: nw_proxy_config_t, - username: *const c_char, - password: *const c_char, - ); - #[allow(dead_code)] - fn nw_relay_hop_create( - http3_relay_endpoint: nw_endpoint_t, - http2_relay_endpoint: nw_endpoint_t, - relay_tls_options: nw_protocol_options_t, - ) -> nw_relay_hop_t; - #[allow(dead_code)] - fn nw_proxy_config_create_relay( - first_hop: nw_relay_hop_t, - second_hop: nw_relay_hop_t, - ) -> nw_proxy_config_t; pub fn nw_proxy_config_create_socksv5(proxy_endpoint: nw_endpoint_t) -> nw_proxy_config_t; pub fn nw_proxy_config_create_http_connect( proxy_endpoint: nw_endpoint_t, @@ -53,14 +28,16 @@ impl TryFrom for nw_endpoint_t { type Error = Error; fn try_from(endpoint: ProxyEndpoint) -> Result { unsafe { - let endpoint_host = NSString::new(&endpoint.host).to_cstr(); - let endpoint_port = NSString::new(&endpoint.port).to_cstr(); - let endpoint = nw_endpoint_create_host(endpoint_host, endpoint_port); + let endpoint_host = + CString::new(endpoint.host).map_err(|_| Error::ProxyEndpointCreationFailed)?; + let endpoint_port = + CString::new(endpoint.port).map_err(|_| Error::ProxyEndpointCreationFailed)?; + let endpoint = nw_endpoint_create_host(endpoint_host.as_ptr(), endpoint_port.as_ptr()); - match endpoint { - #[allow(non_upper_case_globals)] - nil => Err(Error::ProxyEndpointCreationFailed), - _ => Ok(endpoint), + if endpoint.is_null() { + Err(Error::ProxyEndpointCreationFailed) + } else { + Ok(endpoint) } } } diff --git a/src/wkwebview/synthetic_mouse_events.rs b/src/wkwebview/synthetic_mouse_events.rs index d965090b9..97c427313 100644 --- a/src/wkwebview/synthetic_mouse_events.rs +++ b/src/wkwebview/synthetic_mouse_events.rs @@ -1,85 +1,76 @@ -use super::NSString; -use cocoa::{ - appkit::{NSEvent, NSEventModifierFlags, NSEventType, NSView}, - base::{id, nil}, +use objc2_app_kit::{ + NSAlternateKeyMask, NSCommandKeyMask, NSControlKeyMask, NSEvent, NSEventType, NSShiftKeyMask, + NSView, }; -use objc::{ - declare::ClassDecl, - runtime::{Object, Sel}, -}; -use std::{ffi::c_void, ptr::null}; +use objc2_foundation::NSString; -pub unsafe fn setup(decl: &mut ClassDecl) { - decl.add_method( - sel!(otherMouseDown:), - other_mouse_down as extern "C" fn(&mut Object, Sel, id), - ); - decl.add_method( - sel!(otherMouseUp:), - other_mouse_up as extern "C" fn(&mut Object, Sel, id), - ); -} +use super::WryWebView; -extern "C" fn other_mouse_down(this: &mut Object, _sel: Sel, event: id) { +pub(crate) fn other_mouse_down(this: &WryWebView, event: &NSEvent) { unsafe { - if event.eventType() == NSEventType::NSOtherMouseDown { + if event.r#type() == NSEventType::OtherMouseDown { let button_number = event.buttonNumber(); match button_number { // back button 3 => { let js = create_js_mouse_event(this, event, true, true); - let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()]; + this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); return; } // forward button 4 => { let js = create_js_mouse_event(this, event, true, false); - let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()]; + this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); return; } _ => {} } } - let _: () = msg_send![this, mouseDown: event]; + this.mouseDown(event); } } -extern "C" fn other_mouse_up(this: &mut Object, _sel: Sel, event: id) { +pub(crate) fn other_mouse_up(this: &WryWebView, event: &NSEvent) { unsafe { - if event.eventType() == NSEventType::NSOtherMouseUp { + if event.r#type() == NSEventType::OtherMouseUp { let button_number = event.buttonNumber(); match button_number { // back button 3 => { let js = create_js_mouse_event(this, event, false, true); - let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()]; + this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); return; } // forward button 4 => { let js = create_js_mouse_event(this, event, false, false); - let _: id = msg_send![this, evaluateJavaScript:NSString::new(&js) completionHandler:null::<*const c_void>()]; + this.evaluateJavaScript_completionHandler(&NSString::from_str(&js), None); return; } _ => {} } } - let _: () = msg_send![this, mouseUp: event]; + this.mouseUp(event); } } -unsafe fn create_js_mouse_event(view: id, event: id, down: bool, back_button: bool) -> String { +unsafe fn create_js_mouse_event( + view: &NSView, + event: &NSEvent, + down: bool, + back_button: bool, +) -> String { let event_name = if down { "mousedown" } else { "mouseup" }; // js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button let button = if back_button { 3 } else { 4 }; let mods_flags = event.modifierFlags(); let window_point = event.locationInWindow(); - let view_point = view.convertPoint_fromView_(window_point, nil); + let view_point = view.convertPoint_fromView(window_point, None); let x = view_point.x as u32; let y = view_point.y as u32; // js equivalent https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons - let buttons = NSEvent::pressedMouseButtons(event); + let buttons = NSEvent::pressedMouseButtons(); format!( r#"(() => {{ @@ -122,10 +113,10 @@ unsafe fn create_js_mouse_event(view: id, event: id, down: bool, back_button: bo x = x, y = y, detail = event.clickCount(), - ctrl_key = mods_flags.contains(NSEventModifierFlags::NSControlKeyMask), - alt_key = mods_flags.contains(NSEventModifierFlags::NSAlternateKeyMask), - shift_key = mods_flags.contains(NSEventModifierFlags::NSShiftKeyMask), - meta_key = mods_flags.contains(NSEventModifierFlags::NSCommandKeyMask), + ctrl_key = mods_flags.contains(NSControlKeyMask), + alt_key = mods_flags.contains(NSAlternateKeyMask), + shift_key = mods_flags.contains(NSShiftKeyMask), + meta_key = mods_flags.contains(NSCommandKeyMask), button = button, buttons = buttons, ) diff --git a/src/wkwebview/util.rs b/src/wkwebview/util.rs index 31dfe029b..ca396ae53 100644 --- a/src/wkwebview/util.rs +++ b/src/wkwebview/util.rs @@ -2,16 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use cocoa::{base::id, foundation::NSOperatingSystemVersion}; +use objc2_foundation::NSProcessInfo; -pub fn operating_system_version() -> (u64, u64, u64) { - unsafe { - let process_info: id = msg_send![class!(NSProcessInfo), processInfo]; - let version: NSOperatingSystemVersion = msg_send![process_info, operatingSystemVersion]; - ( - version.majorVersion, - version.minorVersion, - version.patchVersion, - ) - } +pub fn operating_system_version() -> (isize, isize, isize) { + let process_info = NSProcessInfo::processInfo(); + let version = process_info.operatingSystemVersion(); + ( + version.majorVersion, + version.minorVersion, + version.patchVersion, + ) }