git_ui: Design Polish (#26361)

Polish PR

- [ ] Horizontal scrollbar for git panel
- [ ] Allow shift clicking a checkbox in any section to stage the whole
section
- [ ] Clean up design of no changes/pending push state in panel
- [x] Ensure checkbox placeholder dot is centered in the checkbox
- [x] Improve spacing between elements in git panel entries
- [x] Update git branch icon to match branch selector text when disabled
- [x] Truncate last commit message less aggressively in panel
- [x] Clean up new panel header design
- [x] Remove `_background` version control keys (backgrounds are derived
from the foreground colors)

### Previous message truncation:

Before:

![CleanShot 2025-03-10 at 11 54
32@2x](https://github.com/user-attachments/assets/46b18f66-bb5c-435e-a0da-6cc931bd8a15)

After:

![CleanShot 2025-03-10 at 11 55
24@2x](https://github.com/user-attachments/assets/fcf688c7-b949-41a2-a7b8-1a198eb7fa4a)

### Make branch icon match when menu is disabled

Before:

![CleanShot 2025-03-10 at 12 02
14@2x](https://github.com/user-attachments/assets/1990f4b3-c2f0-4e02-89ad-211aaebb3821)

After:

![CleanShot 2025-03-10 at 12 02
53@2x](https://github.com/user-attachments/assets/9b1caf65-c48f-44c9-924b-484892fb543f)

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nate Butler 2025-03-10 16:19:02 -04:00 committed by GitHub
parent 63091459d8
commit 976fc3ee97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 180 additions and 156 deletions

View file

@ -41,7 +41,8 @@ use language_model::{
use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use multi_buffer::ExcerptInfo;
use panel::{
panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, PanelHeader,
panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
panel_icon_button, PanelHeader,
};
use project::{
git::{GitEvent, Repository},
@ -103,13 +104,13 @@ enum TrashCancel {
}
fn git_panel_context_menu(
focus_handle: Option<FocusHandle>,
focus_handle: FocusHandle,
window: &mut Window,
cx: &mut App,
) -> Entity<ContextMenu> {
ContextMenu::build(window, cx, |context_menu, _, _| {
context_menu
.when_some(focus_handle, |el, focus_handle| el.context(focus_handle))
.context(focus_handle)
.action("Stage All", StageAll.boxed_clone())
.action("Unstage All", UnstageAll.boxed_clone())
.separator()
@ -2309,6 +2310,18 @@ impl GitPanel {
self.has_staged_changes()
}
fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
let focus_handle = self.focus_handle.clone();
PopoverMenu::new(id.into())
.trigger(
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
.anchor(Corner::TopRight)
}
pub(crate) fn render_generate_commit_message_button(
&self,
cx: &Context<Self>,
@ -2458,9 +2471,17 @@ impl GitPanel {
tooltip = "git add --all ."
}
let change_string = match self.entry_count {
0 => "No Changes".to_string(),
1 => "1 Change".to_string(),
_ => format!("{} Changes", self.entry_count),
};
self.panel_header_container(window, cx)
.px_2()
.child(
Button::new("diff", "Open Diff")
panel_button(change_string)
.color(Color::Muted)
.tooltip(Tooltip::for_action_title_in(
"Open diff",
&Diff,
@ -2473,8 +2494,9 @@ impl GitPanel {
}),
)
.child(div().flex_grow()) // spacer
.child(self.render_overflow_menu("overflow_menu"))
.child(
Button::new("stage-unstage-all", text)
panel_filled_button(text)
.tooltip(Tooltip::for_action_title_in(
tooltip,
action.as_ref(),
@ -2631,15 +2653,14 @@ impl GitPanel {
.items_center()
.py_2()
.px(px(8.))
// .bg(cx.theme().colors().background)
// .border_t_1()
.border_color(cx.theme().colors().border)
.gap_1p5()
.child(
div()
.flex_grow()
.overflow_hidden()
.max_w(relative(0.6))
.items_center()
.max_w(relative(0.85))
.h_full()
.child(
Label::new(commit.subject.clone())
@ -2946,7 +2967,7 @@ impl GitPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
let context_menu = git_panel_context_menu(Some(self.focus_handle.clone()), window, cx);
let context_menu = git_panel_context_menu(self.focus_handle.clone(), window, cx);
self.set_context_menu(context_menu, position, window, cx);
}
@ -2993,6 +3014,9 @@ impl GitPanel {
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
let status = entry.status;
let modifiers = self.current_modifiers;
let shift_held = modifiers.shift;
let has_conflict = status.is_conflicted();
let is_modified = status.is_modified();
let is_deleted = status.is_deleted();
@ -3078,7 +3102,7 @@ impl GitPanel {
.px(rems(0.75)) // ~12px
.overflow_hidden()
.flex_none()
.gap(DynamicSpacing::Base04.rems(cx))
.gap_1p5()
.bg(base_bg)
.hover(|this| this.bg(hover_bg))
.active(|this| this.bg(active_bg))
@ -3123,6 +3147,7 @@ impl GitPanel {
.flex_none()
.occlude()
.cursor_pointer()
.ml_neg_0p5()
.child(
Checkbox::new(checkbox_id, is_staged)
.disabled(!has_write_access)
@ -3144,17 +3169,35 @@ impl GitPanel {
})
})
.tooltip(move |window, cx| {
let tooltip_name = if entry_staging.is_fully_staged() {
"Unstage"
let is_staged = entry_staging.is_fully_staged();
let action = if is_staged { "Unstage" } else { "Stage" };
let tooltip_name = if shift_held {
format!("{} section", action)
} else {
"Stage"
action.to_string()
};
Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
let meta = if shift_held {
format!(
"Release shift to {} single entry",
action.to_lowercase()
)
} else {
format!("Shift click to {} section", action.to_lowercase())
};
Tooltip::with_meta(
tooltip_name,
Some(&ToggleStaged),
meta,
window,
cx,
)
}),
),
)
.child(git_status_icon(status, cx))
.child(git_status_icon(status))
.child(
h_flex()
.items_center()
@ -3456,27 +3499,11 @@ impl PanelRepoFooter {
git_panel: None,
}
}
fn render_overflow_menu(&self, id: impl Into<ElementId>, cx: &App) -> impl IntoElement {
let focus_handle = self
.git_panel
.as_ref()
.map(|git_panel| git_panel.focus_handle(cx));
PopoverMenu::new(id.into())
.trigger(
IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),
)
.menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
.anchor(Corner::TopRight)
}
}
impl RenderOnce for PanelRepoFooter {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let active_repo = self.active_repository.clone();
let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
let repo_selector_trigger = Button::new("repo-selector", active_repo)
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
@ -3565,7 +3592,11 @@ impl RenderOnce for PanelRepoFooter {
div().child(
Icon::new(IconName::GitBranchSmall)
.size(IconSize::Small)
.color(Color::Muted),
.color(if single_repo {
Color::Disabled
} else {
Color::Muted
}),
),
)
.child(repo_selector)
@ -3584,7 +3615,6 @@ impl RenderOnce for PanelRepoFooter {
.gap_1()
.flex_shrink_0()
.children(spinner)
.child(self.render_overflow_menu(overflow_menu_id, cx))
.when_some(branch, |this, branch| {
let mut focus_handle = None;
if let Some(git_panel) = self.git_panel.as_ref() {

View file

@ -1,13 +1,13 @@
use ::settings::Settings;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::FileStatus,
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
};
use git_panel_settings::GitPanelSettings;
use gpui::{App, Entity, FocusHandle};
use project::Project;
use project_diff::ProjectDiff;
use ui::{ActiveTheme, Color, Icon, IconName, IntoElement, SharedString};
use ui::prelude::*;
use workspace::Workspace;
mod askpass_modal;
@ -86,30 +86,8 @@ pub fn init(cx: &mut App) {
.detach();
}
// TODO: Add updated status colors to theme
pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
Icon::new(icon_name).color(Color::Custom(color))
pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
GitStatusIcon::new(status)
}
fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
@ -465,3 +443,79 @@ mod remote_button {
}
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct GitStatusIcon {
status: FileStatus,
}
impl GitStatusIcon {
pub fn new(status: FileStatus) -> Self {
Self { status }
}
}
impl RenderOnce for GitStatusIcon {
fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
let status = self.status;
let (icon_name, color) = if status.is_conflicted() {
(
IconName::Warning,
cx.theme().colors().version_control_conflict,
)
} else if status.is_deleted() {
(
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
} else if status.is_modified() {
(
IconName::SquareDot,
cx.theme().colors().version_control_modified,
)
} else {
(
IconName::SquarePlus,
cx.theme().colors().version_control_added,
)
};
Icon::new(icon_name).color(Color::Custom(color))
}
}
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for GitStatusIcon {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
fn tracked_file_status(code: StatusCode) -> FileStatus {
FileStatus::Tracked(git::status::TrackedStatus {
index_status: code,
worktree_status: code,
})
}
let modified = tracked_file_status(StatusCode::Modified);
let added = tracked_file_status(StatusCode::Added);
let deleted = tracked_file_status(StatusCode::Deleted);
let conflict = UnmergedStatus {
first_head: UnmergedStatusCode::Updated,
second_head: UnmergedStatusCode::Updated,
}
.into();
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
single_example("Added", GitStatusIcon::new(added).into_any_element()),
single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
single_example(
"Conflicted",
GitStatusIcon::new(conflict).into_any_element(),
),
])])
.into_any_element()
}
}