1use gpui::prelude::*;
6use gpui::*;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum AvatarSize {
11 Xs,
13 Sm,
15 #[default]
17 Md,
18 Lg,
20 Xl,
22 Xxl,
24}
25
26impl AvatarSize {
27 fn size(&self) -> Pixels {
28 match self {
29 AvatarSize::Xs => px(20.0),
30 AvatarSize::Sm => px(24.0),
31 AvatarSize::Md => px(32.0),
32 AvatarSize::Lg => px(40.0),
33 AvatarSize::Xl => px(48.0),
34 AvatarSize::Xxl => px(64.0),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub enum AvatarShape {
42 #[default]
44 Circle,
45 Square,
47}
48
49pub struct Avatar {
51 name: Option<SharedString>,
52 src: Option<SharedString>,
53 size: AvatarSize,
54 shape: AvatarShape,
55 status: Option<AvatarStatus>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum AvatarStatus {
61 Online,
62 Offline,
63 Away,
64 Busy,
65}
66
67impl AvatarStatus {
68 fn color(&self) -> Rgba {
69 match self {
70 AvatarStatus::Online => rgb(0x2da44e),
71 AvatarStatus::Offline => rgb(0x666666),
72 AvatarStatus::Away => rgb(0xd29922),
73 AvatarStatus::Busy => rgb(0xcc3333),
74 }
75 }
76}
77
78impl Avatar {
79 pub fn new() -> Self {
81 Self {
82 name: None,
83 src: None,
84 size: AvatarSize::default(),
85 shape: AvatarShape::default(),
86 status: None,
87 }
88 }
89
90 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
92 self.name = Some(name.into());
93 self
94 }
95
96 pub fn src(mut self, src: impl Into<SharedString>) -> Self {
98 self.src = Some(src.into());
99 self
100 }
101
102 pub fn size(mut self, size: AvatarSize) -> Self {
104 self.size = size;
105 self
106 }
107
108 pub fn shape(mut self, shape: AvatarShape) -> Self {
110 self.shape = shape;
111 self
112 }
113
114 pub fn status(mut self, status: AvatarStatus) -> Self {
116 self.status = Some(status);
117 self
118 }
119
120 fn get_initials(&self) -> String {
122 if let Some(name) = &self.name {
123 name.split_whitespace()
124 .filter_map(|word| word.chars().next())
125 .take(2)
126 .collect::<String>()
127 .to_uppercase()
128 } else {
129 "?".to_string()
130 }
131 }
132
133 fn get_bg_color(&self) -> Rgba {
135 if let Some(name) = &self.name {
136 let hash: u32 = name.chars().fold(0u32, |acc, c| acc.wrapping_add(c as u32));
137 let colors = [
138 rgb(0x007acc), rgb(0x2da44e), rgb(0xd29922), rgb(0xcc3333), rgb(0x8b5cf6), rgb(0x06b6d4), rgb(0xf97316), rgb(0xec4899), ];
147 colors[(hash as usize) % colors.len()]
148 } else {
149 rgb(0x3a3a3a)
150 }
151 }
152
153 pub fn build(self) -> Div {
155 let size = self.size.size();
156 let initials = self.get_initials();
157 let bg_color = self.get_bg_color();
158
159 let mut avatar = div()
160 .relative()
161 .flex()
162 .items_center()
163 .justify_center()
164 .w(size)
165 .h(size)
166 .bg(bg_color)
167 .text_color(rgb(0xffffff))
168 .overflow_hidden();
169
170 match self.shape {
172 AvatarShape::Circle => {
173 avatar = avatar.rounded_full();
174 }
175 AvatarShape::Square => {
176 avatar = avatar.rounded_md();
177 }
178 }
179
180 avatar = match self.size {
182 AvatarSize::Xs => avatar.text_xs(),
183 AvatarSize::Sm => avatar.text_xs(),
184 AvatarSize::Md => avatar.text_sm(),
185 AvatarSize::Lg => avatar.text_sm(),
186 AvatarSize::Xl => avatar,
187 AvatarSize::Xxl => avatar.text_lg(),
188 };
189
190 if let Some(_src) = self.src {
192 avatar = avatar.child(initials);
195 } else {
196 avatar = avatar.font_weight(FontWeight::SEMIBOLD).child(initials);
197 }
198
199 if let Some(status) = self.status {
201 let status_size = match self.size {
202 AvatarSize::Xs | AvatarSize::Sm => px(6.0),
203 AvatarSize::Md | AvatarSize::Lg => px(8.0),
204 AvatarSize::Xl | AvatarSize::Xxl => px(10.0),
205 };
206
207 let status_indicator = div()
208 .absolute()
209 .bottom_0()
210 .right_0()
211 .w(status_size)
212 .h(status_size)
213 .rounded_full()
214 .bg(status.color())
215 .border_2()
216 .border_color(rgb(0x1e1e1e));
217
218 avatar = avatar.child(status_indicator);
219 }
220
221 avatar
222 }
223}
224
225impl Default for Avatar {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231impl IntoElement for Avatar {
232 type Element = Div;
233
234 fn into_element(self) -> Self::Element {
235 self.build()
236 }
237}
238
239pub struct AvatarGroup {
241 avatars: Vec<Avatar>,
242 max_display: usize,
243 size: AvatarSize,
244}
245
246impl AvatarGroup {
247 pub fn new() -> Self {
249 Self {
250 avatars: Vec::new(),
251 max_display: 4,
252 size: AvatarSize::default(),
253 }
254 }
255
256 pub fn avatars(mut self, avatars: Vec<Avatar>) -> Self {
258 self.avatars = avatars;
259 self
260 }
261
262 pub fn max_display(mut self, max: usize) -> Self {
264 self.max_display = max;
265 self
266 }
267
268 pub fn size(mut self, size: AvatarSize) -> Self {
270 self.size = size;
271 self
272 }
273
274 pub fn build(self) -> Div {
276 let size = self.size.size();
277 let overlap = size * 0.3;
278
279 let mut container = div().flex().items_center();
280
281 let display_count = self.avatars.len().min(self.max_display);
282 let remaining = self.avatars.len().saturating_sub(self.max_display);
283
284 for (i, avatar) in self.avatars.into_iter().take(display_count).enumerate() {
285 let avatar_el = avatar.size(self.size).build();
286 let mut wrapper = div().relative();
287
288 if i > 0 {
289 wrapper = wrapper.ml(-overlap);
290 }
291
292 wrapper = wrapper.child(avatar_el);
293 container = container.child(wrapper);
294 }
295
296 if remaining > 0 {
298 container = container.child(
299 div()
300 .ml(-overlap)
301 .flex()
302 .items_center()
303 .justify_center()
304 .w(size)
305 .h(size)
306 .rounded_full()
307 .bg(rgb(0x3a3a3a))
308 .text_color(rgb(0xcccccc))
309 .text_xs()
310 .font_weight(FontWeight::MEDIUM)
311 .child(format!("+{}", remaining)),
312 );
313 }
314
315 container
316 }
317}
318
319impl Default for AvatarGroup {
320 fn default() -> Self {
321 Self::new()
322 }
323}
324
325impl IntoElement for AvatarGroup {
326 type Element = Div;
327
328 fn into_element(self) -> Self::Element {
329 self.build()
330 }
331}