gpui_component/avatar/
avatar.rs1use 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#[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 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}
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}