From a332b791899d90bc6bfaca3d153cb37254139036 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:18:08 -0300 Subject: [PATCH] ui: Add `DiffStat` component (#43192) Release Notes: - N/A --- 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(-) create mode 100644 crates/ui/src/components/diff_stat.rs diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 712a07d3bdddb1d0c2300f6d256fa5634b16e764..b6318f18c973ca5ca7eefa1ba39517ef65cad6df 100644 --- a/crates/ui/src/components.rs +++ b/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::*; diff --git a/crates/ui/src/components/diff_stat.rs b/crates/ui/src/components/diff_stat.rs new file mode 100644 index 0000000000000000000000000000000000000000..2606963555c682d9d949d19d57471e02c53351d7 --- /dev/null +++ b/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, 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 { + 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(), + ) + } +} diff --git a/crates/ui/src/components/thread_item.rs b/crates/ui/src/components/thread_item.rs index 0cb6a42ad11d16eddd3a2afb3d8a9dc9475b6165..dcf159f502e2d3c67576f9c6eefaff10585992eb 100644 --- a/crates/ui/src/components/thread_item.rs +++ b/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, + removed: Option, worktree: Option, on_click: Option>, } @@ -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),