From 531f7c6d3fb483767efde7008988739036c113a8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 30 Dec 2022 01:52:54 -0500 Subject: [PATCH] chore: clean up msg in desktop even more --- packages/desktop/src/controller.rs | 151 ------------------------ packages/desktop/src/desktop_context.rs | 30 +++-- packages/desktop/src/eval.rs | 10 -- packages/desktop/src/events.rs | 4 +- packages/desktop/src/lib.rs | 102 ++++++++++------ packages/desktop/src/waker.rs | 10 +- packages/desktop/src/webview.rs | 44 ++----- 7 files changed, 104 insertions(+), 247 deletions(-) delete mode 100644 packages/desktop/src/controller.rs diff --git a/packages/desktop/src/controller.rs b/packages/desktop/src/controller.rs deleted file mode 100644 index 109985ca8..000000000 --- a/packages/desktop/src/controller.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::desktop_context::{DesktopContext, UserWindowEvent}; -use dioxus_core::*; -use dioxus_html::HtmlEvent; -use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use futures_util::StreamExt; -#[cfg(target_os = "ios")] -use objc::runtime::Object; -use serde_json::Value; -use std::{ - collections::HashMap, - sync::Arc, - sync::{atomic::AtomicBool, Mutex}, - time::Duration, -}; -use wry::{ - self, - application::{event_loop::ControlFlow, event_loop::EventLoopProxy, window::WindowId}, - webview::WebView, -}; - -pub(super) struct DesktopController { - pub(super) webviews: HashMap, - pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, - - pub(super) quit_app_on_close: bool, - pub(super) is_ready: Arc, - pub(super) proxy: EventLoopProxy, - - pub(super) event_tx: UnboundedSender, - #[cfg(debug_assertions)] - pub(super) templates_tx: UnboundedSender>, -} - -impl DesktopController { - pub(super) fn close_window(&mut self, window_id: WindowId, control_flow: &mut ControlFlow) { - self.webviews.remove(&window_id); - - if self.webviews.is_empty() && self.quit_app_on_close { - *control_flow = ControlFlow::Exit; - } - } - - // pub(super) fn try_load_ready_webviews(&mut self) { - // if self.is_ready.load(std::sync::atomic::Ordering::Relaxed) { - // let mut new_queue = Vec::new(); - - // { - // let mut queue = self.pending_edits.lock().unwrap(); - // std::mem::swap(&mut new_queue, &mut *queue); - // } - - // let (_id, view) = self.webviews.iter_mut().next().unwrap(); - - // for edit in new_queue.drain(..) { - // view.evaluate_script(&format!("window.interpreter.handleEdits({})", edit)) - // .unwrap(); - // } - // } - // } -} - -// // Launch the virtualdom on its own thread managed by tokio -// // returns the desktop state -// pub(super) fn new_on_tokio( -// root: Component

, -// props: P, -// proxy: EventLoopProxy, -// ) -> Self { -// let edit_queue = Arc::new(Mutex::new(Vec::new())); -// let (event_tx, mut event_rx) = unbounded(); -// let (templates_tx, mut templates_rx) = unbounded(); -// let proxy2 = proxy.clone(); - -// 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 -// // I'd personally not require tokio to be built-in to Dioxus-Desktop, but the DX is worse without it - -// let runtime = tokio::runtime::Builder::new_multi_thread() -// .enable_all() -// .build() -// .unwrap(); - -// runtime.block_on(async move { -// let mut dom = VirtualDom::new_with_props(root, props) -// .with_root_context(DesktopContext::new(desktop_context_proxy, eval_reciever)); -// { -// let edits = dom.rebuild(); -// let mut queue = edit_queue.lock().unwrap(); -// queue.push(serde_json::to_string(&edits).unwrap()); -// proxy.send_event(UserWindowEvent::EditsReady).unwrap(); -// } - -// loop { -// tokio::select! { -// template = { -// #[allow(unused)] -// fn maybe_future<'a>(templates_rx: &'a mut UnboundedReceiver>) -> impl Future> + 'a { -// #[cfg(debug_assertions)] -// return templates_rx.select_next_some(); -// #[cfg(not(debug_assertions))] -// return std::future::pending(); -// } -// maybe_future(&mut templates_rx) -// } => { -// dom.replace_template(template); -// } -// _ = dom.wait_for_work() => {} -// Some(json_value) = event_rx.next() => { -// if let Ok(value) = serde_json::from_value::(json_value) { -// let HtmlEvent { -// name, -// element, -// bubbles, -// data -// } = value; -// dom.handle_event(&name, data.into_any(), element, bubbles); -// } -// } -// } - -// let muts = dom -// .render_with_deadline(tokio::time::sleep(Duration::from_millis(16))) -// .await; - -// edit_queue -// .lock() -// .unwrap() -// .push(serde_json::to_string(&muts).unwrap()); -// let _ = proxy.send_event(UserWindowEvent::EditsReady); -// } -// }) -// }); - -// Self { -// pending_edits, -// eval_sender, -// webviews: HashMap::new(), -// is_ready: Arc::new(AtomicBool::new(false)), -// quit_app_on_close: true, -// proxy: proxy2, -// event_tx, -// #[cfg(debug_assertions)] -// templates_tx, -// #[cfg(target_os = "ios")] -// views: vec![], -// } -// } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index b7a4405fd..75a458cb7 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,11 +1,10 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::controller::DesktopController; use crate::eval::EvalResult; +use crate::events::IpcMessage; use dioxus_core::ScopeState; use serde_json::Value; -use wry::application::event_loop::ControlFlow; use wry::application::event_loop::EventLoopProxy; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; @@ -42,11 +41,14 @@ pub struct DesktopContext { /// The proxy to the event loop pub proxy: ProxyType, + /// The receiver for eval results since eval is async + pub(super) eval_sender: tokio::sync::mpsc::UnboundedSender, + /// The receiver for eval results since eval is async pub(super) eval_reciever: Rc>>, #[cfg(target_os = "ios")] - pub(crate) views: Rc>>>, + pub(crate) views: Rc>>, } /// A smart pointer to the current window. @@ -59,15 +61,17 @@ impl std::ops::Deref for DesktopContext { } impl DesktopContext { - pub(crate) fn new( - webview: Rc, - proxy: ProxyType, - eval_reciever: tokio::sync::mpsc::UnboundedReceiver, - ) -> Self { + pub(crate) fn new(webview: Rc, proxy: ProxyType) -> Self { + let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); + Self { webview, proxy, eval_reciever: Rc::new(RefCell::new(eval_reciever)), + eval_sender, + + #[cfg(target_os = "ios")] + views: Default::default(), } } @@ -155,6 +159,8 @@ impl DesktopContext { /// Push an objc view to the window #[cfg(target_os = "ios")] pub fn push_view(&self, view: objc_id::ShareId) { + let window = self.webview.window(); + unsafe { use objc::runtime::Object; use objc::*; @@ -173,6 +179,8 @@ impl DesktopContext { /// Pop an objc view from the window #[cfg(target_os = "ios")] pub fn pop_view(&self) { + let window = self.webview.window(); + unsafe { use objc::runtime::Object; use objc::*; @@ -187,15 +195,11 @@ impl DesktopContext { #[derive(Debug)] pub enum UserWindowEvent { - Initialize, - Poll, - UserEvent(serde_json::Value), + Ipc(IpcMessage), CloseWindow, - - EvalResult(serde_json::Value), } #[cfg(target_os = "ios")] diff --git a/packages/desktop/src/eval.rs b/packages/desktop/src/eval.rs index 358825fa3..80baba2d4 100644 --- a/packages/desktop/src/eval.rs +++ b/packages/desktop/src/eval.rs @@ -1,22 +1,12 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::controller::DesktopController; use crate::use_window; 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::dpi::LogicalSize; -use wry::application::event_loop::ControlFlow; -use wry::application::event_loop::EventLoopProxy; -#[cfg(target_os = "ios")] -use wry::application::platform::ios::WindowExtIOS; -use wry::application::window::Fullscreen as WryFullscreen; -use wry::application::window::Window; -use wry::webview::WebView; /// A future that resolves to the result of a JavaScript evaluation. pub struct EvalResult { diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index f9b3fe5cb..ae4d77c68 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize)] -pub(crate) struct IpcMessage { +#[derive(Deserialize, Serialize, Debug)] +pub struct IpcMessage { method: String, params: serde_json::Value, } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 7dda7e679..9d10f36f5 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -4,24 +4,23 @@ #![deny(missing_docs)] mod cfg; -mod controller; mod desktop_context; mod escape; +mod eval; mod events; mod protocol; +mod waker; +mod webview; #[cfg(all(feature = "hot-reload", debug_assertions))] mod hot_reload; -mod eval; -mod waker; -mod webview; - pub use cfg::Config; use desktop_context::UserWindowEvent; pub use desktop_context::{use_window, DesktopContext}; use dioxus_core::*; use dioxus_html::HtmlEvent; +pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; use std::collections::HashMap; use std::rc::Rc; @@ -89,7 +88,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// use dioxus::prelude::*; /// /// fn main() { -/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, |c| c); +/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, Config::default()); /// } /// /// struct AppProps { @@ -102,7 +101,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// }) /// } /// ``` -pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { +pub fn launch_with_props(root: Component

, props: P, mut cfg: Config) { let mut dom = VirtualDom::new_with_props(root, props); let event_loop = EventLoop::with_user_event(); @@ -123,47 +122,46 @@ pub fn launch_with_props(root: Component

, props: P, mut cf let waker = waker::tao_waker(&proxy); // We only have one webview right now, but we'll have more later + // Store them in a hashmap so we can remove them when they're closed let mut webviews = HashMap::new(); event_loop.run(move |window_event, event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { - Event::NewEvents(StartCause::Init) => { - let (eval_sender, eval_reciever) = tokio::sync::mpsc::unbounded_channel(); - let window = Rc::new(webview::build(&mut cfg, event_loop, proxy.clone())); - let ctx = DesktopContext::new(window.clone(), proxy.clone(), eval_reciever); - dom.base_scope().provide_context(ctx); - webviews.insert(window.window().id(), window.clone()); - proxy.send_event(UserWindowEvent::Poll).unwrap(); - } + Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, - Event::MainEventsCleared => {} - Event::Resumed => {} - Event::Suspended => {} - Event::LoopDestroyed => {} - Event::RedrawRequested(_id) => {} - - Event::NewEvents(cause) => {} - - Event::WindowEvent { event, .. } => match event { + Event::WindowEvent { + event, window_id, .. + } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::Destroyed { .. } => { - // desktop.close_window(window_id, control_flow); + webviews.remove(&window_id); + + if webviews.is_empty() { + *control_flow = ControlFlow::Exit; + } } _ => {} }, - Event::UserEvent(UserWindowEvent::Initialize) => { - send_edits(dom.rebuild(), &mut webviews); + Event::NewEvents(StartCause::Init) => { + let window = webview::build(&mut cfg, event_loop, proxy.clone()); + + dom.base_scope() + .provide_context(DesktopContext::new(window.clone(), proxy.clone())); + + webviews.insert(window.window().id(), window); + + _ = proxy.send_event(UserWindowEvent::Poll); } - Event::UserEvent(UserWindowEvent::CloseWindow) => *control_flow = ControlFlow::Exit, + Event::UserEvent(UserWindowEvent::Poll) => { + poll_vdom(&waker, &mut dom, &mut webviews); + } - Event::UserEvent(UserWindowEvent::EvalResult(_)) => todo!(), - - Event::UserEvent(UserWindowEvent::UserEvent(json_value)) => { - let evt = match serde_json::from_value::(json_value) { + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "user_event" => { + let evt = match serde_json::from_value::(msg.params()) { Ok(value) => value, Err(_) => return, }; @@ -173,19 +171,49 @@ pub fn launch_with_props(root: Component

, props: P, mut cf send_edits(dom.render_immediate(), &mut webviews); } - Event::UserEvent(UserWindowEvent::Poll) => { - poll_vdom(&waker, &mut dom, &mut webviews); + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "initialize" => { + send_edits(dom.rebuild(), &mut webviews); } - _ => todo!(), + // When the webview chirps back with the result of the eval, we send it to the active receiver + // + // This currently doesn't perform any targeting to the callsite, so if you eval multiple times at once, + // you might the wrong result. This should be fixed + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "eval_result" => { + dom.base_scope() + .consume_context::() + .unwrap() + .eval_sender + .send(msg.params()) + .unwrap(); + } + + Event::UserEvent(UserWindowEvent::Ipc(msg)) if msg.method() == "browser_open" => { + if let Some(temp) = msg.params().as_object() { + if temp.contains_key("href") { + let open = webbrowser::open(temp["href"].as_str().unwrap()); + if let Err(e) = open { + log::error!("Open Browser error: {:?}", e); + } + } + } + } + + _ => {} } }) } type Webviews = HashMap>; +/// Poll the virtualdom until it's pending +/// +/// The waker we give it is connected to the event loop, so it will wake up the event loop when it's ready to be polled again +/// +/// All IO is done on the tokio runtime we started earlier fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { let mut cx = std::task::Context::from_waker(waker); + loop { { let fut = dom.wait_for_work(); @@ -201,8 +229,12 @@ fn poll_vdom(waker: &Waker, dom: &mut VirtualDom, webviews: &mut Webviews) { } } +/// Send a list of mutations to the webview fn send_edits(edits: Mutations, webviews: &mut Webviews) { let serialized = serde_json::to_string(&edits).unwrap(); + let (_id, view) = webviews.iter_mut().next().unwrap(); + + // todo: use SSE and binary data to send the edits with lower overhead _ = view.evaluate_script(&format!("window.interpreter.handleEdits({})", serialized)); } diff --git a/packages/desktop/src/waker.rs b/packages/desktop/src/waker.rs index a08c93bd3..a2586ffed 100644 --- a/packages/desktop/src/waker.rs +++ b/packages/desktop/src/waker.rs @@ -1,9 +1,8 @@ +use crate::desktop_context::UserWindowEvent; use futures_util::task::ArcWake; use std::sync::Arc; use wry::application::event_loop::EventLoopProxy; -use crate::desktop_context::UserWindowEvent; - /// Create a waker that will send a poll event to the event loop. /// /// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView. @@ -12,9 +11,14 @@ use crate::desktop_context::UserWindowEvent; pub fn tao_waker(proxy: &EventLoopProxy) -> std::task::Waker { struct DomHandle(EventLoopProxy); + // this should be implemented by most platforms, but ios is missing this until + // https://github.com/tauri-apps/wry/issues/830 is resolved + unsafe impl Send for DomHandle {} + unsafe impl Sync for DomHandle {} + impl ArcWake for DomHandle { fn wake_by_ref(arc_self: &Arc) { - arc_self.0.send_event(UserWindowEvent::Poll).unwrap(); + _ = arc_self.0.send_event(UserWindowEvent::Poll); } } diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 5cb80e41b..6c286c35b 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,16 +1,19 @@ +use std::rc::Rc; + use crate::events::parse_ipc_message; use crate::protocol; use crate::{desktop_context::UserWindowEvent, Config}; +use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use wry; pub use wry::application as tao; use wry::application::window::Window; -use wry::webview::WebViewBuilder; +use wry::webview::{WebView, WebViewBuilder}; pub fn build( cfg: &mut Config, - event_loop: &tao::event_loop::EventLoopWindowTarget, - proxy: tao::event_loop::EventLoopProxy, -) -> wry::webview::WebView { + event_loop: &EventLoopWindowTarget, + proxy: EventLoopProxy, +) -> Rc { let builder = cfg.window.clone(); let window = builder.build(event_loop).unwrap(); let file_handler = cfg.file_drop_handler.take(); @@ -37,34 +40,9 @@ pub fn build( .with_url("dioxus://index.html/") .unwrap() .with_ipc_handler(move |_window: &Window, payload: String| { - let message = match parse_ipc_message(&payload) { - Some(message) => message, - None => { - log::error!("Failed to parse IPC message: {}", payload); - return; - } - }; - - match message.method() { - "eval_result" => { - let _ = proxy.send_event(UserWindowEvent::EvalResult(message.params())); - } - "user_event" => { - let _ = proxy.send_event(UserWindowEvent::UserEvent(message.params())); - } - "initialize" => { - let _ = proxy.send_event(UserWindowEvent::Initialize); - } - "browser_open" => match message.params().as_object() { - Some(temp) if temp.contains_key("href") => { - let open = webbrowser::open(temp["href"].as_str().unwrap()); - if let Err(e) = open { - log::error!("Open Browser error: {:?}", e); - } - } - _ => (), - }, - _ => (), + // defer the event to the main thread + if let Some(message) = parse_ipc_message(&payload) { + _ = proxy.send_event(UserWindowEvent::Ipc(message)); } }) .with_custom_protocol(String::from("dioxus"), move |r| { @@ -107,5 +85,5 @@ pub fn build( webview = webview.with_devtools(true); } - webview.build().unwrap() + Rc::new(webview.build().unwrap()) }