tweaks to the LastWindowClose to make it more imperative

This commit is contained in:
Jonathan Kelley committed 2025-07-01 19:58:41 -07:00
1 parent 9573af115a
commit ff762cec84
11 files changed
+74 -214

No files matched your search

Generated
+4 -25
View File
@@ -3965,7 +3965,7 @@ dependencies = [
"jni", "jni",
"lazy-js-bundle", "lazy-js-bundle",
"libc", "libc",
"muda 0.17.0", "muda",
"ndk", "ndk",
"ndk-context", "ndk-context",
"ndk-sys 0.6.0+11769913", "ndk-sys 0.6.0+11769913",
@@ -8985,27 +8985,6 @@ dependencies = [
"nasm-rs", "nasm-rs",
] ]
[[package]]
name = "muda"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4de14a9b5d569ca68d7c891d613b390cf5ab4f851c77aaa2f9e435555d3d9492"
dependencies = [
"crossbeam-channel",
"dpi",
"gtk",
"keyboard-types",
"libxdo",
"objc2 0.6.1",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation 0.3.1",
"once_cell",
"png",
"thiserror 2.0.12",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.17.0" version = "0.17.0"
@@ -15177,14 +15156,14 @@ dependencies = [
[[package]] [[package]]
name = "tray-icon" name = "tray-icon"
version = "0.20.1" version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0" checksum = "2da75ec677957aa21f6e0b361df0daab972f13a5bee3606de0638fd4ee1c666a"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"dirs 6.0.0", "dirs 6.0.0",
"libappindicator", "libappindicator",
"muda 0.16.1", "muda",
"objc2 0.6.1", "objc2 0.6.1",
"objc2-app-kit 0.3.1", "objc2-app-kit 0.3.1",
"objc2-core-foundation", "objc2-core-foundation",
+1 -1
View File
@@ -325,7 +325,7 @@ cocoa = "0.26"
core-foundation = "0.10.1" core-foundation = "0.10.1"
objc = { version = "0.2.7", features = ["exception"] } objc = { version = "0.2.7", features = ["exception"] }
objc_id = "0.1.1" objc_id = "0.1.1"
tray-icon = "0.20.0" tray-icon = "0.21.0"
open = "5.1.2" open = "5.1.2"
# web # web
+2 -10
View File
@@ -5,22 +5,14 @@
//! own context, root elements, etc. //! own context, root elements, etc.
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus::{desktop::Config, desktop::DefaultWindowCloseBehaviour};
fn main() { fn main() {
dioxus::LaunchBuilder::desktop() dioxus::launch(app);
// 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);
} }
fn app() -> Element { fn app() -> Element {
let onclick = move |_| { let onclick = move |_| {
let dom = VirtualDom::new(popup); dioxus::desktop::window().new_window(VirtualDom::new(popup), Default::default());
dioxus::desktop::window().new_window(dom, Default::default());
}; };
rsx! { rsx! {
+18 -12
View File
@@ -3,31 +3,37 @@
//! This example shows how to implement a simple multiwindow application and tray icon using dioxus. //! 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 //! 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. //! own context, root elements, etc.
//!
//! This is useful for apps that incorporate settings panels or persistent windows like Raycast.
use dioxus::desktop::{ use dioxus::desktop::{
trayicon::{default_tray_icon, init_tray_icon}, trayicon::{default_tray_icon, init_tray_icon},
Config, WindowCloseBehaviour, window, WindowCloseBehaviour,
}; };
use dioxus::prelude::*; use dioxus::prelude::*;
fn main() { fn main() {
dioxus::LaunchBuilder::desktop() dioxus::launch(app);
// 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 { fn app() -> Element {
// async should not be needed, check if issue 3542 has been resolved use_hook(|| {
let onclick = move |_| async { // Set the close behavior for the main window
let dom = VirtualDom::new(popup); // This will hide the window instead of closing it when the user clicks the close button
dioxus::desktop::window().new_window(dom, Default::default()); window().set_close_behavior(WindowCloseBehaviour::WindowHides);
};
init_tray_icon(default_tray_icon(), None); // Initialize the tray icon with a default icon and no menu
// This will provide the tray into context for the application
init_tray_icon(default_tray_icon(), None)
});
rsx! { rsx! {
button { onclick, "New Window" } button {
onclick: move |_| {
window().new_window(VirtualDom::new(popup), Default::default());
},
"New Window"
}
} }
} }
+20 -107
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
config::{Config, DefaultWindowCloseBehaviour, WindowCloseBehaviour}, config::{Config, WindowCloseBehaviour},
edits::EditWebsocket, edits::EditWebsocket,
event_handlers::WindowEventHandlers, event_handlers::WindowEventHandlers,
file_upload::{DesktopFileUploadForm, FileDialogRequest, NativeFileEngine}, file_upload::{DesktopFileUploadForm, FileDialogRequest, NativeFileEngine},
@@ -23,7 +23,7 @@ use tao::{
dpi::PhysicalSize, dpi::PhysicalSize,
event::Event, event::Event,
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}, event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget},
window::{Window, WindowId}, window::WindowId,
}; };
/// The single top-level object that manages all the running windows, assets, shortcuts, etc /// The single top-level object that manages all the running windows, assets, shortcuts, etc
@@ -36,7 +36,7 @@ pub(crate) struct App {
// Stuff we need mutable access to // Stuff we need mutable access to
pub(crate) control_flow: ControlFlow, pub(crate) control_flow: ControlFlow,
pub(crate) is_visible_before_start: bool, pub(crate) is_visible_before_start: bool,
pub(crate) default_window_close_behavior: DefaultWindowCloseBehaviour, pub(crate) exit_on_last_window_close: bool,
pub(crate) webviews: HashMap<WindowId, WebviewInstance>, pub(crate) webviews: HashMap<WindowId, WebviewInstance>,
pub(crate) float_all: bool, pub(crate) float_all: bool,
pub(crate) show_devtools: bool, pub(crate) show_devtools: bool,
@@ -65,7 +65,7 @@ impl App {
.unwrap_or_else(|| EventLoopBuilder::<UserWindowEvent>::with_user_event().build()); .unwrap_or_else(|| EventLoopBuilder::<UserWindowEvent>::with_user_event().build());
let app = Self { let app = Self {
default_window_close_behavior: cfg.default_window_close_behaviour, exit_on_last_window_close: cfg.exit_on_last_window_close,
is_visible_before_start: true, is_visible_before_start: true,
webviews: HashMap::new(), webviews: HashMap::new(),
control_flow: ControlFlow::Wait, control_flow: ControlFlow::Wait,
@@ -188,89 +188,37 @@ impl App {
} }
} }
pub fn change_window_close_behaviour(
&mut self,
id: WindowId,
behaviour: Option<WindowCloseBehaviour>,
) {
if let Some(webview) = self.webviews.get_mut(&id) {
webview.close_behaviour = behaviour
}
}
pub fn handle_close_requested(&mut self, id: WindowId) { pub fn handle_close_requested(&mut self, id: WindowId) {
use DefaultWindowCloseBehaviour::*; let Some(window) = self.webviews.get(&id) else {
use WindowCloseBehaviour::*; // If the window is not found, we can just return
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; return;
} };
match self.default_window_close_behavior { match window.desktop_context.close_behaviour.get() {
LastWindowExitsApp => { // If the window is just set to hide when closed, we can just hide it
WindowCloseBehaviour::WindowHides => {
window.desktop_context.window.set_visible(false);
}
// If the window is set to close, we can remove it from the list of webviews
// If the app is set to exit when the last window closes, we should also exit the app
WindowCloseBehaviour::WindowCloses => {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
self.persist_window_state(); self.persist_window_state();
self.webviews.remove(&id); self.webviews.remove(&id);
if self.webviews.is_empty() {
if self.exit_on_last_window_close && self.webviews.is_empty() {
self.control_flow = ControlFlow::Exit self.control_flow = ControlFlow::Exit
} }
} }
};
LastWindowHides if self.webviews.len() > 1 => {
self.webviews.remove(&id);
}
WindowsHides | LastWindowHides => {
if let Some(webview) = self.webviews.get(&id) {
hide_window(&webview.desktop_context.window);
}
}
WindowsCloses => {
self.webviews.remove(&id);
}
}
} }
pub fn window_destroyed(&mut self, id: WindowId) { pub fn window_destroyed(&mut self, id: WindowId) {
self.webviews.remove(&id); self.webviews.remove(&id);
if matches!( if self.exit_on_last_window_close && self.webviews.is_empty() {
self.default_window_close_behavior,
DefaultWindowCloseBehaviour::LastWindowExitsApp
) && self.webviews.is_empty()
{
self.control_flow = ControlFlow::Exit self.control_flow = ControlFlow::Exit
} }
} }
@@ -723,41 +671,6 @@ struct PreservedWindowState {
url: Option<String>, url: Option<String>,
} }
/// 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_window(window: &Window) {
#[cfg(target_os = "windows")]
{
use tao::platform::windows::WindowExtWindows;
window.set_visible(false);
}
#[cfg(target_os = "linux")]
{
use tao::platform::unix::WindowExtUnix;
window.set_visible(false);
}
#[cfg(target_os = "macos")]
{
// window.set_visible(false); has the wrong behaviour on macOS
// It will hide the window but not show it again when the user switches
// back to the app. `NSApplication::hide:` has the correct behaviour
use objc::runtime::Object;
use objc::{msg_send, sel, sel_impl};
#[allow(unexpected_cfgs)]
objc::rc::autoreleasepool(|| unsafe {
let app: *mut Object = msg_send![objc::class!(NSApplication), sharedApplication];
let nil = std::ptr::null_mut::<Object>();
let _: () = msg_send![app, hide: nil];
});
}
}
/// Return the location of a tempfile with our window state in it such that we can restore it later /// Return the location of a tempfile with our window state in it such that we can restore it later
fn restore_file() -> std::path::PathBuf { fn restore_file() -> std::path::PathBuf {
let dir = dioxus_cli_config::session_cache_dir().unwrap_or_else(std::env::temp_dir); let dir = dioxus_cli_config::session_cache_dir().unwrap_or_else(std::env::temp_dir);
+15 -29
View File
@@ -17,28 +17,13 @@ type CustomEventHandler = Box<
), ),
>; >;
/// 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 DefaultWindowCloseBehaviour {
/// Default behaviour, closing the last window will exit the app, others will close,
LastWindowExitsApp,
/// Closing the last window will hide it, others will close
LastWindowHides,
/// Every window will hide
WindowsHides,
/// Every window will close
WindowsCloses,
}
/// The closing behaviour of specific application window. /// The closing behaviour of specific application window.
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
pub enum WindowCloseBehaviour { pub enum WindowCloseBehaviour {
/// Closing the window will exit the app /// Window will hide instead of closing
WindowExitsApp,
/// Window will hide
WindowHides, WindowHides,
/// Window will close /// Window will close
WindowCloses, WindowCloses,
} }
@@ -75,8 +60,8 @@ pub struct Config {
pub(crate) custom_index: Option<String>, pub(crate) custom_index: Option<String>,
pub(crate) root_name: String, pub(crate) root_name: String,
pub(crate) background_color: Option<(u8, u8, u8, u8)>, pub(crate) background_color: Option<(u8, u8, u8, u8)>,
pub(crate) default_window_close_behaviour: DefaultWindowCloseBehaviour, pub(crate) exit_on_last_window_close: bool,
pub(crate) window_close_behaviour: Option<WindowCloseBehaviour>, pub(crate) window_close_behavior: WindowCloseBehaviour,
pub(crate) custom_event_handler: Option<CustomEventHandler>, pub(crate) custom_event_handler: Option<CustomEventHandler>,
pub(crate) disable_file_drop_handler: bool, pub(crate) disable_file_drop_handler: bool,
} }
@@ -122,8 +107,8 @@ impl Config {
custom_index: None, custom_index: None,
root_name: "main".to_string(), root_name: "main".to_string(),
background_color: None, background_color: None,
default_window_close_behaviour: DefaultWindowCloseBehaviour::LastWindowExitsApp, exit_on_last_window_close: true,
window_close_behaviour: None, window_close_behavior: WindowCloseBehaviour::WindowCloses,
custom_event_handler: None, custom_event_handler: None,
disable_file_drop_handler: false, disable_file_drop_handler: false,
} }
@@ -185,18 +170,19 @@ impl Config {
self self
} }
/// Sets the behaviour of the application when the last window is closed. /// When the last window is closed, the application will exit.
pub fn with_default_window_close_behaviour( ///
mut self, /// This is the default behaviour.
behaviour: DefaultWindowCloseBehaviour, ///
) -> Self { /// If the last window is hidden, the application will not exit.
self.default_window_close_behaviour = behaviour; pub fn with_exits_when_last_window_closes(mut self, exit: bool) -> Self {
self.exit_on_last_window_close = exit;
self self
} }
/// Sets the behaviour of the application when the last window is closed. /// Sets the behaviour of the application when the last window is closed.
pub fn with_window_close_behaviour(mut self, behaviour: WindowCloseBehaviour) -> Self { pub fn with_close_behaviour(mut self, behaviour: WindowCloseBehaviour) -> Self {
self.window_close_behaviour = Some(behaviour); self.window_close_behavior = behaviour;
self self
} }
+12 -20
View File
@@ -10,6 +10,7 @@ use crate::{
}; };
use dioxus_core::{prelude::Callback, VirtualDom}; use dioxus_core::{prelude::Callback, VirtualDom};
use std::{ use std::{
cell::Cell,
future::{Future, IntoFuture}, future::{Future, IntoFuture},
pin::Pin, pin::Pin,
rc::{Rc, Weak}, rc::{Rc, Weak},
@@ -66,6 +67,7 @@ pub struct DesktopService {
pub(super) query: QueryEngine, pub(super) query: QueryEngine,
pub(crate) asset_handlers: AssetHandlerRegistry, pub(crate) asset_handlers: AssetHandlerRegistry,
pub(crate) file_hover: NativeFileHover, pub(crate) file_hover: NativeFileHover,
pub(crate) close_behaviour: Rc<Cell<WindowCloseBehaviour>>,
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
pub(crate) views: Rc<std::cell::RefCell<Vec<*mut objc::runtime::Object>>>, pub(crate) views: Rc<std::cell::RefCell<Vec<*mut objc::runtime::Object>>>,
@@ -87,6 +89,7 @@ impl DesktopService {
shared: Rc<SharedContext>, shared: Rc<SharedContext>,
asset_handlers: AssetHandlerRegistry, asset_handlers: AssetHandlerRegistry,
file_hover: NativeFileHover, file_hover: NativeFileHover,
close_behaviour: WindowCloseBehaviour,
) -> Self { ) -> Self {
Self { Self {
window, window,
@@ -94,6 +97,7 @@ impl DesktopService {
shared, shared,
asset_handlers, asset_handlers,
file_hover, file_hover,
close_behaviour: Rc::new(Cell::new(close_behaviour)),
query: Default::default(), query: Default::default(),
#[cfg(target_os = "ios")] #[cfg(target_os = "ios")]
views: Default::default(), views: Default::default(),
@@ -165,6 +169,14 @@ impl DesktopService {
self.window.set_maximized(!self.window.is_maximized()) self.window.set_maximized(!self.window.is_maximized())
} }
/// Set the close behavior of this window
///
/// By default, windows close when the user clicks the close button.
/// If this is set to `WindowCloseBehaviour::WindowHides`, the window will hide instead of closing.
pub fn set_close_behavior(&self, behaviour: WindowCloseBehaviour) {
self.close_behaviour.set(behaviour);
}
/// Close this window /// Close this window
pub fn close(&self) { pub fn close(&self) {
let _ = self let _ = self
@@ -181,26 +193,6 @@ impl DesktopService {
.send_event(UserWindowEvent::CloseWindow(id)); .send_event(UserWindowEvent::CloseWindow(id));
} }
/// Change close behaviour of this window
pub fn change_close_behaviour(&self, behaviour: Option<WindowCloseBehaviour>) {
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<WindowCloseBehaviour>,
) {
let _ = self
.shared
.proxy
.send_event(UserWindowEvent::CloseBehaviour(id, behaviour));
}
/// change window to fullscreen /// change window to fullscreen
pub fn set_fullscreen(&self, fullscreen: bool) { pub fn set_fullscreen(&self, fullscreen: bool) {
if let Some(handle) = &self.window.current_monitor() { if let Some(handle) = &self.window.current_monitor() {
-4
View File
@@ -1,8 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tao::window::WindowId; use tao::window::WindowId;
use crate::WindowCloseBehaviour;
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum UserWindowEvent { pub enum UserWindowEvent {
@@ -19,8 +17,6 @@ pub enum UserWindowEvent {
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
TrayMenuEvent(tray_icon::menu::MenuEvent), TrayMenuEvent(tray_icon::menu::MenuEvent),
CloseBehaviour(WindowId, Option<WindowCloseBehaviour>),
/// Poll the virtualdom /// Poll the virtualdom
Poll(WindowId), Poll(WindowId),
-3
View File
@@ -103,9 +103,6 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config:
IpcMethod::BrowserOpen => app.handle_browser_open(msg), IpcMethod::BrowserOpen => app.handle_browser_open(msg),
IpcMethod::Other(_) => {} IpcMethod::Other(_) => {}
}, },
UserWindowEvent::CloseBehaviour(window_id, window_close_behaviour) => {
app.change_window_close_behaviour(window_id, window_close_behaviour)
}
}, },
_ => {} _ => {}
} }
+1 -1
View File
@@ -47,7 +47,7 @@ pub mod trayicon;
// Public exports // Public exports
pub use assets::AssetRequest; pub use assets::AssetRequest;
pub use config::{Config, DefaultWindowCloseBehaviour, WindowCloseBehaviour}; pub use config::{Config, WindowCloseBehaviour};
pub use desktop_context::{ pub use desktop_context::{
window, DesktopContext, DesktopService, PendingDesktopContext, WeakDesktopContext, window, DesktopContext, DesktopService, PendingDesktopContext, WeakDesktopContext,
}; };
+1 -2
View File
@@ -161,7 +161,6 @@ pub(crate) struct WebviewInstance {
pub edits: WebviewEdits, pub edits: WebviewEdits,
pub desktop_context: DesktopContext, pub desktop_context: DesktopContext,
pub waker: Waker, pub waker: Waker,
pub close_behaviour: Option<WindowCloseBehaviour>,
// Wry assumes the webcontext is alive for the lifetime of the webview. // Wry assumes the webcontext is alive for the lifetime of the webview.
// We need to keep the webcontext alive, otherwise the webview will crash // We need to keep the webcontext alive, otherwise the webview will crash
@@ -414,6 +413,7 @@ impl WebviewInstance {
shared.clone(), shared.clone(),
asset_handlers, asset_handlers,
file_hover, file_hover,
WindowCloseBehaviour::WindowCloses,
)); ));
// Provide the desktop context to the virtual dom and edit handler // Provide the desktop context to the virtual dom and edit handler
@@ -433,7 +433,6 @@ impl WebviewInstance {
desktop_context, desktop_context,
_menu: menu, _menu: menu,
_web_context: web_context, _web_context: web_context,
close_behaviour: cfg.window_close_behaviour,
} }
} }