gpui_component/avatar/
avatar.rs1use gpui::{
2 App, Hsla, ImageSource, InteractiveElement, Interactivity, IntoElement, ParentElement as _,
3 RenderOnce, SharedString, StyleRefinement, Styled, Window, div, prelude::FluentBuilder,
4};
5use gpui_base::{Avatar as BaseAvatar, AvatarFallback, AvatarImage};
6
7use crate::{
8 ActiveTheme, Colorize, Icon, IconName, Sizable, Size, StyledExt, ThemeStyled as _,
9 avatar::{AvatarSized as _, avatar_size},
10};
11
12#[derive(IntoElement)]
16pub struct Avatar {
17 base: BaseAvatar,
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: BaseAvatar::new(),
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 pub fn src(mut self, source: impl Into<ImageSource>) -> Self {
41 self.src = Some(source.into());
42 self
43 }
44
45 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 pub fn placeholder(mut self, icon: impl Into<Icon>) -> Self {
57 self.placeholder = icon.into();
58 self
59 }
60}
61
62impl Sizable for Avatar {
63 fn with_size(mut self, size: impl Into<Size>) -> Self {
64 self.size = size.into();
65 self
66 }
67}
68
69impl Styled for Avatar {
70 fn style(&mut self) -> &mut StyleRefinement {
71 &mut self.style
72 }
73}
74
75impl InteractiveElement for Avatar {
76 fn interactivity(&mut self) -> &mut Interactivity {
77 self.base.interactivity()
78 }
79}
80
81impl RenderOnce for Avatar {
82 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
83 let corner_radii = self.style.corner_radii.clone();
84 let mut inner_style = StyleRefinement::default();
85 inner_style.corner_radii = corner_radii;
86
87 const COLOR_COUNT: u64 = 360 / 15;
88 fn default_color(ix: u64, cx: &mut App) -> Hsla {
89 let h = (ix * 15).clamp(0, 360) as f32;
90 cx.theme().blue.hue(h / 360.0)
91 }
92
93 const BG_OPACITY: f32 = 0.2;
94
95 let fallback = AvatarFallback::new()
96 .size_full()
97 .flex()
98 .items_center()
99 .justify_center()
100 .rounded_full_style(cx)
101 .overflow_hidden()
102 .when(self.name.is_none(), |this| {
103 this.text_size(avatar_size(self.size) * 0.6)
104 .child(self.placeholder)
105 })
106 .when(self.name.is_some(), |this| {
107 let color_ix = gpui::hash(&self.short_name) % COLOR_COUNT;
108 let color = default_color(color_ix, cx);
109 this.bg(color.opacity(BG_OPACITY))
110 .text_color(color)
111 .child(div().avatar_text_size(self.size).child(self.short_name))
112 })
113 .refine_style(&inner_style);
114
115 self.base
116 .size(avatar_size(self.size))
117 .flex_shrink_0()
118 .rounded_full_style(cx)
119 .overflow_hidden()
120 .bg(cx.theme().tokens.secondary)
121 .text_color(cx.theme().background)
122 .border_1()
123 .border_color(cx.theme().border)
124 .fallback(fallback)
125 .when_some(self.src, |this, src| {
126 this.image(
127 AvatarImage::new(src)
128 .size_full()
129 .rounded_full_style(cx)
130 .refine_style(&inner_style),
131 )
132 })
133 .refine_style(&self.style)
134 }
135}
136
137fn extract_text_initials(text: &str) -> String {
138 let mut result = text
139 .split(" ")
140 .flat_map(|word| word.chars().next().map(|c| c.to_string()))
141 .take(2)
142 .collect::<Vec<String>>()
143 .join("");
144
145 if result.len() == 1 {
146 result = text.chars().take(2).collect::<String>();
147 }
148
149 result.to_uppercase()
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_avatar_text_initials() {
158 assert_eq!(extract_text_initials(&"Jason Lee"), "JL".to_string());
159 assert_eq!(extract_text_initials(&"Foo Bar Dar"), "FB".to_string());
160 assert_eq!(extract_text_initials(&"huacnlee"), "HU".to_string());
161 }
162
163 #[gpui::test]
164 fn test_avatar_builder(_cx: &mut gpui::TestAppContext) {
165 let avatar = Avatar::new()
166 .name("Jason Lee")
167 .placeholder(Icon::new(IconName::User))
168 .large();
169
170 assert_eq!(avatar.name, Some(SharedString::from("Jason Lee")));
171 assert_eq!(avatar.short_name, SharedString::from("JL"));
172 assert_eq!(avatar.size, Size::Large);
173 }
174}