From e46af92f34e018defccc32df80962b2645ddba1b Mon Sep 17 00:00:00 2001 From: Klemen <50655512+Klemen2@users.noreply.github.com> Date: Sun, 19 Jan 2025 11:43:47 +0100 Subject: [PATCH] Added close behaviour option for specific window --- examples/multiwindow.rs | 9 ++-- examples/multiwindow_with_tray_icon.rs | 38 ++++++++++++++ examples/window_focus.rs | 7 ++- packages/desktop/src/app.rs | 67 +++++++++++++++++++++---- packages/desktop/src/config.rs | 45 +++++++++++++---- packages/desktop/src/desktop_context.rs | 22 +++++++- packages/desktop/src/ipc.rs | 4 ++ packages/desktop/src/launch.rs | 3 ++ packages/desktop/src/lib.rs | 2 +- packages/desktop/src/webview.rs | 3 ++ 10 files changed, 172 insertions(+), 28 deletions(-) create mode 100644 examples/multiwindow_with_tray_icon.rs diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index c22049b3d6..06ad7ad5d3 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 0000000000..3d75fa49e3 --- /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 1c44a8df19..20b81f8758 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 48b1ac67da..6f00ea63bd 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}, event_handlers::WindowEventHandlers, file_upload::{DesktopFileUploadForm, FileDialogRequest, NativeFileEngine}, ipc::{IpcMessage, UserWindowEvent}, @@ -33,7 +33,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, @@ -61,7 +61,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, @@ -184,10 +184,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(); @@ -202,13 +247,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); } } @@ -218,8 +263,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 @@ -572,13 +617,13 @@ struct PreservedWindowState { monitor: String, } -/// 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 9a89b4f055..03efa16e62 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 1966b0a7e6..5dad590141 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, ShortcutHandle, ShortcutRegistryError}, webview::WebviewInstance, - AssetRequest, Config, WryEventHandler, + AssetRequest, Config, WindowCloseBehaviour, WryEventHandler, }; use dioxus_core::{ prelude::{Callback, ScopeId}, @@ -160,6 +160,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 9dfbf104b9..8aeeeb135c 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 d159dac8ff..d0d1b7ce41 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 9eecdc6feb..61f39be3ec 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, WeakDesktopContext}; pub use event_handlers::WryEventHandler; pub use hooks::*; diff --git a/packages/desktop/src/webview.rs b/packages/desktop/src/webview.rs index 05ff7c0ad1..fcfbd369c5 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::WindowCloseBehaviour; use crate::{ app::SharedContext, assets::AssetHandlerRegistry, @@ -159,6 +160,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 @@ -434,6 +436,7 @@ impl WebviewInstance { desktop_context, _menu: menu, _web_context: web_context, + close_behaviour: cfg.window_close_behaviour, } }