1use gpui_kit::base::StyledExt as _;
2use gpui_kit::{
3 AnyElement, App, ElementId, FontWeight, InteractiveElement as _, IntoElement, ParentElement,
4 RenderOnce, StyleRefinement, Styled, TestSupportExt as _, Window, div, relative,
5};
6
7use crate::{ActiveTheme as _, Icon, TagVariant};
8
9#[derive(Clone, Default)]
11enum Mark {
12 #[default]
14 Count,
15 Dot,
17 Icon(Box<Icon>),
19}
20
21#[derive(IntoElement)]
39pub struct Badge {
40 id: ElementId,
41 style: StyleRefinement,
42 mark: Mark,
43 count: usize,
44 max: usize,
45 variant: TagVariant,
46 children: Vec<AnyElement>,
47}
48
49impl Badge {
50 pub fn new(id: impl Into<ElementId>) -> Self {
53 Self {
54 id: id.into(),
55 style: StyleRefinement::default(),
56 mark: Mark::Count,
57 count: 0,
58 max: 99,
59 variant: TagVariant::Primary,
60 children: Vec::new(),
61 }
62 }
63
64 pub fn count(mut self, count: usize) -> Self {
66 self.mark = Mark::Count;
67 self.count = count;
68 self
69 }
70
71 pub fn max(mut self, max: usize) -> Self {
74 self.max = max;
75 self
76 }
77
78 pub fn dot(mut self) -> Self {
80 self.mark = Mark::Dot;
81 self
82 }
83
84 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
86 self.mark = Mark::Icon(Box::new(icon.into()));
87 self
88 }
89
90 pub fn variant(mut self, variant: TagVariant) -> Self {
92 self.variant = variant;
93 self
94 }
95
96 pub fn secondary(self) -> Self {
98 self.variant(TagVariant::Secondary)
99 }
100
101 pub fn danger(self) -> Self {
103 self.variant(TagVariant::Danger)
104 }
105
106 pub fn success(self) -> Self {
108 self.variant(TagVariant::Success)
109 }
110
111 pub fn warning(self) -> Self {
113 self.variant(TagVariant::Warning)
114 }
115
116 pub fn info(self) -> Self {
118 self.variant(TagVariant::Info)
119 }
120}
121
122impl Styled for Badge {
123 fn style(&mut self) -> &mut StyleRefinement {
124 &mut self.style
125 }
126}
127
128impl ParentElement for Badge {
129 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
130 self.children.extend(elements);
131 }
132}
133
134impl RenderOnce for Badge {
135 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
136 let theme = cx.theme();
137 let metrics = &theme.metrics;
138 let (fill, ink) = match self.variant {
139 TagVariant::Primary => (theme.primary(), theme.primary_foreground()),
140 TagVariant::Secondary => (theme.selected, theme.foreground()),
141 TagVariant::Danger => (theme.destructive_solid, theme.solid_foreground),
142 TagVariant::Success => (theme.success_solid, theme.solid_foreground),
143 TagVariant::Warning => (theme.warning_solid, theme.solid_foreground),
144 TagVariant::Info => (theme.info_solid, theme.solid_foreground),
145 };
146 let ring = theme.background();
147 let offset = -metrics.badge_count_offset;
148 let (count, count_text, dot, icon_size) = (
149 metrics.badge_count,
150 metrics.badge_count_text,
151 metrics.badge_dot,
152 metrics.badge_icon,
153 );
154
155 let mark = div()
156 .id(self.id)
157 .test_support()
158 .absolute()
159 .flex()
160 .items_center()
161 .justify_center()
162 .rounded_full()
163 .bg(fill)
164 .text_color(ink);
165 let mark = match self.mark {
166 Mark::Count if self.count == 0 => None,
167 Mark::Count => {
168 let text = if self.count > self.max {
169 format!("{}+", self.max)
170 } else {
171 self.count.to_string()
172 };
173 Some(
174 mark.top(offset)
175 .right(offset)
176 .h(count)
177 .min_w(count)
178 .px_1()
179 .text_size(count_text)
180 .line_height(relative(1.))
181 .font_weight(FontWeight::MEDIUM)
182 .child(text),
183 )
184 }
185 Mark::Dot => Some(mark.top_0().right_0().size(dot)),
186 Mark::Icon(icon) => Some(
187 mark.bottom_0()
188 .right_0()
189 .size(icon_size)
190 .border_1()
191 .border_color(ring)
192 .child(icon.size(icon_size / 2.)),
193 ),
194 };
195
196 div()
197 .relative()
198 .flex_none()
199 .children(self.children)
200 .children(mark)
201 .refine_style(&self.style)
202 }
203}