From e863ef29b8841aec550b5d8d7e0a56ed8b6c9555 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 8 Dec 2022 10:44:56 -0600 Subject: [PATCH 1/2] allow returning values from use_eval --- examples/eval.rs | 15 ++++++- packages/desktop/src/controller.rs | 6 ++- packages/desktop/src/desktop_context.rs | 55 ++++++++++++++++++++++--- packages/desktop/src/lib.rs | 12 +++++- packages/web/Cargo.toml | 1 + packages/web/src/util.rs | 24 +++++++++-- 6 files changed, 100 insertions(+), 13 deletions(-) diff --git a/examples/eval.rs b/examples/eval.rs index 495288fcb..d2bfcfa25 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), + 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 f59874f85..a4c76f79f 100644 --- a/packages/desktop/src/controller.rs +++ b/packages/desktop/src/controller.rs @@ -3,6 +3,7 @@ use crate::desktop_context::{DesktopContext, UserWindowEvent}; use dioxus_core::*; #[cfg(target_os = "ios")] use objc::runtime::Object; +use serde_json::Value; use std::{ collections::HashMap, sync::Arc, @@ -17,6 +18,7 @@ use wry::{ pub(super) struct DesktopController { pub(super) webviews: HashMap, pub(super) sender: futures_channel::mpsc::UnboundedSender, + 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, @@ -38,6 +40,7 @@ impl DesktopController { let pending_edits = edit_queue.clone(); let return_sender = sender.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 "spawn" onto multiple threads @@ -50,7 +53,7 @@ impl DesktopController { let mut dom = VirtualDom::new_with_props_and_scheduler(root, props, (sender, receiver)); - let window_context = DesktopContext::new(desktop_context_proxy); + let window_context = DesktopContext::new(desktop_context_proxy, eval_reciever); dom.base_scope().provide_context(window_context); @@ -88,6 +91,7 @@ impl DesktopController { Self { pending_edits, sender: return_sender, + 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 f4d19bb68..463435dfe 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,5 +1,11 @@ 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 std::rc::Rc; use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] @@ -33,11 +39,18 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { 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 @@ -245,7 +258,13 @@ pub(super) fn handler( } Eval(code) => { - if let Err(e) = webview.evaluate_script(code.as_str()) { + 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}"); } @@ -280,10 +299,36 @@ pub(super) fn handler( } /// Get a closure that executes any JavaScript in the WebView context. -pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) { +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 } + } + }) +} - cx.use_hook(|| move |script| desktop.eval(script)) +/// 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")] diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index e218570f4..fccc3ac02 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -13,7 +13,7 @@ mod hot_reload; mod protocol; use desktop_context::UserWindowEvent; -pub use desktop_context::{use_eval, use_window, DesktopContext}; +pub use desktop_context::{use_eval, use_window, DesktopContext, EvalResult}; pub use wry; pub use wry::application as tao; @@ -126,7 +126,11 @@ pub fn launch_with_props(root: Component

, props: P, mut cf let window = builder.build(event_loop).unwrap(); let window_id = window.id(); - let (is_ready, sender) = (desktop.is_ready.clone(), desktop.sender.clone()); + let (is_ready, sender, eval_sender) = ( + desktop.is_ready.clone(), + desktop.sender.clone(), + desktop.eval_sender.clone(), + ); let proxy = proxy.clone(); @@ -143,6 +147,10 @@ pub fn launch_with_props(root: Component

, props: P, mut cf .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" => { let event = trigger_from_serialized(message.params()); log::trace!("User event: {:?}", event); diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index a36197e50..c3e13b1da 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" } [dependencies.web-sys] version = "0.3.56" diff --git a/packages/web/src/util.rs b/packages/web/src/util.rs index 772f53608..b6518b8c7 100644 --- a/packages/web/src/util.rs +++ b/packages/web/src/util.rs @@ -1,6 +1,10 @@ //! Utilities specific to websys +use std::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 +19,24 @@ 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) -> Result { 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(); + 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) { + 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 execute script")) + } } }) } From 35bebbf0cb3a28aaf63db74c49b6fd133e123079 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 8 Dec 2022 10:57:09 -0600 Subject: [PATCH 2/2] wrap the web eval result in a compatability layer --- packages/web/src/lib.rs | 2 +- packages/web/src/util.rs | 58 ++++++++++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 4c0831ef5..a807317f9 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -57,7 +57,7 @@ use std::rc::Rc; pub use crate::cfg::Config; -pub use crate::util::use_eval; +pub use crate::util::{use_eval, EvalResult}; use dioxus_core::prelude::Component; use dioxus_core::SchedulerMsg; use dioxus_core::VirtualDom; diff --git a/packages/web/src/util.rs b/packages/web/src/util.rs index b6518b8c7..3ed8bd479 100644 --- a/packages/web/src/util.rs +++ b/packages/web/src/util.rs @@ -1,6 +1,9 @@ //! Utilities specific to websys -use std::str::FromStr; +use std::{ + future::{IntoFuture, Ready}, + str::FromStr, +}; use dioxus_core::*; use serde::de::Error; @@ -19,24 +22,51 @@ use serde_json::Value; /// /// 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) -> Result { +pub fn use_eval(cx: &ScopeState) -> &dyn Fn(S) -> EvalResult { cx.use_hook(|| { |script: S| { let body = script.to_string(); - 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) { - let string: String = stringified.into(); - Value::from_str(&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 stringify result")) - } - } else { - Err(serde_json::Error::custom("Failed to execute script")) + 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) + } +}