1use gpui::{FontWeight, Hsla, white};
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum BadgeVariant {
8 #[default]
10 Soft,
11 Solid,
13 Outline,
15}
16
17#[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#[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 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}