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_1()
81            .px_2()
82            .py_0p5()
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(div().size_1p5().rounded_full().bg(dot_color))
129        })
130        .child(
131            Label::new(self.label)
132                .size(LabelSize::XSmall)
133                .weight(FontWeight::MEDIUM)
134                .color(Color::Custom(text_color)),
135        )
136    }
137}
138
139impl Component for Badge {
140    fn scope() -> ComponentScope {
141        ComponentScope::Status
142    }
143
144    fn description() -> Option<&'static str> {
145        Some("A small pill label conveying status or category.")
146    }
147
148    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
149        let colors = [
150            ("Neutral", BadgeColor::Neutral),
151            ("Secondary", BadgeColor::Secondary),
152            ("Primary", BadgeColor::Primary),
153            ("Success", BadgeColor::Success),
154            ("Warning", BadgeColor::Warning),
155            ("Danger", BadgeColor::Danger),
156        ];
157        let row = |variant: BadgeVariant, dot: bool| {
158            let mut r = h_flex().gap_2();
159            for (name, color) in colors {
160                r = r.child(Badge::new(name).variant(variant).color(color).dot(dot));
161            }
162            r
163        };
164        let status_row = || {
165            h_flex()
166                .gap_2()
167                .child(
168                    Badge::new("Active")
169                        .variant(BadgeVariant::Soft)
170                        .color(BadgeColor::Success)
171                        .dot(true),
172                )
173                .child(
174                    Badge::new("Pending")
175                        .variant(BadgeVariant::Soft)
176                        .color(BadgeColor::Warning)
177                        .dot(true),
178                )
179                .child(
180                    Badge::new("Failed")
181                        .variant(BadgeVariant::Solid)
182                        .color(BadgeColor::Danger),
183                )
184                .child(
185                    Badge::new("Draft")
186                        .variant(BadgeVariant::Outline)
187                        .color(BadgeColor::Neutral),
188                )
189        };
190
191        Some(
192            v_flex()
193                .gap_3()
194                .child(row(BadgeVariant::Soft, true))
195                .child(row(BadgeVariant::Solid, true))
196                .child(row(BadgeVariant::Outline, true))
197                .child(row(BadgeVariant::Soft, false))
198                .child(row(BadgeVariant::Solid, false))
199                .child(row(BadgeVariant::Outline, false))
200                .child(status_row())
201                .into_any_element(),
202        )
203    }
204}