1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
use gpui_kit::base::StyledExt as _;
use gpui_kit::{
AnyElement, App, ElementId, FontWeight, InteractiveElement as _, IntoElement, ParentElement,
RenderOnce, StyleRefinement, Styled, TestSupportExt as _, Window, div, relative,
};
use crate::{ActiveTheme as _, Icon, TagVariant};
/// What a [`Badge`] shows at the corner of its element.
#[derive(Clone, Default)]
enum Mark {
/// A count, hidden at zero.
#[default]
Count,
/// A dot.
Dot,
/// An icon in a circle.
Icon(Box<Icon>),
}
/// A count, a dot, or an icon at the corner of another element, such as
/// unread messages on an avatar. gpui-kit's `Badge`.
///
/// A count sits over the top trailing corner and hides at zero; past the
/// maximum it shows `max+`. A dot sits inside that corner. An icon sits on
/// the bottom trailing corner with a ring in the window background. The
/// fill is the primary color, as quiet as the rest of the window. A
/// status variant, such as [`Badge::danger`] for an alert, paints the
/// theme's solid status fill, which keeps its text at WCAG AA. For a
/// labeled pill, use [`Tag`](crate::Tag).
///
/// ```
/// use gpui_cn::{Avatar, Badge};
/// use gpui_kit::ParentElement as _;
///
/// let _ = Badge::new("inbox").count(3).child(Avatar::new("ada").name("Ada Lovelace"));
/// ```
#[derive(IntoElement)]
pub struct Badge {
id: ElementId,
style: StyleRefinement,
mark: Mark,
count: usize,
max: usize,
variant: TagVariant,
children: Vec<AnyElement>,
}
impl Badge {
/// A badge with a stable id. The mark carries the id; the element it
/// sits on is a child.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
style: StyleRefinement::default(),
mark: Mark::Count,
count: 0,
max: 99,
variant: TagVariant::Primary,
children: Vec::new(),
}
}
/// Shows a count. A count of zero shows nothing.
pub fn count(mut self, count: usize) -> Self {
self.mark = Mark::Count;
self.count = count;
self
}
/// The largest count to show as a number: 99 unless set. A larger
/// count shows as `max+`.
pub fn max(mut self, max: usize) -> Self {
self.max = max;
self
}
/// Shows a dot.
pub fn dot(mut self) -> Self {
self.mark = Mark::Dot;
self
}
/// Shows an icon.
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.mark = Mark::Icon(Box::new(icon.into()));
self
}
/// The color of the mark, from the same set as a [`Tag`](crate::Tag).
pub fn variant(mut self, variant: TagVariant) -> Self {
self.variant = variant;
self
}
/// [`TagVariant::Secondary`].
pub fn secondary(self) -> Self {
self.variant(TagVariant::Secondary)
}
/// [`TagVariant::Danger`].
pub fn danger(self) -> Self {
self.variant(TagVariant::Danger)
}
/// [`TagVariant::Success`].
pub fn success(self) -> Self {
self.variant(TagVariant::Success)
}
/// [`TagVariant::Warning`].
pub fn warning(self) -> Self {
self.variant(TagVariant::Warning)
}
/// [`TagVariant::Info`].
pub fn info(self) -> Self {
self.variant(TagVariant::Info)
}
}
impl Styled for Badge {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl ParentElement for Badge {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for Badge {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let metrics = &theme.metrics;
let (fill, ink) = match self.variant {
TagVariant::Primary => (theme.primary(), theme.primary_foreground()),
TagVariant::Secondary => (theme.selected, theme.foreground()),
TagVariant::Danger => (theme.destructive_solid, theme.solid_foreground),
TagVariant::Success => (theme.success_solid, theme.solid_foreground),
TagVariant::Warning => (theme.warning_solid, theme.solid_foreground),
TagVariant::Info => (theme.info_solid, theme.solid_foreground),
};
let ring = theme.background();
let offset = -metrics.badge_count_offset;
let (count, count_text, dot, icon_size) = (
metrics.badge_count,
metrics.badge_count_text,
metrics.badge_dot,
metrics.badge_icon,
);
let mark = div()
.id(self.id)
.test_support()
.absolute()
.flex()
.items_center()
.justify_center()
.rounded_full()
.bg(fill)
.text_color(ink);
let mark = match self.mark {
Mark::Count if self.count == 0 => None,
Mark::Count => {
let text = if self.count > self.max {
format!("{}+", self.max)
} else {
self.count.to_string()
};
Some(
mark.top(offset)
.right(offset)
.h(count)
.min_w(count)
.px_1()
.text_size(count_text)
.line_height(relative(1.))
.font_weight(FontWeight::MEDIUM)
.child(text),
)
}
Mark::Dot => Some(mark.top_0().right_0().size(dot)),
Mark::Icon(icon) => Some(
mark.bottom_0()
.right_0()
.size(icon_size)
.border_1()
.border_color(ring)
.child(icon.size(icon_size / 2.)),
),
};
div()
.relative()
.flex_none()
.children(self.children)
.children(mark)
.refine_style(&self.style)
}
}