git_ui: Clean up file history view design (#43941)

Danilo Leal created

Mostly cleaning up the UI code. The UI looks fairly the same just a bit
more polished, with proper colors and spacing. Also added a scrollbar in
there. Next step would be to make it keyboard navigable.

<img width="500" height="1948" alt="Screenshot 2025-12-01 at 5  38@2x"
src="https://github.com/user-attachments/assets/c266b97c-4a79-4a0e-8e78-2f1ed1ba495f"
/>

Release Notes:

- N/A

Change summary

crates/git_ui/src/file_history_view.rs | 213 ++++++++++-----------------
crates/ui/src/components/avatar.rs     |  12 +
2 files changed, 92 insertions(+), 133 deletions(-)

Detailed changes

crates/git_ui/src/file_history_view.rs 🔗

@@ -4,8 +4,7 @@ use git::repository::{FileHistory, FileHistoryEntry, RepoPath};
 use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
 use gpui::{
     AnyElement, AnyEntity, App, Asset, Context, Entity, EventEmitter, FocusHandle, Focusable,
-    IntoElement, ListSizingBehavior, Render, Task, UniformListScrollHandle, WeakEntity, Window,
-    actions, rems, uniform_list,
+    IntoElement, Render, Task, UniformListScrollHandle, WeakEntity, Window, actions, uniform_list,
 };
 use project::{
     Project, ProjectPath,
@@ -14,11 +13,8 @@ use project::{
 use std::any::{Any, TypeId};
 
 use time::OffsetDateTime;
-use ui::{
-    Avatar, Button, ButtonStyle, Color, Icon, IconName, IconSize, Label, LabelCommon as _,
-    LabelSize, SharedString, prelude::*,
-};
-use util::{ResultExt, truncate_and_trailoff};
+use ui::{Avatar, Chip, Divider, ListItem, WithScrollbar, prelude::*};
+use util::ResultExt;
 use workspace::{
     Item, Workspace,
     item::{ItemEvent, SaveOptions},
@@ -195,38 +191,24 @@ impl FileHistoryView {
         task.detach();
     }
 
-    fn list_item_height(&self) -> Rems {
-        rems(2.0)
-    }
-
-    fn fallback_commit_avatar() -> AnyElement {
-        Icon::new(IconName::Person)
-            .color(Color::Muted)
-            .size(IconSize::Small)
-            .into_element()
-            .into_any()
-    }
-
     fn render_commit_avatar(
         &self,
         sha: &SharedString,
         window: &mut Window,
         cx: &mut App,
-    ) -> AnyElement {
+    ) -> impl IntoElement {
         let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
+        let size = rems_from_px(20.);
 
         if let Some(remote) = remote {
             let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());
             if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
-                Avatar::new(url.to_string())
-                    .size(rems(1.25))
-                    .into_element()
-                    .into_any()
+                Avatar::new(url.to_string()).size(size)
             } else {
-                Self::fallback_commit_avatar()
+                Avatar::new("").size(size)
             }
         } else {
-            Self::fallback_commit_avatar()
+            Avatar::new("").size(size)
         }
     }
 
@@ -263,34 +245,52 @@ impl FileHistoryView {
             time_format::TimestampFormat::Relative,
         );
 
-        let selected = self.selected_entry == Some(ix);
         let sha = entry.sha.clone();
         let repo = self.repository.clone();
         let workspace = self.workspace.clone();
         let file_path = self.history.path.clone();
 
-        let base_bg = if selected {
-            cx.theme().status().info.alpha(0.1)
-        } else {
-            cx.theme().colors().editor_background
-        };
-
-        let hover_bg = if selected {
-            cx.theme().status().info.alpha(0.15)
-        } else {
-            cx.theme().colors().element_hover
-        };
-
-        h_flex()
-            .id(("commit", ix))
-            .h(self.list_item_height())
-            .w_full()
-            .items_center()
-            .px(rems(0.75))
-            .gap_2()
-            .bg(base_bg)
-            .hover(|style| style.bg(hover_bg))
-            .cursor_pointer()
+        ListItem::new(("commit", ix))
+            .child(
+                h_flex()
+                    .h_8()
+                    .w_full()
+                    .pl_0p5()
+                    .pr_2p5()
+                    .gap_2()
+                    .child(
+                        div()
+                            .w(rems_from_px(52.))
+                            .flex_none()
+                            .child(Chip::new(pr_number)),
+                    )
+                    .child(self.render_commit_avatar(&entry.sha, window, cx))
+                    .child(
+                        h_flex()
+                            .w_full()
+                            .justify_between()
+                            .child(
+                                h_flex()
+                                    .gap_1()
+                                    .child(
+                                        Label::new(entry.author_name.clone())
+                                            .size(LabelSize::Small)
+                                            .color(Color::Default),
+                                    )
+                                    .child(
+                                        Label::new(&entry.subject)
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted)
+                                            .truncate(),
+                                    ),
+                            )
+                            .child(
+                                Label::new(relative_timestamp)
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            ),
+                    ),
+            )
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.selected_entry = Some(ix);
                 cx.notify();
@@ -308,56 +308,6 @@ impl FileHistoryView {
                     );
                 }
             }))
-            .child(
-                div().flex_none().min_w(rems(4.0)).child(
-                    div()
-                        .px(rems(0.5))
-                        .py(rems(0.25))
-                        .rounded_md()
-                        .bg(cx.theme().colors().element_background)
-                        .border_1()
-                        .border_color(cx.theme().colors().border)
-                        .child(
-                            Label::new(pr_number)
-                                .size(LabelSize::Small)
-                                .color(Color::Muted)
-                                .single_line(),
-                        ),
-                ),
-            )
-            .child(
-                div()
-                    .flex_none()
-                    .w(rems(1.75))
-                    .child(self.render_commit_avatar(&entry.sha, window, cx)),
-            )
-            .child(
-                div().flex_1().overflow_hidden().child(
-                    h_flex()
-                        .gap_3()
-                        .items_center()
-                        .child(
-                            Label::new(entry.author_name.clone())
-                                .size(LabelSize::Small)
-                                .color(Color::Default)
-                                .single_line(),
-                        )
-                        .child(
-                            Label::new(truncate_and_trailoff(&entry.subject, 100))
-                                .size(LabelSize::Small)
-                                .color(Color::Muted)
-                                .single_line(),
-                        ),
-                ),
-            )
-            .child(
-                div().flex_none().child(
-                    Label::new(relative_timestamp)
-                        .size(LabelSize::Small)
-                        .color(Color::Muted)
-                        .single_line(),
-                ),
-            )
             .into_any_element()
     }
 }
@@ -419,31 +369,49 @@ impl Focusable for FileHistoryView {
 }
 
 impl Render for FileHistoryView {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let _file_name = self.history.path.file_name().unwrap_or("File");
         let entry_count = self.history.entries.len();
 
         v_flex()
             .size_full()
+            .bg(cx.theme().colors().editor_background)
             .child(
                 h_flex()
-                    .px(rems(0.75))
-                    .py(rems(0.5))
-                    .border_b_1()
-                    .border_color(cx.theme().colors().border)
-                    .bg(cx.theme().colors().title_bar_background)
-                    .items_center()
+                    .h(rems_from_px(41.))
+                    .pl_3()
+                    .pr_2()
                     .justify_between()
+                    .border_b_1()
+                    .border_color(cx.theme().colors().border_variant)
                     .child(
-                        h_flex().gap_2().items_center().child(
-                            Label::new(format!("History: {}", self.history.path.as_unix_str()))
-                                .size(LabelSize::Default),
-                        ),
+                        Label::new(self.history.path.as_unix_str().to_string())
+                            .color(Color::Muted)
+                            .buffer_font(cx),
                     )
                     .child(
-                        Label::new(format!("{} commits", entry_count))
-                            .size(LabelSize::Small)
-                            .color(Color::Muted),
+                        h_flex()
+                            .gap_1p5()
+                            .child(
+                                Label::new(format!("{} commits", entry_count))
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted)
+                                    .when(self.has_more, |this| this.mr_1()),
+                            )
+                            .when(self.has_more, |this| {
+                                this.child(Divider::vertical()).child(
+                                    Button::new("load-more", "Load More")
+                                        .disabled(self.loading_more)
+                                        .label_size(LabelSize::Small)
+                                        .icon(IconName::ArrowCircle)
+                                        .icon_size(IconSize::Small)
+                                        .icon_color(Color::Muted)
+                                        .icon_position(IconPosition::Start)
+                                        .on_click(cx.listener(|this, _, window, cx| {
+                                            this.load_more(window, cx);
+                                        })),
+                                )
+                            }),
                     ),
             )
             .child(
@@ -474,24 +442,9 @@ impl Render for FileHistoryView {
                         )
                         .flex_1()
                         .size_full()
-                        .with_sizing_behavior(ListSizingBehavior::Auto)
                         .track_scroll(&self.scroll_handle)
                     })
-                    .when(self.has_more, |this| {
-                        this.child(
-                            div().p(rems(0.75)).flex().justify_start().child(
-                                Button::new("load-more", "Load more")
-                                    .style(ButtonStyle::Subtle)
-                                    .disabled(self.loading_more)
-                                    .label_size(LabelSize::Small)
-                                    .icon(IconName::ArrowCircle)
-                                    .icon_position(IconPosition::Start)
-                                    .on_click(cx.listener(|this, _, window, cx| {
-                                        this.load_more(window, cx);
-                                    })),
-                            ),
-                        )
-                    }),
+                    .vertical_scrollbar_for(&self.scroll_handle, window, cx),
             )
     }
 }
@@ -518,7 +471,7 @@ impl Item for FileHistoryView {
     }
 
     fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
-        Some(Icon::new(IconName::FileGit))
+        Some(Icon::new(IconName::GitBranch))
     }
 
     fn telemetry_event_text(&self) -> Option<&'static str> {

crates/ui/src/components/avatar.rs 🔗

@@ -91,10 +91,16 @@ impl RenderOnce for Avatar {
                 self.image
                     .size(image_size)
                     .rounded_full()
-                    .bg(cx.theme().colors().ghost_element_background)
+                    .bg(cx.theme().colors().element_disabled)
                     .with_fallback(|| {
-                        Icon::new(IconName::Person)
-                            .color(Color::Muted)
+                        h_flex()
+                            .size_full()
+                            .justify_center()
+                            .child(
+                                Icon::new(IconName::Person)
+                                    .color(Color::Muted)
+                                    .size(IconSize::Small),
+                            )
                             .into_any_element()
                     }),
             )