Allow joining worktrees by clicking them in the people panel

Max Brunsfeld , Antonio Scandurra , and Nathan Sobo created

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

server/src/rpc/store.rs      |  1 
zed/assets/themes/_base.toml | 14 +++++--
zed/src/menus.rs             |  5 --
zed/src/people_panel.rs      | 67 +++++++++++++++++++++++--------------
zed/src/theme.rs             |  9 ++--
zed/src/user.rs              | 10 +++--
zed/src/workspace.rs         | 10 +++--
zrpc/proto/zed.proto         |  7 ++-
8 files changed, 74 insertions(+), 49 deletions(-)

Detailed changes

server/src/rpc/store.rs 🔗

@@ -202,6 +202,7 @@ impl Store {
                             worktrees: Vec::new(),
                         });
                 host.worktrees.push(proto::WorktreeMetadata {
+                    id: *worktree_id,
                     root_name: worktree.root_name.clone(),
                     is_shared: worktree.share().is_ok(),
                     guests: guests.into_iter().collect(),

zed/assets/themes/_base.toml 🔗

@@ -125,14 +125,20 @@ color = "$text.1.color"
 
 [people_panel]
 extends = "$panel"
+host_avatar = { corner_radius = 10, width = 20 }
 host_username = { extends = "$text.0", padding.left = 5 }
-worktree_host_avatar = { corner_radius = 10, width = 20 }
-worktree_guest_avatar = { corner_radius = 8, width = 16 }
+guest_avatar = { corner_radius = 8, width = 16 }
 
-[people_panel.worktree_name]
-extends = "$text.0"
+[people_panel.worktree]
+extends = "$text.1"
 padding = { left = 5 }
 
+[people_panel.hovered_worktree]
+extends = "$text.0"
+padding = { left = 8 }
+# extends = "$people_panel.worktree"
+# color = "$text.0.color"
+
 [people_panel.tree_branch]
 width = 1
 color = "$surface.2"

zed/src/menus.rs 🔗

@@ -26,11 +26,6 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
                     keystroke: None,
                     action: Box::new(workspace::ShareWorktree),
                 },
-                MenuItem::Action {
-                    name: "Join",
-                    keystroke: None,
-                    action: Box::new(workspace::JoinWorktree(state.clone())),
-                },
                 MenuItem::Action {
                     name: "Quit",
                     keystroke: Some("cmd-q"),

zed/src/people_panel.rs 🔗

@@ -4,13 +4,17 @@ use crate::{
     Settings,
 };
 use gpui::{
+    action,
     elements::*,
     geometry::{rect::RectF, vector::vec2f},
-    Element, ElementBox, Entity, FontCache, ModelHandle, RenderContext, Subscription, View,
+    platform::CursorStyle,
+    Element, ElementBox, Entity, LayoutContext, ModelHandle, RenderContext, Subscription, View,
     ViewContext,
 };
 use postage::watch;
 
+action!(JoinWorktree, u64);
+
 pub struct PeoplePanel {
     collaborators: ListState,
     user_store: ModelHandle<UserStore>,
@@ -33,11 +37,8 @@ impl PeoplePanel {
                     let user_store = user_store.clone();
                     let settings = settings.clone();
                     move |ix, cx| {
-                        Self::render_collaborator(
-                            &user_store.read(cx).collaborators()[ix],
-                            &settings.borrow().theme,
-                            cx.font_cache(),
-                        )
+                        let collaborators = user_store.read(cx).collaborators().clone();
+                        Self::render_collaborator(&collaborators[ix], &settings.borrow().theme, cx)
                     }
                 },
             ),
@@ -56,23 +57,26 @@ impl PeoplePanel {
     fn render_collaborator(
         collaborator: &Collaborator,
         theme: &Theme,
-        font_cache: &FontCache,
+        cx: &mut LayoutContext,
     ) -> ElementBox {
         let theme = &theme.people_panel;
         let worktree_count = collaborator.worktrees.len();
-        let line_height = theme.worktree_name.text.line_height(font_cache);
-        let cap_height = theme.worktree_name.text.cap_height(font_cache);
-        let baseline_offset = theme.worktree_name.text.baseline_offset(font_cache);
+        let font_cache = cx.font_cache();
+        let line_height = theme.worktree.text.line_height(font_cache);
+        let cap_height = theme.worktree.text.cap_height(font_cache);
+        let baseline_offset = theme.worktree.text.baseline_offset(font_cache);
         let tree_branch = theme.tree_branch;
 
         Flex::column()
             .with_child(
                 Flex::row()
-                    .with_children(collaborator.user.avatar.clone().map(|avatar| {
-                        Image::new(avatar)
-                            .with_style(theme.worktree_host_avatar)
-                            .boxed()
-                    }))
+                    .with_children(
+                        collaborator
+                            .user
+                            .avatar
+                            .clone()
+                            .map(|avatar| Image::new(avatar).with_style(theme.host_avatar).boxed()),
+                    )
                     .with_child(
                         Container::new(
                             Label::new(
@@ -92,6 +96,7 @@ impl PeoplePanel {
                     .iter()
                     .enumerate()
                     .map(|(ix, worktree)| {
+                        let worktree_id = worktree.id;
                         Flex::row()
                             .with_child(
                                 ConstrainedBox::new(
@@ -136,21 +141,33 @@ impl PeoplePanel {
                                 .boxed(),
                             )
                             .with_child(
-                                Container::new(
-                                    Label::new(
-                                        worktree.root_name.clone(),
-                                        theme.worktree_name.text.clone(),
-                                    )
-                                    .boxed(),
+                                MouseEventHandler::new::<PeoplePanel, _, _, _>(
+                                    worktree_id as usize,
+                                    cx,
+                                    |mouse_state, _| {
+                                        let style = if mouse_state.hovered {
+                                            &theme.hovered_worktree
+                                        } else {
+                                            &theme.worktree
+                                        };
+                                        Container::new(
+                                            Label::new(
+                                                worktree.root_name.clone(),
+                                                style.text.clone(),
+                                            )
+                                            .boxed(),
+                                        )
+                                        .with_style(style.container)
+                                        .boxed()
+                                    },
                                 )
-                                .with_style(theme.worktree_name.container)
+                                .with_cursor_style(CursorStyle::PointingHand)
+                                .on_click(move |cx| cx.dispatch_action(JoinWorktree(worktree_id)))
                                 .boxed(),
                             )
                             .with_children(worktree.guests.iter().filter_map(|participant| {
                                 participant.avatar.clone().map(|avatar| {
-                                    Image::new(avatar)
-                                        .with_style(theme.worktree_guest_avatar)
-                                        .boxed()
+                                    Image::new(avatar).with_style(theme.guest_avatar).boxed()
                                 })
                             }))
                             .boxed()

zed/src/theme.rs 🔗

@@ -109,10 +109,11 @@ pub struct ChatPanel {
 pub struct PeoplePanel {
     #[serde(flatten)]
     pub container: ContainerStyle,
+    pub host_avatar: ImageStyle,
     pub host_username: ContainedText,
-    pub worktree_name: ContainedText,
-    pub worktree_host_avatar: ImageStyle,
-    pub worktree_guest_avatar: ImageStyle,
+    pub worktree: ContainedText,
+    pub hovered_worktree: ContainedText,
+    pub guest_avatar: ImageStyle,
     pub tree_branch: TreeBranch,
 }
 
@@ -161,7 +162,7 @@ pub struct Selector {
     pub active_item: ContainedLabel,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 pub struct ContainedText {
     #[serde(flatten)]
     pub container: ContainerStyle,

zed/src/user.rs 🔗

@@ -28,6 +28,7 @@ pub struct Collaborator {
 
 #[derive(Debug)]
 pub struct WorktreeMetadata {
+    pub id: u64,
     pub root_name: String,
     pub is_shared: bool,
     pub guests: Vec<Arc<User>>,
@@ -36,7 +37,7 @@ pub struct WorktreeMetadata {
 pub struct UserStore {
     users: HashMap<u64, Arc<User>>,
     current_user: watch::Receiver<Option<Arc<User>>>,
-    collaborators: Vec<Collaborator>,
+    collaborators: Arc<[Collaborator]>,
     rpc: Arc<Client>,
     http: Arc<dyn HttpClient>,
     _maintain_collaborators: Task<()>,
@@ -64,7 +65,7 @@ impl UserStore {
         Self {
             users: Default::default(),
             current_user: current_user_rx,
-            collaborators: Default::default(),
+            collaborators: Arc::from([]),
             rpc: rpc.clone(),
             http,
             _maintain_collaborators: cx.spawn_weak(|this, mut cx| async move {
@@ -127,7 +128,7 @@ impl UserStore {
             }
 
             this.update(&mut cx, |this, cx| {
-                this.collaborators = collaborators;
+                this.collaborators = collaborators.into();
                 cx.notify();
             });
 
@@ -135,7 +136,7 @@ impl UserStore {
         })
     }
 
-    pub fn collaborators(&self) -> &[Collaborator] {
+    pub fn collaborators(&self) -> &Arc<[Collaborator]> {
         &self.collaborators
     }
 
@@ -235,6 +236,7 @@ impl Collaborator {
                 );
             }
             worktrees.push(WorktreeMetadata {
+                id: worktree.id,
                 root_name: worktree.root_name,
                 is_shared: worktree.is_shared,
                 guests,

zed/src/workspace.rs 🔗

@@ -7,7 +7,7 @@ use crate::{
     editor::Buffer,
     fs::Fs,
     language::LanguageRegistry,
-    people_panel::PeoplePanel,
+    people_panel::{JoinWorktree, PeoplePanel},
     project_browser::ProjectBrowser,
     rpc,
     settings::Settings,
@@ -44,7 +44,6 @@ action!(Open, Arc<AppState>);
 action!(OpenPaths, OpenParams);
 action!(OpenNew, Arc<AppState>);
 action!(ShareWorktree);
-action!(JoinWorktree, Arc<AppState>);
 action!(Save);
 action!(DebugElements);
 
@@ -59,6 +58,7 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Workspace::open_new_file);
     cx.add_action(Workspace::share_worktree);
     cx.add_action(Workspace::toggle_sidebar_item);
+    cx.add_action(Workspace::join_worktree);
     cx.add_bindings(vec![
         Binding::new("cmd-s", Save, None),
         Binding::new("cmd-alt-i", DebugElements, None),
@@ -839,14 +839,16 @@ impl Workspace {
         .detach();
     }
 
-    fn join_worktree(&mut self, id: u64, cx: &mut ViewContext<Self>) {
+    fn join_worktree(&mut self, action: &JoinWorktree, cx: &mut ViewContext<Self>) {
         let rpc = self.rpc.clone();
         let languages = self.languages.clone();
+        let worktree_id = action.0;
 
         cx.spawn(|this, mut cx| {
             async move {
                 rpc.authenticate_and_connect(&cx).await?;
-                let worktree = Worktree::open_remote(rpc.clone(), id, languages, &mut cx).await?;
+                let worktree =
+                    Worktree::open_remote(rpc.clone(), worktree_id, languages, &mut cx).await?;
                 this.update(&mut cx, |workspace, cx| {
                     cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
                     workspace.worktrees.insert(worktree);

zrpc/proto/zed.proto 🔗

@@ -343,7 +343,8 @@ message Collaborator {
 }
 
 message WorktreeMetadata {
-    string root_name = 1;
-    bool is_shared = 2;
-    repeated uint64 guests = 3;
+    uint64 id = 1;
+    string root_name = 2;
+    bool is_shared = 3;
+    repeated uint64 guests = 4;
 }