diff --git a/assets/settings/default.json b/assets/settings/default.json index d87ddf49e0..7832a8f5e9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -126,6 +126,13 @@ // 3. Never close the window // "when_closing_with_no_tabs": "keep_window_open", "when_closing_with_no_tabs": "platform_default", + // What to do when the last window is closed. + // May take 2 values: + // 1. Use the current platform's convention + // "on_last_window_closed": "platform_default" + // 2. Always quit the application + // "on_last_window_closed": "quit_app", + "on_last_window_closed": "platform_default", // Whether to use the system provided dialogs for Open and Save As. // When set to false, Zed will use the built-in keyboard-first pickers. "use_system_path_prompts": true, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 4698cee157..cc5d1c0447 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -121,7 +121,7 @@ fn main() -> Result<()> { // Intercept version designators #[cfg(target_os = "macos")] if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) { - // When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along. + //When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along. use std::str::FromStr as _; if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) { diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index abaa486af4..1ea6fba95e 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -22,7 +22,14 @@ test-support = [ "x11", ] runtime_shaders = [] -macos-blade = ["blade-graphics", "blade-macros", "blade-util", "bytemuck", "objc2", "objc2-metal"] +macos-blade = [ + "blade-graphics", + "blade-macros", + "blade-util", + "bytemuck", + "objc2", + "objc2-metal", +] wayland = [ "blade-graphics", "blade-macros", @@ -133,7 +140,10 @@ pathfinder_geometry = "0.5" [target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] # Always used flume = "0.11" -oo7 = { version = "0.4.0", default-features = false, features = ["async-std", "native_crypto"] } +oo7 = { version = "0.4.0", default-features = false, features = [ + "async-std", + "native_crypto", +] } # Used in both windowing options ashpd = { workspace = true, optional = true } @@ -265,3 +275,7 @@ path = "examples/uniform_list.rs" [[example]] name = "window_shadow" path = "examples/window_shadow.rs" + +[[example]] +name = "on_window_close_quit" +path = "examples/on_window_close_quit.rs" diff --git a/crates/gpui/examples/on_window_close_quit.rs b/crates/gpui/examples/on_window_close_quit.rs new file mode 100644 index 0000000000..9aaaeb6b77 --- /dev/null +++ b/crates/gpui/examples/on_window_close_quit.rs @@ -0,0 +1,82 @@ +use gpui::{ + actions, div, prelude::*, px, rgb, size, App, Application, Bounds, Context, FocusHandle, + KeyBinding, Window, WindowBounds, WindowOptions, +}; + +actions!(example, [CloseWindow]); + +struct ExampleWindow { + focus_handle: FocusHandle, +} + +impl Render for ExampleWindow { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div() + .on_action(|_: &CloseWindow, window, _| { + window.remove_window(); + }) + .track_focus(&self.focus_handle) + .flex() + .flex_col() + .gap_3() + .bg(rgb(0x505050)) + .size(px(500.0)) + .justify_center() + .items_center() + .shadow_lg() + .border_1() + .border_color(rgb(0x0000ff)) + .text_xl() + .text_color(rgb(0xffffff)) + .child( + "Closing this window with cmd-w or the traffic lights should quit the application!", + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let mut bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); + + cx.bind_keys([KeyBinding::new("cmd-w", CloseWindow, None)]); + cx.on_window_closed(|cx| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + cx.activate(false); + cx.new(|cx| { + let focus_handle = cx.focus_handle(); + focus_handle.focus(window); + ExampleWindow { focus_handle } + }) + }, + ) + .unwrap(); + + bounds.origin.x += bounds.size.width; + + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + cx.new(|cx| { + let focus_handle = cx.focus_handle(); + focus_handle.focus(window); + ExampleWindow { focus_handle } + }) + }, + ) + .unwrap(); + }); +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1408f44a93..00b50e1e41 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -218,6 +218,7 @@ type Listener = Box bool + 'static>; pub(crate) type KeystrokeObserver = Box bool + 'static>; type QuitHandler = Box LocalBoxFuture<'static, ()> + 'static>; +type WindowClosedHandler = Box; type ReleaseListener = Box; type NewEntityListener = Box, &mut App) + 'static>; @@ -260,6 +261,7 @@ pub struct App { pub(crate) release_listeners: SubscriberSet, pub(crate) global_observers: SubscriberSet, pub(crate) quit_observers: SubscriberSet<(), QuitHandler>, + pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>, pub(crate) layout_id_buffer: Vec, // We recycle this memory across layout requests. pub(crate) propagate_event: bool, pub(crate) prompt_builder: Option, @@ -325,6 +327,7 @@ impl App { keyboard_layout_observers: SubscriberSet::new(), global_observers: SubscriberSet::new(), quit_observers: SubscriberSet::new(), + window_closed_observers: SubscriberSet::new(), layout_id_buffer: Default::default(), propagate_event: true, prompt_builder: Some(PromptBuilder::Default), @@ -1008,6 +1011,11 @@ impl App { if window.removed { cx.window_handles.remove(&id); cx.windows.remove(id); + + cx.window_closed_observers.clone().retain(&(), |callback| { + callback(cx); + true + }); } else { cx.windows .get_mut(id) @@ -1367,6 +1375,14 @@ impl App { subscription } + /// Register a callback to be invoked when a window is closed + /// The window is no longer accessible at the point this callback is invoked. + pub fn on_window_closed(&self, mut on_closed: impl FnMut(&mut App) + 'static) -> Subscription { + let (subscription, activate) = self.window_closed_observers.insert((), Box::new(on_closed)); + activate(); + subscription + } + pub(crate) fn clear_pending_keystrokes(&mut self) { for window in self.windows() { window diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8098450ef4..e14ce465f5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1804,6 +1804,7 @@ impl Workspace { } pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context) { + println!("workspace::close_window"); let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx); cx.spawn_in(window, |_, mut cx| async move { if prepare.await? { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 575b4ac261..3a86667bd8 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -18,11 +18,31 @@ pub struct WorkspaceSettings { pub autosave: AutosaveSetting, pub restore_on_startup: RestoreOnStartupBehavior, pub drop_target_size: f32, - pub when_closing_with_no_tabs: CloseWindowWhenNoItems, pub use_system_path_prompts: bool, pub command_aliases: HashMap, pub show_user_picture: bool, pub max_tabs: Option, + pub when_closing_with_no_tabs: CloseWindowWhenNoItems, + pub on_last_window_closed: OnLastWindowClosed, +} + +#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum OnLastWindowClosed { + /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms + #[default] + PlatformDefault, + /// Quit the application the last window is closed + QuitApp, +} + +impl OnLastWindowClosed { + pub fn is_quit_app(&self) -> bool { + match self { + OnLastWindowClosed::PlatformDefault => false, + OnLastWindowClosed::QuitApp => true, + } + } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] @@ -136,11 +156,15 @@ pub struct WorkspaceSettingsContent { /// /// Default: true pub show_user_picture: Option, - // Maximum open tabs in a pane. Will not close an unsaved - // tab. Set to `None` for unlimited tabs. - // - // Default: none + /// Maximum open tabs in a pane. Will not close an unsaved + /// tab. Set to `None` for unlimited tabs. + /// + /// Default: none pub max_tabs: Option, + /// What to do when the last window is closed + /// + /// Default: auto (nothing on macOS, "app quit" otherwise) + pub on_last_window_closed: Option, } #[derive(Deserialize)] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 00cb6c924f..f64b66b1a9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -104,6 +104,19 @@ pub fn init(cx: &mut App) { } } +fn bind_on_window_closed(cx: &mut App) -> Option { + WorkspaceSettings::get_global(cx) + .on_last_window_closed + .is_quit_app() + .then(|| { + cx.on_window_closed(|cx| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + }) +} + pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowOptions { let display = display_uuid.and_then(|uuid| { cx.displays() @@ -144,6 +157,12 @@ pub fn initialize_workspace( prompt_builder: Arc, cx: &mut App, ) { + let mut _on_close_subscription = bind_on_window_closed(cx); + cx.observe_global::(move |cx| { + _on_close_subscription = bind_on_window_closed(cx); + }) + .detach(); + cx.observe_new(move |workspace: &mut Workspace, window, cx| { let Some(window) = window else { return;