gpui_component/avatar/
avatar.rs

1use gpui::{
2    div, img, prelude::FluentBuilder, App, Div, Hsla, ImageSource, InteractiveElement,
3    Interactivity, IntoElement, ParentElement as _, RenderOnce, SharedString, StyleRefinement,
4    Styled, Window,
5};
6
7use crate::{
8    avatar::{avatar_size, AvatarSized as _},
9    ActiveTheme, Colorize, Icon, IconName, Sizable, Size, StyledExt,
10};
11
12/// User avatar element.
13///
14/// We can use [`Sizable`] trait to set the size of the avatar (see also: [`avatar_size`] about the size in pixels).
15#[derive(IntoElement)]
16pub struct Avatar {
17    base: Div,
18    style: StyleRefinement,
19    src: Option<ImageSource>,
20    name: Option<SharedString>,
21    short_name: SharedString,
22    placeholder: Icon,
23    size: Size,
24}
25
26impl Avatar {
27    pub fn new() -> Self {
28        Self {
29            base: div(),
30            style: StyleRefinement::default(),
31            src: None,
32            name: None,
33            short_name: SharedString::default(),
34            placeholder: Icon::new(IconName::User),
35            size: Size::Medium,
36        }
37    }
38
39    /// Set to use image source for the avatar.
40    pub fn src(mut self, source: impl Into<ImageSource>) -> Self {
41        self.src = Some(source.into());
42        self
43    }
44
45    /// Set name of the avatar user, if `src` is none, will use this name as placeholder.
46    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
47        let name: SharedString = name.into();
48        let short: SharedString = extract_text_initials(&name).into();
49
50        self.name = Some(name);
51        self.short_name = short;
52        self
53    }
54
55    /// Set placeholder icon, default: [`IconName::User`]
56    pub fn placeholder(mut self, icon: impl Into<Icon>) -> Self {
57        self.placeholder = icon.into();
58        self
59    }
60}
61impl Sizable for Avatar {
62    fn with_size(mut self, size: impl Into<Size>) -> Self {
63        self.size = size.into();
64        self
65    }
66}
67impl Styled for Avatar {
68    fn style(&mut self) -> &mut StyleRefinement {
69        &mut self.style
70    }
71}
72
73impl InteractiveElement for Avatar {
74    fn interactivity(&mut self) -> &mut Interactivity {
75        self.base.interactivity()
76    }
77}
78
79impl RenderOnce for Avatar {
80    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
81        let corner_radii = self.style.corner_radii.clone();
82        let mut inner_style = StyleRefinement::default();
83        inner_style.corner_radii = corner_radii;
84
85        const COLOR_COUNT: u64 = 360 / 15;
86        fn default_color(ix: u64, cx: &mut App) -> Hsla {
87            let h = (ix * 15).clamp(0, 360) as f32;
88            cx.theme().blue.hue(h / 360.0)
89        }
90
91        const BG_OPACITY: f32 = 0.2;
92
93        self.base
94            .avatar_size(self.size)
95            .flex()
96            .items_center()
97            .justify_center()
98            .flex_shrink_0()
99            .rounded_full()
100            .overflow_hidden()
101            .bg(cx.theme().secondary)
102            .text_color(cx.theme().background)
103            .border_1()
104            .border_color(cx.theme().background)
105            .when(self.name.is_none() && self.src.is_none(), |this| {
106                this.text_size(avatar_size(self.size) * 0.6)
107                    .child(self.placeholder)
108            })
109            .map(|this| match self.src {
110                None => this.when(self.name.is_some(), |this| {
111                    let color_ix = gpui::hash(&self.short_name) % COLOR_COUNT;
112                    let color = default_color(color_ix, cx);
113
114                    this.bg(color.opacity(BG_OPACITY))
115                        .text_color(color)
116                        .child(div().avatar_text_size(self.size).child(self.short_name))
117                }),
118                Some(src) => this.child(
119                    img(src)
120                        .avatar_size(self.size)
121                        .rounded_full()
122                        .refine_style(&inner_style),
123                ),
124            })
125            .refine_style(&self.style)
126    }
127}
128
129fn extract_text_initials(text: &str) -> String {
130    let mut result = text
131        .split(" ")
132        .flat_map(|word| word.chars().next().map(|c| c.to_string()))
133        .take(2)
134        .collect::<Vec<String>>()
135        .join("");
136
137    if result.len() == 1 {
138        result = text.chars().take(2).collect::<String>();
139    }
140
141    result.to_uppercase()
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_avatar_text_initials() {
150        assert_eq!(extract_text_initials(&"Jason Lee"), "JL".to_string());
151        assert_eq!(extract_text_initials(&"Foo Bar Dar"), "FB".to_string());
152        assert_eq!(extract_text_initials(&"huacnlee"), "HU".to_string());
153    }
154}