gpui_component/
badge.rs

1use gpui::{
2    div, prelude::FluentBuilder, px, relative, AnyElement, App, Hsla, IntoElement, ParentElement,
3    RenderOnce, StyleRefinement, Styled, Window,
4};
5
6use crate::{h_flex, white, ActiveTheme, Icon, Sizable, Size, StyledExt};
7
8#[derive(Default, Clone)]
9enum BadgeVariant {
10    #[default]
11    Number,
12    Dot,
13    Icon(Box<Icon>),
14}
15
16#[allow(unused)]
17impl BadgeVariant {
18    #[inline]
19    fn is_icon(&self) -> bool {
20        matches!(self, BadgeVariant::Icon(_))
21    }
22
23    #[inline]
24    fn is_number(&self) -> bool {
25        matches!(self, BadgeVariant::Number)
26    }
27}
28
29/// A badge for displaying a count, dot, or icon on an element.
30#[derive(IntoElement)]
31pub struct Badge {
32    style: StyleRefinement,
33    count: usize,
34    max: usize,
35    variant: BadgeVariant,
36    children: Vec<AnyElement>,
37    color: Option<Hsla>,
38    size: Size,
39}
40
41impl Badge {
42    /// Create a new badge.
43    pub fn new() -> Self {
44        Self {
45            style: StyleRefinement::default(),
46            count: 0,
47            max: 99,
48            variant: Default::default(),
49            color: None,
50            children: Vec::new(),
51            size: Size::default(),
52        }
53    }
54
55    /// Set to use [`BadgeVariant::Dot`] to show a dot.
56    pub fn dot(mut self) -> Self {
57        self.variant = BadgeVariant::Dot;
58        self
59    }
60
61    /// Set to use [`BadgeVariant::Number`] to show a count.
62    ///
63    /// If count is 0, the badge will be hidden.
64    pub fn count(mut self, count: usize) -> Self {
65        self.count = count;
66        self
67    }
68
69    /// Set to use [`BadgeVariant::Icon`] to show an icon.
70    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
71        self.variant = BadgeVariant::Icon(Box::new(icon.into()));
72        self
73    }
74
75    /// Set the maximum count to show (Only if [`BadgeVariant::Number`] is used).
76    pub fn max(mut self, max: usize) -> Self {
77        self.max = max;
78        self
79    }
80
81    /// Set the color (background) of the badge.
82    pub fn color(mut self, color: impl Into<Hsla>) -> Self {
83        self.color = Some(color.into());
84        self
85    }
86}
87
88impl ParentElement for Badge {
89    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
90        self.children.extend(elements);
91    }
92}
93
94impl Sizable for Badge {
95    fn with_size(mut self, size: impl Into<Size>) -> Self {
96        self.size = size.into();
97        self
98    }
99}
100
101impl RenderOnce for Badge {
102    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
103        let visible = match self.variant {
104            BadgeVariant::Number => self.count > 0,
105            BadgeVariant::Dot | BadgeVariant::Icon(_) => true,
106        };
107
108        let (size, text_size) = match self.size {
109            Size::Large => (px(24.), px(14.)),
110            Size::Medium | Size::Size(_) => (px(16.), px(10.)),
111            Size::Small | Size::XSmall => (px(10.), px(8.)),
112        };
113
114        div()
115            .relative()
116            .refine_style(&self.style)
117            .children(self.children)
118            .when(visible, |this| {
119                this.child(
120                    h_flex()
121                        .absolute()
122                        .justify_center()
123                        .items_center()
124                        .rounded_full()
125                        .bg(self.color.unwrap_or(cx.theme().red))
126                        .text_color(white())
127                        .text_size(text_size)
128                        .map(|this| match self.variant {
129                            BadgeVariant::Dot => this.top_0().right_0().size(px(6.)),
130                            BadgeVariant::Number => {
131                                let count = if self.count > self.max {
132                                    format!("{}+", self.max)
133                                } else {
134                                    self.count.to_string()
135                                };
136
137                                let (top, left) = match self.size {
138                                    Size::Large => (px(2.), -px(count.len() as f32)),
139                                    Size::Medium | Size::Size(_) => {
140                                        (-px(3.), -px(3.) * count.len())
141                                    }
142                                    Size::Small | Size::XSmall => (-px(4.), -px(4.) * count.len()),
143                                };
144
145                                this.top(top)
146                                    .right(left)
147                                    .py_0p5()
148                                    .px_0p5()
149                                    .min_w_3p5()
150                                    .text_size(px(10.))
151                                    .line_height(relative(1.))
152                                    .child(count)
153                            }
154                            BadgeVariant::Icon(icon) => this
155                                .right_0()
156                                .bottom_0()
157                                .size(size)
158                                .border_1()
159                                .border_color(cx.theme().background)
160                                .child(*icon),
161                        }),
162                )
163            })
164    }
165}