Add global events to MutableAppContext and raise global event when new workspace is created

Keith Simmons created

Change summary

crates/gpui/src/app.rs            | 93 +++++++++++++++++++++++++++++---
crates/workspace/src/workspace.rs | 21 ++++--
2 files changed, 95 insertions(+), 19 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -740,6 +740,7 @@ type ActionCallback =
 type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext);
 
 type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
+type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
 
@@ -757,6 +758,7 @@ pub struct MutableAppContext {
     next_subscription_id: usize,
     frame_count: usize,
     subscriptions: Arc<Mutex<HashMap<usize, BTreeMap<usize, SubscriptionCallback>>>>,
+    global_subscriptions: Arc<Mutex<HashMap<TypeId, BTreeMap<usize, GlobalSubscriptionCallback>>>>,
     observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ObservationCallback>>>>,
     release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
     presenters_and_platform_windows:
@@ -804,6 +806,7 @@ impl MutableAppContext {
             next_subscription_id: 0,
             frame_count: 0,
             subscriptions: Default::default(),
+            global_subscriptions: Default::default(),
             observations: Default::default(),
             release_observations: Default::default(),
             presenters_and_platform_windows: HashMap::new(),
@@ -1062,6 +1065,12 @@ impl MutableAppContext {
         self.foreground_platform.prompt_for_new_path(directory)
     }
 
+    pub fn emit_global<E: Any>(&mut self, payload: E) {
+        self.pending_effects.push_back(Effect::GlobalEvent {
+            payload: Box::new(payload),
+        });
+    }
+
     pub fn subscribe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
     where
         E: Entity,
@@ -1075,6 +1084,31 @@ impl MutableAppContext {
         })
     }
 
+    pub fn global_subscribe<E, F>(&mut self, mut callback: F) -> Subscription
+    where
+        E: Any + Copy,
+        F: 'static + FnMut(&E, &mut Self),
+    {
+        let id = post_inc(&mut self.next_subscription_id);
+        let type_id = TypeId::of::<E>();
+        self.global_subscriptions
+            .lock()
+            .entry(type_id)
+            .or_default()
+            .insert(
+                id, 
+                Box::new(move |payload, cx| {
+                    let payload = payload.downcast_ref().expect("downcast is type safe");
+                    callback(payload, cx)
+                }));
+        Subscription::GlobalSubscription {
+            id,
+            type_id,
+            subscriptions: Some(Arc::downgrade(&self.global_subscriptions))
+        }
+    }
+
+
     pub fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
     where
         E: Entity,
@@ -1573,6 +1607,7 @@ impl MutableAppContext {
                 if let Some(effect) = self.pending_effects.pop_front() {
                     match effect {
                         Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload),
+                        Effect::GlobalEvent { payload } => self.emit_global_event(payload),
                         Effect::ModelNotification { model_id } => {
                             self.notify_model_observers(model_id)
                         }
@@ -1700,6 +1735,16 @@ impl MutableAppContext {
         }
     }
 
+    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 (_, mut callback) in callbacks {
+                callback(payload.as_ref(), self)
+            }
+        }
+    }
+
     fn notify_model_observers(&mut self, observed_id: usize) {
         let callbacks = self.observations.lock().remove(&observed_id);
         if let Some(callbacks) = callbacks {
@@ -2071,6 +2116,9 @@ pub enum Effect {
         entity_id: usize,
         payload: Box<dyn Any>,
     },
+    GlobalEvent {
+        payload: Box<dyn Any>,
+    },
     ModelNotification {
         model_id: usize,
     },
@@ -2104,6 +2152,10 @@ impl Debug for Effect {
                 .debug_struct("Effect::Event")
                 .field("entity_id", entity_id)
                 .finish(),
+            Effect::GlobalEvent { payload, .. } => f
+                .debug_struct("Effect::GlobalEvent")
+                .field("type_id", &(&*payload).type_id())
+                .finish(),
             Effect::ModelNotification { model_id } => f
                 .debug_struct("Effect::ModelNotification")
                 .field("model_id", model_id)
@@ -3762,6 +3814,11 @@ pub enum Subscription {
         entity_id: usize,
         subscriptions: Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, SubscriptionCallback>>>>>,
     },
+    GlobalSubscription {
+        id: usize,
+        type_id: TypeId,
+        subscriptions: Option<Weak<Mutex<HashMap<TypeId, BTreeMap<usize, GlobalSubscriptionCallback>>>>>,
+    },
     Observation {
         id: usize,
         entity_id: usize,
@@ -3781,6 +3838,9 @@ impl Subscription {
             Subscription::Subscription { subscriptions, .. } => {
                 subscriptions.take();
             }
+            Subscription::GlobalSubscription { subscriptions, .. } => {
+                subscriptions.take();
+            }
             Subscription::Observation { observations, .. } => {
                 observations.take();
             }
@@ -3794,18 +3854,29 @@ impl Subscription {
 impl Drop for Subscription {
     fn drop(&mut self) {
         match self {
-            Subscription::Observation {
+            Subscription::Subscription {
                 id,
                 entity_id,
-                observations,
+                subscriptions,
             } => {
-                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
-                    if let Some(observations) = observations.lock().get_mut(entity_id) {
-                        observations.remove(id);
+                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
+                    if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) {
+                        subscriptions.remove(id);
                     }
                 }
             }
-            Subscription::ReleaseObservation {
+            Subscription::GlobalSubscription {
+                id,
+                type_id,
+                subscriptions,
+            } => {
+                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
+                    if let Some(subscriptions) = subscriptions.lock().get_mut(type_id) {
+                        subscriptions.remove(id);
+                    }
+                }
+            }
+            Subscription::Observation {
                 id,
                 entity_id,
                 observations,
@@ -3816,14 +3887,14 @@ impl Drop for Subscription {
                     }
                 }
             }
-            Subscription::Subscription {
+            Subscription::ReleaseObservation {
                 id,
                 entity_id,
-                subscriptions,
+                observations,
             } => {
-                if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
-                    if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) {
-                        subscriptions.remove(id);
+                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
+                    if let Some(observations) = observations.lock().get_mut(entity_id) {
+                        observations.remove(id);
                     }
                 }
             }

crates/workspace/src/workspace.rs 🔗

@@ -1511,6 +1511,8 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
     .detach();
 }
 
+pub struct WorkspaceBuilt(WeakViewHandle<Workspace>);
+
 pub fn open_paths(
     abs_paths: &[PathBuf],
     app_state: &Arc<AppState>,
@@ -1537,7 +1539,7 @@ pub fn open_paths(
     }
 
     let workspace = existing.unwrap_or_else(|| {
-        cx.add_window((app_state.build_window_options)(), |cx| {
+        let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
             let project = Project::local(
                 app_state.client.clone(),
                 app_state.user_store.clone(),
@@ -1546,8 +1548,9 @@ pub fn open_paths(
                 cx,
             );
             (app_state.build_workspace)(project, &app_state, cx)
-        })
-        .1
+        });
+        cx.emit_global(WorkspaceBuilt(workspace.downgrade()));
+        workspace
     });
 
     let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx));
@@ -1581,12 +1584,13 @@ pub fn join_project(
             &mut cx,
         )
         .await?;
-        let (_, workspace) = cx.update(|cx| {
-            cx.add_window((app_state.build_window_options)(), |cx| {
+        Ok(cx.update(|cx| {
+            let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
                 (app_state.build_workspace)(project, &app_state, cx)
-            })
-        });
-        Ok(workspace)
+            });
+            cx.emit_global(WorkspaceBuilt(workspace.downgrade()));
+            workspace
+        }))
     })
 }
 
@@ -1601,5 +1605,6 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
         );
         (app_state.build_workspace)(project, &app_state, cx)
     });
+    cx.emit_global(WorkspaceBuilt(workspace.downgrade()));
     cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
 }