Jelajahi Sumber

Merge pull request #647 from Demonthos/return-from-js

Return values from use_eval
Jon Kelley 2 tahun lalu
induk
melakukan
d78d6c8a1a

+ 14 - 1
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<Option<EvalResult>> = 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"
             }
         }

+ 5 - 1
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<WindowId, WebView>,
+    pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender<Value>,
     pub(super) pending_edits: Arc<Mutex<Vec<String>>>,
     pub(super) quit_app_on_close: bool,
     pub(super) is_ready: Arc<AtomicBool>,
@@ -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::<Value>();
 
         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,

+ 59 - 19
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<dyn Fn(String)> {
-    let desktop = use_window(cx);
-
-    &*cx.use_hook(|| {
-        let desktop = desktop.clone();
-        Rc::new(move |script| desktop.eval(script))
-    } as Rc<dyn Fn(String)>)
-}
-
 /// 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<dyn Fn(String)> {
 pub struct DesktopContext {
     /// The wry/tao proxy to the current window
     pub proxy: ProxyType,
+    pub(super) eval_reciever: Rc<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<Value>>>,
 }
 
 impl DesktopContext {
-    pub(crate) fn new(proxy: ProxyType) -> Self {
-        Self { proxy }
+    pub(crate) fn new(
+        proxy: ProxyType,
+        eval_reciever: tokio::sync::mpsc::UnboundedReceiver<Value>,
+    ) -> 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<S: std::string::ToString>(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<tokio::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<serde_json::Value>>>,
+}
+
+impl IntoFuture for EvalResult {
+    type Output = Result<serde_json::Value, serde_json::Error>;
+
+    type IntoFuture = Pin<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>;
+
+    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<Box<dyn Future<Output = Result<serde_json::Value, serde_json::Error>>>>
+    }
+}
+
 #[cfg(target_os = "ios")]
 fn is_main_thread() -> bool {
     use objc::runtime::{Class, BOOL, NO};

+ 7 - 1
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<UserWindowEvent>,
     is_ready: Arc<AtomicBool>,
     proxy: tao::event_loop::EventLoopProxy<UserWindowEvent>,
+    eval_sender: tokio::sync::mpsc::UnboundedSender<serde_json::Value>,
     event_tx: UnboundedSender<serde_json::Value>,
 ) -> 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());
                     }

+ 1 - 0
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]

+ 1 - 2
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;

+ 50 - 4
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<S: std::string::ToString>(cx: &ScopeState) -> &dyn Fn(S) {
+pub fn use_eval<S: std::string::ToString>(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<Value, serde_json::Error>,
+}
+
+impl EvalResult {
+    /// Get the result of the Javascript execution.
+    pub fn get(self) -> Result<Value, serde_json::Error> {
+        self.value
+    }
+}
+
+impl IntoFuture for EvalResult {
+    type Output = Result<Value, serde_json::Error>;
+
+    type IntoFuture = Ready<Result<Value, serde_json::Error>>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        std::future::ready(self.value)
+    }
+}