ui: Add `DiffStat` component (#43192)

Danilo Leal created

Release Notes:

- N/A

Change summary

crates/ui/src/components.rs             |  2 
crates/ui/src/components/diff_stat.rs   | 85 ++++++++++++++++++++++++++
crates/ui/src/components/thread_item.rs | 44 +++++++++++--
3 files changed, 123 insertions(+), 8 deletions(-)

Detailed changes

crates/ui/src/components.rs 🔗

@@ -6,6 +6,7 @@ mod chip;
 mod content_group;
 mod context_menu;
 mod data_table;
+mod diff_stat;
 mod disclosure;
 mod divider;
 mod dropdown_menu;
@@ -50,6 +51,7 @@ pub use chip::*;
 pub use content_group::*;
 pub use context_menu::*;
 pub use data_table::*;
+pub use diff_stat::*;
 pub use disclosure::*;
 pub use divider::*;
 pub use dropdown_menu::*;

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

@@ -0,0 +1,85 @@
+use crate::prelude::*;
+
+#[derive(IntoElement, RegisterComponent)]
+pub struct DiffStat {
+    id: ElementId,
+    added: usize,
+    removed: usize,
+}
+
+impl DiffStat {
+    pub fn new(id: impl Into<ElementId>, added: usize, removed: usize) -> Self {
+        Self {
+            id: id.into(),
+            added,
+            removed,
+        }
+    }
+}
+
+impl RenderOnce for DiffStat {
+    fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement {
+        h_flex()
+            .id(self.id)
+            .gap_1()
+            .child(
+                h_flex()
+                    .gap_0p5()
+                    .child(
+                        Icon::new(IconName::Plus)
+                            .size(IconSize::XSmall)
+                            .color(Color::Success),
+                    )
+                    .child(
+                        Label::new(self.added.to_string())
+                            .color(Color::Success)
+                            .size(LabelSize::Small),
+                    ),
+            )
+            .child(
+                h_flex()
+                    .gap_0p5()
+                    .child(
+                        Icon::new(IconName::Dash)
+                            .size(IconSize::XSmall)
+                            .color(Color::Error),
+                    )
+                    .child(
+                        Label::new(self.removed.to_string())
+                            .color(Color::Error)
+                            .size(LabelSize::Small),
+                    ),
+            )
+    }
+}
+
+impl Component for DiffStat {
+    fn scope() -> ComponentScope {
+        ComponentScope::VersionControl
+    }
+
+    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
+        let container = || {
+            h_flex()
+                .py_4()
+                .w_72()
+                .justify_center()
+                .border_1()
+                .border_color(cx.theme().colors().border_variant)
+                .bg(cx.theme().colors().panel_background)
+        };
+
+        let diff_stat_example = vec![single_example(
+            "Default",
+            container()
+                .child(DiffStat::new("id", 1, 2))
+                .into_any_element(),
+        )];
+
+        Some(
+            example_group(diff_stat_example)
+                .vertical()
+                .into_any_element(),
+        )
+    }
+}

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

@@ -1,4 +1,4 @@
-use crate::{Chip, Indicator, SpinnerLabel, prelude::*};
+use crate::{Chip, DiffStat, Indicator, SpinnerLabel, prelude::*};
 use gpui::{ClickEvent, SharedString};
 
 #[derive(IntoElement, RegisterComponent)]
@@ -10,7 +10,8 @@ pub struct ThreadItem {
     running: bool,
     generation_done: bool,
     selected: bool,
-    has_changes: bool,
+    added: Option<usize>,
+    removed: Option<usize>,
     worktree: Option<SharedString>,
     on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
 }
@@ -25,7 +26,8 @@ impl ThreadItem {
             running: false,
             generation_done: false,
             selected: false,
-            has_changes: false,
+            added: None,
+            removed: None,
             worktree: None,
             on_click: None,
         }
@@ -56,8 +58,13 @@ impl ThreadItem {
         self
     }
 
-    pub fn has_changes(mut self, has_changes: bool) -> Self {
-        self.has_changes = has_changes;
+    pub fn added(mut self, added: usize) -> Self {
+        self.added = Some(added);
+        self
+    }
+
+    pub fn removed(mut self, removed: usize) -> Self {
+        self.removed = Some(removed);
         self
     }
 
@@ -90,8 +97,10 @@ impl RenderOnce for ThreadItem {
             )
         };
 
+        let has_no_changes = self.added.is_none() && self.removed.is_none();
+
         v_flex()
-            .id(self.id)
+            .id(self.id.clone())
             .cursor_pointer()
             .p_2()
             .when(self.selected, |this| {
@@ -123,12 +132,19 @@ impl RenderOnce for ThreadItem {
                             .color(Color::Muted)
                             .alpha(0.5),
                     )
-                    .when(!self.has_changes, |this| {
+                    .when(has_no_changes, |this| {
                         this.child(
                             Label::new("No Changes")
                                 .size(LabelSize::Small)
                                 .color(Color::Muted),
                         )
+                    })
+                    .when(self.added.is_some() || self.removed.is_some(), |this| {
+                        this.child(DiffStat::new(
+                            self.id,
+                            self.added.unwrap_or(0),
+                            self.removed.unwrap_or(0),
+                        ))
                     }),
             )
             .when_some(self.on_click, |this, on_click| this.on_click(on_click))
@@ -192,11 +208,23 @@ impl Component for ThreadItem {
                     )
                     .into_any_element(),
             ),
+            single_example(
+                "With Changes",
+                container()
+                    .child(
+                        ThreadItem::new("ti-5", "Managing user and project settings interactions")
+                            .icon(IconName::AiClaude)
+                            .timestamp("7:37 PM")
+                            .added(10)
+                            .removed(3),
+                    )
+                    .into_any_element(),
+            ),
             single_example(
                 "Selected Item",
                 container()
                     .child(
-                        ThreadItem::new("ti-5", "Refine textarea interaction behavior")
+                        ThreadItem::new("ti-6", "Refine textarea interaction behavior")
                             .icon(IconName::AiGemini)
                             .timestamp("3:00 PM")
                             .selected(true),