diff --git a/examples/compose.rs b/examples/compose.rs new file mode 100644 index 000000000..fa5d6e4c2 --- /dev/null +++ b/examples/compose.rs @@ -0,0 +1,82 @@ +//! This example shows how to create a popup window and send data back to the parent window. + +use dioxus::prelude::*; +use dioxus_desktop::use_window; +use futures_util::StreamExt; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let window = use_window(cx); + let emails_sent = use_ref(cx, || vec![]); + + let tx = use_coroutine(cx, |mut rx: UnboundedReceiver| { + to_owned![emails_sent]; + async move { + while let Some(message) = rx.next().await { + emails_sent.write().push(message); + } + } + }); + + cx.render(rsx! { + div { + h1 { "This is your email" } + + button { + onclick: move |_| { + let dom = VirtualDom::new_with_props(compose, ComposeProps { + app_tx: tx.clone() + }); + + // this returns a weak reference to the other window + // Be careful not to keep a strong reference to the other window or it will never be dropped + // and the window will never close. + window.new_window(dom, Default::default()); + }, + "Click to compose a new email" + } + + ul { + emails_sent.read().iter().map(|message| cx.render(rsx! { + li { + h3 { "email" } + span {"{message}"} + } + })) + } + } + }) +} + +struct ComposeProps { + app_tx: Coroutine, +} + +fn compose(cx: Scope) -> Element { + let user_input = use_state(cx, || String::new()); + let window = use_window(cx); + + cx.render(rsx! { + div { + h1 { "Compose a new email" } + + button { + onclick: move |_| { + cx.props.app_tx.send(user_input.get().clone()); + window.close(); + }, + "Click to send" + } + + input { + oninput: move |e| { + user_input.set(e.value.clone()); + }, + value: "{user_input}" + } + } + }) +} diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 0fb340c25..d3b9216b0 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -1,13 +1,17 @@ use std::cell::RefCell; use std::rc::Rc; +use std::rc::Weak; +use crate::create_new_window; use crate::eval::EvalResult; use crate::events::IpcMessage; use crate::Config; +use crate::WebviewHandler; use dioxus_core::ScopeState; use dioxus_core::VirtualDom; use serde_json::Value; use wry::application::event_loop::EventLoopProxy; +use wry::application::event_loop::EventLoopWindowTarget; #[cfg(target_os = "ios")] use wry::application::platform::ios::WindowExtIOS; use wry::application::window::Fullscreen as WryFullscreen; @@ -24,7 +28,7 @@ pub fn use_window(cx: &ScopeState) -> &DesktopContext { .unwrap() } -pub type WebviewQueue = Rc>>; +pub(crate) type WebviewQueue = Rc>>; /// An imperative interface to the current window. /// @@ -51,6 +55,8 @@ pub struct DesktopContext { pub(super) pending_windows: WebviewQueue, + pub(crate) event_loop: EventLoopWindowTarget, + #[cfg(target_os = "ios")] pub(crate) views: Rc>>, } @@ -65,10 +71,16 @@ impl std::ops::Deref for DesktopContext { } impl DesktopContext { - pub(crate) fn new(webview: Rc, proxy: ProxyType, webviews: WebviewQueue) -> Self { + pub(crate) fn new( + webview: Rc, + proxy: ProxyType, + event_loop: EventLoopWindowTarget, + webviews: WebviewQueue, + ) -> Self { Self { webview, proxy, + event_loop, eval: tokio::sync::broadcast::channel(8).0, pending_windows: webviews, #[cfg(target_os = "ios")] @@ -77,11 +89,36 @@ impl DesktopContext { } /// Create a new window using the props and window builder - pub fn new_window(&self, dom: VirtualDom, cfg: Config) { - self.pending_windows.borrow_mut().push((dom, cfg)); + /// + /// Returns the webview handle for the new window. + /// + /// You can use this to control other windows from the current window. + /// + /// Be careful to not create a cycle of windows, or you might leak memory. + pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> Weak { + let window = create_new_window( + cfg, + &self.event_loop, + &self.proxy, + dom, + &self.pending_windows, + ); + + let id = window.webview.window().id(); + self.proxy - .send_event(UserWindowEvent(EventData::NewWindow, self.id())) + .send_event(UserWindowEvent(EventData::NewWindow, id)) .unwrap(); + + self.proxy + .send_event(UserWindowEvent(EventData::Poll, id)) + .unwrap(); + + let webview = window.webview.clone(); + + self.pending_windows.borrow_mut().push(window); + + Rc::downgrade(&webview) } /// trigger the drag-window event @@ -115,6 +152,13 @@ impl DesktopContext { .send_event(UserWindowEvent(EventData::CloseWindow, self.id())); } + /// close window + pub fn close_window(&self, id: WindowId) { + let _ = self + .proxy + .send_event(UserWindowEvent(EventData::CloseWindow, id)); + } + /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { if let Some(handle) = self.webview.window().current_monitor() { @@ -209,10 +253,10 @@ impl DesktopContext { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UserWindowEvent(pub EventData, pub WindowId); -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum EventData { Poll, diff --git a/packages/desktop/src/events.rs b/packages/desktop/src/events.rs index 50d3fbf3d..7059a7c85 100644 --- a/packages/desktop/src/events.rs +++ b/packages/desktop/src/events.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct IpcMessage { method: String, params: serde_json::Value, diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 52907c1b1..a75e6bbbb 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -17,16 +17,16 @@ mod hot_reload; pub use cfg::Config; pub use desktop_context::{use_window, DesktopContext}; -use desktop_context::{EventData, UserWindowEvent}; +use desktop_context::{EventData, UserWindowEvent, WebviewQueue}; use dioxus_core::*; use dioxus_html::HtmlEvent; pub use eval::{use_eval, EvalResult}; use futures_util::{pin_mut, FutureExt}; -use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::task::Waker; pub use tao::dpi::{LogicalSize, PhysicalSize}; +use tao::event_loop::{EventLoopProxy, EventLoopWindowTarget}; pub use tao::window::WindowBuilder; use tao::{ event::{Event, StartCause, WindowEvent}, @@ -105,8 +105,6 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// } /// ``` pub fn launch_with_props(root: Component

, props: P, cfg: Config) { - let mut _dom = VirtualDom::new_with_props(root, props); - let event_loop = EventLoop::::with_user_event(); let proxy = event_loop.create_proxy(); @@ -125,9 +123,18 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) // Store them in a hashmap so we can remove them when they're closed let mut webviews = HashMap::::new(); - let queue = Rc::new(RefCell::new(vec![(_dom, cfg)])); + let queue = WebviewQueue::default(); - event_loop.run(move |window_event, event_loop, control_flow| { + // By default, we'll create a new window when the app starts + queue.borrow_mut().push(create_new_window( + cfg, + &event_loop, + &proxy, + VirtualDom::new_with_props(root, props), + &queue, + )); + + event_loop.run(move |window_event, _event_loop, control_flow| { *control_flow = ControlFlow::Wait; match window_event { @@ -153,28 +160,9 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) Event::NewEvents(StartCause::Init) | Event::UserEvent(UserWindowEvent(EventData::NewWindow, _)) => { - for (dom, mut cfg) in queue.borrow_mut().drain(..) { - let webview = webview::build(&mut cfg, event_loop, proxy.clone()); - - dom.base_scope().provide_context(DesktopContext::new( - webview.clone(), - proxy.clone(), - queue.clone(), - )); - - let id = webview.window().id(); - - // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both - let waker = waker::tao_waker(&proxy, id); - - let handler = WebviewHandler { - webview, - waker, - dom, - }; - + for handler in queue.borrow_mut().drain(..) { + let id = handler.webview.window().id(); webviews.insert(id, handler); - _ = proxy.send_event(UserWindowEvent(EventData::Poll, id)); } } @@ -246,6 +234,32 @@ pub fn launch_with_props(root: Component

, props: P, cfg: Config) }) } +fn create_new_window( + mut cfg: Config, + event_loop: &EventLoopWindowTarget, + proxy: &EventLoopProxy, + dom: VirtualDom, + queue: &WebviewQueue, +) -> WebviewHandler { + let webview = webview::build(&mut cfg, event_loop, proxy.clone()); + + dom.base_scope().provide_context(DesktopContext::new( + webview.clone(), + proxy.clone(), + event_loop.clone(), + queue.clone(), + )); + + let id = webview.window().id(); + + // We want to poll the virtualdom and the event loop at the same time, so the waker will be connected to both + WebviewHandler { + webview, + dom, + waker: waker::tao_waker(proxy, id), + } +} + struct WebviewHandler { dom: VirtualDom, webview: Rc,