Extract contacts titlebar item into a separate crate

Antonio Scandurra created

This allows us to implement a new contacts popover that uses the
`editor` crate.

Change summary

Cargo.lock                                                  |  24 
crates/contacts_titlebar_item/Cargo.toml                    |  48 +
crates/contacts_titlebar_item/src/contacts_titlebar_item.rs | 304 +++++++
crates/workspace/Cargo.toml                                 |   1 
crates/workspace/src/workspace.rs                           | 289 ------
crates/zed/Cargo.toml                                       |   1 
crates/zed/src/zed.rs                                       |   7 
7 files changed, 408 insertions(+), 266 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1151,6 +1151,28 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "contacts_titlebar_item"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "clock",
+ "collections",
+ "editor",
+ "futures",
+ "fuzzy",
+ "gpui",
+ "log",
+ "postage",
+ "project",
+ "serde",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "context_menu"
 version = "0.1.0"
@@ -7084,7 +7106,6 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "client",
- "clock",
  "collections",
  "context_menu",
  "drag_and_drop",
@@ -7163,6 +7184,7 @@ dependencies = [
  "command_palette",
  "contacts_panel",
  "contacts_status_item",
+ "contacts_titlebar_item",
  "context_menu",
  "ctor",
  "diagnostics",

crates/contacts_titlebar_item/Cargo.toml 🔗

@@ -0,0 +1,48 @@
+[package]
+name = "contacts_titlebar_item"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+path = "src/contacts_titlebar_item.rs"
+doctest = false
+
+[features]
+test-support = [
+    "client/test-support",
+    "collections/test-support",
+    "editor/test-support",
+    "gpui/test-support",
+    "project/test-support",
+    "settings/test-support",
+    "util/test-support",
+    "workspace/test-support",
+]
+
+[dependencies]
+client = { path = "../client" }
+clock = { path = "../clock" }
+collections = { path = "../collections" }
+editor = { path = "../editor" }
+fuzzy = { path = "../fuzzy" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+settings = { path = "../settings" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+workspace = { path = "../workspace" }
+anyhow = "1.0"
+futures = "0.3"
+log = "0.4"
+postage = { version = "0.4.1", features = ["futures-traits"] }
+serde = { version = "1.0", features = ["derive", "rc"] }
+
+[dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
+editor = { path = "../editor", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }

crates/contacts_titlebar_item/src/contacts_titlebar_item.rs 🔗

@@ -0,0 +1,304 @@
+use client::{Authenticate, PeerId};
+use clock::ReplicaId;
+use gpui::{
+    color::Color,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f, PathBuilder},
+    json::{self, ToJson},
+    Border, CursorStyle, Entity, ImageData, MouseButton, RenderContext, Subscription, View,
+    ViewContext, ViewHandle, WeakViewHandle,
+};
+use settings::Settings;
+use std::{ops::Range, sync::Arc};
+use theme::Theme;
+use workspace::{FollowNextCollaborator, ToggleFollow, Workspace};
+
+pub struct ContactsTitlebarItem {
+    workspace: WeakViewHandle<Workspace>,
+    _subscriptions: Vec<Subscription>,
+}
+
+impl Entity for ContactsTitlebarItem {
+    type Event = ();
+}
+
+impl View for ContactsTitlebarItem {
+    fn ui_name() -> &'static str {
+        "ContactsTitlebarItem"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
+            workspace
+        } else {
+            return Empty::new().boxed();
+        };
+
+        let theme = cx.global::<Settings>().theme.clone();
+        Flex::row()
+            .with_children(self.render_collaborators(&workspace, &theme, cx))
+            .with_children(self.render_current_user(&workspace, &theme, cx))
+            .with_children(self.render_connection_status(&workspace, cx))
+            .boxed()
+    }
+}
+
+impl ContactsTitlebarItem {
+    pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+        let observe_workspace = cx.observe(workspace, |_, _, cx| cx.notify());
+        Self {
+            workspace: workspace.downgrade(),
+            _subscriptions: vec![observe_workspace],
+        }
+    }
+
+    fn render_collaborators(
+        &self,
+        workspace: &ViewHandle<Workspace>,
+        theme: &Theme,
+        cx: &mut RenderContext<Self>,
+    ) -> Vec<ElementBox> {
+        let mut collaborators = workspace
+            .read(cx)
+            .project()
+            .read(cx)
+            .collaborators()
+            .values()
+            .cloned()
+            .collect::<Vec<_>>();
+        collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
+        collaborators
+            .into_iter()
+            .filter_map(|collaborator| {
+                Some(self.render_avatar(
+                    collaborator.user.avatar.clone()?,
+                    collaborator.replica_id,
+                    Some((collaborator.peer_id, &collaborator.user.github_login)),
+                    workspace,
+                    theme,
+                    cx,
+                ))
+            })
+            .collect()
+    }
+
+    fn render_current_user(
+        &self,
+        workspace: &ViewHandle<Workspace>,
+        theme: &Theme,
+        cx: &mut RenderContext<Self>,
+    ) -> Option<ElementBox> {
+        let user = workspace.read(cx).user_store().read(cx).current_user();
+        let replica_id = workspace.read(cx).project().read(cx).replica_id();
+        let status = *workspace.read(cx).client().status().borrow();
+        if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
+            Some(self.render_avatar(avatar, replica_id, None, workspace, theme, cx))
+        } else if matches!(status, client::Status::UpgradeRequired) {
+            None
+        } else {
+            Some(
+                MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
+                    let style = theme
+                        .workspace
+                        .titlebar
+                        .sign_in_prompt
+                        .style_for(state, false);
+                    Label::new("Sign in".to_string(), style.text.clone())
+                        .contained()
+                        .with_style(style.container)
+                        .boxed()
+                })
+                .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
+                .with_cursor_style(CursorStyle::PointingHand)
+                .aligned()
+                .boxed(),
+            )
+        }
+    }
+
+    fn render_avatar(
+        &self,
+        avatar: Arc<ImageData>,
+        replica_id: ReplicaId,
+        peer: Option<(PeerId, &str)>,
+        workspace: &ViewHandle<Workspace>,
+        theme: &Theme,
+        cx: &mut RenderContext<Self>,
+    ) -> ElementBox {
+        let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
+        let is_followed = peer.map_or(false, |(peer_id, _)| {
+            workspace.read(cx).is_following(peer_id)
+        });
+        let mut avatar_style = theme.workspace.titlebar.avatar;
+        if is_followed {
+            avatar_style.border = Border::all(1.0, replica_color);
+        }
+        let content = Stack::new()
+            .with_child(
+                Image::new(avatar)
+                    .with_style(avatar_style)
+                    .constrained()
+                    .with_width(theme.workspace.titlebar.avatar_width)
+                    .aligned()
+                    .boxed(),
+            )
+            .with_child(
+                AvatarRibbon::new(replica_color)
+                    .constrained()
+                    .with_width(theme.workspace.titlebar.avatar_ribbon.width)
+                    .with_height(theme.workspace.titlebar.avatar_ribbon.height)
+                    .aligned()
+                    .bottom()
+                    .boxed(),
+            )
+            .constrained()
+            .with_width(theme.workspace.titlebar.avatar_width)
+            .contained()
+            .with_margin_left(theme.workspace.titlebar.avatar_margin)
+            .boxed();
+
+        if let Some((peer_id, peer_github_login)) = peer {
+            MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, cx| {
+                    cx.dispatch_action(ToggleFollow(peer_id))
+                })
+                .with_tooltip::<ToggleFollow, _>(
+                    peer_id.0 as usize,
+                    if is_followed {
+                        format!("Unfollow {}", peer_github_login)
+                    } else {
+                        format!("Follow {}", peer_github_login)
+                    },
+                    Some(Box::new(FollowNextCollaborator)),
+                    theme.tooltip.clone(),
+                    cx,
+                )
+                .boxed()
+        } else {
+            content
+        }
+    }
+
+    fn render_connection_status(
+        &self,
+        workspace: &ViewHandle<Workspace>,
+        cx: &mut RenderContext<Self>,
+    ) -> Option<ElementBox> {
+        let theme = &cx.global::<Settings>().theme;
+        match &*workspace.read(cx).client().status().borrow() {
+            client::Status::ConnectionError
+            | client::Status::ConnectionLost
+            | client::Status::Reauthenticating { .. }
+            | client::Status::Reconnecting { .. }
+            | client::Status::ReconnectionError { .. } => Some(
+                Container::new(
+                    Align::new(
+                        ConstrainedBox::new(
+                            Svg::new("icons/cloud_slash_12.svg")
+                                .with_color(theme.workspace.titlebar.offline_icon.color)
+                                .boxed(),
+                        )
+                        .with_width(theme.workspace.titlebar.offline_icon.width)
+                        .boxed(),
+                    )
+                    .boxed(),
+                )
+                .with_style(theme.workspace.titlebar.offline_icon.container)
+                .boxed(),
+            ),
+            client::Status::UpgradeRequired => Some(
+                Label::new(
+                    "Please update Zed to collaborate".to_string(),
+                    theme.workspace.titlebar.outdated_warning.text.clone(),
+                )
+                .contained()
+                .with_style(theme.workspace.titlebar.outdated_warning.container)
+                .aligned()
+                .boxed(),
+            ),
+            _ => None,
+        }
+    }
+}
+
+pub struct AvatarRibbon {
+    color: Color,
+}
+
+impl AvatarRibbon {
+    pub fn new(color: Color) -> AvatarRibbon {
+        AvatarRibbon { color }
+    }
+}
+
+impl Element for AvatarRibbon {
+    type LayoutState = ();
+
+    type PaintState = ();
+
+    fn layout(
+        &mut self,
+        constraint: gpui::SizeConstraint,
+        _: &mut gpui::LayoutContext,
+    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
+        (constraint.max, ())
+    }
+
+    fn paint(
+        &mut self,
+        bounds: gpui::geometry::rect::RectF,
+        _: gpui::geometry::rect::RectF,
+        _: &mut Self::LayoutState,
+        cx: &mut gpui::PaintContext,
+    ) -> Self::PaintState {
+        let mut path = PathBuilder::new();
+        path.reset(bounds.lower_left());
+        path.curve_to(
+            bounds.origin() + vec2f(bounds.height(), 0.),
+            bounds.origin(),
+        );
+        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
+        path.curve_to(bounds.lower_right(), bounds.upper_right());
+        path.line_to(bounds.lower_left());
+        cx.scene.push_path(path.build(self.color, None));
+    }
+
+    fn dispatch_event(
+        &mut self,
+        _: &gpui::Event,
+        _: RectF,
+        _: RectF,
+        _: &mut Self::LayoutState,
+        _: &mut Self::PaintState,
+        _: &mut gpui::EventContext,
+    ) -> bool {
+        false
+    }
+
+    fn rect_for_text_range(
+        &self,
+        _: Range<usize>,
+        _: RectF,
+        _: RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::MeasurementContext,
+    ) -> Option<RectF> {
+        None
+    }
+
+    fn debug(
+        &self,
+        bounds: gpui::geometry::rect::RectF,
+        _: &Self::LayoutState,
+        _: &Self::PaintState,
+        _: &gpui::DebugContext,
+    ) -> gpui::json::Value {
+        json::json!({
+            "type": "AvatarRibbon",
+            "bounds": bounds.to_json(),
+            "color": self.color.to_json(),
+        })
+    }
+}

crates/workspace/Cargo.toml 🔗

@@ -12,7 +12,6 @@ test-support = ["client/test-support", "project/test-support", "settings/test-su
 
 [dependencies]
 client = { path = "../client" }
-clock = { path = "../clock" }
 collections = { path = "../collections" }
 context_menu = { path = "../context_menu" }
 drag_and_drop = { path = "../drag_and_drop" }

crates/workspace/src/workspace.rs 🔗

@@ -13,25 +13,19 @@ mod toolbar;
 mod waiting_room;
 
 use anyhow::{anyhow, Context, Result};
-use client::{
-    proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
-};
-use clock::ReplicaId;
+use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore};
 use collections::{hash_map, HashMap, HashSet};
 use dock::{DefaultItemFactory, Dock, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
 use futures::{channel::oneshot, FutureExt};
 use gpui::{
     actions,
-    color::Color,
     elements::*,
-    geometry::{rect::RectF, vector::vec2f, PathBuilder},
     impl_actions, impl_internal_actions,
-    json::{self, ToJson},
     platform::{CursorStyle, WindowOptions},
-    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData,
-    ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel,
-    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
+    MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use language::LanguageRegistry;
 use log::{error, warn};
@@ -53,7 +47,6 @@ use std::{
     fmt,
     future::Future,
     mem,
-    ops::Range,
     path::{Path, PathBuf},
     rc::Rc,
     sync::{
@@ -895,6 +888,7 @@ pub struct Workspace {
     active_pane: ViewHandle<Pane>,
     last_active_center_pane: Option<ViewHandle<Pane>>,
     status_bar: ViewHandle<StatusBar>,
+    titlebar_item: Option<AnyViewHandle>,
     dock: Dock,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: ModelHandle<Project>,
@@ -1024,6 +1018,7 @@ impl Workspace {
             active_pane: center_pane.clone(),
             last_active_center_pane: Some(center_pane.clone()),
             status_bar,
+            titlebar_item: None,
             notifications: Default::default(),
             client,
             remote_entity_subscription: None,
@@ -1068,6 +1063,19 @@ impl Workspace {
         &self.project
     }
 
+    pub fn client(&self) -> &Arc<Client> {
+        &self.client
+    }
+
+    pub fn set_titlebar_item(
+        &mut self,
+        item: impl Into<AnyViewHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.titlebar_item = Some(item.into());
+        cx.notify();
+    }
+
     /// Call the given callback with a workspace whose project is local.
     ///
     /// If the given workspace has a local project, then it will be passed
@@ -1968,46 +1976,12 @@ impl Workspace {
         None
     }
 
-    fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
-        let theme = &cx.global::<Settings>().theme;
-        match &*self.client.status().borrow() {
-            client::Status::ConnectionError
-            | client::Status::ConnectionLost
-            | client::Status::Reauthenticating { .. }
-            | client::Status::Reconnecting { .. }
-            | client::Status::ReconnectionError { .. } => Some(
-                Container::new(
-                    Align::new(
-                        ConstrainedBox::new(
-                            Svg::new("icons/cloud_slash_12.svg")
-                                .with_color(theme.workspace.titlebar.offline_icon.color)
-                                .boxed(),
-                        )
-                        .with_width(theme.workspace.titlebar.offline_icon.width)
-                        .boxed(),
-                    )
-                    .boxed(),
-                )
-                .with_style(theme.workspace.titlebar.offline_icon.container)
-                .boxed(),
-            ),
-            client::Status::UpgradeRequired => Some(
-                Label::new(
-                    "Please update Zed to collaborate".to_string(),
-                    theme.workspace.titlebar.outdated_warning.text.clone(),
-                )
-                .contained()
-                .with_style(theme.workspace.titlebar.outdated_warning.container)
-                .aligned()
-                .boxed(),
-            ),
-            _ => None,
-        }
+    pub fn is_following(&self, peer_id: PeerId) -> bool {
+        self.follower_states_by_leader.contains_key(&peer_id)
     }
 
     fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
         let project = &self.project.read(cx);
-        let replica_id = project.replica_id();
         let mut worktree_root_names = String::new();
         for (i, name) in project.worktree_root_names(cx).enumerate() {
             if i > 0 {
@@ -2029,7 +2003,7 @@ impl Workspace {
 
         enum TitleBar {}
         ConstrainedBox::new(
-            MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| {
+            MouseEventHandler::<TitleBar>::new(0, cx, |_, _| {
                 Container::new(
                     Stack::new()
                         .with_child(
@@ -2038,21 +2012,10 @@ impl Workspace {
                                 .left()
                                 .boxed(),
                         )
-                        .with_child(
-                            Align::new(
-                                Flex::row()
-                                    .with_children(self.render_collaborators(theme, cx))
-                                    .with_children(self.render_current_user(
-                                        self.user_store.read(cx).current_user().as_ref(),
-                                        replica_id,
-                                        theme,
-                                        cx,
-                                    ))
-                                    .with_children(self.render_connection_status(cx))
-                                    .boxed(),
-                            )
-                            .right()
-                            .boxed(),
+                        .with_children(
+                            self.titlebar_item
+                                .as_ref()
+                                .map(|item| ChildView::new(item).aligned().right().boxed()),
                         )
                         .boxed(),
                 )
@@ -2121,125 +2084,6 @@ impl Workspace {
         }
     }
 
-    fn render_collaborators(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> Vec<ElementBox> {
-        let mut collaborators = self
-            .project
-            .read(cx)
-            .collaborators()
-            .values()
-            .cloned()
-            .collect::<Vec<_>>();
-        collaborators.sort_unstable_by_key(|collaborator| collaborator.replica_id);
-        collaborators
-            .into_iter()
-            .filter_map(|collaborator| {
-                Some(self.render_avatar(
-                    collaborator.user.avatar.clone()?,
-                    collaborator.replica_id,
-                    Some((collaborator.peer_id, &collaborator.user.github_login)),
-                    theme,
-                    cx,
-                ))
-            })
-            .collect()
-    }
-
-    fn render_current_user(
-        &self,
-        user: Option<&Arc<User>>,
-        replica_id: ReplicaId,
-        theme: &Theme,
-        cx: &mut RenderContext<Self>,
-    ) -> Option<ElementBox> {
-        let status = *self.client.status().borrow();
-        if let Some(avatar) = user.and_then(|user| user.avatar.clone()) {
-            Some(self.render_avatar(avatar, replica_id, None, theme, cx))
-        } else if matches!(status, client::Status::UpgradeRequired) {
-            None
-        } else {
-            Some(
-                MouseEventHandler::<Authenticate>::new(0, cx, |state, _| {
-                    let style = theme
-                        .workspace
-                        .titlebar
-                        .sign_in_prompt
-                        .style_for(state, false);
-                    Label::new("Sign in".to_string(), style.text.clone())
-                        .contained()
-                        .with_style(style.container)
-                        .boxed()
-                })
-                .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(Authenticate))
-                .with_cursor_style(CursorStyle::PointingHand)
-                .aligned()
-                .boxed(),
-            )
-        }
-    }
-
-    fn render_avatar(
-        &self,
-        avatar: Arc<ImageData>,
-        replica_id: ReplicaId,
-        peer: Option<(PeerId, &str)>,
-        theme: &Theme,
-        cx: &mut RenderContext<Self>,
-    ) -> ElementBox {
-        let replica_color = theme.editor.replica_selection_style(replica_id).cursor;
-        let is_followed = peer.map_or(false, |(peer_id, _)| {
-            self.follower_states_by_leader.contains_key(&peer_id)
-        });
-        let mut avatar_style = theme.workspace.titlebar.avatar;
-        if is_followed {
-            avatar_style.border = Border::all(1.0, replica_color);
-        }
-        let content = Stack::new()
-            .with_child(
-                Image::new(avatar)
-                    .with_style(avatar_style)
-                    .constrained()
-                    .with_width(theme.workspace.titlebar.avatar_width)
-                    .aligned()
-                    .boxed(),
-            )
-            .with_child(
-                AvatarRibbon::new(replica_color)
-                    .constrained()
-                    .with_width(theme.workspace.titlebar.avatar_ribbon.width)
-                    .with_height(theme.workspace.titlebar.avatar_ribbon.height)
-                    .aligned()
-                    .bottom()
-                    .boxed(),
-            )
-            .constrained()
-            .with_width(theme.workspace.titlebar.avatar_width)
-            .contained()
-            .with_margin_left(theme.workspace.titlebar.avatar_margin)
-            .boxed();
-
-        if let Some((peer_id, peer_github_login)) = peer {
-            MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| content)
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, cx| {
-                    cx.dispatch_action(ToggleFollow(peer_id))
-                })
-                .with_tooltip::<ToggleFollow, _>(
-                    peer_id.0 as usize,
-                    if is_followed {
-                        format!("Unfollow {}", peer_github_login)
-                    } else {
-                        format!("Follow {}", peer_github_login)
-                    },
-                    Some(Box::new(FollowNextCollaborator)),
-                    theme.tooltip.clone(),
-                    cx,
-                )
-                .boxed()
-        } else {
-            content
-        }
-    }
-
     fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
         if self.project.read(cx).is_read_only() {
             enum DisconnectedOverlay {}
@@ -2714,87 +2558,6 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
     }
 }
 
-pub struct AvatarRibbon {
-    color: Color,
-}
-
-impl AvatarRibbon {
-    pub fn new(color: Color) -> AvatarRibbon {
-        AvatarRibbon { color }
-    }
-}
-
-impl Element for AvatarRibbon {
-    type LayoutState = ();
-
-    type PaintState = ();
-
-    fn layout(
-        &mut self,
-        constraint: gpui::SizeConstraint,
-        _: &mut gpui::LayoutContext,
-    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
-        (constraint.max, ())
-    }
-
-    fn paint(
-        &mut self,
-        bounds: gpui::geometry::rect::RectF,
-        _: gpui::geometry::rect::RectF,
-        _: &mut Self::LayoutState,
-        cx: &mut gpui::PaintContext,
-    ) -> Self::PaintState {
-        let mut path = PathBuilder::new();
-        path.reset(bounds.lower_left());
-        path.curve_to(
-            bounds.origin() + vec2f(bounds.height(), 0.),
-            bounds.origin(),
-        );
-        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
-        path.curve_to(bounds.lower_right(), bounds.upper_right());
-        path.line_to(bounds.lower_left());
-        cx.scene.push_path(path.build(self.color, None));
-    }
-
-    fn dispatch_event(
-        &mut self,
-        _: &gpui::Event,
-        _: RectF,
-        _: RectF,
-        _: &mut Self::LayoutState,
-        _: &mut Self::PaintState,
-        _: &mut gpui::EventContext,
-    ) -> bool {
-        false
-    }
-
-    fn rect_for_text_range(
-        &self,
-        _: Range<usize>,
-        _: RectF,
-        _: RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &gpui::MeasurementContext,
-    ) -> Option<RectF> {
-        None
-    }
-
-    fn debug(
-        &self,
-        bounds: gpui::geometry::rect::RectF,
-        _: &Self::LayoutState,
-        _: &Self::PaintState,
-        _: &gpui::DebugContext,
-    ) -> gpui::json::Value {
-        json::json!({
-            "type": "AvatarRibbon",
-            "bounds": bounds.to_json(),
-            "color": self.color.to_json(),
-        })
-    }
-}
-
 impl std::fmt::Debug for OpenPaths {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("OpenPaths")

crates/zed/Cargo.toml 🔗

@@ -27,6 +27,7 @@ context_menu = { path = "../context_menu" }
 client = { path = "../client" }
 clock = { path = "../clock" }
 contacts_panel = { path = "../contacts_panel" }
+contacts_titlebar_item = { path = "../contacts_titlebar_item" }
 contacts_status_item = { path = "../contacts_status_item" }
 diagnostics = { path = "../diagnostics" }
 editor = { path = "../editor" }

crates/zed/src/zed.rs 🔗

@@ -13,6 +13,7 @@ pub use client;
 use collections::VecDeque;
 pub use contacts_panel;
 use contacts_panel::ContactsPanel;
+use contacts_titlebar_item::ContactsTitlebarItem;
 pub use editor;
 use editor::{Editor, MultiBuffer};
 use gpui::{
@@ -224,7 +225,8 @@ pub fn initialize_workspace(
     app_state: &Arc<AppState>,
     cx: &mut ViewContext<Workspace>,
 ) {
-    cx.subscribe(&cx.handle(), {
+    let workspace_handle = cx.handle();
+    cx.subscribe(&workspace_handle, {
         move |_, _, event, cx| {
             if let workspace::Event::PaneAdded(pane) = event {
                 pane.update(cx, |pane, cx| {
@@ -278,6 +280,9 @@ pub fn initialize_workspace(
         }));
     });
 
+    let contacts_titlebar_item = cx.add_view(|cx| ContactsTitlebarItem::new(&workspace_handle, cx));
+    workspace.set_titlebar_item(contacts_titlebar_item, cx);
+
     let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
     let contact_panel = cx.add_view(|cx| {
         ContactsPanel::new(