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#[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 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 pub fn dot(mut self) -> Self {
57 self.variant = BadgeVariant::Dot;
58 self
59 }
60
61 pub fn count(mut self, count: usize) -> Self {
65 self.count = count;
66 self
67 }
68
69 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
71 self.variant = BadgeVariant::Icon(Box::new(icon.into()));
72 self
73 }
74
75 pub fn max(mut self, max: usize) -> Self {
77 self.max = max;
78 self
79 }
80
81 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}