Skip to content

Commit

Permalink
feat: support callback function in eval (#778)
Browse files Browse the repository at this point in the history
* feat: support callback fn in eval

* chore: enable javascriptcore-rs feature v2_28

* chore: add more example to eval

* feat(eval): add public fn evaluate_script_with_callback

* fix(linux): use ValueExt trait to convert JSValue to JSON

* fix(windows): return eval result directly

* chore: add changelog

* chore(eval): add exception example and doc to the function

* Update src/webview/mod.rs

Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <[email protected]>

* Fix typo

---------

Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <[email protected]>
  • Loading branch information
pewsheen and wusyong authored Mar 23, 2023
1 parent 4d61cf1 commit 2647731
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 13 deletions.
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);

0 comments on commit 2647731

Please sign in to comment.