From c8a48e8990f643b735d579c9af42175ac2635556 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Sep 2022 12:17:00 +0200 Subject: [PATCH] Extract contacts titlebar item into a separate crate This allows us to implement a new contacts popover that uses the `editor` crate. --- Cargo.lock | 24 +- crates/contacts_titlebar_item/Cargo.toml | 48 +++ .../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(-) create mode 100644 crates/contacts_titlebar_item/Cargo.toml create mode 100644 crates/contacts_titlebar_item/src/contacts_titlebar_item.rs diff --git a/Cargo.lock b/Cargo.lock index 4738e69852..8537c51611 100644 --- a/Cargo.lock +++ b/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", diff --git a/crates/contacts_titlebar_item/Cargo.toml b/crates/contacts_titlebar_item/Cargo.toml new file mode 100644 index 0000000000..771e364218 --- /dev/null +++ b/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"] } diff --git a/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs b/crates/contacts_titlebar_item/src/contacts_titlebar_item.rs new file mode 100644 index 0000000000..7035585a2f --- /dev/null +++ b/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, + _subscriptions: Vec, +} + +impl Entity for ContactsTitlebarItem { + type Event = (); +} + +impl View for ContactsTitlebarItem { + fn ui_name() -> &'static str { + "ContactsTitlebarItem" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let workspace = if let Some(workspace) = self.workspace.upgrade(cx) { + workspace + } else { + return Empty::new().boxed(); + }; + + let theme = cx.global::().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, cx: &mut ViewContext) -> Self { + let observe_workspace = cx.observe(workspace, |_, _, cx| cx.notify()); + Self { + workspace: workspace.downgrade(), + _subscriptions: vec![observe_workspace], + } + } + + fn render_collaborators( + &self, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> Vec { + let mut collaborators = workspace + .read(cx) + .project() + .read(cx) + .collaborators() + .values() + .cloned() + .collect::>(); + 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, + theme: &Theme, + cx: &mut RenderContext, + ) -> Option { + 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::::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, + replica_id: ReplicaId, + peer: Option<(PeerId, &str)>, + workspace: &ViewHandle, + theme: &Theme, + cx: &mut RenderContext, + ) -> 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::::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::( + 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, + cx: &mut RenderContext, + ) -> Option { + let theme = &cx.global::().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, + _: RectF, + _: RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &gpui::MeasurementContext, + ) -> Option { + 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(), + }) + } +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index c40ce56389..759bff2cbd 100644 --- a/crates/workspace/Cargo.toml +++ b/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" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 017964d9a1..04bbc094b2 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, last_active_center_pane: Option>, status_bar: ViewHandle, + titlebar_item: Option, dock: Dock, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, @@ -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 { + &self.client + } + + pub fn set_titlebar_item( + &mut self, + item: impl Into, + cx: &mut ViewContext, + ) { + 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) -> Option { - let theme = &cx.global::().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) -> 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::::new(0, cx, |_, cx| { + MouseEventHandler::::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) -> Vec { - let mut collaborators = self - .project - .read(cx) - .collaborators() - .values() - .cloned() - .collect::>(); - 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>, - replica_id: ReplicaId, - theme: &Theme, - cx: &mut RenderContext, - ) -> Option { - 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::::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, - replica_id: ReplicaId, - peer: Option<(PeerId, &str)>, - theme: &Theme, - cx: &mut RenderContext, - ) -> 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::::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::( - 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) -> Option { if self.project.read(cx).is_read_only() { enum DisconnectedOverlay {} @@ -2714,87 +2558,6 @@ impl WorkspaceHandle for ViewHandle { } } -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, - _: RectF, - _: RectF, - _: &Self::LayoutState, - _: &Self::PaintState, - _: &gpui::MeasurementContext, - ) -> Option { - 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") diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index dc2b0abd03..170f554814 100644 --- a/crates/zed/Cargo.toml +++ b/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" } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cd906500ee..42bcd6b9bd 100644 --- a/crates/zed/src/zed.rs +++ b/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, cx: &mut ViewContext, ) { - 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(