Add last window closed setting (#25185)
Release Notes: - Added an `on_last_window_closed` setting, that allows users to quit the app when the last window is closed --------- Co-authored-by: Richard <richard@zed.dev>
This commit is contained in:
parent
ffc7558a1d
commit
40425093df
8 changed files with 171 additions and 8 deletions
|
@ -126,6 +126,13 @@
|
||||||
// 3. Never close the window
|
// 3. Never close the window
|
||||||
// "when_closing_with_no_tabs": "keep_window_open",
|
// "when_closing_with_no_tabs": "keep_window_open",
|
||||||
"when_closing_with_no_tabs": "platform_default",
|
"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.
|
// 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.
|
// When set to false, Zed will use the built-in keyboard-first pickers.
|
||||||
"use_system_path_prompts": true,
|
"use_system_path_prompts": true,
|
||||||
|
|
|
@ -121,7 +121,7 @@ fn main() -> Result<()> {
|
||||||
// Intercept version designators
|
// Intercept version designators
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
|
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 _;
|
use std::str::FromStr as _;
|
||||||
|
|
||||||
if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
|
if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
|
||||||
|
|
|
@ -22,7 +22,14 @@ test-support = [
|
||||||
"x11",
|
"x11",
|
||||||
]
|
]
|
||||||
runtime_shaders = []
|
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 = [
|
wayland = [
|
||||||
"blade-graphics",
|
"blade-graphics",
|
||||||
"blade-macros",
|
"blade-macros",
|
||||||
|
@ -133,7 +140,10 @@ pathfinder_geometry = "0.5"
|
||||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
|
||||||
# Always used
|
# Always used
|
||||||
flume = "0.11"
|
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
|
# Used in both windowing options
|
||||||
ashpd = { workspace = true, optional = true }
|
ashpd = { workspace = true, optional = true }
|
||||||
|
@ -265,3 +275,7 @@ path = "examples/uniform_list.rs"
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "window_shadow"
|
name = "window_shadow"
|
||||||
path = "examples/window_shadow.rs"
|
path = "examples/window_shadow.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "on_window_close_quit"
|
||||||
|
path = "examples/on_window_close_quit.rs"
|
||||||
|
|
82
crates/gpui/examples/on_window_close_quit.rs
Normal file
82
crates/gpui/examples/on_window_close_quit.rs
Normal file
|
@ -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<Self>) -> 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();
|
||||||
|
});
|
||||||
|
}
|
|
@ -218,6 +218,7 @@ type Listener = Box<dyn FnMut(&dyn Any, &mut App) -> bool + 'static>;
|
||||||
pub(crate) type KeystrokeObserver =
|
pub(crate) type KeystrokeObserver =
|
||||||
Box<dyn FnMut(&KeystrokeEvent, &mut Window, &mut App) -> bool + 'static>;
|
Box<dyn FnMut(&KeystrokeEvent, &mut Window, &mut App) -> bool + 'static>;
|
||||||
type QuitHandler = Box<dyn FnOnce(&mut App) -> LocalBoxFuture<'static, ()> + 'static>;
|
type QuitHandler = Box<dyn FnOnce(&mut App) -> LocalBoxFuture<'static, ()> + 'static>;
|
||||||
|
type WindowClosedHandler = Box<dyn FnMut(&mut App)>;
|
||||||
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut App) + 'static>;
|
type ReleaseListener = Box<dyn FnOnce(&mut dyn Any, &mut App) + 'static>;
|
||||||
type NewEntityListener = Box<dyn FnMut(AnyEntity, &mut Option<&mut Window>, &mut App) + 'static>;
|
type NewEntityListener = Box<dyn FnMut(AnyEntity, &mut Option<&mut Window>, &mut App) + 'static>;
|
||||||
|
|
||||||
|
@ -260,6 +261,7 @@ pub struct App {
|
||||||
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
pub(crate) release_listeners: SubscriberSet<EntityId, ReleaseListener>,
|
||||||
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
pub(crate) global_observers: SubscriberSet<TypeId, Handler>,
|
||||||
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
pub(crate) quit_observers: SubscriberSet<(), QuitHandler>,
|
||||||
|
pub(crate) window_closed_observers: SubscriberSet<(), WindowClosedHandler>,
|
||||||
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
pub(crate) layout_id_buffer: Vec<LayoutId>, // We recycle this memory across layout requests.
|
||||||
pub(crate) propagate_event: bool,
|
pub(crate) propagate_event: bool,
|
||||||
pub(crate) prompt_builder: Option<PromptBuilder>,
|
pub(crate) prompt_builder: Option<PromptBuilder>,
|
||||||
|
@ -325,6 +327,7 @@ impl App {
|
||||||
keyboard_layout_observers: SubscriberSet::new(),
|
keyboard_layout_observers: SubscriberSet::new(),
|
||||||
global_observers: SubscriberSet::new(),
|
global_observers: SubscriberSet::new(),
|
||||||
quit_observers: SubscriberSet::new(),
|
quit_observers: SubscriberSet::new(),
|
||||||
|
window_closed_observers: SubscriberSet::new(),
|
||||||
layout_id_buffer: Default::default(),
|
layout_id_buffer: Default::default(),
|
||||||
propagate_event: true,
|
propagate_event: true,
|
||||||
prompt_builder: Some(PromptBuilder::Default),
|
prompt_builder: Some(PromptBuilder::Default),
|
||||||
|
@ -1008,6 +1011,11 @@ impl App {
|
||||||
if window.removed {
|
if window.removed {
|
||||||
cx.window_handles.remove(&id);
|
cx.window_handles.remove(&id);
|
||||||
cx.windows.remove(id);
|
cx.windows.remove(id);
|
||||||
|
|
||||||
|
cx.window_closed_observers.clone().retain(&(), |callback| {
|
||||||
|
callback(cx);
|
||||||
|
true
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
cx.windows
|
cx.windows
|
||||||
.get_mut(id)
|
.get_mut(id)
|
||||||
|
@ -1367,6 +1375,14 @@ impl App {
|
||||||
subscription
|
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) {
|
pub(crate) fn clear_pending_keystrokes(&mut self) {
|
||||||
for window in self.windows() {
|
for window in self.windows() {
|
||||||
window
|
window
|
||||||
|
|
|
@ -1804,6 +1804,7 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
println!("workspace::close_window");
|
||||||
let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
|
let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
|
||||||
cx.spawn_in(window, |_, mut cx| async move {
|
cx.spawn_in(window, |_, mut cx| async move {
|
||||||
if prepare.await? {
|
if prepare.await? {
|
||||||
|
|
|
@ -18,11 +18,31 @@ pub struct WorkspaceSettings {
|
||||||
pub autosave: AutosaveSetting,
|
pub autosave: AutosaveSetting,
|
||||||
pub restore_on_startup: RestoreOnStartupBehavior,
|
pub restore_on_startup: RestoreOnStartupBehavior,
|
||||||
pub drop_target_size: f32,
|
pub drop_target_size: f32,
|
||||||
pub when_closing_with_no_tabs: CloseWindowWhenNoItems,
|
|
||||||
pub use_system_path_prompts: bool,
|
pub use_system_path_prompts: bool,
|
||||||
pub command_aliases: HashMap<String, String>,
|
pub command_aliases: HashMap<String, String>,
|
||||||
pub show_user_picture: bool,
|
pub show_user_picture: bool,
|
||||||
pub max_tabs: Option<NonZeroUsize>,
|
pub max_tabs: Option<NonZeroUsize>,
|
||||||
|
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)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
@ -136,11 +156,15 @@ pub struct WorkspaceSettingsContent {
|
||||||
///
|
///
|
||||||
/// Default: true
|
/// Default: true
|
||||||
pub show_user_picture: Option<bool>,
|
pub show_user_picture: Option<bool>,
|
||||||
// Maximum open tabs in a pane. Will not close an unsaved
|
/// Maximum open tabs in a pane. Will not close an unsaved
|
||||||
// tab. Set to `None` for unlimited tabs.
|
/// tab. Set to `None` for unlimited tabs.
|
||||||
//
|
///
|
||||||
// Default: none
|
/// Default: none
|
||||||
pub max_tabs: Option<NonZeroUsize>,
|
pub max_tabs: Option<NonZeroUsize>,
|
||||||
|
/// What to do when the last window is closed
|
||||||
|
///
|
||||||
|
/// Default: auto (nothing on macOS, "app quit" otherwise)
|
||||||
|
pub on_last_window_closed: Option<OnLastWindowClosed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -104,6 +104,19 @@ pub fn init(cx: &mut App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
|
||||||
|
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<Uuid>, cx: &mut App) -> WindowOptions {
|
pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
|
||||||
let display = display_uuid.and_then(|uuid| {
|
let display = display_uuid.and_then(|uuid| {
|
||||||
cx.displays()
|
cx.displays()
|
||||||
|
@ -144,6 +157,12 @@ pub fn initialize_workspace(
|
||||||
prompt_builder: Arc<PromptBuilder>,
|
prompt_builder: Arc<PromptBuilder>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
|
let mut _on_close_subscription = bind_on_window_closed(cx);
|
||||||
|
cx.observe_global::<SettingsStore>(move |cx| {
|
||||||
|
_on_close_subscription = bind_on_window_closed(cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.observe_new(move |workspace: &mut Workspace, window, cx| {
|
cx.observe_new(move |workspace: &mut Workspace, window, cx| {
|
||||||
let Some(window) = window else {
|
let Some(window) = window else {
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue