Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support callback function in eval #778

Merged
merged 12 commits into from
Mar 23, 2023
6 changes: 6 additions & 0 deletions .changes/eval-with-callback.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
90 changes: 90 additions & 0 deletions examples/eval_js.rs
Original file line number Diff line number Diff line change
@@ -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::<UserEvents>::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#"
<button onclick="window.ipc.postMessage('exec-eval')">Exec eval</button>
"#,
)?
.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,
_ => (),
}
});
}
2 changes: 1 addition & 1 deletion src/webview/android/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<impl Fn(String) + Send + 'static>) -> Result<()> {
MainPipe::send(WebViewMessage::Eval(js.into()));
Ok(())
}
Expand Down
23 changes: 22 additions & 1 deletion src/webview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Box<dyn Fn(String) + Send + 'static>>)
}

/// 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.
Expand Down
34 changes: 31 additions & 3 deletions src/webview/webkitgtk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ use crate::{
mod file_drop;
mod web_context;

use javascriptcore::ValueExt;

pub(crate) struct InnerWebView {
pub webview: Rc<WebView>,
#[cfg(any(debug_assertions, feature = "devtools"))]
Expand Down Expand Up @@ -391,7 +393,10 @@ impl InnerWebView {
}

pub fn print(&self) {
let _ = self.eval("window.print()");
let _ = self.eval(
"window.print()",
None::<Box<dyn FnOnce(String) + Send + 'static>>,
);
}

pub fn url(&self) -> Url {
Expand All @@ -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<impl FnOnce(String) + Send + 'static>,
) -> 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(())
}

Expand Down
30 changes: 24 additions & 6 deletions src/webview/webview2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Box<dyn FnOnce(String) + Send + 'static>>,
);
}

pub fn url(&self) -> Url {
Expand All @@ -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<impl FnOnce(String) + Send + 'static>,
) -> 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"))]
Expand Down
42 changes: 40 additions & 2 deletions src/webview/wkwebview/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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<impl Fn(String) + Send + 'static>) -> 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(())
}

Expand Down Expand Up @@ -1078,3 +1102,17 @@ impl NSString {
self.0
}
}

impl From<NSData> 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);