1use frus_core::{Color, Point, Rect, ResolvedTextStyle, Scene};
5use frus_layout::{Dimension, Style};
6
7use crate::interaction::Status;
8use crate::theme::Theme;
9use crate::widget::Widget;
10
11const SIZE: f32 = 36.0;
12
13pub struct CircleAvatar {
15 initials: String,
16 color: Option<Color>,
17 size: f32,
18}
19
20impl CircleAvatar {
21 pub fn new(initials: impl Into<String>) -> Self {
23 let source: String = initials.into();
24 let initials: String = source
25 .split_whitespace()
26 .filter_map(|word| word.chars().next())
27 .take(2)
28 .collect::<String>()
29 .to_uppercase();
30 let initials = if initials.is_empty() {
31 source.chars().take(2).collect::<String>().to_uppercase()
32 } else {
33 initials
34 };
35 Self {
36 initials,
37 color: None,
38 size: SIZE,
39 }
40 }
41
42 pub fn color(mut self, color: Color) -> Self {
44 self.color = Some(color);
45 self
46 }
47
48 pub fn size(mut self, size: f32) -> Self {
50 self.size = size;
51 self
52 }
53}
54
55impl<Msg> Widget<Msg> for CircleAvatar {
56 fn style(&self) -> Style {
57 Style {
58 width: Dimension::Length(self.size),
59 height: Dimension::Length(self.size),
60 ..Default::default()
61 }
62 }
63
64 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
65 &[]
66 }
67
68 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
69 let o = status.opacity;
70 let bg = self.color.unwrap_or(theme.primary);
71 scene.draw_rect(
73 bounds,
74 bg.fade(o),
75 bounds.height * 0.5,
76 0.0,
77 Color::TRANSPARENT,
78 );
79 let fsize = self.size * 0.4;
80 let style = ResolvedTextStyle::exact(fsize);
84 let measured = frus_text::measure_resolved(&self.initials, &style);
85 scene.text(
86 Point::new(
87 bounds.x + (bounds.width - measured.width) * 0.5,
88 bounds.y + (bounds.height - measured.height) * 0.5,
89 ),
90 self.initials.clone(),
91 &style,
92 theme.on_primary.fade(o),
93 );
94 }
95
96 fn on_click(&self) -> Option<Msg> {
97 None
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use frus_core::Primitive;
105
106 #[test]
107 fn takes_two_uppercased_initials() {
108 let a = CircleAvatar::new("ada lovelace");
109 assert_eq!(a.initials, "AL");
110 let b = CircleAvatar::new("bob");
111 assert_eq!(b.initials, "B");
112 }
113
114 #[test]
115 fn paints_circle_and_initials() {
116 let a = CircleAvatar::new("Zoe");
117 let mut scene = Scene::new();
118 Widget::<()>::paint(
119 &a,
120 Rect::new(0.0, 0.0, 36.0, 36.0),
121 Status::default(),
122 &Theme::default(),
123 &mut scene,
124 );
125 assert!(scene
126 .primitives()
127 .iter()
128 .any(|p| matches!(p, Primitive::Rect { .. })));
129 assert!(scene
130 .primitives()
131 .iter()
132 .any(|p| matches!(p, Primitive::Text { text, .. } if text == "Z")));
133 }
134}