Skip to main content

gpui_cn/
badge.rs

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/// What a [`Badge`] shows at the corner of its element.
10#[derive(Clone, Default)]
11enum Mark {
12    /// A count, hidden at zero.
13    #[default]
14    Count,
15    /// A dot.
16    Dot,
17    /// An icon in a circle.
18    Icon(Box<Icon>),
19}
20
21/// A count, a dot, or an icon at the corner of another element, such as
22/// unread messages on an avatar. gpui-kit's `Badge`.
23///
24/// A count sits over the top trailing corner and hides at zero; past the
25/// maximum it shows `max+`. A dot sits inside that corner. An icon sits on
26/// the bottom trailing corner with a ring in the window background. The
27/// fill is the primary color, as quiet as the rest of the window. A
28/// status variant, such as [`Badge::danger`] for an alert, paints the
29/// theme's solid status fill, which keeps its text at WCAG AA. For a
30/// labeled pill, use [`Tag`](crate::Tag).
31///
32/// ```
33/// use gpui_cn::{Avatar, Badge};
34/// use gpui_kit::ParentElement as _;
35///
36/// let _ = Badge::new("inbox").count(3).child(Avatar::new("ada").name("Ada Lovelace"));
37/// ```
38#[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    /// A badge with a stable id. The mark carries the id; the element it
51    /// sits on is a child.
52    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    /// Shows a count. A count of zero shows nothing.
65    pub fn count(mut self, count: usize) -> Self {
66        self.mark = Mark::Count;
67        self.count = count;
68        self
69    }
70
71    /// The largest count to show as a number: 99 unless set. A larger
72    /// count shows as `max+`.
73    pub fn max(mut self, max: usize) -> Self {
74        self.max = max;
75        self
76    }
77
78    /// Shows a dot.
79    pub fn dot(mut self) -> Self {
80        self.mark = Mark::Dot;
81        self
82    }
83
84    /// Shows an icon.
85    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
86        self.mark = Mark::Icon(Box::new(icon.into()));
87        self
88    }
89
90    /// The color of the mark, from the same set as a [`Tag`](crate::Tag).
91    pub fn variant(mut self, variant: TagVariant) -> Self {
92        self.variant = variant;
93        self
94    }
95
96    /// [`TagVariant::Secondary`].
97    pub fn secondary(self) -> Self {
98        self.variant(TagVariant::Secondary)
99    }
100
101    /// [`TagVariant::Danger`].
102    pub fn danger(self) -> Self {
103        self.variant(TagVariant::Danger)
104    }
105
106    /// [`TagVariant::Success`].
107    pub fn success(self) -> Self {
108        self.variant(TagVariant::Success)
109    }
110
111    /// [`TagVariant::Warning`].
112    pub fn warning(self) -> Self {
113        self.variant(TagVariant::Warning)
114    }
115
116    /// [`TagVariant::Info`].
117    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}