diff --git a/.changes/eval-with-callback.md b/.changes/eval-with-callback.md new file mode 100644 index 000000000..82b05d4ed --- /dev/null +++ b/.changes/eval-with-callback.md @@ -0,0 +1,6 @@ +--- +"wry": patch +--- + +On Windows, Linux and macOS, add method `evaluate_script_with_callback` to execute javascipt with a callback. +Evaluated result will be serialized into JSON string and pass to the callback. diff --git a/Cargo.toml b/Cargo.toml index 554732b25..df9e87ff6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ dirs = "4.0.0" base64 = "0.13.1" [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] +javascriptcore-rs = { version = "0.17.0", features = [ "v2_28" ] } webkit2gtk = { version = "0.19.2", features = [ "v2_38" ] } webkit2gtk-sys = "0.19.1" gio = "0.16" diff --git a/examples/eval_js.rs b/examples/eval_js.rs new file mode 100644 index 000000000..00f32be49 --- /dev/null +++ b/examples/eval_js.rs @@ -0,0 +1,90 @@ +// Copyright 2020-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +fn main() -> wry::Result<()> { + use wry::{ + application::{ + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowBuilder}, + }, + webview::WebViewBuilder, + }; + + enum UserEvents { + ExecEval(), + } + + let event_loop = EventLoop::::with_user_event(); + let proxy = event_loop.create_proxy(); + + let window = WindowBuilder::new() + .with_title("Hello World") + .build(&event_loop)?; + + let ipc_handler = move |_: &Window, req: String| match req.as_str() { + "exec-eval" => { + let _ = proxy.send_event(UserEvents::ExecEval()); + } + _ => {} + }; + + let _webview = WebViewBuilder::new(window)? + .with_html( + r#" + + "#, + )? + .with_ipc_handler(ipc_handler) + .build()?; + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::UserEvent(UserEvents::ExecEval()) => { + // String + _webview + .evaluate_script_with_callback( + "if (!foo) { var foo = 'morbin'; } `${foo} time`", + |result| println!("String: {:?}", result), + ) + .unwrap(); + + // Number + _webview + .evaluate_script_with_callback("var num = 9527; num", |result| { + println!("Number: {:?}", result) + }) + .unwrap(); + + // Object + _webview + .evaluate_script_with_callback("var obj = { thank: 'you', '95': 27 }; obj", |result| { + println!("Object: {:?}", result) + }) + .unwrap(); + + // Array + _webview + .evaluate_script_with_callback("var ary = [1,2,3,4,'5']; ary", |result| { + println!("Array: {:?}", result) + }) + .unwrap(); + // Exception thrown + _webview + .evaluate_script_with_callback("throw new Error()", |result| { + println!("Exception Occured: {:?}", result) + }) + .unwrap(); + } + Event::NewEvents(StartCause::Init) => println!("Wry has started!"), + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/src/webview/android/mod.rs b/src/webview/android/mod.rs index 676e1acc1..78cc5e7d4 100644 --- a/src/webview/android/mod.rs +++ b/src/webview/android/mod.rs @@ -306,7 +306,7 @@ impl InnerWebView { Url::parse(uri.as_str()).unwrap() } - pub fn eval(&self, js: &str) -> Result<()> { + pub fn eval(&self, js: &str, _callback: Option) -> Result<()> { MainPipe::send(WebViewMessage::Eval(js.into())); Ok(()) } diff --git a/src/webview/mod.rs b/src/webview/mod.rs index eb62f69bf..cbcf715a6 100644 --- a/src/webview/mod.rs +++ b/src/webview/mod.rs @@ -799,8 +799,29 @@ impl WebView { /// [`WebView`]. Use [`EventLoopProxy`] and a custom event to send scripts from other threads. /// /// [`EventLoopProxy`]: crate::application::event_loop::EventLoopProxy + /// pub fn evaluate_script(&self, js: &str) -> Result<()> { - self.webview.eval(js) + self + .webview + .eval(js, None::>) + } + + /// Evaluate and run javascript code with callback function. The evaluation result will be + /// serialized into a JSON string and passed to the callback function. Must be called on the + /// same thread who created the [`WebView`]. Use [`EventLoopProxy`] and a custom event to + /// send scripts from other threads. + /// + /// [`EventLoopProxy`]: crate::application::event_loop::EventLoopProxy + /// + /// Exception is ignored because of the limitation on windows. You can catch it yourself and return as string as a workaround. + /// + /// - ** Android:** Not implemented yet. + pub fn evaluate_script_with_callback( + &self, + js: &str, + callback: impl Fn(String) + Send + 'static, + ) -> Result<()> { + self.webview.eval(js, Some(callback)) } /// Launch print modal for the webview content. diff --git a/src/webview/webkitgtk/mod.rs b/src/webview/webkitgtk/mod.rs index f26fdd24d..93f8c24f0 100644 --- a/src/webview/webkitgtk/mod.rs +++ b/src/webview/webkitgtk/mod.rs @@ -36,6 +36,8 @@ use crate::{ mod file_drop; mod web_context; +use javascriptcore::ValueExt; + pub(crate) struct InnerWebView { pub webview: Rc, #[cfg(any(debug_assertions, feature = "devtools"))] @@ -391,7 +393,10 @@ impl InnerWebView { } pub fn print(&self) { - let _ = self.eval("window.print()"); + let _ = self.eval( + "window.print()", + None::>, + ); } pub fn url(&self) -> Url { @@ -400,13 +405,36 @@ impl InnerWebView { Url::parse(uri.as_str()).unwrap() } - pub fn eval(&self, js: &str) -> Result<()> { + pub fn eval( + &self, + js: &str, + callback: Option, + ) -> Result<()> { if let Some(pending_scripts) = &mut *self.pending_scripts.lock().unwrap() { pending_scripts.push(js.into()); } else { let cancellable: Option<&Cancellable> = None; - self.webview.run_javascript(js, cancellable, |_| ()); + + match callback { + Some(callback) => { + self.webview.run_javascript(js, cancellable, |result| { + let mut result_str = String::new(); + + if let Ok(js_result) = result { + if let Some(js_value) = js_result.js_value() { + if let Some(json_str) = js_value.to_json(0) { + result_str = json_str.to_string(); + } + } + } + + callback(result_str); + }); + } + None => self.webview.run_javascript(js, cancellable, |_| ()), + }; } + Ok(()) } diff --git a/src/webview/webview2/mod.rs b/src/webview/webview2/mod.rs index 2d708a65e..348bfd11e 100644 --- a/src/webview/webview2/mod.rs +++ b/src/webview/webview2/mod.rs @@ -804,17 +804,27 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ ) } - fn execute_script(webview: &ICoreWebView2, js: String) -> windows::core::Result<()> { + fn execute_script( + webview: &ICoreWebView2, + js: String, + callback: impl FnOnce(String) + Send + 'static, + ) -> windows::core::Result<()> { unsafe { webview.ExecuteScript( PCWSTR::from_raw(encode_wide(js).as_ptr()), - &ExecuteScriptCompletedHandler::create(Box::new(|_, _| (Ok(())))), + &ExecuteScriptCompletedHandler::create(Box::new(|_, return_str| { + callback(return_str); + Ok(()) + })), ) } } pub fn print(&self) { - let _ = self.eval("window.print()"); + let _ = self.eval( + "window.print()", + None::>, + ); } pub fn url(&self) -> Url { @@ -827,9 +837,17 @@ window.addEventListener('mousemove', (e) => window.chrome.webview.postMessage('_ Url::parse(&uri).unwrap() } - pub fn eval(&self, js: &str) -> Result<()> { - Self::execute_script(&self.webview, js.to_string()) - .map_err(|err| Error::WebView2Error(webview2_com::Error::WindowsError(err))) + pub fn eval( + &self, + js: &str, + callback: Option, + ) -> Result<()> { + match callback { + Some(callback) => Self::execute_script(&self.webview, js.to_string(), callback) + .map_err(|err| Error::WebView2Error(webview2_com::Error::WindowsError(err))), + None => Self::execute_script(&self.webview, js.to_string(), |_| ()) + .map_err(|err| Error::WebView2Error(webview2_com::Error::WindowsError(err))), + } } #[cfg(any(debug_assertions, feature = "devtools"))] diff --git a/src/webview/wkwebview/mod.rs b/src/webview/wkwebview/mod.rs index 4137085e3..9e45a2b81 100644 --- a/src/webview/wkwebview/mod.rs +++ b/src/webview/wkwebview/mod.rs @@ -67,6 +67,8 @@ use http::{ const IPC_MESSAGE_HANDLER_NAME: &str = "ipc"; const ACCEPT_FIRST_MOUSE: &str = "accept_first_mouse"; +const NS_JSON_WRITING_FRAGMENTS_ALLOWED: u64 = 4; + pub(crate) struct InnerWebView { pub webview: id, #[cfg(target_os = "macos")] @@ -848,15 +850,37 @@ r#"Object.defineProperty(window, 'ipc', { Url::parse(std::str::from_utf8(bytes).unwrap()).unwrap() } - pub fn eval(&self, js: &str) -> Result<()> { + pub fn eval(&self, js: &str, callback: Option) -> Result<()> { if let Some(scripts) = &mut *self.pending_scripts.lock().unwrap() { scripts.push(js.into()); } else { // Safety: objc runtime calls are unsafe unsafe { - let _: id = msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:null::<*const c_void>()]; + let _: id = match callback { + Some(callback) => { + let handler = block::ConcreteBlock::new(|val: id, _err: id| { + 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(); + } + + callback(result) + }); + + msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler] + } + None => { + msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:null::<*const c_void>()] + } + }; } } + Ok(()) } @@ -1078,3 +1102,17 @@ impl NSString { 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 + }) + } +} + +struct NSData(id);