Focus/blur views when application windows become active/inactive

Max Brunsfeld created

Change summary

crates/gpui/src/app.rs                 | 64 +++++++++++++++++++++++++++
crates/gpui/src/platform.rs            |  1 
crates/gpui/src/platform/mac/window.rs | 34 ++++++++++++++
crates/gpui/src/platform/test.rs       |  2 
4 files changed, 99 insertions(+), 2 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -1591,6 +1591,7 @@ impl MutableAppContext {
                 Window {
                     root_view: root_view.clone().into(),
                     focused_view_id: Some(root_view.id()),
+                    is_active: true,
                     invalidation: None,
                 },
             );
@@ -1638,10 +1639,17 @@ impl MutableAppContext {
             }));
         }
 
+        {
+            let mut app = self.upgrade();
+            window.on_active_status_change(Box::new(move |is_active| {
+                app.update(|cx| cx.window_changed_active_status(window_id, is_active))
+            }));
+        }
+
         {
             let mut app = self.upgrade();
             window.on_resize(Box::new(move || {
-                app.update(|cx| cx.resize_window(window_id))
+                app.update(|cx| cx.window_was_resized(window_id))
             }));
         }
 
@@ -1856,6 +1864,10 @@ impl MutableAppContext {
                                     .get_or_insert(WindowInvalidation::default());
                             }
                         }
+                        Effect::ActivateWindow {
+                            window_id,
+                            is_active,
+                        } => self.handle_activation_effect(window_id, is_active),
                         Effect::RefreshWindows => {
                             refreshing = true;
                         }
@@ -1914,11 +1926,18 @@ impl MutableAppContext {
         }
     }
 
-    fn resize_window(&mut self, window_id: usize) {
+    fn window_was_resized(&mut self, window_id: usize) {
         self.pending_effects
             .push_back(Effect::ResizeWindow { window_id });
     }
 
+    fn window_changed_active_status(&mut self, window_id: usize, is_active: bool) {
+        self.pending_effects.push_back(Effect::ActivateWindow {
+            window_id,
+            is_active,
+        });
+    }
+
     pub fn refresh_windows(&mut self) {
         self.pending_effects.push_back(Effect::RefreshWindows);
     }
@@ -2204,6 +2223,34 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_activation_effect(&mut self, window_id: usize, active: bool) {
+        if self
+            .cx
+            .windows
+            .get(&window_id)
+            .map(|w| w.is_active)
+            .map_or(false, |cur_active| cur_active == active)
+        {
+            return;
+        }
+
+        self.update(|this| {
+            let window = this.cx.windows.get_mut(&window_id)?;
+            window.is_active = active;
+            let view_id = window.focused_view_id?;
+            if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
+                if active {
+                    view.on_focus(this, window_id, view_id);
+                } else {
+                    view.on_blur(this, window_id, view_id);
+                }
+                this.cx.views.insert((window_id, view_id), view);
+            }
+
+            Some(())
+        });
+    }
+
     fn handle_focus_effect(&mut self, window_id: usize, focused_id: Option<usize>) {
         self.pending_focus_index.take();
 
@@ -2567,6 +2614,7 @@ impl ReadView for AppContext {
 struct Window {
     root_view: AnyViewHandle,
     focused_view_id: Option<usize>,
+    is_active: bool,
     invalidation: Option<WindowInvalidation>,
 }
 
@@ -2633,6 +2681,10 @@ pub enum Effect {
     ResizeWindow {
         window_id: usize,
     },
+    ActivateWindow {
+        window_id: usize,
+        is_active: bool,
+    },
     RefreshWindows,
 }
 
@@ -2714,6 +2766,14 @@ impl Debug for Effect {
                 .debug_struct("Effect::RefreshWindow")
                 .field("window_id", window_id)
                 .finish(),
+            Effect::ActivateWindow {
+                window_id,
+                is_active,
+            } => f
+                .debug_struct("Effect::ActivateWindow")
+                .field("window_id", window_id)
+                .field("is_active", is_active)
+                .finish(),
             Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(),
         }
     }

crates/gpui/src/platform.rs 🔗

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

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

@@ -73,6 +73,14 @@ unsafe fn build_classes() {
             sel!(windowDidResize:),
             window_did_resize as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(windowDidBecomeKey:),
+            window_did_become_key as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(windowDidResignKey:),
+            window_did_resign_key as extern "C" fn(&Object, Sel, id),
+        );
         decl.add_method(sel!(close), close_window as extern "C" fn(&Object, Sel));
         decl.register()
     };
@@ -157,6 +165,7 @@ struct WindowState {
     id: usize,
     native_window: id,
     event_callback: Option<Box<dyn FnMut(Event)>>,
+    activate_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut()>>,
     close_callback: Option<Box<dyn FnOnce()>>,
     synthetic_drag_counter: usize,
@@ -234,6 +243,7 @@ impl Window {
                 event_callback: None,
                 resize_callback: None,
                 close_callback: None,
+                activate_callback: None,
                 synthetic_drag_counter: 0,
                 executor,
                 scene_to_render: Default::default(),
@@ -333,6 +343,10 @@ impl platform::Window for Window {
         self.0.as_ref().borrow_mut().close_callback = Some(callback);
     }
 
+    fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>) {
+        self.0.as_ref().borrow_mut().activate_callback = Some(callback);
+    }
+
     fn prompt(
         &self,
         level: platform::PromptLevel,
@@ -598,6 +612,26 @@ extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
     window_state.as_ref().borrow().move_traffic_light();
 }
 
+extern "C" fn window_did_become_key(this: &Object, _: Sel, _: id) {
+    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.activate_callback.take() {
+        drop(window_state_borrow);
+        callback(true);
+        window_state.borrow_mut().activate_callback = Some(callback);
+    };
+}
+
+extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) {
+    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.activate_callback.take() {
+        drop(window_state_borrow);
+        callback(false);
+        window_state.borrow_mut().activate_callback = Some(callback);
+    };
+}
+
 extern "C" fn close_window(this: &Object, _: Sel) {
     unsafe {
         let close_callback = {

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

@@ -229,6 +229,8 @@ impl super::Window for Window {
         self.event_handlers.push(callback);
     }
 
+    fn on_active_status_change(&mut self, _: Box<dyn FnMut(bool)>) {}
+
     fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
         self.resize_handlers.push(callback);
     }