From e6ca0baa46a86e1d3701452f4c9aedf5ff130e9c Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 19 Jun 2025 16:32:00 -0500 Subject: [PATCH] Make new_window asynchronous (#4297) * Make new_window asynchronous * fix clippy --- packages/desktop/src/app.rs | 11 +-- packages/desktop/src/desktop_context.rs | 95 ++++++++++++++++++++----- packages/desktop/src/lib.rs | 4 +- packages/desktop/src/webview.rs | 32 +++++++++ 4 files changed, 118 insertions(+), 24 deletions(-) diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index b2571ab89..361551897 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -5,7 +5,7 @@ use crate::{ ipc::{IpcMessage, UserWindowEvent}, query::QueryResult, shortcut::ShortcutRegistry, - webview::WebviewInstance, + webview::{PendingWebview, WebviewInstance}, }; use dioxus_core::{ElementId, ScopeId, VirtualDom}; use dioxus_history::History; @@ -49,7 +49,7 @@ pub(crate) struct App { /// A bundle of state shared between all the windows, providing a way for us to communicate with running webview. pub(crate) struct SharedContext { pub(crate) event_handlers: WindowEventHandlers, - pub(crate) pending_webviews: RefCell>, + pub(crate) pending_webviews: RefCell>, pub(crate) shortcut_manager: ShortcutRegistry, pub(crate) proxy: EventLoopProxy, pub(crate) target: EventLoopWindowTarget, @@ -177,9 +177,10 @@ impl App { } pub fn handle_new_window(&mut self) { - for handler in self.shared.pending_webviews.borrow_mut().drain(..) { - let id = handler.desktop_context.window.id(); - self.webviews.insert(id, handler); + for pending_webview in self.shared.pending_webviews.borrow_mut().drain(..) { + let window = pending_webview.create_window(&self.shared); + let id = window.desktop_context.window.id(); + self.webviews.insert(id, window); _ = self.shared.proxy.send_event(UserWindowEvent::Poll(id)); } } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index da8015217..b3638ef49 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -5,14 +5,15 @@ use crate::{ ipc::UserWindowEvent, query::QueryEngine, shortcut::{HotKey, HotKeyState, ShortcutHandle, ShortcutRegistryError}, - webview::WebviewInstance, + webview::PendingWebview, AssetRequest, Config, WryEventHandler, }; -use dioxus_core::{ - prelude::{Callback, ScopeId}, - VirtualDom, +use dioxus_core::{prelude::Callback, VirtualDom}; +use std::{ + future::{Future, IntoFuture}, + pin::Pin, + rc::{Rc, Weak}, }; -use std::rc::{Rc, Weak}; use tao::{ event::Event, event_loop::EventLoopWindowTarget, @@ -99,21 +100,39 @@ impl DesktopService { } } - /// Create a new window using the props and window builder + /// Start the creation of a new window using the props and window builder /// - /// Returns the webview handle for the new window. - /// - /// You can use this to control other windows from the current window. + /// Returns a future that resolves to the webview handle for the new window. You can use this + /// to control other windows from the current window once the new window is created. /// /// Be careful to not create a cycle of windows, or you might leak memory. - pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> WeakDesktopContext { - let window = WebviewInstance::new(cfg, dom, self.shared.clone()); - - let cx = window.dom.in_runtime(|| { - ScopeId::ROOT - .consume_context::>() - .unwrap() - }); + /// + /// # Example + /// + /// ```rust, no_run + /// use dioxus::prelude::*; + /// fn popup() -> Element { + /// rsx! { + /// div { "This is a popup window!" } + /// } + /// } + /// + /// // Create a new window with a component that will be rendered in the new window. + /// let dom = VirtualDom::new(popup); + /// // Create and wait for the window + /// let window = dioxus::desktop::window().new_window(dom, Default::default()).await; + /// // Fullscreen the new window + /// window.set_fullscreen(true); + /// ``` + // Note: This method is asynchronous because webview2 does not support creating a new window from + // inside of an existing webview callback. Dioxus runs event handlers synchronously inside of a webview + // callback. See [this page](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/threading-model#reentrancy) for more information. + // + // Related issues: + // - https://github.com/tauri-apps/wry/issues/583 + // - https://github.com/DioxusLabs/dioxus/issues/3080 + pub fn new_window(&self, dom: VirtualDom, cfg: Config) -> PendingDesktopContext { + let (window, context) = PendingWebview::new(dom, cfg); self.shared .proxy @@ -122,7 +141,7 @@ impl DesktopService { self.shared.pending_webviews.borrow_mut().push(window); - Rc::downgrade(&cx) + context } /// trigger the drag-window event @@ -300,3 +319,43 @@ fn is_main_thread() -> bool { let result: BOOL = unsafe { msg_send![cls, isMainThread] }; result != NO } + +/// A [`DesktopContext`] that is pending creation. +/// +/// # Example +/// ```rust +/// // Create a new window asynchronously +/// let pending_context = desktop_service.new_window(dom, config); +/// // Wait for the context to be created +/// let window = pending_context.await; +/// window.set_fullscreen(true); +/// ``` +pub struct PendingDesktopContext { + pub(crate) receiver: tokio::sync::oneshot::Receiver, +} + +impl PendingDesktopContext { + /// Resolve the pending context into a [`DesktopContext`]. + pub async fn resolve(self) -> DesktopContext { + self.try_resolve() + .await + .expect("Failed to resolve pending desktop context") + } + + /// Try to resolve the pending context into a [`DesktopContext`]. + pub async fn try_resolve( + self, + ) -> Result { + self.receiver.await + } +} + +impl IntoFuture for PendingDesktopContext { + type Output = DesktopContext; + + type IntoFuture = Pin>>; + + fn into_future(self) -> Self::IntoFuture { + Box::pin(self.resolve()) + } +} diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 341306870..02dc2f0e3 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -48,7 +48,9 @@ pub mod trayicon; // Public exports pub use assets::AssetRequest; pub use config::{Config, WindowCloseBehaviour}; -pub use desktop_context::{window, DesktopContext, DesktopService, WeakDesktopContext}; +pub use desktop_context::{ + window, DesktopContext, DesktopService, PendingDesktopContext, WeakDesktopContext, +}; pub use event_handlers::WryEventHandler; pub use hooks::*; pub use shortcut::{HotKeyState, ShortcutHandle, ShortcutRegistryError}; diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index d370b19d1..b84b00d8f 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -1,6 +1,7 @@ use crate::element::DesktopElement; use crate::file_upload::DesktopFileDragEvent; use crate::menubar::DioxusMenu; +use crate::PendingDesktopContext; use crate::{ app::SharedContext, assets::AssetHandlerRegistry, @@ -207,6 +208,7 @@ impl WebviewInstance { // https://developer.apple.com/documentation/appkit/nswindowcollectionbehavior/nswindowcollectionbehaviormanaged #[cfg(target_os = "macos")] + #[allow(deprecated)] { use cocoa::appkit::NSWindowCollectionBehavior; use cocoa::base::id; @@ -524,3 +526,33 @@ impl SynchronousEventResponse { Self { prevent_default } } } + +/// A webview that is queued to be created. We can't spawn webviews outside of the main event loop because it may +/// block on windows so we queue them into the shared context and then create them when the main event loop is ready. +pub(crate) struct PendingWebview { + dom: VirtualDom, + cfg: Config, + sender: tokio::sync::oneshot::Sender, +} + +impl PendingWebview { + pub(crate) fn new(dom: VirtualDom, cfg: Config) -> (Self, PendingDesktopContext) { + let (sender, receiver) = tokio::sync::oneshot::channel(); + let webview = Self { dom, cfg, sender }; + let pending = PendingDesktopContext { receiver }; + (webview, pending) + } + + pub(crate) fn create_window(self, shared: &Rc) -> WebviewInstance { + let window = WebviewInstance::new(self.cfg, self.dom, shared.clone()); + + let cx = window.dom.in_runtime(|| { + ScopeId::ROOT + .consume_context::>() + .unwrap() + }); + _ = self.sender.send(cx); + + window + } +}