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

Nate Butler and Cole Miller created

Title

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>

Change summary

crates/git_ui/src/project_diff.rs | 291 ++++++++++++++++++++++++++++++++
1 file changed, 288 insertions(+), 3 deletions(-)

Detailed changes

crates/git_ui/src/project_diff.rs 🔗

@@ -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 {