Extract contacts titlebar item into a separate crate

This allows us to implement a new contacts popover that uses the
`editor` crate.
This commit is contained in:
Antonio Scandurra 2022-09-27 12:17:00 +02:00
parent 80ab144bf3
commit c8a48e8990
7 changed files with 408 additions and 266 deletions

24
Cargo.lock generated
View file

@ -1151,6 +1151,28 @@ dependencies = [
"workspace", "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]] [[package]]
name = "context_menu" name = "context_menu"
version = "0.1.0" version = "0.1.0"
@ -7084,7 +7106,6 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"client", "client",
"clock",
"collections", "collections",
"context_menu", "context_menu",
"drag_and_drop", "drag_and_drop",
@ -7163,6 +7184,7 @@ dependencies = [
"command_palette", "command_palette",
"contacts_panel", "contacts_panel",
"contacts_status_item", "contacts_status_item",
"contacts_titlebar_item",
"context_menu", "context_menu",
"ctor", "ctor",
"diagnostics", "diagnostics",

View file

@ -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"] }

View file

@ -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(),
})
}
}

View file

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

View file

@ -13,25 +13,19 @@ mod toolbar;
mod waiting_room; mod waiting_room;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use client::{ use client::{proto, Client, Contact, PeerId, Subscription, TypedEnvelope, UserStore};
proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
};
use clock::ReplicaId;
use collections::{hash_map, HashMap, HashSet}; use collections::{hash_map, HashMap, HashSet};
use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use dock::{DefaultItemFactory, Dock, ToggleDockButton};
use drag_and_drop::DragAndDrop; use drag_and_drop::DragAndDrop;
use futures::{channel::oneshot, FutureExt}; use futures::{channel::oneshot, FutureExt};
use gpui::{ use gpui::{
actions, actions,
color::Color,
elements::*, elements::*,
geometry::{rect::RectF, vector::vec2f, PathBuilder},
impl_actions, impl_internal_actions, impl_actions, impl_internal_actions,
json::{self, ToJson},
platform::{CursorStyle, WindowOptions}, platform::{CursorStyle, WindowOptions},
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, Entity, ImageData, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
ModelContext, ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
use language::LanguageRegistry; use language::LanguageRegistry;
use log::{error, warn}; use log::{error, warn};
@ -53,7 +47,6 @@ use std::{
fmt, fmt,
future::Future, future::Future,
mem, mem,
ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
sync::{ sync::{
@ -895,6 +888,7 @@ pub struct Workspace {
active_pane: ViewHandle<Pane>, active_pane: ViewHandle<Pane>,
last_active_center_pane: Option<ViewHandle<Pane>>, last_active_center_pane: Option<ViewHandle<Pane>>,
status_bar: ViewHandle<StatusBar>, status_bar: ViewHandle<StatusBar>,
titlebar_item: Option<AnyViewHandle>,
dock: Dock, dock: Dock,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>, notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
@ -1024,6 +1018,7 @@ impl Workspace {
active_pane: center_pane.clone(), active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.clone()), last_active_center_pane: Some(center_pane.clone()),
status_bar, status_bar,
titlebar_item: None,
notifications: Default::default(), notifications: Default::default(),
client, client,
remote_entity_subscription: None, remote_entity_subscription: None,
@ -1068,6 +1063,19 @@ impl Workspace {
&self.project &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. /// Call the given callback with a workspace whose project is local.
/// ///
/// If the given workspace has a local project, then it will be passed /// If the given workspace has a local project, then it will be passed
@ -1968,46 +1976,12 @@ impl Workspace {
None None
} }
fn render_connection_status(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> { pub fn is_following(&self, peer_id: PeerId) -> bool {
let theme = &cx.global::<Settings>().theme; self.follower_states_by_leader.contains_key(&peer_id)
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,
}
} }
fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox { fn render_titlebar(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
let project = &self.project.read(cx); let project = &self.project.read(cx);
let replica_id = project.replica_id();
let mut worktree_root_names = String::new(); let mut worktree_root_names = String::new();
for (i, name) in project.worktree_root_names(cx).enumerate() { for (i, name) in project.worktree_root_names(cx).enumerate() {
if i > 0 { if i > 0 {
@ -2029,7 +2003,7 @@ impl Workspace {
enum TitleBar {} enum TitleBar {}
ConstrainedBox::new( ConstrainedBox::new(
MouseEventHandler::<TitleBar>::new(0, cx, |_, cx| { MouseEventHandler::<TitleBar>::new(0, cx, |_, _| {
Container::new( Container::new(
Stack::new() Stack::new()
.with_child( .with_child(
@ -2038,21 +2012,10 @@ impl Workspace {
.left() .left()
.boxed(), .boxed(),
) )
.with_child( .with_children(
Align::new( self.titlebar_item
Flex::row() .as_ref()
.with_children(self.render_collaborators(theme, cx)) .map(|item| ChildView::new(item).aligned().right().boxed()),
.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(),
) )
.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> { fn render_disconnected_overlay(&self, cx: &mut RenderContext<Workspace>) -> Option<ElementBox> {
if self.project.read(cx).is_read_only() { if self.project.read(cx).is_read_only() {
enum DisconnectedOverlay {} 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 { impl std::fmt::Debug for OpenPaths {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenPaths") f.debug_struct("OpenPaths")

View file

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

View file

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