diff --git a/assets/icons/box_open.svg b/assets/icons/box_open.svg new file mode 100644 index 0000000000000000000000000000000000000000..5e30fc40c3446485412e2a2607b0d07dc2f68b4b --- /dev/null +++ b/assets/icons/box_open.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index cfb7b6bd2a2fb6c57f17244e0e57a4a637866418..d9adc079bf470e121c99b2d13c2ef25ffea7a68f 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -559,8 +559,7 @@ impl PickerDelegate for BranchListDelegate { match self.state { PickerState::List | PickerState::NewRemote | PickerState::NewBranch => { match self.branch_filter { - BranchFilter::All => "Select branch or remote…", - BranchFilter::Remote => "Select remote…", + BranchFilter::All | BranchFilter::Remote => "Select branch…", } } PickerState::CreateRemote(_) => "Enter a name for this remote…", @@ -884,13 +883,13 @@ impl PickerDelegate for BranchListDelegate { let entry_icon = match entry { Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. } => { - Icon::new(IconName::Plus).color(Color::Muted) + IconName::Plus } Entry::Branch { branch, .. } => { if branch.is_remote() { - Icon::new(IconName::Screen).color(Color::Muted) + IconName::Screen } else { - Icon::new(IconName::GitBranchAlt).color(Color::Muted) + IconName::GitBranchAlt } } }; @@ -922,8 +921,11 @@ impl PickerDelegate for BranchListDelegate { Entry::NewUrl { .. } | Entry::NewBranch { .. } | Entry::NewRemoteName { .. } ); - let deleted_branch_icon = |entry_ix: usize, is_head_branch: bool| { + let is_head_branch = entry.as_branch().is_some_and(|branch| branch.is_head); + + let deleted_branch_icon = |entry_ix: usize| { IconButton::new(("delete", entry_ix), IconName::Trash) + .icon_size(IconSize::Small) .tooltip(move |_, cx| { Tooltip::for_action_in( "Delete Branch", @@ -932,7 +934,6 @@ impl PickerDelegate for BranchListDelegate { cx, ) }) - .disabled(is_head_branch) .on_click(cx.listener(move |this, _, window, cx| { this.delegate.delete_at(entry_ix, window, cx); })) @@ -943,6 +944,7 @@ impl PickerDelegate for BranchListDelegate { let focus_handle = self.focus_handle.clone(); IconButton::new("create_from_default", IconName::GitBranchPlus) + .icon_size(IconSize::Small) .tooltip(move |_, cx| { Tooltip::for_action_in( tooltip_label.clone(), @@ -965,105 +967,132 @@ impl PickerDelegate for BranchListDelegate { .child( h_flex() .w_full() - .gap_3() + .gap_2p5() .flex_grow() - .child(entry_icon) + .child( + Icon::new(entry_icon) + .color(Color::Muted) + .size(IconSize::Small), + ) .child( v_flex() .id("info_container") .w_full() .child(entry_title) - .child( - h_flex() - .w_full() - .justify_between() - .gap_1p5() - .when(self.style == BranchListStyle::Modal, |el| { - el.child(div().max_w_96().child({ - let message = match entry { - Entry::NewUrl { url } => { - format!("Based off {url}") - } - Entry::NewRemoteName { url, .. } => { - format!("Based off {url}") - } - Entry::NewBranch { .. } => { - if let Some(current_branch) = - self.repo.as_ref().and_then(|repo| { - repo.read(cx) - .branch - .as_ref() - .map(|b| b.name()) - }) - { - format!("Based off {}", current_branch) - } else { - "Based off the current branch" - .to_string() - } - } - Entry::Branch { .. } => { - let show_author_name = - ProjectSettings::get_global(cx) - .git - .branch_picker - .show_author_name; - - subject.map_or( - "No commits found".into(), - |subject| { - if show_author_name - && let Some(author) = - author_name - { - format!( - "{} • {}", - author, subject - ) - } else { - subject.to_string() - } - }, - ) - } - }; - - Label::new(message) - .size(LabelSize::Small) - .color(Color::Muted) - .truncate() - })) - }) - .when_some(commit_time, |label, commit_time| { - label.child( - Label::new(commit_time) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }), - ) + .child({ + let message = match entry { + Entry::NewUrl { url } => format!("Based off {url}"), + Entry::NewRemoteName { url, .. } => { + format!("Based off {url}") + } + Entry::NewBranch { .. } => { + if let Some(current_branch) = + self.repo.as_ref().and_then(|repo| { + repo.read(cx).branch.as_ref().map(|b| b.name()) + }) + { + format!("Based off {}", current_branch) + } else { + "Based off the current branch".to_string() + } + } + Entry::Branch { .. } => String::new(), + }; + + if matches!(entry, Entry::Branch { .. }) { + let show_author_name = ProjectSettings::get_global(cx) + .git + .branch_picker + .show_author_name; + let has_author = show_author_name && author_name.is_some(); + let has_commit = commit_time.is_some(); + let author_for_meta = + if show_author_name { author_name } else { None }; + + let dot = || { + Label::new("•") + .alpha(0.5) + .color(Color::Muted) + .size(LabelSize::Small) + }; + + h_flex() + .w_full() + .min_w_0() + .gap_1p5() + .when_some(author_for_meta, |this, author| { + this.child( + Label::new(author) + .color(Color::Muted) + .size(LabelSize::Small), + ) + }) + .when_some(commit_time, |this, time| { + this.when(has_author, |this| this.child(dot())) + .child( + Label::new(time) + .color(Color::Muted) + .size(LabelSize::Small), + ) + }) + .when_some(subject, |this, subj| { + this.when(has_commit, |this| this.child(dot())) + .child( + Label::new(subj.to_string()) + .color(Color::Muted) + .size(LabelSize::Small) + .truncate() + .flex_1(), + ) + }) + .when(!has_commit, |this| { + this.child( + Label::new("No commits found") + .color(Color::Muted) + .size(LabelSize::Small), + ) + }) + .into_any_element() + } else { + Label::new(message) + .size(LabelSize::Small) + .color(Color::Muted) + .truncate() + .into_any_element() + } + }) .when_some( entry.as_branch().map(|b| b.name().to_string()), - |this, branch_name| this.tooltip(Tooltip::text(branch_name)), + |this, branch_name| { + this.map(|this| { + if is_head_branch { + this.tooltip(move |_, cx| { + Tooltip::with_meta( + branch_name.clone(), + None, + "Current Branch", + cx, + ) + }) + } else { + this.tooltip(Tooltip::text(branch_name)) + } + }) + }, ), ), ) - .when( - self.editor_position() == PickerEditorPosition::End && !is_new_items, - |this| { - this.map(|this| { - let is_head_branch = - entry.as_branch().is_some_and(|branch| branch.is_head); - if self.selected_index() == ix { - this.end_slot(deleted_branch_icon(ix, is_head_branch)) - } else { - this.end_hover_slot(deleted_branch_icon(ix, is_head_branch)) - } - }) - }, - ) + .when(!is_new_items && !is_head_branch, |this| { + this.map(|this| { + if self.selected_index() == ix { + this.end_slot(deleted_branch_icon(ix)) + } else { + this.end_hover_slot(deleted_branch_icon(ix)) + } + }) + }) .when_some( - if self.editor_position() == PickerEditorPosition::End && is_new_items { + if is_new_items { create_from_default_button } else { None diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index e736dd806a35703991e1fb51e27e3952e5692d99..8947ed233790ab65557e13518d51bd383fc93c2d 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -468,7 +468,7 @@ impl PickerDelegate for StashListDelegate { ix: usize, selected: bool, _window: &mut Window, - _cx: &mut Context>, + cx: &mut Context>, ) -> Option { let entry_match = &self.matches[ix]; @@ -501,16 +501,46 @@ impl PickerDelegate for StashListDelegate { .size(LabelSize::Small), ); + let focus_handle = self.focus_handle.clone(); + + let drop_button = |entry_ix: usize| { + IconButton::new(("drop-stash", entry_ix), IconName::Trash) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in("Drop Stash", &DropStashItem, &focus_handle, cx) + }) + .on_click(cx.listener(move |this, _, window, cx| { + this.delegate.drop_stash_at(entry_ix, window, cx); + })) + }; + Some( ListItem::new(format!("stash-{ix}")) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) - .child(v_flex().w_full().child(stash_label).child(branch_info)) + .child( + h_flex() + .w_full() + .gap_2p5() + .child( + Icon::new(IconName::BoxOpen) + .size(IconSize::Small) + .color(Color::Muted), + ) + .child(div().w_full().child(stash_label).child(branch_info)), + ) .tooltip(Tooltip::text(format!( "stash@{{{}}}", entry_match.entry.index - ))), + ))) + .map(|this| { + if selected { + this.end_slot(drop_button(ix)) + } else { + this.end_hover_slot(drop_button(ix)) + } + }), ) } diff --git a/crates/git_ui/src/worktree_picker.rs b/crates/git_ui/src/worktree_picker.rs index 3806054038151c3dd80a7bb1cfdedf966d4219b5..2cbba9347f973aa084413b03af7314f30d22fc99 100644 --- a/crates/git_ui/src/worktree_picker.rs +++ b/crates/git_ui/src/worktree_picker.rs @@ -18,7 +18,7 @@ use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier}; use remote_connection::{RemoteConnectionModal, connect}; use settings::Settings; use std::{path::PathBuf, sync::Arc}; -use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; +use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::{ModalView, MultiWorkspace, Workspace, notifications::DetachAndPromptErr}; @@ -769,38 +769,83 @@ impl PickerDelegate for WorktreeListDelegate { ) }; + let focus_handle = self.focus_handle.clone(); + + let delete_button = |entry_ix: usize| { + IconButton::new(("delete-worktree", entry_ix), IconName::Trash) + .icon_size(IconSize::Small) + .tooltip(move |_, cx| { + Tooltip::for_action_in("Delete Worktree", &DeleteWorktree, &focus_handle, cx) + }) + .on_click(cx.listener(move |this, _, window, cx| { + this.delegate.delete_at(entry_ix, window, cx); + })) + }; + + let entry_icon = if entry.is_new { + IconName::Plus + } else { + IconName::GitWorktree + }; + Some( ListItem::new(format!("worktree-menu-{ix}")) .inset(true) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child( - v_flex() + h_flex() .w_full() + .gap_2p5() .child( - h_flex() - .gap_2() - .justify_between() - .overflow_x_hidden() - .child(branch_name) - .when(!entry.is_new, |this| { - this.child( - Label::new(sha) - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx) - .into_element(), - ) - }), - ) - .child( - Label::new(sublabel) - .size(LabelSize::Small) + Icon::new(entry_icon) .color(Color::Muted) - .truncate() - .into_any_element(), - ), - ), + .size(IconSize::Small), + ) + .child(v_flex().w_full().child(branch_name).map(|this| { + if entry.is_new { + this.child( + Label::new(sublabel) + .size(LabelSize::Small) + .color(Color::Muted) + .truncate(), + ) + } else { + this.child( + h_flex() + .w_full() + .min_w_0() + .gap_1p5() + .child( + Label::new(sha) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Label::new("•") + .alpha(0.5) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .child( + Label::new(sublabel) + .truncate() + .color(Color::Muted) + .size(LabelSize::Small) + .flex_1(), + ) + .into_any_element(), + ) + } + })), + ) + .when(!entry.is_new, |this| { + if selected { + this.end_slot(delete_button(ix)) + } else { + this.end_hover_slot(delete_button(ix)) + } + }), ) } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index bcdd156e6ef7ab55770598eda8e4a2e7f0ae530b..625b51f9c1b9f5cbf04f4474b72d08557542352f 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -53,6 +53,7 @@ pub enum IconName { Book, BookCopy, Box, + BoxOpen, CaseSensitive, Chat, Check,