Skip to main content

frus_widgets/
avatar.rs

1//! [`CircleAvatar`]: a round pill of initials, or of colour, in the accent colours — for
2//! lists, headers and the like.
3
4use 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
13/// A circular avatar showing up to two initials.
14pub struct CircleAvatar {
15    initials: String,
16    color: Option<Color>,
17    size: f32,
18}
19
20impl CircleAvatar {
21    /// Creates an avatar from a text: its first two letters, upper-cased.
22    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    /// The background colour; the theme's accent otherwise.
43    pub fn color(mut self, color: Color) -> Self {
44        self.color = Some(color);
45        self
46    }
47
48    /// The size, that is, the diameter, in logical pixels.
49    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        // A circle: a fully rounded rectangle.
72        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        // `exact`: the initials are two fifths of the circle's diameter, which is
81        // geometry and not type. A reader who doubles the system font would have them
82        // spill out of a disc that did not move.
83        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}