Browse Source

Make new_window asynchronous (#4297)

* Make new_window asynchronous

* fix clippy
Evan Almloff 1 week ago
parent
commit
e6ca0baa46

+ 6 - 5
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<Vec<WebviewInstance>>,
+    pub(crate) pending_webviews: RefCell<Vec<PendingWebview>>,
     pub(crate) shortcut_manager: ShortcutRegistry,
     pub(crate) proxy: EventLoopProxy<UserWindowEvent>,
     pub(crate) target: EventLoopWindowTarget<UserWindowEvent>,
@@ -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));
         }
     }

+ 77 - 18
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::<Rc<DesktopService>>()
-                .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<DesktopContext>,
+}
+
+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<DesktopContext, tokio::sync::oneshot::error::RecvError> {
+        self.receiver.await
+    }
+}
+
+impl IntoFuture for PendingDesktopContext {
+    type Output = DesktopContext;
+
+    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;
+
+    fn into_future(self) -> Self::IntoFuture {
+        Box::pin(self.resolve())
+    }
+}

+ 3 - 1
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};

+ 32 - 0
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<DesktopContext>,
+}
+
+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<SharedContext>) -> WebviewInstance {
+        let window = WebviewInstance::new(self.cfg, self.dom, shared.clone());
+
+        let cx = window.dom.in_runtime(|| {
+            ScopeId::ROOT
+                .consume_context::<Rc<DesktopService>>()
+                .unwrap()
+        });
+        _ = self.sender.send(cx);
+
+        window
+    }
+}