Skip to main content

ui/components/
badge.rs

1use gpui::{FontWeight, Hsla, white};
2
3use crate::prelude::*;
4
5/// Fill style of a [`Badge`].
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum BadgeVariant {
8    /// Light tinted background with dark text.
9    #[default]
10    Soft,
11    /// Strong filled background with light text.
12    Solid,
13    /// Transparent with a colored border.
14    Outline,
15}
16
17/// Semantic color role of a [`Badge`].
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum BadgeColor {
20    #[default]
21    Neutral,
22    Secondary,
23    Primary,
24    Success,
25    Warning,
26    Danger,
27}
28
29impl BadgeColor {
30    fn shade(self, step: u16) -> Hsla {
31        match self {
32            BadgeColor::Neutral | BadgeColor::Secondary => palette::neutral(step),
33            BadgeColor::Primary => palette::primary(step),
34            BadgeColor::Success => palette::success(step),
35            BadgeColor::Warning => palette::warning(step),
36            BadgeColor::Danger => palette::danger(step),
37        }
38    }
39}
40
41/// A small pill label conveying status or category.
42#[derive(IntoElement, RegisterComponent)]
43pub struct Badge {
44    label: SharedString,
45    variant: BadgeVariant,
46    color: BadgeColor,
47    dot: bool,
48}
49
50impl Badge {
51    pub fn new(label: impl Into<SharedString>) -> Self {
52        Self {
53            label: label.into(),
54            variant: BadgeVariant::default(),
55            color: BadgeColor::default(),
56            dot: false,
57        }
58    }
59
60    pub fn variant(mut self, variant: BadgeVariant) -> Self {
61        self.variant = variant;
62        self
63    }
64
65    pub fn color(mut self, color: BadgeColor) -> Self {
66        self.color = color;
67        self
68    }
69
70    pub fn dot(mut self, dot: bool) -> Self {
71        self.dot = dot;
72        self
73    }
74}
75
76impl RenderOnce for Badge {
77    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
78        let mut base = h_flex()
79            .items_center()
80            .gap(DynamicSpacing::Base04.rems(cx))
81            .px(DynamicSpacing::Base08.px(cx))
82            .py(DynamicSpacing::Base02.px(cx))
83            .rounded_full();
84
85        // shadcn's `secondary` badge is a distinct neutral-solid role
86        // (theme `--secondary`/`--secondary-foreground`), not just a
87        // lighter/darker shade of `Neutral` — read it from `semantic` so it
88        // tracks the active theme instead of aliasing `Neutral`'s static
89        // palette shades.
90        let is_secondary = self.color == BadgeColor::Secondary;
91        let (fill, fill_text, dot_color, outline_border, outline_text) = if is_secondary {
92            let bg = semantic::secondary_bg(cx);
93            let fg = semantic::secondary_fg(cx);
94            (bg, fg, fg, semantic::border(cx), fg)
95        } else {
96            match self.variant {
97                BadgeVariant::Solid => (
98                    self.color.shade(600),
99                    white(),
100                    self.color.shade(500),
101                    self.color.shade(300),
102                    self.color.shade(700),
103                ),
104                BadgeVariant::Soft | BadgeVariant::Outline => (
105                    self.color.shade(100),
106                    self.color.shade(800),
107                    self.color.shade(500),
108                    self.color.shade(300),
109                    self.color.shade(700),
110                ),
111            }
112        };
113
114        base = match self.variant {
115            BadgeVariant::Soft | BadgeVariant::Solid => base.bg(fill).text_color(fill_text),
116            BadgeVariant::Outline => base
117                .border_1()
118                .border_color(outline_border)
119                .text_color(outline_text),
120        };
121
122        let text_color = match self.variant {
123            BadgeVariant::Outline => outline_text,
124            _ => fill_text,
125        };
126
127        base.when(self.dot, |this| {
128            this.child(
129                div()
130                    .size(DynamicSpacing::Base06.px(cx))
131                    .rounded_full()
132                    .bg(dot_color),
133            )
134        })
135        .child(
136            Label::new(self.label)
137                .size(LabelSize::XSmall)
138                .weight(FontWeight::MEDIUM)
139                .color(Color::Custom(text_color)),
140        )
141    }
142}
143
144impl Component for Badge {
145    fn scope() -> ComponentScope {
146        ComponentScope::Status
147    }
148
149    fn description() -> Option<&'static str> {
150        Some("A small pill label conveying status or category.")
151    }
152
153    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
154        let colors = [
155            ("Neutral", BadgeColor::Neutral),
156            ("Secondary", BadgeColor::Secondary),
157            ("Primary", BadgeColor::Primary),
158            ("Success", BadgeColor::Success),
159            ("Warning", BadgeColor::Warning),
160            ("Danger", BadgeColor::Danger),
161        ];
162        let row = |variant: BadgeVariant, dot: bool| {
163            let mut r = h_flex().gap(DynamicSpacing::Base08.rems(cx));
164            for (name, color) in colors {
165                r = r.child(Badge::new(name).variant(variant).color(color).dot(dot));
166            }
167            r
168        };
169        let status_row = || {
170            h_flex()
171                .gap(DynamicSpacing::Base08.rems(cx))
172                .child(
173                    Badge::new("Active")
174                        .variant(BadgeVariant::Soft)
175                        .color(BadgeColor::Success)
176                        .dot(true),
177                )
178                .child(
179                    Badge::new("Pending")
180                        .variant(BadgeVariant::Soft)
181                        .color(BadgeColor::Warning)
182                        .dot(true),
183                )
184                .child(
185                    Badge::new("Failed")
186                        .variant(BadgeVariant::Solid)
187                        .color(BadgeColor::Danger),
188                )
189                .child(
190                    Badge::new("Draft")
191                        .variant(BadgeVariant::Outline)
192                        .color(BadgeColor::Neutral),
193                )
194        };
195
196        Some(
197            v_flex()
198                .gap(DynamicSpacing::Base12.rems(cx))
199                .child(row(BadgeVariant::Soft, true))
200                .child(row(BadgeVariant::Solid, true))
201                .child(row(BadgeVariant::Outline, true))
202                .child(row(BadgeVariant::Soft, false))
203                .child(row(BadgeVariant::Solid, false))
204                .child(row(BadgeVariant::Outline, false))
205                .child(status_row())
206                .into_any_element(),
207        )
208    }
209}