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    Primary,
23    Success,
24    Warning,
25    Danger,
26}
27
28impl BadgeColor {
29    fn shade(self, step: u16) -> Hsla {
30        match self {
31            BadgeColor::Neutral => palette::neutral(step),
32            BadgeColor::Primary => palette::primary(step),
33            BadgeColor::Success => palette::success(step),
34            BadgeColor::Warning => palette::warning(step),
35            BadgeColor::Danger => palette::danger(step),
36        }
37    }
38}
39
40/// A small pill label conveying status or category.
41#[derive(IntoElement, RegisterComponent)]
42pub struct Badge {
43    label: SharedString,
44    variant: BadgeVariant,
45    color: BadgeColor,
46    dot: bool,
47}
48
49impl Badge {
50    pub fn new(label: impl Into<SharedString>) -> Self {
51        Self {
52            label: label.into(),
53            variant: BadgeVariant::default(),
54            color: BadgeColor::default(),
55            dot: false,
56        }
57    }
58
59    pub fn variant(mut self, variant: BadgeVariant) -> Self {
60        self.variant = variant;
61        self
62    }
63
64    pub fn color(mut self, color: BadgeColor) -> Self {
65        self.color = color;
66        self
67    }
68
69    pub fn dot(mut self, dot: bool) -> Self {
70        self.dot = dot;
71        self
72    }
73}
74
75impl RenderOnce for Badge {
76    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
77        let mut base = h_flex()
78            .items_center()
79            .gap_1()
80            .px_2()
81            .py_0p5()
82            .rounded_full();
83
84        base = match self.variant {
85            BadgeVariant::Soft => base
86                .bg(self.color.shade(100))
87                .text_color(self.color.shade(800)),
88            BadgeVariant::Solid => base.bg(self.color.shade(600)).text_color(white()),
89            BadgeVariant::Outline => base
90                .border_1()
91                .border_color(self.color.shade(300))
92                .text_color(self.color.shade(700)),
93        };
94
95        base.when(self.dot, |this| {
96            this.child(div().size_1p5().rounded_full().bg(self.color.shade(500)))
97        })
98        .child(
99            Label::new(self.label)
100                .size(LabelSize::XSmall)
101                .weight(FontWeight::MEDIUM)
102                .color(Color::Custom(match self.variant {
103                    BadgeVariant::Solid => white(),
104                    BadgeVariant::Soft => self.color.shade(800),
105                    BadgeVariant::Outline => self.color.shade(700),
106                })),
107        )
108    }
109}
110
111impl Component for Badge {
112    fn scope() -> ComponentScope {
113        ComponentScope::Status
114    }
115
116    fn description() -> Option<&'static str> {
117        Some("A small pill label conveying status or category.")
118    }
119
120    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
121        let colors = [
122            ("Neutral", BadgeColor::Neutral),
123            ("Primary", BadgeColor::Primary),
124            ("Success", BadgeColor::Success),
125            ("Warning", BadgeColor::Warning),
126            ("Danger", BadgeColor::Danger),
127        ];
128        let row = |variant: BadgeVariant, dot: bool| {
129            let mut r = h_flex().gap_2();
130            for (name, color) in colors {
131                r = r.child(Badge::new(name).variant(variant).color(color).dot(dot));
132            }
133            r
134        };
135        let status_row = || {
136            h_flex()
137                .gap_2()
138                .child(
139                    Badge::new("Active")
140                        .variant(BadgeVariant::Soft)
141                        .color(BadgeColor::Success)
142                        .dot(true),
143                )
144                .child(
145                    Badge::new("Pending")
146                        .variant(BadgeVariant::Soft)
147                        .color(BadgeColor::Warning)
148                        .dot(true),
149                )
150                .child(
151                    Badge::new("Failed")
152                        .variant(BadgeVariant::Solid)
153                        .color(BadgeColor::Danger),
154                )
155                .child(
156                    Badge::new("Draft")
157                        .variant(BadgeVariant::Outline)
158                        .color(BadgeColor::Neutral),
159                )
160        };
161
162        Some(
163            v_flex()
164                .gap_3()
165                .child(row(BadgeVariant::Soft, true))
166                .child(row(BadgeVariant::Solid, true))
167                .child(row(BadgeVariant::Outline, true))
168                .child(row(BadgeVariant::Soft, false))
169                .child(row(BadgeVariant::Solid, false))
170                .child(row(BadgeVariant::Outline, false))
171                .child(status_row())
172                .into_any_element(),
173        )
174    }
175}