Merge pull request #1485 from zed-industries/fullscreen-workspace-title-padding

Mikayla Maki created

Fullscreen workspace title padding

Change summary

crates/gpui/src/app.rs                     | 636 +++++++++--------------
crates/gpui/src/app/callback_collection.rs | 106 ++++
crates/gpui/src/platform.rs                |   1 
crates/gpui/src/platform/mac/window.rs     |  32 +
crates/gpui/src/platform/test.rs           |   6 
crates/gpui/src/presenter.rs               |   4 
crates/workspace/src/workspace.rs          |  15 
7 files changed, 412 insertions(+), 388 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -1,4 +1,5 @@
 pub mod action;
+mod callback_collection;
 
 use crate::{
     elements::ElementBox,
@@ -13,7 +14,8 @@ use crate::{
 };
 pub use action::*;
 use anyhow::{anyhow, Context, Result};
-use collections::btree_map;
+use callback_collection::CallbackCollection;
+use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
 use keymap::MatchResult;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
@@ -24,7 +26,6 @@ use smol::prelude::*;
 use std::{
     any::{type_name, Any, TypeId},
     cell::RefCell,
-    collections::{hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque},
     fmt::{self, Debug},
     hash::{Hash, Hasher},
     marker::PhantomData,
@@ -37,6 +38,8 @@ use std::{
     time::Duration,
 };
 
+use self::callback_collection::Mapping;
+
 pub trait Entity: 'static {
     type Event;
 
@@ -959,6 +962,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 WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
+type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
 type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
 type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 
@@ -976,18 +980,18 @@ pub struct MutableAppContext {
     next_window_id: usize,
     next_subscription_id: usize,
     frame_count: usize,
-    subscriptions: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<SubscriptionCallback>>>>>,
-    global_subscriptions:
-        Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
-    observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>,
-    focus_observations:
-        Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<FocusObservationCallback>>>>>,
-    global_observations:
-        Arc<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
+
+    focus_observations: CallbackCollection<usize, FocusObservationCallback>,
+    global_subscriptions: CallbackCollection<TypeId, GlobalSubscriptionCallback>,
+    global_observations: CallbackCollection<TypeId, GlobalObservationCallback>,
+    subscriptions: CallbackCollection<usize, SubscriptionCallback>,
+    observations: CallbackCollection<usize, ObservationCallback>,
+    window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
+    window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
+
     release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
-    window_activation_observations:
-        Arc<Mutex<HashMap<usize, BTreeMap<usize, Option<WindowActivationCallback>>>>>,
     action_dispatch_observations: Arc<Mutex<BTreeMap<usize, ActionObservationCallback>>>,
+
     presenters_and_platform_windows:
         HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
     foreground: Rc<executor::Foreground>,
@@ -1025,10 +1029,10 @@ impl MutableAppContext {
                 font_cache,
                 platform,
             },
-            action_deserializers: HashMap::new(),
-            capture_actions: HashMap::new(),
-            actions: HashMap::new(),
-            global_actions: HashMap::new(),
+            action_deserializers: Default::default(),
+            capture_actions: Default::default(),
+            actions: Default::default(),
+            global_actions: Default::default(),
             keystroke_matcher: keymap::Matcher::default(),
             next_entity_id: 0,
             next_window_id: 0,
@@ -1041,13 +1045,14 @@ impl MutableAppContext {
             release_observations: Default::default(),
             global_observations: Default::default(),
             window_activation_observations: Default::default(),
+            window_fullscreen_observations: Default::default(),
             action_dispatch_observations: Default::default(),
-            presenters_and_platform_windows: HashMap::new(),
+            presenters_and_platform_windows: Default::default(),
             foreground,
             pending_effects: VecDeque::new(),
             pending_focus_index: None,
-            pending_notifications: HashSet::new(),
-            pending_global_notifications: HashSet::new(),
+            pending_notifications: Default::default(),
+            pending_global_notifications: Default::default(),
             pending_flushes: 0,
             flushing_effects: false,
             halt_action_dispatch: false,
@@ -1248,6 +1253,13 @@ impl MutableAppContext {
             .map_or(false, |window| window.is_active)
     }
 
+    pub fn window_is_fullscreen(&self, window_id: usize) -> bool {
+        self.cx
+            .windows
+            .get(&window_id)
+            .map_or(false, |window| window.is_fullscreen)
+    }
+
     pub fn render_view(&mut self, params: RenderParams) -> Result<ElementBox> {
         let window_id = params.window_id;
         let view_id = params.view_id;
@@ -1385,10 +1397,11 @@ impl MutableAppContext {
                 callback(payload, cx)
             }),
         });
+
         Subscription::GlobalSubscription {
             id: subscription_id,
             type_id,
-            subscriptions: Some(Arc::downgrade(&self.global_subscriptions)),
+            subscriptions: Some(self.global_subscriptions.downgrade()),
         }
     }
 
@@ -1429,7 +1442,7 @@ impl MutableAppContext {
         Subscription::Subscription {
             id: subscription_id,
             entity_id: handle.id(),
-            subscriptions: Some(Arc::downgrade(&self.subscriptions)),
+            subscriptions: Some(self.subscriptions.downgrade()),
         }
     }
 
@@ -1457,7 +1470,7 @@ impl MutableAppContext {
         Subscription::Observation {
             id: subscription_id,
             entity_id,
-            observations: Some(Arc::downgrade(&self.observations)),
+            observations: Some(self.observations.downgrade()),
         }
     }
 
@@ -1469,6 +1482,7 @@ impl MutableAppContext {
         let subscription_id = post_inc(&mut self.next_subscription_id);
         let observed = handle.downgrade();
         let view_id = handle.id();
+
         self.pending_effects.push_back(Effect::FocusObservation {
             view_id,
             subscription_id,
@@ -1480,10 +1494,11 @@ impl MutableAppContext {
                 }
             }),
         });
+
         Subscription::FocusObservation {
             id: subscription_id,
             view_id,
-            observations: Some(Arc::downgrade(&self.focus_observations)),
+            observations: Some(self.focus_observations.downgrade()),
         }
     }
 
@@ -1495,20 +1510,16 @@ impl MutableAppContext {
         let type_id = TypeId::of::<G>();
         let id = post_inc(&mut self.next_subscription_id);
 
-        self.global_observations
-            .lock()
-            .entry(type_id)
-            .or_default()
-            .insert(
-                id,
-                Some(Box::new(move |cx: &mut MutableAppContext| observe(cx))
-                    as GlobalObservationCallback),
-            );
+        self.global_observations.add_callback(
+            type_id,
+            id,
+            Box::new(move |cx: &mut MutableAppContext| observe(cx)),
+        );
 
         Subscription::GlobalObservation {
             id,
             type_id,
-            observations: Some(Arc::downgrade(&self.global_observations)),
+            observations: Some(self.global_observations.downgrade()),
         }
     }
 
@@ -1566,7 +1577,25 @@ impl MutableAppContext {
         Subscription::WindowActivationObservation {
             id: subscription_id,
             window_id,
-            observations: Some(Arc::downgrade(&self.window_activation_observations)),
+            observations: Some(self.window_activation_observations.downgrade()),
+        }
+    }
+
+    fn observe_fullscreen<F>(&mut self, window_id: usize, callback: F) -> Subscription
+    where
+        F: 'static + FnMut(bool, &mut MutableAppContext) -> bool,
+    {
+        let subscription_id = post_inc(&mut self.next_subscription_id);
+        self.pending_effects
+            .push_back(Effect::WindowFullscreenObservation {
+                window_id,
+                subscription_id,
+                callback: Box::new(callback),
+            });
+        Subscription::WindowFullscreenObservation {
+            id: subscription_id,
+            window_id,
+            observations: Some(self.window_activation_observations.downgrade()),
         }
     }
 
@@ -1934,6 +1963,7 @@ impl MutableAppContext {
                     focused_view_id: Some(root_view.id()),
                     is_active: false,
                     invalidation: None,
+                    is_fullscreen: false,
                 },
             );
             root_view.update(this, |view, cx| view.on_focus(cx));
@@ -2010,6 +2040,13 @@ impl MutableAppContext {
             }));
         }
 
+        {
+            let mut app = self.upgrade();
+            window.on_fullscreen(Box::new(move |is_fullscreen| {
+                app.update(|cx| cx.window_was_fullscreen_changed(window_id, is_fullscreen))
+            }));
+        }
+
         {
             let mut app = self.upgrade();
             window.on_close(Box::new(move || {
@@ -2092,8 +2129,8 @@ impl MutableAppContext {
             }
 
             for model_id in dropped_models {
-                self.subscriptions.lock().remove(&model_id);
-                self.observations.lock().remove(&model_id);
+                self.subscriptions.remove(model_id);
+                self.observations.remove(model_id);
                 let mut model = self.cx.models.remove(&model_id).unwrap();
                 model.release(self);
                 self.pending_effects
@@ -2101,8 +2138,8 @@ impl MutableAppContext {
             }
 
             for (window_id, view_id) in dropped_views {
-                self.subscriptions.lock().remove(&view_id);
-                self.observations.lock().remove(&view_id);
+                self.subscriptions.remove(view_id);
+                self.observations.remove(view_id);
                 let mut view = self.cx.views.remove(&(window_id, view_id)).unwrap();
                 view.release(self);
                 let change_focus_to = self.cx.windows.get_mut(&window_id).and_then(|window| {
@@ -2150,32 +2187,59 @@ impl MutableAppContext {
                             entity_id,
                             subscription_id,
                             callback,
-                        } => self.handle_subscription_effect(entity_id, subscription_id, callback),
-                        Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload),
+                        } => self.subscriptions.add_or_remove_callback(
+                            entity_id,
+                            subscription_id,
+                            callback,
+                        ),
+
+                        Effect::Event { entity_id, payload } => {
+                            let mut subscriptions = self.subscriptions.clone();
+                            subscriptions.emit_and_cleanup(entity_id, self, |callback, this| {
+                                callback(payload.as_ref(), this)
+                            })
+                        }
+
                         Effect::GlobalSubscription {
                             type_id,
                             subscription_id,
                             callback,
-                        } => self.handle_global_subscription_effect(
+                        } => self.global_subscriptions.add_or_remove_callback(
                             type_id,
                             subscription_id,
                             callback,
                         ),
+
                         Effect::GlobalEvent { payload } => self.emit_global_event(payload),
+
                         Effect::Observation {
                             entity_id,
                             subscription_id,
                             callback,
-                        } => self.handle_observation_effect(entity_id, subscription_id, callback),
+                        } => self.observations.add_or_remove_callback(
+                            entity_id,
+                            subscription_id,
+                            callback,
+                        ),
+
                         Effect::ModelNotification { model_id } => {
-                            self.handle_model_notification_effect(model_id)
+                            let mut observations = self.observations.clone();
+                            observations
+                                .emit_and_cleanup(model_id, self, |callback, this| callback(this));
                         }
+
                         Effect::ViewNotification { window_id, view_id } => {
                             self.handle_view_notification_effect(window_id, view_id)
                         }
+
                         Effect::GlobalNotification { type_id } => {
-                            self.handle_global_notification_effect(type_id)
+                            let mut subscriptions = self.global_observations.clone();
+                            subscriptions.emit_and_cleanup(type_id, self, |callback, this| {
+                                callback(this);
+                                true
+                            });
                         }
+
                         Effect::Deferred {
                             callback,
                             after_window_update,
@@ -2186,22 +2250,31 @@ impl MutableAppContext {
                                 callback(self)
                             }
                         }
+
                         Effect::ModelRelease { model_id, model } => {
                             self.handle_entity_release_effect(model_id, model.as_any())
                         }
+
                         Effect::ViewRelease { view_id, view } => {
                             self.handle_entity_release_effect(view_id, view.as_any())
                         }
+
                         Effect::Focus { window_id, view_id } => {
                             self.handle_focus_effect(window_id, view_id);
                         }
+
                         Effect::FocusObservation {
                             view_id,
                             subscription_id,
                             callback,
                         } => {
-                            self.handle_focus_observation_effect(view_id, subscription_id, callback)
+                            self.focus_observations.add_or_remove_callback(
+                                view_id,
+                                subscription_id,
+                                callback,
+                            );
                         }
+
                         Effect::ResizeWindow { window_id } => {
                             if let Some(window) = self.cx.windows.get_mut(&window_id) {
                                 window
@@ -2209,19 +2282,37 @@ impl MutableAppContext {
                                     .get_or_insert(WindowInvalidation::default());
                             }
                         }
+
                         Effect::WindowActivationObservation {
                             window_id,
                             subscription_id,
                             callback,
-                        } => self.handle_window_activation_observation_effect(
+                        } => self.window_activation_observations.add_or_remove_callback(
                             window_id,
                             subscription_id,
                             callback,
                         ),
+
                         Effect::ActivateWindow {
                             window_id,
                             is_active,
                         } => self.handle_window_activation_effect(window_id, is_active),
+
+                        Effect::WindowFullscreenObservation {
+                            window_id,
+                            subscription_id,
+                            callback,
+                        } => self.window_fullscreen_observations.add_or_remove_callback(
+                            window_id,
+                            subscription_id,
+                            callback,
+                        ),
+
+                        Effect::FullscreenWindow {
+                            window_id,
+                            is_fullscreen,
+                        } => self.handle_fullscreen_effect(window_id, is_fullscreen),
+
                         Effect::RefreshWindows => {
                             refreshing = true;
                         }
@@ -2265,7 +2356,7 @@ impl MutableAppContext {
     }
 
     fn update_windows(&mut self) {
-        let mut invalidations = HashMap::new();
+        let mut invalidations: HashMap<_, _> = Default::default();
         for (window_id, window) in &mut self.cx.windows {
             if let Some(invalidation) = window.invalidation.take() {
                 invalidations.insert(*window_id, invalidation);
@@ -2294,6 +2385,13 @@ impl MutableAppContext {
             .push_back(Effect::ResizeWindow { window_id });
     }
 
+    fn window_was_fullscreen_changed(&mut self, window_id: usize, is_fullscreen: bool) {
+        self.pending_effects.push_back(Effect::FullscreenWindow {
+            window_id,
+            is_fullscreen,
+        });
+    }
+
     fn window_changed_active_status(&mut self, window_id: usize, is_active: bool) {
         self.pending_effects.push_back(Effect::ActivateWindow {
             window_id,
@@ -2326,182 +2424,14 @@ impl MutableAppContext {
         self.presenters_and_platform_windows = presenters;
     }
 
-    fn handle_subscription_effect(
-        &mut self,
-        entity_id: usize,
-        subscription_id: usize,
-        callback: SubscriptionCallback,
-    ) {
-        match self
-            .subscriptions
-            .lock()
-            .entry(entity_id)
-            .or_default()
-            .entry(subscription_id)
-        {
-            btree_map::Entry::Vacant(entry) => {
-                entry.insert(Some(callback));
-            }
-            // Subscription was dropped before effect was processed
-            btree_map::Entry::Occupied(entry) => {
-                debug_assert!(entry.get().is_none());
-                entry.remove();
-            }
-        }
-    }
-
-    fn emit_event(&mut self, entity_id: usize, payload: Box<dyn Any>) {
-        let callbacks = self.subscriptions.lock().remove(&entity_id);
-        if let Some(callbacks) = callbacks {
-            for (id, callback) in callbacks {
-                if let Some(mut callback) = callback {
-                    let alive = callback(payload.as_ref(), self);
-                    if alive {
-                        match self
-                            .subscriptions
-                            .lock()
-                            .entry(entity_id)
-                            .or_default()
-                            .entry(id)
-                        {
-                            btree_map::Entry::Vacant(entry) => {
-                                entry.insert(Some(callback));
-                            }
-                            btree_map::Entry::Occupied(entry) => {
-                                entry.remove();
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    fn handle_global_subscription_effect(
-        &mut self,
-        type_id: TypeId,
-        subscription_id: usize,
-        callback: GlobalSubscriptionCallback,
-    ) {
-        match self
-            .global_subscriptions
-            .lock()
-            .entry(type_id)
-            .or_default()
-            .entry(subscription_id)
-        {
-            btree_map::Entry::Vacant(entry) => {
-                entry.insert(Some(callback));
-            }
-            // Subscription was dropped before effect was processed
-            btree_map::Entry::Occupied(entry) => {
-                debug_assert!(entry.get().is_none());
-                entry.remove();
-            }
-        }
-    }
-
     fn emit_global_event(&mut self, payload: Box<dyn Any>) {
         let type_id = (&*payload).type_id();
-        let callbacks = self.global_subscriptions.lock().remove(&type_id);
-        if let Some(callbacks) = callbacks {
-            for (id, callback) in callbacks {
-                if let Some(mut callback) = callback {
-                    callback(payload.as_ref(), self);
-                    match self
-                        .global_subscriptions
-                        .lock()
-                        .entry(type_id)
-                        .or_default()
-                        .entry(id)
-                    {
-                        btree_map::Entry::Vacant(entry) => {
-                            entry.insert(Some(callback));
-                        }
-                        btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    fn handle_observation_effect(
-        &mut self,
-        entity_id: usize,
-        subscription_id: usize,
-        callback: ObservationCallback,
-    ) {
-        match self
-            .observations
-            .lock()
-            .entry(entity_id)
-            .or_default()
-            .entry(subscription_id)
-        {
-            btree_map::Entry::Vacant(entry) => {
-                entry.insert(Some(callback));
-            }
-            // Observation was dropped before effect was processed
-            btree_map::Entry::Occupied(entry) => {
-                debug_assert!(entry.get().is_none());
-                entry.remove();
-            }
-        }
-    }
 
-    fn handle_focus_observation_effect(
-        &mut self,
-        view_id: usize,
-        subscription_id: usize,
-        callback: FocusObservationCallback,
-    ) {
-        match self
-            .focus_observations
-            .lock()
-            .entry(view_id)
-            .or_default()
-            .entry(subscription_id)
-        {
-            btree_map::Entry::Vacant(entry) => {
-                entry.insert(Some(callback));
-            }
-            // Observation was dropped before effect was processed
-            btree_map::Entry::Occupied(entry) => {
-                debug_assert!(entry.get().is_none());
-                entry.remove();
-            }
-        }
-    }
-
-    fn handle_model_notification_effect(&mut self, observed_id: usize) {
-        let callbacks = self.observations.lock().remove(&observed_id);
-        if let Some(callbacks) = callbacks {
-            if self.cx.models.contains_key(&observed_id) {
-                for (id, callback) in callbacks {
-                    if let Some(mut callback) = callback {
-                        let alive = callback(self);
-                        if alive {
-                            match self
-                                .observations
-                                .lock()
-                                .entry(observed_id)
-                                .or_default()
-                                .entry(id)
-                            {
-                                btree_map::Entry::Vacant(entry) => {
-                                    entry.insert(Some(callback));
-                                }
-                                btree_map::Entry::Occupied(entry) => {
-                                    entry.remove();
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
+        let mut subscriptions = self.global_subscriptions.clone();
+        subscriptions.emit_and_cleanup(type_id, self, |callback, this| {
+            callback(payload.as_ref(), this);
+            true //Always alive
+        });
     }
 
     fn handle_view_notification_effect(
@@ -2509,8 +2439,6 @@ impl MutableAppContext {
         observed_window_id: usize,
         observed_view_id: usize,
     ) {
-        let callbacks = self.observations.lock().remove(&observed_view_id);
-
         if self
             .cx
             .views
@@ -2524,54 +2452,8 @@ impl MutableAppContext {
                     .insert(observed_view_id);
             }
 
-            if let Some(callbacks) = callbacks {
-                for (id, callback) in callbacks {
-                    if let Some(mut callback) = callback {
-                        let alive = callback(self);
-                        if alive {
-                            match self
-                                .observations
-                                .lock()
-                                .entry(observed_view_id)
-                                .or_default()
-                                .entry(id)
-                            {
-                                btree_map::Entry::Vacant(entry) => {
-                                    entry.insert(Some(callback));
-                                }
-                                btree_map::Entry::Occupied(entry) => {
-                                    entry.remove();
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    fn handle_global_notification_effect(&mut self, observed_type_id: TypeId) {
-        let callbacks = self.global_observations.lock().remove(&observed_type_id);
-        if let Some(callbacks) = callbacks {
-            for (id, callback) in callbacks {
-                if let Some(mut callback) = callback {
-                    callback(self);
-                    match self
-                        .global_observations
-                        .lock()
-                        .entry(observed_type_id)
-                        .or_default()
-                        .entry(id)
-                    {
-                        collections::btree_map::Entry::Vacant(entry) => {
-                            entry.insert(Some(callback));
-                        }
-                        collections::btree_map::Entry::Occupied(entry) => {
-                            entry.remove();
-                        }
-                    }
-                }
-            }
+            let mut observations = self.observations.clone();
+            observations.emit_and_cleanup(observed_view_id, self, |callback, this| callback(this));
         }
     }
 
@@ -2584,37 +2466,39 @@ impl MutableAppContext {
         }
     }
 
-    fn handle_window_activation_observation_effect(
-        &mut self,
-        window_id: usize,
-        subscription_id: usize,
-        callback: WindowActivationCallback,
-    ) {
-        match self
-            .window_activation_observations
-            .lock()
-            .entry(window_id)
-            .or_default()
-            .entry(subscription_id)
+    fn handle_fullscreen_effect(&mut self, window_id: usize, is_fullscreen: bool) {
+        //Short circuit evaluation if we're already g2g
+        if self
+            .cx
+            .windows
+            .get(&window_id)
+            .map(|w| w.is_fullscreen == is_fullscreen)
+            .unwrap_or(false)
         {
-            btree_map::Entry::Vacant(entry) => {
-                entry.insert(Some(callback));
-            }
-            // Observation was dropped before effect was processed
-            btree_map::Entry::Occupied(entry) => {
-                debug_assert!(entry.get().is_none());
-                entry.remove();
-            }
+            return;
         }
+
+        self.update(|this| {
+            let window = this.cx.windows.get_mut(&window_id)?;
+            window.is_fullscreen = is_fullscreen;
+
+            let mut observations = this.window_fullscreen_observations.clone();
+            observations.emit_and_cleanup(window_id, this, |callback, this| {
+                callback(is_fullscreen, this)
+            });
+
+            Some(())
+        });
     }
 
     fn handle_window_activation_effect(&mut self, window_id: usize, active: bool) {
+        //Short circuit evaluation if we're already g2g
         if self
             .cx
             .windows
             .get(&window_id)
-            .map(|w| w.is_active)
-            .map_or(false, |cur_active| cur_active == active)
+            .map(|w| w.is_active == active)
+            .unwrap_or(false)
         {
             return;
         }
@@ -2622,6 +2506,8 @@ impl MutableAppContext {
         self.update(|this| {
             let window = this.cx.windows.get_mut(&window_id)?;
             window.is_active = active;
+
+            //Handle focus
             let view_id = window.focused_view_id?;
             if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) {
                 if active {
@@ -2632,33 +2518,8 @@ impl MutableAppContext {
                 this.cx.views.insert((window_id, view_id), view);
             }
 
-            let callbacks = this
-                .window_activation_observations
-                .lock()
-                .remove(&window_id);
-            if let Some(callbacks) = callbacks {
-                for (id, callback) in callbacks {
-                    if let Some(mut callback) = callback {
-                        let alive = callback(active, this);
-                        if alive {
-                            match this
-                                .window_activation_observations
-                                .lock()
-                                .entry(window_id)
-                                .or_default()
-                                .entry(id)
-                            {
-                                btree_map::Entry::Vacant(entry) => {
-                                    entry.insert(Some(callback));
-                                }
-                                btree_map::Entry::Occupied(entry) => {
-                                    entry.remove();
-                                }
-                            }
-                        }
-                    }
-                }
-            }
+            let mut observations = this.window_activation_observations.clone();
+            observations.emit_and_cleanup(window_id, this, |callback, this| callback(active, this));
 
             Some(())
         });
@@ -2689,30 +2550,9 @@ impl MutableAppContext {
                     blurred_view.on_blur(this, window_id, blurred_id);
                     this.cx.views.insert((window_id, blurred_id), blurred_view);
 
-                    let callbacks = this.focus_observations.lock().remove(&blurred_id);
-                    if let Some(callbacks) = callbacks {
-                        for (id, callback) in callbacks {
-                            if let Some(mut callback) = callback {
-                                let alive = callback(false, this);
-                                if alive {
-                                    match this
-                                        .focus_observations
-                                        .lock()
-                                        .entry(blurred_id)
-                                        .or_default()
-                                        .entry(id)
-                                    {
-                                        btree_map::Entry::Vacant(entry) => {
-                                            entry.insert(Some(callback));
-                                        }
-                                        btree_map::Entry::Occupied(entry) => {
-                                            entry.remove();
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
+                    let mut subscriptions = this.focus_observations.clone();
+                    subscriptions
+                        .emit_and_cleanup(blurred_id, this, |callback, this| callback(false, this));
                 }
             }
 
@@ -2721,30 +2561,9 @@ impl MutableAppContext {
                     focused_view.on_focus(this, window_id, focused_id);
                     this.cx.views.insert((window_id, focused_id), focused_view);
 
-                    let callbacks = this.focus_observations.lock().remove(&focused_id);
-                    if let Some(callbacks) = callbacks {
-                        for (id, callback) in callbacks {
-                            if let Some(mut callback) = callback {
-                                let alive = callback(true, this);
-                                if alive {
-                                    match this
-                                        .focus_observations
-                                        .lock()
-                                        .entry(focused_id)
-                                        .or_default()
-                                        .entry(id)
-                                    {
-                                        btree_map::Entry::Vacant(entry) => {
-                                            entry.insert(Some(callback));
-                                        }
-                                        btree_map::Entry::Occupied(entry) => {
-                                            entry.remove();
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
+                    let mut subscriptions = this.focus_observations.clone();
+                    subscriptions
+                        .emit_and_cleanup(focused_id, this, |callback, this| callback(true, this));
                 }
             }
         })
@@ -3072,6 +2891,7 @@ struct Window {
     root_view: AnyViewHandle,
     focused_view_id: Option<usize>,
     is_active: bool,
+    is_fullscreen: bool,
     invalidation: Option<WindowInvalidation>,
 }
 
@@ -3138,6 +2958,10 @@ pub enum Effect {
     ResizeWindow {
         window_id: usize,
     },
+    FullscreenWindow {
+        window_id: usize,
+        is_fullscreen: bool,
+    },
     ActivateWindow {
         window_id: usize,
         is_active: bool,
@@ -3147,6 +2971,11 @@ pub enum Effect {
         subscription_id: usize,
         callback: WindowActivationCallback,
     },
+    WindowFullscreenObservation {
+        window_id: usize,
+        subscription_id: usize,
+        callback: WindowFullscreenCallback,
+    },
     RefreshWindows,
     ActionDispatchNotification {
         action_id: TypeId,
@@ -3256,6 +3085,23 @@ impl Debug for Effect {
                 .field("window_id", window_id)
                 .field("is_active", is_active)
                 .finish(),
+            Effect::FullscreenWindow {
+                window_id,
+                is_fullscreen,
+            } => f
+                .debug_struct("Effect::FullscreenWindow")
+                .field("window_id", window_id)
+                .field("is_fullscreen", is_fullscreen)
+                .finish(),
+            Effect::WindowFullscreenObservation {
+                window_id,
+                subscription_id,
+                callback: _,
+            } => f
+                .debug_struct("Effect::WindowFullscreenObservation")
+                .field("window_id", window_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(),
             Effect::WindowShouldCloseSubscription { window_id, .. } => f
                 .debug_struct("Effect::WindowShouldCloseSubscription")
@@ -3927,6 +3773,24 @@ impl<'a, T: View> ViewContext<'a, T> {
             })
     }
 
+    pub fn observe_fullscreen<F>(&mut self, mut callback: F) -> Subscription
+    where
+        F: 'static + FnMut(&mut T, bool, &mut ViewContext<T>),
+    {
+        let observer = self.weak_handle();
+        self.app
+            .observe_fullscreen(self.window_id(), move |active, cx| {
+                if let Some(observer) = observer.upgrade(cx) {
+                    observer.update(cx, |observer, cx| {
+                        callback(observer, active, cx);
+                    });
+                    true
+                } else {
+                    false
+                }
+            })
+    }
+
     pub fn emit(&mut self, payload: T::Event) {
         self.app.pending_effects.push_back(Effect::Event {
             entity_id: self.view_id,
@@ -4032,6 +3896,10 @@ impl<'a, V: View> RenderContext<'a, V> {
         WeakViewHandle::new(self.window_id, self.view_id)
     }
 
+    pub fn window_id(&self) -> usize {
+        self.window_id
+    }
+
     pub fn view_id(&self) -> usize {
         self.view_id
     }
@@ -5199,35 +5067,39 @@ pub enum Subscription {
     Subscription {
         id: usize,
         entity_id: usize,
-        subscriptions:
-            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<SubscriptionCallback>>>>>>,
+        subscriptions: Option<Weak<Mapping<usize, SubscriptionCallback>>>,
     },
     GlobalSubscription {
         id: usize,
         type_id: TypeId,
-        subscriptions: Option<
-            Weak<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalSubscriptionCallback>>>>>,
-        >,
+        subscriptions: Option<Weak<Mapping<TypeId, GlobalSubscriptionCallback>>>,
     },
     Observation {
         id: usize,
         entity_id: usize,
-        observations:
-            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<ObservationCallback>>>>>>,
+        observations: Option<Weak<Mapping<usize, ObservationCallback>>>,
     },
     GlobalObservation {
         id: usize,
         type_id: TypeId,
-        observations: Option<
-            Weak<Mutex<HashMap<TypeId, BTreeMap<usize, Option<GlobalObservationCallback>>>>>,
-        >,
+        observations: Option<Weak<Mapping<TypeId, GlobalObservationCallback>>>,
     },
     FocusObservation {
         id: usize,
         view_id: usize,
-        observations:
-            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<FocusObservationCallback>>>>>>,
+        observations: Option<Weak<Mapping<usize, FocusObservationCallback>>>,
+    },
+    WindowActivationObservation {
+        id: usize,
+        window_id: usize,
+        observations: Option<Weak<Mapping<usize, WindowActivationCallback>>>,
     },
+    WindowFullscreenObservation {
+        id: usize,
+        window_id: usize,
+        observations: Option<Weak<Mapping<usize, WindowFullscreenCallback>>>,
+    },
+
     ReleaseObservation {
         id: usize,
         entity_id: usize,
@@ -5238,12 +5110,6 @@ pub enum Subscription {
         id: usize,
         observations: Option<Weak<Mutex<BTreeMap<usize, ActionObservationCallback>>>>,
     },
-    WindowActivationObservation {
-        id: usize,
-        window_id: usize,
-        observations:
-            Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, Option<WindowActivationCallback>>>>>>,
-    },
 }
 
 impl Subscription {

crates/gpui/src/app/callback_collection.rs 🔗

@@ -0,0 +1,106 @@
+use std::sync::Arc;
+use std::{hash::Hash, sync::Weak};
+
+use parking_lot::Mutex;
+
+use collections::{btree_map, BTreeMap, HashMap};
+
+use crate::MutableAppContext;
+
+pub type Mapping<K, F> = Mutex<HashMap<K, BTreeMap<usize, Option<F>>>>;
+
+pub struct CallbackCollection<K: Hash + Eq, F> {
+    internal: Arc<Mapping<K, F>>,
+}
+
+impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
+    fn clone(&self) -> Self {
+        Self {
+            internal: self.internal.clone(),
+        }
+    }
+}
+
+impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
+    fn default() -> Self {
+        CallbackCollection {
+            internal: Arc::new(Mutex::new(Default::default())),
+        }
+    }
+}
+
+impl<K: Hash + Eq + Copy, F> CallbackCollection<K, F> {
+    pub fn downgrade(&self) -> Weak<Mapping<K, F>> {
+        Arc::downgrade(&self.internal)
+    }
+
+    #[cfg(test)]
+    pub fn is_empty(&self) -> bool {
+        self.internal.lock().is_empty()
+    }
+
+    pub fn add_callback(&mut self, id: K, subscription_id: usize, callback: F) {
+        self.internal
+            .lock()
+            .entry(id)
+            .or_default()
+            .insert(subscription_id, Some(callback));
+    }
+
+    pub fn remove(&mut self, id: K) {
+        self.internal.lock().remove(&id);
+    }
+
+    pub fn add_or_remove_callback(&mut self, id: K, subscription_id: usize, callback: F) {
+        match self
+            .internal
+            .lock()
+            .entry(id)
+            .or_default()
+            .entry(subscription_id)
+        {
+            btree_map::Entry::Vacant(entry) => {
+                entry.insert(Some(callback));
+            }
+
+            btree_map::Entry::Occupied(entry) => {
+                // TODO: This seems like it should never be called because no code
+                // should ever attempt to remove an existing callback
+                debug_assert!(entry.get().is_none());
+                entry.remove();
+            }
+        }
+    }
+
+    pub fn emit_and_cleanup<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
+        &mut self,
+        id: K,
+        cx: &mut MutableAppContext,
+        mut call_callback: C,
+    ) {
+        let callbacks = self.internal.lock().remove(&id);
+        if let Some(callbacks) = callbacks {
+            for (subscription_id, callback) in callbacks {
+                if let Some(mut callback) = callback {
+                    let alive = call_callback(&mut callback, cx);
+                    if alive {
+                        match self
+                            .internal
+                            .lock()
+                            .entry(id)
+                            .or_default()
+                            .entry(subscription_id)
+                        {
+                            btree_map::Entry::Vacant(entry) => {
+                                entry.insert(Some(callback));
+                            }
+                            btree_map::Entry::Occupied(entry) => {
+                                entry.remove();
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

crates/gpui/src/platform.rs 🔗

@@ -112,6 +112,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_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
     fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
     fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);

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

@@ -128,6 +128,14 @@ unsafe fn build_classes() {
             sel!(windowDidResize:),
             window_did_resize as extern "C" fn(&Object, Sel, id),
         );
+        decl.add_method(
+            sel!(windowWillEnterFullScreen:),
+            window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
+        );
+        decl.add_method(
+            sel!(windowWillExitFullScreen:),
+            window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
+        );
         decl.add_method(
             sel!(windowDidBecomeKey:),
             window_did_change_key_status as extern "C" fn(&Object, Sel, id),
@@ -276,6 +284,7 @@ struct WindowState {
     event_callback: Option<Box<dyn FnMut(Event) -> bool>>,
     activate_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut()>>,
+    fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
     should_close_callback: Option<Box<dyn FnMut() -> bool>>,
     close_callback: Option<Box<dyn FnOnce()>>,
     input_handler: Option<Box<dyn InputHandler>>,
@@ -368,6 +377,7 @@ impl Window {
                 should_close_callback: None,
                 close_callback: None,
                 activate_callback: None,
+                fullscreen_callback: None,
                 input_handler: None,
                 pending_key_down: None,
                 performed_key_equivalent: false,
@@ -467,6 +477,10 @@ impl platform::Window for Window {
         self.0.as_ref().borrow_mut().resize_callback = Some(callback);
     }
 
+    fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
+        self.0.as_ref().borrow_mut().fullscreen_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);
     }
@@ -908,6 +922,24 @@ extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
     window_state.as_ref().borrow().move_traffic_light();
 }
 
+extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
+    window_fullscreen_changed(this, true);
+}
+
+extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
+    window_fullscreen_changed(this, false);
+}
+
+fn window_fullscreen_changed(this: &Object, is_fullscreen: 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.fullscreen_callback.take() {
+        drop(window_state_borrow);
+        callback(is_fullscreen);
+        window_state.borrow_mut().fullscreen_callback = Some(callback);
+    }
+}
+
 extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
     let is_active = if selector == sel!(windowDidBecomeKey:) {
         true

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()>>,
+    fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
     pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
     pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
     pub(crate) title: Option<String>,
@@ -199,6 +200,7 @@ impl Window {
             close_handlers: Default::default(),
             should_close_handler: Default::default(),
             active_status_change_handlers: Default::default(),
+            fullscreen_handlers: Default::default(),
             scale_factor: 1.0,
             current_scene: None,
             title: None,
@@ -253,6 +255,10 @@ impl super::Window for Window {
         self.active_status_change_handlers.push(callback);
     }
 
+    fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
+        self.fullscreen_handlers.push(callback)
+    }
+
     fn on_resize(&mut self, callback: Box<dyn FnMut()>) {
         self.resize_handlers.push(callback);
     }

crates/gpui/src/presenter.rs 🔗

@@ -13,11 +13,11 @@ use crate::{
     ReadModel, ReadView, RenderContext, RenderParams, Scene, UpgradeModelHandle, UpgradeViewHandle,
     View, ViewHandle, WeakModelHandle, WeakViewHandle,
 };
+use collections::{HashMap, HashSet};
 use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
 use smallvec::SmallVec;
 use std::{
-    collections::{HashMap, HashSet},
     marker::PhantomData,
     ops::{Deref, DerefMut, Range},
     sync::Arc,
@@ -52,7 +52,7 @@ impl Presenter {
         Self {
             window_id,
             rendered_views: cx.render_views(window_id, titlebar_height),
-            parents: HashMap::new(),
+            parents: Default::default(),
             cursor_regions: Default::default(),
             mouse_regions: Default::default(),
             font_cache,

crates/workspace/src/workspace.rs 🔗

@@ -823,6 +823,8 @@ enum FollowerItem {
 
 impl Workspace {
     pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+        cx.observe_fullscreen(|_, _, cx| cx.notify()).detach();
+
         cx.observe_window_activation(Self::on_window_activation_changed)
             .detach();
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
@@ -1856,6 +1858,17 @@ impl Workspace {
             worktree_root_names.push_str(name);
         }
 
+        // TODO: There should be a better system in place for this
+        // (https://github.com/zed-industries/zed/issues/1290)
+        let is_fullscreen = cx.window_is_fullscreen(cx.window_id());
+        let container_theme = if is_fullscreen {
+            let mut container_theme = theme.workspace.titlebar.container;
+            container_theme.padding.left = container_theme.padding.right;
+            container_theme
+        } else {
+            theme.workspace.titlebar.container
+        };
+
         ConstrainedBox::new(
             Container::new(
                 Stack::new()
@@ -1883,7 +1896,7 @@ impl Workspace {
                     )
                     .boxed(),
             )
-            .with_style(theme.workspace.titlebar.container)
+            .with_style(container_theme)
             .boxed(),
         )
         .with_height(theme.workspace.titlebar.height)