Introduce `ViewContext::on_window_should_close`

Antonio Scandurra created

This is a new callback that can be used to interrupt closing the window
when the user has unsaved changes.

Change summary

crates/gpui/src/app.rs                 | 45 ++++++++++++++++++++++++++++
crates/gpui/src/platform.rs            |  1 
crates/gpui/src/platform/mac/window.rs | 23 ++++++++++++++
crates/gpui/src/platform/test.rs       | 12 +++++-
4 files changed, 78 insertions(+), 3 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -780,6 +780,7 @@ type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
 type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
 type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
 type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
+type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 
 pub struct MutableAppContext {
     weak_self: Option<rc::Weak<RefCell<Self>>>,
@@ -2004,6 +2005,12 @@ impl MutableAppContext {
                         Effect::ActionDispatchNotification { action_id } => {
                             self.handle_action_dispatch_notification_effect(action_id)
                         }
+                        Effect::WindowShouldCloseSubscription {
+                            window_id,
+                            callback,
+                        } => {
+                            self.handle_window_should_close_subscription_effect(window_id, callback)
+                        }
                     }
                     self.pending_notifications.clear();
                     self.remove_dropped_entities();
@@ -2451,6 +2458,17 @@ impl MutableAppContext {
         self.action_dispatch_observations.lock().extend(callbacks);
     }
 
+    fn handle_window_should_close_subscription_effect(
+        &mut self,
+        window_id: usize,
+        mut callback: WindowShouldCloseSubscriptionCallback,
+    ) {
+        let mut app = self.upgrade();
+        if let Some((_, window)) = self.presenters_and_platform_windows.get_mut(&window_id) {
+            window.on_should_close(Box::new(move || app.update(|cx| callback(cx))))
+        }
+    }
+
     pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
         if let Some(pending_focus_index) = self.pending_focus_index {
             self.pending_effects.remove(pending_focus_index);
@@ -2828,6 +2846,10 @@ pub enum Effect {
     ActionDispatchNotification {
         action_id: TypeId,
     },
+    WindowShouldCloseSubscription {
+        window_id: usize,
+        callback: WindowShouldCloseSubscriptionCallback,
+    },
 }
 
 impl Debug for Effect {
@@ -2921,6 +2943,10 @@ impl Debug for Effect {
                 .field("is_active", is_active)
                 .finish(),
             Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(),
+            Effect::WindowShouldCloseSubscription { window_id, .. } => f
+                .debug_struct("Effect::WindowShouldCloseSubscription")
+                .field("window_id", window_id)
+                .finish(),
         }
     }
 }
@@ -3346,6 +3372,25 @@ impl<'a, T: View> ViewContext<'a, T> {
         }
     }
 
+    pub fn on_window_should_close<F>(&mut self, mut callback: F)
+    where
+        F: 'static + FnMut(&mut T, &mut ViewContext<T>) -> bool,
+    {
+        let window_id = self.window_id();
+        let view = self.weak_handle();
+        self.pending_effects
+            .push_back(Effect::WindowShouldCloseSubscription {
+                window_id,
+                callback: Box::new(move |cx| {
+                    if let Some(view) = view.upgrade(cx) {
+                        view.update(cx, |view, cx| callback(view, cx))
+                    } else {
+                        true
+                    }
+                }),
+            });
+    }
+
     pub fn add_model<S, F>(&mut self, build_model: F) -> ModelHandle<S>
     where
         S: Entity,

crates/gpui/src/platform.rs 🔗

@@ -93,6 +93,7 @@ pub trait Window: WindowContext {
     fn on_event(&mut self, callback: Box<dyn FnMut(Event) -> bool>);
     fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
     fn on_resize(&mut self, callback: Box<dyn FnMut()>);
+    fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
     fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
     fn activate(&self);

crates/gpui/src/platform/mac/window.rs 🔗

@@ -81,6 +81,10 @@ unsafe fn build_classes() {
             sel!(windowDidResignKey:),
             window_did_change_key_status as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(windowShouldClose:),
+            window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
+        );
         decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
         decl.register()
     };
@@ -167,6 +171,7 @@ struct WindowState {
     event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
     activate_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut()>>,
+    should_close_callback: Option<Box<dyn FnMut() -> bool>>,
     close_callback: Option<Box<dyn FnOnce()>>,
     synthetic_drag_counter: usize,
     executor: Rc<executor::Foreground>,
@@ -242,6 +247,7 @@ impl Window {
                 native_window,
                 event_callback: None,
                 resize_callback: None,
+                should_close_callback: None,
                 close_callback: None,
                 activate_callback: None,
                 synthetic_drag_counter: 0,
@@ -339,6 +345,10 @@ impl platform::Window for Window {
         self.0.as_ref().borrow_mut().resize_callback = Some(callback);
     }
 
+    fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
+        self.0.as_ref().borrow_mut().should_close_callback = Some(callback);
+    }
+
     fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
         self.0.as_ref().borrow_mut().close_callback = Some(callback);
     }
@@ -674,6 +684,19 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id)
         .detach();
 }
 
+extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
+    let window_state = unsafe { get_window_state(this) };
+    let mut window_state_borrow = window_state.as_ref().borrow_mut();
+    if let Some(mut callback) = window_state_borrow.should_close_callback.take() {
+        drop(window_state_borrow);
+        let should_close = callback();
+        window_state.borrow_mut().should_close_callback = Some(callback);
+        should_close as BOOL
+    } else {
+        YES
+    }
+}
+
 extern "C" fn close_window(this: &Object, _: Sel) {
     unsafe {
         let close_callback = {

crates/gpui/src/platform/test.rs 🔗

@@ -37,6 +37,7 @@ pub struct Window {
     event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
     resize_handlers: Vec<Box<dyn FnMut()>>,
     close_handlers: Vec<Box<dyn FnOnce()>>,
+    should_close_handler: Option<Box<dyn FnMut() -> bool>>,
     pub(crate) title: Option<String>,
     pub(crate) edited: bool,
     pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
@@ -186,9 +187,10 @@ impl Window {
     fn new(size: Vector2F) -> Self {
         Self {
             size,
-            event_handlers: Vec::new(),
-            resize_handlers: Vec::new(),
-            close_handlers: Vec::new(),
+            event_handlers: Default::default(),
+            resize_handlers: Default::default(),
+            close_handlers: Default::default(),
+            should_close_handler: Default::default(),
             scale_factor: 1.0,
             current_scene: None,
             title: None,
@@ -264,6 +266,10 @@ impl super::Window for Window {
     fn set_edited(&mut self, edited: bool) {
         self.edited = edited;
     }
+
+    fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>) {
+        self.should_close_handler = Some(callback);
+    }
 }
 
 pub fn platform() -> Platform {