diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 1adf10328..1036bb5df 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -5,12 +5,15 @@ //! own context, root elements, etc. use dioxus::prelude::*; -use dioxus::{desktop::Config, desktop::WindowCloseBehaviour}; +use dioxus::{desktop::Config, desktop::DefaultWindowCloseBehaviour}; fn main() { dioxus::LaunchBuilder::desktop() - // We can choose the close behavior of the last window to hide. See WindowCloseBehaviour for more options. - .with_cfg(Config::new().with_close_behaviour(WindowCloseBehaviour::LastWindowHides)) + // We can choose the close behavior of the last window to hide. See DefaultWindowCloseBehaviour for more options. + .with_cfg( + Config::new() + .with_default_window_close_behaviour(DefaultWindowCloseBehaviour::LastWindowHides), + ) .launch(app); } diff --git a/examples/multiwindow_with_tray_icon.rs b/examples/multiwindow_with_tray_icon.rs new file mode 100644 index 000000000..3d75fa49e --- /dev/null +++ b/examples/multiwindow_with_tray_icon.rs @@ -0,0 +1,38 @@ +//! Multiwindow with tray icon example +//! +//! This example shows how to implement a simple multiwindow application and tray icon using dioxus. +//! This works by spawning a new window when the user clicks a button. We have to build a new virtualdom which has its +//! own context, root elements, etc. + +use dioxus::desktop::{ + trayicon::{default_tray_icon, init_tray_icon}, + Config, WindowCloseBehaviour, +}; +use dioxus::prelude::*; + +fn main() { + dioxus::LaunchBuilder::desktop() + // We can choose the close behavior of this window to hide. See WindowCloseBehaviour for more options. + .with_cfg(Config::new().with_window_close_behaviour(WindowCloseBehaviour::WindowHides)) + .launch(app); +} + +fn app() -> Element { + // async should not be needed, check if issue 3542 has been resolved + let onclick = move |_| async { + let dom = VirtualDom::new(popup); + dioxus::desktop::window().new_window(dom, Default::default()); + }; + + init_tray_icon(default_tray_icon(), None); + + rsx! { + button { onclick, "New Window" } + } +} + +fn popup() -> Element { + rsx! { + div { "This is a popup window!" } + } +} diff --git a/examples/window_focus.rs b/examples/window_focus.rs index 1c44a8df1..20b81f875 100644 --- a/examples/window_focus.rs +++ b/examples/window_focus.rs @@ -8,12 +8,15 @@ use dioxus::desktop::tao::event::Event as WryEvent; use dioxus::desktop::tao::event::WindowEvent; use dioxus::desktop::use_wry_event_handler; -use dioxus::desktop::{Config, WindowCloseBehaviour}; +use dioxus::desktop::{Config, DefaultWindowCloseBehaviour}; use dioxus::prelude::*; fn main() { dioxus::LaunchBuilder::desktop() - .with_cfg(Config::new().with_close_behaviour(WindowCloseBehaviour::CloseWindow)) + .with_cfg( + Config::new() + .with_default_window_close_behaviour(DefaultWindowCloseBehaviour::WindowsCloses), + ) .launch(app) } diff --git a/packages/desktop/src/app.rs b/packages/desktop/src/app.rs index e13ad626c..22b975ea4 100644 --- a/packages/desktop/src/app.rs +++ b/packages/desktop/src/app.rs @@ -1,5 +1,5 @@ use crate::{ - config::{Config, WindowCloseBehaviour}, + config::{Config, DefaultWindowCloseBehaviour, WindowCloseBehaviour}, edits::EditWebsocket, event_handlers::WindowEventHandlers, file_upload::{DesktopFileUploadForm, FileDialogRequest, NativeFileEngine}, @@ -36,7 +36,7 @@ pub(crate) struct App { // Stuff we need mutable access to pub(crate) control_flow: ControlFlow, pub(crate) is_visible_before_start: bool, - pub(crate) window_behavior: WindowCloseBehaviour, + pub(crate) default_window_close_behavior: DefaultWindowCloseBehaviour, pub(crate) webviews: HashMap, pub(crate) float_all: bool, pub(crate) show_devtools: bool, @@ -65,7 +65,7 @@ impl App { .unwrap_or_else(|| EventLoopBuilder::::with_user_event().build()); let app = Self { - window_behavior: cfg.last_window_close_behavior, + default_window_close_behavior: cfg.default_window_close_behaviour, is_visible_before_start: true, webviews: HashMap::new(), control_flow: ControlFlow::Wait, @@ -188,10 +188,55 @@ impl App { } } + pub fn change_window_close_behaviour( + &mut self, + id: WindowId, + behaviour: Option, + ) { + if let Some(webview) = self.webviews.get_mut(&id) { + webview.close_behaviour = behaviour + } + } + pub fn handle_close_requested(&mut self, id: WindowId) { + use DefaultWindowCloseBehaviour::*; use WindowCloseBehaviour::*; - match self.window_behavior { + let mut remove = false; + + if let Some(webview) = self.webviews.get(&id) { + if let Some(close_behaviour) = &webview.close_behaviour { + match close_behaviour { + WindowExitsApp => { + self.control_flow = ControlFlow::Exit; + return; + } + WindowHides => { + hide_window(&webview.desktop_context.window); + return; + } + WindowCloses => { + remove = true; + } + } + } + } + + // needed in case of `default_window_close_behavior WindowsHides | LastWindowHides` since they may not remove a window on `WindowCloses` + if remove { + #[cfg(debug_assertions)] + self.persist_window_state(); + + self.webviews.remove(&id); + if matches!(self.default_window_close_behavior, LastWindowExitsApp) + && self.webviews.is_empty() + { + self.control_flow = ControlFlow::Exit + } + return; + } + + match self.default_window_close_behavior { LastWindowExitsApp => { #[cfg(debug_assertions)] self.persist_window_state(); @@ -206,13 +251,13 @@ impl App { self.webviews.remove(&id); } - LastWindowHides => { + WindowsHides | LastWindowHides => { if let Some(webview) = self.webviews.get(&id) { - hide_last_window(&webview.desktop_context.window); + hide_window(&webview.desktop_context.window); } } - CloseWindow => { + WindowsCloses => { self.webviews.remove(&id); } } @@ -222,8 +267,8 @@ impl App { self.webviews.remove(&id); if matches!( - self.window_behavior, - WindowCloseBehaviour::LastWindowExitsApp + self.default_window_close_behavior, + DefaultWindowCloseBehaviour::LastWindowExitsApp ) && self.webviews.is_empty() { self.control_flow = ControlFlow::Exit @@ -678,13 +723,13 @@ struct PreservedWindowState { url: Option, } -/// Hide the last window when using LastWindowHides. +/// Hides a window. /// /// On macOS, if we use `set_visibility(false)` on the window, it will hide the window but not show /// it again when the user switches back to the app. `NSApplication::hide:` has the correct behaviour, /// so we need to special case it. #[allow(unused)] -fn hide_last_window(window: &Window) { +fn hide_window(window: &Window) { #[cfg(target_os = "windows")] { use tao::platform::windows::WindowExtWindows; diff --git a/packages/desktop/src/config.rs b/packages/desktop/src/config.rs index 4c950d8d8..c54443e6a 100644 --- a/packages/desktop/src/config.rs +++ b/packages/desktop/src/config.rs @@ -17,16 +17,30 @@ type CustomEventHandler = Box< ), >; -/// The behaviour of the application when the last window is closed. +/// The closing behaviour of the application when the last window is closed, you can overwrite this behaviour for specific window with WindowCloseBehaviour. #[derive(Copy, Clone, Eq, PartialEq)] #[non_exhaustive] -pub enum WindowCloseBehaviour { - /// Default behaviour, closing the last window exits the app +pub enum DefaultWindowCloseBehaviour { + /// Default behaviour, closing the last window will exit the app, others will close, LastWindowExitsApp, - /// Closing the last window will not actually close it, just hide it + /// Closing the last window will hide it, others will close LastWindowHides, - /// Closing the last window will close it but the app will keep running so that new windows can be opened - CloseWindow, + /// Every window will hide + WindowsHides, + /// Every window will close + WindowsCloses, +} + +/// The closing behaviour of specific application window. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[non_exhaustive] +pub enum WindowCloseBehaviour { + /// Closing the window will exit the app + WindowExitsApp, + /// Window will hide + WindowHides, + /// Window will close + WindowCloses, } /// The state of the menu builder. We need to keep track of if the state is default @@ -61,7 +75,8 @@ pub struct Config { pub(crate) custom_index: Option, pub(crate) root_name: String, pub(crate) background_color: Option<(u8, u8, u8, u8)>, - pub(crate) last_window_close_behavior: WindowCloseBehaviour, + pub(crate) default_window_close_behaviour: DefaultWindowCloseBehaviour, + pub(crate) window_close_behaviour: Option, pub(crate) custom_event_handler: Option, pub(crate) disable_file_drop_handler: bool, } @@ -107,7 +122,8 @@ impl Config { custom_index: None, root_name: "main".to_string(), background_color: None, - last_window_close_behavior: WindowCloseBehaviour::LastWindowExitsApp, + default_window_close_behaviour: DefaultWindowCloseBehaviour::LastWindowExitsApp, + window_close_behaviour: None, custom_event_handler: None, disable_file_drop_handler: false, } @@ -170,8 +186,17 @@ impl Config { } /// Sets the behaviour of the application when the last window is closed. - pub fn with_close_behaviour(mut self, behaviour: WindowCloseBehaviour) -> Self { - self.last_window_close_behavior = behaviour; + pub fn with_default_window_close_behaviour( + mut self, + behaviour: DefaultWindowCloseBehaviour, + ) -> Self { + self.default_window_close_behaviour = behaviour; + self + } + + /// Sets the behaviour of the application when the last window is closed. + pub fn with_window_close_behaviour(mut self, behaviour: WindowCloseBehaviour) -> Self { + self.window_close_behaviour = Some(behaviour); self } diff --git a/packages/desktop/src/desktop_context.rs b/packages/desktop/src/desktop_context.rs index 5de51ecf6..b248c3846 100644 --- a/packages/desktop/src/desktop_context.rs +++ b/packages/desktop/src/desktop_context.rs @@ -6,7 +6,7 @@ use crate::{ query::QueryEngine, shortcut::{HotKey, HotKeyState, ShortcutHandle, ShortcutRegistryError}, webview::PendingWebview, - AssetRequest, Config, WryEventHandler, + AssetRequest, Config, WindowCloseBehaviour, WryEventHandler, }; use dioxus_core::{prelude::Callback, VirtualDom}; use std::{ @@ -181,6 +181,26 @@ impl DesktopService { .send_event(UserWindowEvent::CloseWindow(id)); } + /// Change close behaviour of this window + pub fn change_close_behaviour(&self, behaviour: Option) { + let _ = self + .shared + .proxy + .send_event(UserWindowEvent::CloseBehaviour(self.id(), behaviour)); + } + + /// Change close behaviour of a specific window, given its ID + pub fn change_window_close_behaviour( + &self, + id: WindowId, + behaviour: Option, + ) { + let _ = self + .shared + .proxy + .send_event(UserWindowEvent::CloseBehaviour(id, behaviour)); + } + /// change window to fullscreen pub fn set_fullscreen(&self, fullscreen: bool) { if let Some(handle) = &self.window.current_monitor() { diff --git a/packages/desktop/src/ipc.rs b/packages/desktop/src/ipc.rs index 9dfbf104b..8aeeeb135 100644 --- a/packages/desktop/src/ipc.rs +++ b/packages/desktop/src/ipc.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use tao::window::WindowId; +use crate::WindowCloseBehaviour; + #[non_exhaustive] #[derive(Debug, Clone)] pub enum UserWindowEvent { @@ -17,6 +19,8 @@ pub enum UserWindowEvent { #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] TrayMenuEvent(tray_icon::menu::MenuEvent), + CloseBehaviour(WindowId, Option), + /// Poll the virtualdom Poll(WindowId), diff --git a/packages/desktop/src/launch.rs b/packages/desktop/src/launch.rs index d159dac8f..d0d1b7ce4 100644 --- a/packages/desktop/src/launch.rs +++ b/packages/desktop/src/launch.rs @@ -103,6 +103,9 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config: IpcMethod::BrowserOpen => app.handle_browser_open(msg), IpcMethod::Other(_) => {} }, + UserWindowEvent::CloseBehaviour(window_id, window_close_behaviour) => { + app.change_window_close_behaviour(window_id, window_close_behaviour) + } }, _ => {} } diff --git a/packages/desktop/src/lib.rs b/packages/desktop/src/lib.rs index 02dc2f0e3..7c951ef48 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -47,7 +47,7 @@ pub mod trayicon; // Public exports pub use assets::AssetRequest; -pub use config::{Config, WindowCloseBehaviour}; +pub use config::{Config, DefaultWindowCloseBehaviour, WindowCloseBehaviour}; pub use desktop_context::{ window, DesktopContext, DesktopService, PendingDesktopContext, WeakDesktopContext, }; diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index b88f9bf3e..fa37ee3c1 100644 --- a/packages/desktop/src/webview.rs +++ b/packages/desktop/src/webview.rs @@ -2,6 +2,7 @@ use crate::element::DesktopElement; use crate::file_upload::DesktopFileDragEvent; use crate::menubar::DioxusMenu; use crate::PendingDesktopContext; +use crate::WindowCloseBehaviour; use crate::{ app::SharedContext, assets::AssetHandlerRegistry, @@ -160,6 +161,7 @@ pub(crate) struct WebviewInstance { pub edits: WebviewEdits, pub desktop_context: DesktopContext, pub waker: Waker, + pub close_behaviour: Option, // Wry assumes the webcontext is alive for the lifetime of the webview. // We need to keep the webcontext alive, otherwise the webview will crash @@ -431,6 +433,7 @@ impl WebviewInstance { desktop_context, _menu: menu, _web_context: web_context, + close_behaviour: cfg.window_close_behaviour, } }