git_ui: Update Project Diff empty state design (#26554)

Title

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
This commit is contained in:
Nate Butler 2025-03-12 12:21:47 -04:00 committed by GitHub
parent 010c5a2c4e
commit 8d259a9dbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,4 +1,7 @@
use crate::git_panel::{GitPanel, GitPanelAddon, GitStatusEntry};
use crate::{
git_panel::{GitPanel, GitPanelAddon, GitStatusEntry},
remote_button::{render_publish_button, render_push_button},
};
use anyhow::Result;
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::HashSet;
@ -9,8 +12,9 @@ use editor::{
};
use futures::StreamExt;
use git::{
repository::Branch, status::FileStatus, Commit, StageAll, StageAndNext, ToggleStaged,
UnstageAll, UnstageAndNext,
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::FileStatus,
Commit, StageAll, StageAndNext, ToggleStaged, UnstageAll, UnstageAndNext,
};
use gpui::{
actions, Action, AnyElement, AnyView, App, AppContext as _, AsyncWindowContext, Entity,
@ -1022,6 +1026,287 @@ impl Render for ProjectDiffToolbar {
}
}
#[derive(IntoElement, IntoComponent)]
#[component(scope = "Version Control")]
pub struct ProjectDiffEmptyState {
pub no_repo: bool,
pub can_push_and_pull: bool,
pub focus_handle: Option<FocusHandle>,
pub current_branch: Option<Branch>,
// has_pending_commits: bool,
// ahead_of_remote: bool,
// no_git_repository: bool,
}
impl RenderOnce for ProjectDiffEmptyState {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let status_against_remote = |ahead_by: usize, behind_by: usize| -> bool {
match self.current_branch {
Some(Branch {
upstream:
Some(Upstream {
tracking:
UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead, behind, ..
}),
..
}),
..
}) if (ahead > 0) == (ahead_by > 0) && (behind > 0) == (behind_by > 0) => true,
_ => false,
}
};
let change_count = |current_branch: &Branch| -> (usize, usize) {
match current_branch {
Branch {
upstream:
Some(Upstream {
tracking:
UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead, behind, ..
}),
..
}),
..
} => (*ahead as usize, *behind as usize),
_ => (0, 0),
}
};
let not_ahead_or_behind = status_against_remote(0, 0);
let ahead_of_remote = status_against_remote(1, 0);
let branch_not_on_remote = if let Some(branch) = self.current_branch.as_ref() {
branch.upstream.is_none()
} else {
false
};
let has_branch_container = |branch: &Branch| {
h_flex()
.max_w(px(420.))
.bg(cx.theme().colors().text.opacity(0.05))
.border_1()
.border_color(cx.theme().colors().border)
.rounded_sm()
.gap_8()
.px_6()
.py_4()
.map(|this| {
if ahead_of_remote {
let ahead_count = change_count(branch).0;
let ahead_string = format!("{} Commits Ahead", ahead_count);
this.child(
v_flex()
.child(Headline::new(ahead_string).size(HeadlineSize::Small))
.child(
Label::new(format!("Push your changes to {}", branch.name))
.color(Color::Muted),
),
)
.child(div().child(render_push_button(
self.focus_handle,
"push".into(),
ahead_count as u32,
)))
} else if branch_not_on_remote {
this.child(
v_flex()
.child(Headline::new("Publish Branch").size(HeadlineSize::Small))
.child(
Label::new(format!("Create {} on remote", branch.name))
.color(Color::Muted),
),
)
.child(
div().child(render_publish_button(self.focus_handle, "publish".into())),
)
} else {
this.child(Label::new("Remote status unknown").color(Color::Muted))
}
})
};
v_flex().size_full().items_center().justify_center().child(
v_flex()
.gap_1()
.when(self.no_repo, |this| {
// TODO: add git init
this.text_center()
.child(Label::new("No Repository").color(Color::Muted))
})
.map(|this| {
if not_ahead_or_behind && self.current_branch.is_some() {
this.text_center()
.child(Label::new("No Changes").color(Color::Muted))
} else {
this.when_some(self.current_branch.as_ref(), |this, branch| {
this.child(has_branch_container(&branch))
})
}
}),
)
}
}
// .when(self.can_push_and_pull, |this| {
// let remote_button = crate::render_remote_button(
// "project-diff-remote-button",
// &branch,
// self.focus_handle.clone(),
// false,
// );
// match remote_button {
// Some(button) => {
// this.child(h_flex().justify_around().child(button))
// }
// None => this.child(
// h_flex()
// .justify_around()
// .child(Label::new("Remote up to date")),
// ),
// }
// }),
//
// // .map(|this| {
// this.child(h_flex().justify_around().mt_1().child(
// Button::new("project-diff-close-button", "Close").when_some(
// self.focus_handle.clone(),
// |this, focus_handle| {
// this.key_binding(KeyBinding::for_action_in(
// &CloseActiveItem::default(),
// &focus_handle,
// window,
// cx,
// ))
// .on_click(move |_, window, cx| {
// window.focus(&focus_handle);
// window
// .dispatch_action(Box::new(CloseActiveItem::default()), cx);
// })
// },
// ),
// ))
// }),
mod preview {
use git::repository::{
Branch, CommitSummary, Upstream, UpstreamTracking, UpstreamTrackingStatus,
};
use ui::prelude::*;
use super::ProjectDiffEmptyState;
// View this component preview using `workspace: open component-preview`
impl ComponentPreview for ProjectDiffEmptyState {
fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
let unknown_upstream: Option<UpstreamTracking> = None;
let ahead_of_upstream: Option<UpstreamTracking> = Some(
UpstreamTrackingStatus {
ahead: 2,
behind: 0,
}
.into(),
);
let not_ahead_or_behind_upstream: Option<UpstreamTracking> = Some(
UpstreamTrackingStatus {
ahead: 0,
behind: 0,
}
.into(),
);
fn branch(upstream: Option<UpstreamTracking>) -> Branch {
Branch {
is_head: true,
name: "some-branch".into(),
upstream: upstream.map(|tracking| Upstream {
ref_name: "origin/some-branch".into(),
tracking,
}),
most_recent_commit: Some(CommitSummary {
sha: "abc123".into(),
subject: "Modify stuff".into(),
commit_timestamp: 1710932954,
has_parent: true,
}),
}
}
let no_repo_state = ProjectDiffEmptyState {
no_repo: true,
can_push_and_pull: false,
focus_handle: None,
current_branch: None,
};
let no_changes_state = ProjectDiffEmptyState {
no_repo: false,
can_push_and_pull: true,
focus_handle: None,
current_branch: Some(branch(not_ahead_or_behind_upstream)),
};
let ahead_of_upstream_state = ProjectDiffEmptyState {
no_repo: false,
can_push_and_pull: true,
focus_handle: None,
current_branch: Some(branch(ahead_of_upstream)),
};
let unknown_upstream_state = ProjectDiffEmptyState {
no_repo: false,
can_push_and_pull: true,
focus_handle: None,
current_branch: Some(branch(unknown_upstream)),
};
let (width, height) = (px(480.), px(320.));
v_flex()
.gap_6()
.children(vec![example_group(vec![
single_example(
"No Repo",
div()
.w(width)
.h(height)
.child(no_repo_state)
.into_any_element(),
),
single_example(
"No Changes",
div()
.w(width)
.h(height)
.child(no_changes_state)
.into_any_element(),
),
single_example(
"Unknown Upstream",
div()
.w(width)
.h(height)
.child(unknown_upstream_state)
.into_any_element(),
),
single_example(
"Ahead of Remote",
div()
.w(width)
.h(height)
.child(ahead_of_upstream_state)
.into_any_element(),
),
])
.vertical()])
.into_any_element()
}
}
}
#[cfg(not(target_os = "windows"))]
#[cfg(test)]
mod tests {