diff --git a/examples/eval.rs b/examples/eval.rs index c0dda3e09..22e036b9c 100644 --- a/examples/eval.rs +++ b/examples/eval.rs @@ -1,4 +1,5 @@ use dioxus::prelude::*; +use dioxus_desktop::EvalResult; fn main() { dioxus_desktop::launch(app); @@ -7,6 +8,15 @@ fn main() { fn app(cx: Scope) -> Element { let script = use_state(cx, String::new); let eval = dioxus_desktop::use_eval(cx); + let future: &UseRef> = use_ref(cx, || None); + if future.read().is_some() { + let future_clone = future.clone(); + cx.spawn(async move { + if let Some(fut) = future_clone.with_mut(|o| o.take()) { + println!("{:?}", fut.await) + } + }); + } cx.render(rsx! { div { @@ -16,7 +26,10 @@ fn app(cx: Scope) -> Element { oninput: move |e| script.set(e.value.clone()), } button { - onclick: move |_| eval(script.to_string()), + onclick: move |_| { + let fut = eval(script); + future.set(Some(fut)); + }, "Execute" } } diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs index 7139c083f..9037189b1 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -5,6 +5,7 @@ use futures_channel::mpsc::{unbounded, UnboundedSender}; use futures_util::StreamExt; #[cfg(target_os = "ios")] use objc::runtime::Object; +use serde_json::Value; use std::{ collections::HashMap, sync::Arc, @@ -19,6 +20,7 @@ use wry::{ pub(super) struct DesktopController { pub(super) webviews: HashMap, + pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, pub(super) pending_edits: Arc>>, pub(super) quit_app_on_close: bool, pub(super) is_ready: Arc, @@ -43,6 +45,7 @@ impl DesktopController { let pending_edits = edit_queue.clone(); let desktop_context_proxy = proxy.clone(); + let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel::(); std::thread::spawn(move || { // We create the runtime as multithreaded, so you can still "tokio::spawn" onto multiple threads @@ -54,7 +57,7 @@ impl DesktopController { runtime.block_on(async move { let mut dom = VirtualDom::new_with_props(root, props) - .with_root_context(DesktopContext::new(desktop_context_proxy)); + .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); { let edits = dom.rebuild(); let mut queue = edit_queue.lock().unwrap(); @@ -88,6 +91,7 @@ impl DesktopController { Self { pending_edits, + eval_sender, webviews: HashMap::new(), is_ready: Arc::new(AtomicBool::new(false)), quit_app_on_close: true, diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0c1751363..756814de0 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -2,6 +2,11 @@ use std::rc::Rc; use crate::controller::DesktopController; use dioxus_core::ScopeState; +use serde::de::Error; +use serde_json::Value; +use std::future::Future; +use std::future::IntoFuture; +use std::pin::Pin; use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] @@ -19,16 +24,6 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } -/// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &Rc { - let desktop = use_window(cx); - - &*cx.use_hook(|| { - let desktop = desktop.clone(); - Rc::new(move |script| desktop.eval(script)) - } as Rc) -} - /// An imperative interface to the current window. /// /// To get a handle to the current window, use the [`use_window`] hook. @@ -45,11 +40,18 @@ pub fn use_eval(cx: &ScopeState) -> &Rc { pub struct DesktopContext { /// The wry/tao proxy to the current window pub proxy: ProxyType, + pub(super) eval_reciever: Rc>>, } impl DesktopContext { - pub(crate) fn new(proxy: ProxyType) -> Self { - Self { proxy } + pub(crate) fn new( + proxy: ProxyType, + eval_reciever: tokio::sync::mpsc::UnboundedReceiver, + ) -> Self { + Self { + proxy, + eval_reciever: Rc::new(tokio::sync::Mutex::new(eval_reciever)), + } } /// trigger the drag-window event @@ -242,6 +244,18 @@ impl DesktopController { Resizable(state) => window.set_resizable(state), AlwaysOnTop(state) => window.set_always_on_top(state), + Eval(code) => { + let script = format!( + r#"window.ipc.postMessage(JSON.stringify({{"method":"eval_result", params: (function(){{ + {} + }})()}}));"#, + code + ); + if let Err(e) = webview.evaluate_script(&script) { + // we can't panic this error. + log::warn!("Eval script error: {e}"); + } + } CursorVisible(state) => window.set_cursor_visible(state), CursorGrab(state) => { let _ = window.set_cursor_grab(state); @@ -265,13 +279,6 @@ impl DesktopController { log::warn!("Devtools are disabled in release builds"); } - Eval(code) => { - if let Err(e) = webview.evaluate_script(code.as_str()) { - // we can't panic this error. - log::warn!("Eval script error: {e}"); - } - } - #[cfg(target_os = "ios")] PushView(view) => unsafe { use objc::runtime::Object; @@ -301,6 +308,39 @@ impl DesktopController { } } +/// Get a closure that executes any JavaScript in the WebView context. +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { + let desktop = use_window(cx).clone(); + cx.use_hook(|| { + move |script| { + desktop.eval(script); + let recv = desktop.eval_reciever.clone(); + EvalResult { reciever: recv } + } + }) +} + +/// A future that resolves to the result of a JavaScript evaluation. +pub struct EvalResult { + reciever: Rc>>, +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Pin>>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(async move { + let mut reciever = self.reciever.lock().await; + match reciever.recv().await { + Some(result) => Ok(result), + None => Err(serde_json::Error::custom("No result returned")), + } + }) as Pin>>> + } +} + #[cfg(target_os = "ios")] fn is_main_thread() -> bool { use objc::runtime::{Class, BOOL, NO}; diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 585a7ff51..a635e3f4e 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -17,7 +17,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use desktop_context::UserWindowEvent; -pub use desktop_context::{use_eval, use_window, DesktopContext}; +pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; use futures_channel::mpsc::UnboundedSender; pub use wry; pub use wry::application as tao; @@ -142,6 +142,7 @@ impl DesktopController { event_loop, self.is_ready.clone(), self.proxy.clone(), + self.eval_sender.clone(), self.event_tx.clone(), ); @@ -154,6 +155,7 @@ fn build_webview( event_loop: &tao::event_loop::EventLoopWindowTarget, is_ready: Arc, proxy: tao::event_loop::EventLoopProxy, + eval_sender: tokio::sync::mpsc::UnboundedSender, event_tx: UnboundedSender, ) -> wry::webview::WebView { let builder = cfg.window.clone(); @@ -183,6 +185,10 @@ fn build_webview( .with_ipc_handler(move |_window: &Window, payload: String| { parse_ipc_message(&payload) .map(|message| match message.method() { + "eval_result" => { + let result = message.params(); + eval_sender.send(result).unwrap(); + } "user_event" => { _ = event_tx.unbounded_send(message.params()); } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 7c048828d..40056eaaa 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -30,6 +30,7 @@ futures-util = "0.3.19" smallstr = "0.2.0" futures-channel = "0.3.21" serde_json = { version = "1.0" } +serde = { version = "1.0" } serde-wasm-bindgen = "0.4.5" [dependencies.web-sys] diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 56f973469..f430a7555 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -55,9 +55,8 @@ pub use crate::cfg::Config; use crate::dom::virtual_event_from_websys_event; -pub use crate::util::use_eval; +pub use crate::util::{use_eval, EvalResult}; use dioxus_core::{Element, ElementId, Scope, VirtualDom}; - use futures_util::{pin_mut, FutureExt, StreamExt}; mod cache; diff --git a/packages/web/src/util.rs b/packages/web/src/util.rs index 772f53608..3ed8bd479 100644 --- a/packages/web/src/util.rs +++ b/packages/web/src/util.rs @@ -1,6 +1,13 @@ //! Utilities specific to websys +use std::{ + future::{IntoFuture, Ready}, + str::FromStr, +}; + use dioxus_core::*; +use serde::de::Error; +use serde_json::Value; /// Get a closure that executes any JavaScript in the webpage. /// @@ -15,12 +22,51 @@ use dioxus_core::*; /// /// The closure will panic if the provided script is not valid JavaScript code /// or if it returns an uncaught error. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) { +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { cx.use_hook(|| { |script: S| { - js_sys::Function::new_no_args(&script.to_string()) - .call0(&wasm_bindgen::JsValue::NULL) - .expect("failed to eval script"); + let body = script.to_string(); + EvalResult { + value: if let Ok(value) = + js_sys::Function::new_no_args(&body).call0(&wasm_bindgen::JsValue::NULL) + { + if let Ok(stringified) = js_sys::JSON::stringify(&value) { + if !stringified.is_undefined() && stringified.is_valid_utf16() { + let string: String = stringified.into(); + Value::from_str(&string) + } else { + Err(serde_json::Error::custom("Failed to stringify result")) + } + } else { + Err(serde_json::Error::custom("Failed to stringify result")) + } + } else { + Err(serde_json::Error::custom("Failed to execute script")) + }, + } } }) } + +/// A wrapper around the result of a JavaScript evaluation. +/// This implements IntoFuture to be compatible with the desktop renderer's EvalResult. +pub struct EvalResult { + value: Result, +} + +impl EvalResult { + /// Get the result of the Javascript execution. + pub fn get(self) -> Result { + self.value + } +} + +impl IntoFuture for EvalResult { + type Output = Result; + + type IntoFuture = Ready>; + + fn into_future(self) -> Self::IntoFuture { + std::future::ready(self.value) + } +}