gpui_kit/display/
avatar.rs1use gpui::{
4 App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
5 prelude::FluentBuilder, px,
6};
7use gpui_kit_semantics::{NodeSpec, Role, Semantic};
8use gpui_kit_theme::ActiveTheme;
9
10use crate::foundation::Ident;
11
12#[derive(Debug, IntoElement)]
17pub struct Avatar {
18 ident: Option<Ident>,
19 name: SharedString,
20 image: Option<SharedString>,
21 size: f32,
22}
23
24impl Avatar {
25 pub fn new(name: impl Into<SharedString>) -> Self {
26 Self {
27 ident: None,
28 name: name.into(),
29 image: None,
30 size: 28.0,
31 }
32 }
33
34 pub fn id(mut self, ident: impl Into<Ident>) -> Self {
36 self.ident = Some(ident.into());
37 self
38 }
39
40 pub fn image(mut self, image: impl Into<SharedString>) -> Self {
42 self.image = Some(image.into());
43 self
44 }
45
46 pub fn size(mut self, size: f32) -> Self {
47 self.size = size;
48 self
49 }
50
51 fn initials(&self) -> SharedString {
53 let letters: String = self
54 .name
55 .split_whitespace()
56 .filter_map(|word| word.chars().next())
57 .take(2)
58 .flat_map(|letter| letter.to_uppercase())
59 .collect();
60 SharedString::from(letters)
61 }
62}
63
64impl RenderOnce for Avatar {
65 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
66 let theme = cx.theme().clone();
67 let initials = self.initials();
68 let spec = self
69 .ident
70 .as_ref()
71 .map(|ident| NodeSpec::new(ident.semantic_id(), Role::Image).text(self.name.clone()));
72
73 let element = div()
74 .size(px(self.size))
75 .flex_none()
76 .flex()
77 .items_center()
78 .justify_center()
79 .overflow_hidden()
80 .rounded_full()
81 .bg(theme.colors.raised)
82 .border(px(theme.borders.hairline))
83 .border_color(theme.colors.hairline)
84 .text_size(px(self.size * 0.36))
85 .text_color(theme.colors.text_muted)
86 .when_some(self.image.clone(), |element, source| {
87 element.child(gpui::img(source).size(px(self.size)))
88 })
89 .when(self.image.is_none() && !initials.is_empty(), |element| {
90 element.child(initials.clone())
91 });
92 match spec {
93 Some(spec) => element.semantic_in(cx, spec).into_any_element(),
94 None => element.into_any_element(),
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn initials_take_at_most_two_words() {
105 assert_eq!(Avatar::new("Ada Lovelace King").initials().as_ref(), "AL");
106 assert_eq!(Avatar::new("ada").initials().as_ref(), "A");
107 }
108
109 #[test]
110 fn a_nameless_avatar_invents_no_letter() {
111 assert_eq!(Avatar::new(" ").initials().as_ref(), "");
112 }
113}