Introduce BrancesButton in title bar

Petros Amoiridis and Mikayla Maki created

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/collab_ui/src/branches_button.rs | 165 +++++++++++++++++++++++++++
crates/collab_ui/src/collab_ui.rs       |   2 
crates/project/src/worktree.rs          |   8 +
3 files changed, 175 insertions(+)

Detailed changes

crates/collab_ui/src/branches_button.rs 🔗

@@ -0,0 +1,165 @@
+use context_menu::{ContextMenu, ContextMenuItem};
+use gpui::{
+    elements::*,
+    platform::{CursorStyle, MouseButton},
+    AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use settings::Settings;
+use workspace::Workspace;
+
+pub struct BranchesButton {
+    workspace: WeakViewHandle<Workspace>,
+    popup_menu: ViewHandle<ContextMenu>,
+}
+
+impl Entity for BranchesButton {
+    type Event = ();
+}
+
+impl View for BranchesButton {
+    fn ui_name() -> &'static str {
+        "BranchesButton"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+        let Some(workspace) = self.workspace.upgrade(cx) else {
+            return Empty::new().into_any();
+        };
+
+        let project = workspace.read(cx).project().read(cx);
+        let only_one_worktree = project.visible_worktrees(cx).count() == 1;
+        let branches_count: usize = project
+            .visible_worktrees(cx)
+            .map(|worktree_handle| worktree_handle.read(cx).snapshot().git_entries().count())
+            .sum();
+        let branch_caption: String = if only_one_worktree {
+            project
+                .visible_worktrees(cx)
+                .next()
+                .unwrap()
+                .read(cx)
+                .snapshot()
+                .root_git_entry()
+                .and_then(|entry| entry.branch())
+                .map(|branch| branch.to_string())
+                .unwrap_or_else(|| "".to_owned())
+        } else {
+            branches_count.to_string()
+        };
+        let is_popup_menu_visible = self.popup_menu.read(cx).visible();
+
+        let theme = cx.global::<Settings>().theme.clone();
+
+        Stack::new()
+            .with_child(
+                MouseEventHandler::<Self, _>::new(0, cx, {
+                    let theme = theme.clone();
+                    move |state, _cx| {
+                        let style = theme
+                            .workspace
+                            .titlebar
+                            .toggle_contacts_button
+                            .style_for(state, is_popup_menu_visible);
+
+                        Flex::row()
+                            .with_child(
+                                Svg::new("icons/version_control_branch_12.svg")
+                                    .with_color(style.color)
+                                    .constrained()
+                                    .with_width(style.icon_width)
+                                    .aligned()
+                                    // .constrained()
+                                    // .with_width(style.button_width)
+                                    // .with_height(style.button_width)
+                                    // .contained()
+                                    // .with_style(style.container)
+                                    .into_any_named("version-control-branch-icon"),
+                            )
+                            .with_child(
+                                Label::new(branch_caption, theme.workspace.titlebar.title.clone())
+                                    .contained()
+                                    .with_style(style.container)
+                                    .aligned(),
+                            )
+                            .constrained()
+                            .with_height(style.button_width)
+                            .contained()
+                            .with_style(style.container)
+                    }
+                })
+                .with_cursor_style(CursorStyle::PointingHand)
+                .on_click(MouseButton::Left, move |_, this, cx| {
+                    this.deploy_branches_menu(cx);
+                })
+                .with_tooltip::<Self>(
+                    0,
+                    "Branches".into(),
+                    None,
+                    theme.tooltip.clone(),
+                    cx,
+                ),
+            )
+            .with_child(
+                ChildView::new(&self.popup_menu, cx)
+                    .aligned()
+                    .bottom()
+                    .left(),
+            )
+            .into_any_named("branches-button")
+    }
+}
+
+impl BranchesButton {
+    pub fn new(workspace: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
+        cx.observe(&workspace, |_, _, cx| cx.notify()).detach();
+        Self {
+            workspace: workspace.downgrade(),
+            popup_menu: cx.add_view(|cx| {
+                let mut menu = ContextMenu::new(cx);
+                menu.set_position_mode(OverlayPositionMode::Local);
+                menu
+            }),
+        }
+    }
+
+    pub fn deploy_branches_menu(&mut self, cx: &mut ViewContext<Self>) {
+        let mut menu_options = vec![];
+
+        if let Some(workspace) = self.workspace.upgrade(cx) {
+            let project = workspace.read(cx).project().read(cx);
+
+            let worktrees_with_branches = project
+                .visible_worktrees(cx)
+                .map(|worktree_handle| {
+                    worktree_handle
+                        .read(cx)
+                        .snapshot()
+                        .git_entries()
+                        .filter_map(|entry| {
+                            entry.branch().map(|branch| {
+                                let repo_name = entry.work_directory();
+                                if let Some(name) = repo_name.file_name() {
+                                    (name.to_string_lossy().to_string(), branch)
+                                } else {
+                                    ("WORKTREE ROOT".into(), branch)
+                                }
+                            })
+                        })
+                        .collect::<Vec<_>>()
+                })
+                .flatten();
+
+            let context_menu_items = worktrees_with_branches.map(|(repo_name, branch_name)| {
+                let caption = format!("{} / {}", repo_name, branch_name);
+                ContextMenuItem::handler(caption.to_owned(), move |_| {
+                    println!("{}", caption);
+                })
+            });
+            menu_options.extend(context_menu_items);
+        }
+
+        self.popup_menu.update(cx, |menu, cx| {
+            menu.show(Default::default(), AnchorCorner::TopLeft, menu_options, cx);
+        });
+    }
+}

crates/collab_ui/src/collab_ui.rs 🔗

@@ -1,3 +1,4 @@
+mod branches_button;
 mod collab_titlebar_item;
 mod contact_finder;
 mod contact_list;
@@ -9,6 +10,7 @@ mod notifications;
 mod project_shared_notification;
 mod sharing_status_indicator;
 
+pub use branches_button::BranchesButton;
 use call::ActiveCall;
 pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
 use gpui::{actions, AppContext, Task};

crates/project/src/worktree.rs 🔗

@@ -137,6 +137,10 @@ impl RepositoryEntry {
     pub fn branch(&self) -> Option<Arc<str>> {
         self.branch.clone()
     }
+
+    pub fn work_directory(&self) -> Arc<Path> {
+        self.work_directory.0.clone()
+    }
 }
 
 /// This path corresponds to the 'content path' (the folder that contains the .git)
@@ -1459,6 +1463,10 @@ impl Snapshot {
             .map(|entry| entry.to_owned())
     }
 
+    pub fn git_entries(&self) -> impl Iterator<Item = &RepositoryEntry> {
+        self.repository_entries.values()
+    }
+
     pub fn scan_id(&self) -> usize {
         self.scan_id
     }