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

Return values from use_eval
This commit is contained in:
Jon Kelley 2022-12-09 14:01:41 -08:00 committed by GitHub
commit d78d6c8a1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 137 additions and 28 deletions

View file

@ -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"
}
}

View file

@ -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,

View file

@ -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};

View file

@ -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());
}

View file

@ -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]

View file

@ -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;

View file

@ -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)
}
}