1use crate::render::{Cell, Modifier};
4use crate::style::Color;
5use crate::widget::traits::{RenderContext, View, WidgetProps};
6use crate::{impl_props_builders, impl_styled_view};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum AvatarSize {
11 Small,
13 #[default]
15 Medium,
16 Large,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum AvatarShape {
23 #[default]
25 Circle,
26 Square,
28 Rounded,
30}
31
32pub struct Avatar {
44 name: String,
46 initials: Option<String>,
48 size: AvatarSize,
50 shape: AvatarShape,
52 bg_color: Option<Color>,
54 fg_color: Option<Color>,
56 status: Option<Color>,
58 icon: Option<char>,
60 props: WidgetProps,
62}
63
64impl Avatar {
65 pub fn new(name: impl Into<String>) -> Self {
67 Self {
68 name: name.into(),
69 initials: None,
70 size: AvatarSize::Medium,
71 shape: AvatarShape::Circle,
72 bg_color: None,
73 fg_color: None,
74 status: None,
75 icon: None,
76 props: WidgetProps::new(),
77 }
78 }
79
80 pub fn from_initials(initials: impl Into<String>) -> Self {
82 Self {
83 name: String::new(),
84 initials: Some(initials.into()),
85 size: AvatarSize::Medium,
86 shape: AvatarShape::Circle,
87 bg_color: None,
88 fg_color: None,
89 status: None,
90 icon: None,
91 props: WidgetProps::new(),
92 }
93 }
94
95 pub fn from_icon(icon: char) -> Self {
97 Self {
98 name: String::new(),
99 initials: None,
100 size: AvatarSize::Medium,
101 shape: AvatarShape::Circle,
102 bg_color: None,
103 fg_color: None,
104 status: None,
105 icon: Some(icon),
106 props: WidgetProps::new(),
107 }
108 }
109
110 pub fn size(mut self, size: AvatarSize) -> Self {
112 self.size = size;
113 self
114 }
115
116 pub fn small(mut self) -> Self {
118 self.size = AvatarSize::Small;
119 self
120 }
121
122 pub fn medium(mut self) -> Self {
124 self.size = AvatarSize::Medium;
125 self
126 }
127
128 pub fn large(mut self) -> Self {
130 self.size = AvatarSize::Large;
131 self
132 }
133
134 pub fn shape(mut self, shape: AvatarShape) -> Self {
136 self.shape = shape;
137 self
138 }
139
140 pub fn circle(mut self) -> Self {
142 self.shape = AvatarShape::Circle;
143 self
144 }
145
146 pub fn square(mut self) -> Self {
148 self.shape = AvatarShape::Square;
149 self
150 }
151
152 pub fn rounded(mut self) -> Self {
154 self.shape = AvatarShape::Rounded;
155 self
156 }
157
158 pub fn bg(mut self, color: Color) -> Self {
160 self.bg_color = Some(color);
161 self
162 }
163
164 pub fn fg(mut self, color: Color) -> Self {
166 self.fg_color = Some(color);
167 self
168 }
169
170 pub fn colors(mut self, bg: Color, fg: Color) -> Self {
172 self.bg_color = Some(bg);
173 self.fg_color = Some(fg);
174 self
175 }
176
177 pub fn online(mut self) -> Self {
179 self.status = Some(Color::rgb(40, 200, 80));
180 self
181 }
182
183 pub fn offline(mut self) -> Self {
185 self.status = Some(Color::rgb(100, 100, 100));
186 self
187 }
188
189 pub fn away(mut self) -> Self {
191 self.status = Some(Color::rgb(200, 180, 40));
192 self
193 }
194
195 pub fn busy(mut self) -> Self {
197 self.status = Some(Color::rgb(200, 60, 60));
198 self
199 }
200
201 pub fn status(mut self, color: Color) -> Self {
203 self.status = Some(color);
204 self
205 }
206
207 pub fn icon(mut self, icon: char) -> Self {
209 self.icon = Some(icon);
210 self
211 }
212
213 fn get_initials(&self) -> String {
215 if let Some(ref initials) = self.initials {
216 return initials.clone();
217 }
218
219 if let Some(icon) = self.icon {
220 return icon.to_string();
221 }
222
223 self.name
225 .split_whitespace()
226 .filter_map(|word| word.chars().next())
227 .take(2)
228 .collect::<String>()
229 .to_uppercase()
230 }
231
232 fn get_bg_color(&self) -> Color {
234 if let Some(color) = self.bg_color {
235 return color;
236 }
237
238 let hash: u32 = self
240 .name
241 .bytes()
242 .fold(0u32, |acc, b| acc.wrapping_add(b as u32));
243 let hue = (hash % 360) as u8;
244
245 let h = hue as f32 / 60.0;
247 let s = 0.6_f32;
248 let l = 0.4_f32;
249
250 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
251 let x = c * (1.0 - ((h % 2.0) - 1.0).abs());
252 let m = l - c / 2.0;
253
254 let (r1, g1, b1) = match h as u8 {
255 0 => (c, x, 0.0),
256 1 => (x, c, 0.0),
257 2 => (0.0, c, x),
258 3 => (0.0, x, c),
259 4 => (x, 0.0, c),
260 _ => (c, 0.0, x),
261 };
262
263 Color::rgb(
264 ((r1 + m) * 255.0) as u8,
265 ((g1 + m) * 255.0) as u8,
266 ((b1 + m) * 255.0) as u8,
267 )
268 }
269}
270
271impl Default for Avatar {
272 fn default() -> Self {
273 Self::new("")
274 }
275}
276
277impl View for Avatar {
278 crate::impl_view_meta!("Avatar");
279
280 fn render(&self, ctx: &mut RenderContext) {
281 let area = ctx.area;
282 let initials = self.get_initials();
283 let bg = self.get_bg_color();
284 let fg = self.fg_color.unwrap_or(Color::WHITE);
285
286 match self.size {
287 AvatarSize::Small => {
288 let ch = initials.chars().next().unwrap_or('?');
290 let mut cell = Cell::new(ch);
291 cell.fg = Some(fg);
292 cell.bg = Some(bg);
293 cell.modifier |= Modifier::BOLD;
294 ctx.buffer.set(area.x, area.y, cell);
295
296 if let Some(status_color) = self.status {
298 let mut dot = Cell::new('●');
299 dot.fg = Some(status_color);
300 ctx.buffer.set(area.x + 1, area.y, dot);
301 }
302 }
303 AvatarSize::Medium => {
304 match self.shape {
306 AvatarShape::Circle => {
307 let mut left = Cell::new('◖');
309 left.fg = Some(bg);
310 ctx.buffer.set(area.x, area.y, left);
311
312 for (i, ch) in initials.chars().take(2).enumerate() {
313 let mut cell = Cell::new(ch);
314 cell.fg = Some(fg);
315 cell.bg = Some(bg);
316 cell.modifier |= Modifier::BOLD;
317 ctx.buffer.set(area.x + 1 + i as u16, area.y, cell);
318 }
319
320 let mut right = Cell::new('◗');
321 right.fg = Some(bg);
322 ctx.buffer.set(area.x + 3, area.y, right);
323
324 if let Some(status_color) = self.status {
326 let mut dot = Cell::new('●');
327 dot.fg = Some(status_color);
328 ctx.buffer.set(area.x + 4, area.y, dot);
329 }
330 }
331 AvatarShape::Square | AvatarShape::Rounded => {
332 let left = if self.shape == AvatarShape::Rounded {
334 '('
335 } else {
336 '['
337 };
338 let right = if self.shape == AvatarShape::Rounded {
339 ')'
340 } else {
341 ']'
342 };
343
344 let mut lc = Cell::new(left);
345 lc.fg = Some(bg);
346 ctx.buffer.set(area.x, area.y, lc);
347
348 for (i, ch) in initials.chars().take(2).enumerate() {
349 let mut cell = Cell::new(ch);
350 cell.fg = Some(fg);
351 cell.bg = Some(bg);
352 cell.modifier |= Modifier::BOLD;
353 ctx.buffer.set(area.x + 1 + i as u16, area.y, cell);
354 }
355
356 let mut rc = Cell::new(right);
357 rc.fg = Some(bg);
358 ctx.buffer.set(area.x + 3, area.y, rc);
359
360 if let Some(status_color) = self.status {
362 let mut dot = Cell::new('●');
363 dot.fg = Some(status_color);
364 ctx.buffer.set(area.x + 4, area.y, dot);
365 }
366 }
367 }
368 }
369 AvatarSize::Large => {
370 if area.height < 3 {
372 let mut cell = Cell::new(initials.chars().next().unwrap_or('?'));
374 cell.fg = Some(fg);
375 cell.bg = Some(bg);
376 ctx.buffer.set(area.x, area.y, cell);
377 return;
378 }
379
380 match self.shape {
381 AvatarShape::Circle => {
382 let chars_top = ['╭', '─', '─', '─', '╮'];
386 let chars_bot = ['╰', '─', '─', '─', '╯'];
387
388 for (i, ch) in chars_top.iter().enumerate() {
389 let mut cell = Cell::new(*ch);
390 cell.fg = Some(bg);
391 ctx.buffer.set(area.x + i as u16, area.y, cell);
392 }
393
394 let mut left = Cell::new('│');
396 left.fg = Some(bg);
397 ctx.buffer.set(area.x, area.y + 1, left);
398
399 let initials_chars: Vec<char> = initials.chars().collect();
401 for i in 1..4 {
402 let ch = if i == 1 || i == 2 {
403 initials_chars.get(i - 1).copied().unwrap_or(' ')
404 } else {
405 ' '
406 };
407 let mut cell = Cell::new(ch);
408 cell.fg = Some(fg);
409 cell.bg = Some(bg);
410 cell.modifier |= Modifier::BOLD;
411 ctx.buffer.set(area.x + i as u16, area.y + 1, cell);
412 }
413
414 let mut right = Cell::new('│');
415 right.fg = Some(bg);
416 ctx.buffer.set(area.x + 4, area.y + 1, right);
417
418 for (i, ch) in chars_bot.iter().enumerate() {
419 let mut cell = Cell::new(*ch);
420 cell.fg = Some(bg);
421 ctx.buffer.set(area.x + i as u16, area.y + 2, cell);
422 }
423
424 if let Some(status_color) = self.status {
426 let mut dot = Cell::new('●');
427 dot.fg = Some(status_color);
428 ctx.buffer.set(area.x + 5, area.y + 2, dot);
429 }
430 }
431 AvatarShape::Square => {
432 let chars_top = ['┌', '─', '─', '─', '┐'];
434 let chars_bot = ['└', '─', '─', '─', '┘'];
435
436 for (i, ch) in chars_top.iter().enumerate() {
437 let mut cell = Cell::new(*ch);
438 cell.fg = Some(bg);
439 ctx.buffer.set(area.x + i as u16, area.y, cell);
440 }
441
442 let mut left = Cell::new('│');
443 left.fg = Some(bg);
444 ctx.buffer.set(area.x, area.y + 1, left);
445
446 let initials_chars: Vec<char> = initials.chars().collect();
448 for i in 1..4 {
449 let ch = if i == 1 || i == 2 {
450 initials_chars.get(i - 1).copied().unwrap_or(' ')
451 } else {
452 ' '
453 };
454 let mut cell = Cell::new(ch);
455 cell.fg = Some(fg);
456 cell.bg = Some(bg);
457 cell.modifier |= Modifier::BOLD;
458 ctx.buffer.set(area.x + i as u16, area.y + 1, cell);
459 }
460
461 let mut right = Cell::new('│');
462 right.fg = Some(bg);
463 ctx.buffer.set(area.x + 4, area.y + 1, right);
464
465 for (i, ch) in chars_bot.iter().enumerate() {
466 let mut cell = Cell::new(*ch);
467 cell.fg = Some(bg);
468 ctx.buffer.set(area.x + i as u16, area.y + 2, cell);
469 }
470
471 if let Some(status_color) = self.status {
472 let mut dot = Cell::new('●');
473 dot.fg = Some(status_color);
474 ctx.buffer.set(area.x + 5, area.y + 2, dot);
475 }
476 }
477 AvatarShape::Rounded => {
478 let chars_top = ['╭', '─', '─', '─', '╮'];
480 let chars_bot = ['╰', '─', '─', '─', '╯'];
481
482 for (i, ch) in chars_top.iter().enumerate() {
483 let mut cell = Cell::new(*ch);
484 cell.fg = Some(bg);
485 ctx.buffer.set(area.x + i as u16, area.y, cell);
486 }
487
488 let mut left = Cell::new('│');
489 left.fg = Some(bg);
490 ctx.buffer.set(area.x, area.y + 1, left);
491
492 let initials_chars: Vec<char> = initials.chars().collect();
494 for i in 1..4 {
495 let ch = if i == 1 || i == 2 {
496 initials_chars.get(i - 1).copied().unwrap_or(' ')
497 } else {
498 ' '
499 };
500 let mut cell = Cell::new(ch);
501 cell.fg = Some(fg);
502 cell.bg = Some(bg);
503 cell.modifier |= Modifier::BOLD;
504 ctx.buffer.set(area.x + i as u16, area.y + 1, cell);
505 }
506
507 let mut right = Cell::new('│');
508 right.fg = Some(bg);
509 ctx.buffer.set(area.x + 4, area.y + 1, right);
510
511 for (i, ch) in chars_bot.iter().enumerate() {
512 let mut cell = Cell::new(*ch);
513 cell.fg = Some(bg);
514 ctx.buffer.set(area.x + i as u16, area.y + 2, cell);
515 }
516
517 if let Some(status_color) = self.status {
518 let mut dot = Cell::new('●');
519 dot.fg = Some(status_color);
520 ctx.buffer.set(area.x + 5, area.y + 2, dot);
521 }
522 }
523 }
524 }
525 }
526 }
527}
528
529impl_styled_view!(Avatar);
530impl_props_builders!(Avatar);
531
532pub fn avatar(name: impl Into<String>) -> Avatar {
534 Avatar::new(name)
535}
536
537pub fn avatar_icon(icon: char) -> Avatar {
539 Avatar::from_icon(icon)
540}
541
542#[cfg(test)]
546mod tests {
547 use super::*;
548
549 #[test]
550 fn test_avatar_new() {
551 let a = Avatar::new("John Doe");
552 assert_eq!(a.name, "John Doe");
553 assert_eq!(a.get_initials(), "JD");
554 }
555
556 #[test]
557 fn test_avatar_initials() {
558 let a = Avatar::new("Alice Bob Charlie");
559 assert_eq!(a.get_initials(), "AB"); let a = Avatar::new("SingleName");
562 assert_eq!(a.get_initials(), "S");
563
564 let a = Avatar::from_initials("XY");
565 assert_eq!(a.get_initials(), "XY");
566 }
567
568 #[test]
569 fn test_avatar_icon() {
570 let a = Avatar::from_icon('🤖');
571 assert_eq!(a.get_initials(), "🤖");
572 }
573
574 #[test]
575 fn test_avatar_sizes() {
576 let a = avatar("John").small();
577 assert_eq!(a.size, AvatarSize::Small);
578
579 let a = avatar("John").large();
580 assert_eq!(a.size, AvatarSize::Large);
581 }
582
583 #[test]
584 fn test_avatar_shapes() {
585 let a = avatar("John").circle();
586 assert_eq!(a.shape, AvatarShape::Circle);
587
588 let a = avatar("John").square();
589 assert_eq!(a.shape, AvatarShape::Square);
590 }
591
592 #[test]
593 fn test_avatar_status() {
594 let a = avatar("John").online();
595 assert!(a.status.is_some());
596
597 let a = avatar("John").busy();
598 assert!(a.status.is_some());
599 }
600
601 #[test]
602 fn test_avatar_color_generation() {
603 let a1 = Avatar::new("Alice");
604 let a2 = Avatar::new("Bob");
605
606 let c1 = a1.get_bg_color();
608 let c2 = a2.get_bg_color();
609 let _ = (c1, c2);
611 }
612
613 #[test]
614 fn test_helper_functions() {
615 let a = avatar("Test");
616 assert_eq!(a.name, "Test");
617
618 let a = avatar_icon('🎨');
619 assert!(a.icon.is_some());
620 }
621}