gpui_kit_material/components/
avatar.rs1use crate::{
14 color::ColorRole,
15 icons::{Icon, Symbol},
16 theme::color,
17 tokens::{
18 state,
19 typography::{TypeRole, rem},
20 },
21};
22use gpui::{prelude::*, *};
23
24pub const AVATAR_SIZE: f32 = 40.;
26
27#[allow(clippy::large_enum_variant)]
31#[derive(Clone)]
32pub enum AvatarContent {
33 Placeholder,
35 Monogram(SharedString),
37 Icon(Icon),
39 Image(ImageSource),
41}
42
43#[derive(gpui::IntoElement)]
45pub struct Avatar {
46 id: ElementId,
47 label: SharedString,
48 content: AvatarContent,
49 size: f32,
50 selected: bool,
51 disabled: bool,
52}
53
54impl Avatar {
55 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
57 Self {
58 id: id.into(),
59 label: label.into(),
60 content: AvatarContent::Placeholder,
61 size: AVATAR_SIZE,
62 selected: false,
63 disabled: false,
64 }
65 }
66 pub fn content(mut self, content: AvatarContent) -> Self {
67 self.content = content;
68 self
69 }
70 pub fn monogram(self, initials: impl Into<SharedString>) -> Self {
72 self.content(AvatarContent::Monogram(initials.into()))
73 }
74 pub fn icon(self, icon: impl Into<Icon>) -> Self {
75 self.content(AvatarContent::Icon(icon.into()))
76 }
77 pub fn image(self, source: impl Into<ImageSource>) -> Self {
78 self.content(AvatarContent::Image(source.into()))
79 }
80 pub fn size(mut self, size: f32) -> Self {
83 self.size = if size.is_finite() && size > 0. {
84 size
85 } else {
86 AVATAR_SIZE
87 };
88 self
89 }
90 pub fn selected(mut self, selected: bool) -> Self {
92 self.selected = selected;
93 self
94 }
95 pub fn disabled(mut self, disabled: bool) -> Self {
98 self.disabled = disabled;
99 self
100 }
101
102 pub fn colors(&self) -> (Rgba, Rgba) {
104 if self.disabled {
105 let on_surface = color(ColorRole::ON_SURFACE);
106 (
107 on_surface.opacity(state::DISABLED_CONTAINER),
108 on_surface.opacity(state::DISABLED_CONTENT),
109 )
110 } else {
111 (
112 color(ColorRole::PRIMARY_CONTAINER),
113 color(ColorRole::ON_PRIMARY_CONTAINER),
114 )
115 }
116 }
117}
118
119pub fn monogram_initials(text: &str) -> SharedString {
121 text.trim()
122 .chars()
123 .filter(|c| !c.is_whitespace())
124 .take(2)
125 .flat_map(char::to_uppercase)
126 .collect::<String>()
127 .into()
128}
129
130fn symbol(name: &str) -> Icon {
131 Icon::from(Symbol::named(name).expect("a Material Symbol in the pinned catalog"))
132}
133
134impl RenderOnce for Avatar {
135 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
136 let (container, ink) = self.colors();
137 let size = self.size;
138 let scale = size / AVATAR_SIZE;
140 let glyph = |icon: Icon, design: f32| icon.size(rem(design * scale)).color(ink);
141 let content: AnyElement = if self.selected {
142 glyph(symbol("check"), 24.).into_any_element()
143 } else {
144 match self.content {
145 AvatarContent::Placeholder => glyph(symbol("person"), 28.).into_any_element(),
146 AvatarContent::Icon(icon) => glyph(icon, 24.).into_any_element(),
147 AvatarContent::Monogram(text) => {
148 let type_scale = TypeRole::TitleMedium.scale();
149 div()
150 .font_family(type_scale.font_family)
151 .font_weight(FontWeight(type_scale.font_weight as f32))
152 .text_size(rem(16. * scale))
153 .line_height(rem(24. * scale))
154 .text_color(ink)
155 .child(monogram_initials(&text))
156 .into_any_element()
157 }
158 AvatarContent::Image(source) => img(source)
159 .size_full()
160 .object_fit(ObjectFit::Cover)
161 .rounded_full()
162 .when(self.disabled, |image| {
163 image.opacity(state::DISABLED_CONTENT)
164 })
165 .into_any_element(),
166 }
167 };
168 div()
169 .id(self.id)
170 .role(Role::Image)
171 .aria_label(self.label)
172 .flex()
173 .flex_shrink_0()
174 .items_center()
175 .justify_center()
176 .size(rem(size))
177 .rounded_full()
178 .overflow_hidden()
179 .bg(container)
180 .child(content)
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use core::prelude::v1::test;
188
189 #[test]
190 fn monograms_draw_two_uppercase_initials() {
191 assert_eq!(monogram_initials("a"), "A");
192 assert_eq!(monogram_initials(" jd "), "JD");
193 assert_eq!(monogram_initials("j d x"), "JD");
194 assert_eq!(monogram_initials("ßa"), "SSA");
195 assert_eq!(monogram_initials(""), "");
196 }
197
198 #[test]
199 fn sizes_fall_back_to_the_kit_diameter() {
200 assert_eq!(Avatar::new("a", "A").size, AVATAR_SIZE);
201 assert_eq!(Avatar::new("a", "A").size(56.).size, 56.);
202 assert_eq!(Avatar::new("a", "A").size(0.).size, AVATAR_SIZE);
203 assert_eq!(Avatar::new("a", "A").size(f32::NAN).size, AVATAR_SIZE);
204 }
205
206 #[test]
207 fn colors_follow_primary_container_and_disabled_roles() {
208 let (container, ink) = Avatar::new("a", "A").colors();
209 assert_eq!(container, color(ColorRole::PRIMARY_CONTAINER));
210 assert_eq!(ink, color(ColorRole::ON_PRIMARY_CONTAINER));
211 let (container, ink) = Avatar::new("a", "A").disabled(true).colors();
212 let on_surface = color(ColorRole::ON_SURFACE);
213 assert_eq!(container, on_surface.opacity(state::DISABLED_CONTAINER));
214 assert_eq!(ink, on_surface.opacity(state::DISABLED_CONTENT));
215 }
216}