1use crate::prelude::*;
2
3use documented::Documented;
4use gpui::{AnyElement, Hsla, ImageSource, Img, IntoElement, Styled, img};
5
6/// An element that renders a user avatar with customizable appearance options.
7///
8/// # Examples
9///
10/// ```
11/// use ui::Avatar;
12///
13/// Avatar::new("path/to/image.png")
14/// .grayscale(true)
15/// .border_color(gpui::red());
16/// ```
17#[derive(IntoElement, Documented, RegisterComponent)]
18pub struct Avatar {
19 image: Img,
20 size: Option<AbsoluteLength>,
21 border_color: Option<Hsla>,
22 indicator: Option<AnyElement>,
23}
24
25impl Avatar {
26 /// Creates a new avatar element with the specified image source.
27 pub fn new(src: impl Into<ImageSource>) -> Self {
28 Avatar {
29 image: img(src),
30 size: None,
31 border_color: None,
32 indicator: None,
33 }
34 }
35
36 /// Applies a grayscale filter to the avatar image.
37 ///
38 /// # Examples
39 ///
40 /// ```
41 /// use ui::Avatar;
42 ///
43 /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
44 /// ```
45 pub fn grayscale(mut self, grayscale: bool) -> Self {
46 self.image = self.image.grayscale(grayscale);
47 self
48 }
49
50 /// Sets the border color of the avatar.
51 ///
52 /// This might be used to match the border to the background color of
53 /// the parent element to create the illusion of cropping another
54 /// shape underneath (for example in face piles.)
55 pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
56 self.border_color = Some(color.into());
57 self
58 }
59
60 /// Size overrides the avatar size. By default they are 1rem.
61 pub fn size<L: Into<AbsoluteLength>>(mut self, size: impl Into<Option<L>>) -> Self {
62 self.size = size.into().map(Into::into);
63 self
64 }
65
66 /// Sets the current indicator to be displayed on the avatar, if any.
67 pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
68 self.indicator = indicator.into().map(IntoElement::into_any_element);
69 self
70 }
71}
72
73impl RenderOnce for Avatar {
74 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
75 let border_width = if self.border_color.is_some() {
76 px(2.)
77 } else {
78 px(0.)
79 };
80
81 let image_size = self.size.unwrap_or_else(|| rems(1.).into());
82 let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.;
83
84 div()
85 .size(container_size)
86 .rounded_full()
87 .when_some(self.border_color, |this, color| {
88 this.border(border_width).border_color(color)
89 })
90 .child(
91 self.image
92 .size(image_size)
93 .rounded_full()
94 .bg(cx.theme().colors().ghost_element_background)
95 .with_fallback(|| {
96 Icon::new(IconName::Person)
97 .color(Color::Muted)
98 .into_any_element()
99 }),
100 )
101 .children(self.indicator.map(|indicator| div().child(indicator)))
102 }
103}
104
105use gpui::AnyView;
106
107/// The audio status of an player, for use in representing
108/// their status visually on their avatar.
109#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
110pub enum AudioStatus {
111 /// The player's microphone is muted.
112 Muted,
113 /// The player's microphone is muted, and collaboration audio is disabled.
114 Deafened,
115}
116
117/// An indicator that shows the audio status of a player.
118#[derive(IntoElement)]
119pub struct AvatarAudioStatusIndicator {
120 audio_status: AudioStatus,
121 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
122}
123
124impl AvatarAudioStatusIndicator {
125 /// Creates a new `AvatarAudioStatusIndicator`
126 pub fn new(audio_status: AudioStatus) -> Self {
127 Self {
128 audio_status,
129 tooltip: None,
130 }
131 }
132
133 /// Sets the tooltip for the indicator.
134 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
135 self.tooltip = Some(Box::new(tooltip));
136 self
137 }
138}
139
140impl RenderOnce for AvatarAudioStatusIndicator {
141 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
142 let icon_size = IconSize::Indicator;
143
144 let width_in_px = icon_size.rems() * window.rem_size();
145 let padding_x = px(4.);
146
147 div()
148 .absolute()
149 .bottom(rems_from_px(-3.))
150 .right(rems_from_px(-6.))
151 .w(width_in_px + padding_x)
152 .h(icon_size.rems())
153 .child(
154 h_flex()
155 .id("muted-indicator")
156 .justify_center()
157 .px(padding_x)
158 .py(px(2.))
159 .bg(cx.theme().status().error_background)
160 .rounded_sm()
161 .child(
162 Icon::new(match self.audio_status {
163 AudioStatus::Muted => IconName::MicMute,
164 AudioStatus::Deafened => IconName::AudioOff,
165 })
166 .size(icon_size)
167 .color(Color::Error),
168 )
169 .when_some(self.tooltip, |this, tooltip| {
170 this.tooltip(move |window, cx| tooltip(window, cx))
171 }),
172 )
173 }
174}
175
176/// Represents the availability status of a collaborator.
177#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
178pub enum CollaboratorAvailability {
179 Free,
180 Busy,
181}
182
183/// Represents the availability and presence status of a collaborator.
184#[derive(IntoElement)]
185pub struct AvatarAvailabilityIndicator {
186 availability: CollaboratorAvailability,
187 avatar_size: Option<Pixels>,
188}
189
190impl AvatarAvailabilityIndicator {
191 /// Creates a new indicator
192 pub fn new(availability: CollaboratorAvailability) -> Self {
193 Self {
194 availability,
195 avatar_size: None,
196 }
197 }
198
199 /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on.
200 pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
201 self.avatar_size = size.into();
202 self
203 }
204}
205
206impl RenderOnce for AvatarAvailabilityIndicator {
207 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
208 let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size());
209
210 // HACK: non-integer sizes result in oval indicators.
211 let indicator_size = (avatar_size * 0.4).round();
212
213 div()
214 .absolute()
215 .bottom_0()
216 .right_0()
217 .size(indicator_size)
218 .rounded(indicator_size)
219 .bg(match self.availability {
220 CollaboratorAvailability::Free => cx.theme().status().created,
221 CollaboratorAvailability::Busy => cx.theme().status().deleted,
222 })
223 }
224}
225
226// View this component preview using `workspace: open component-preview`
227impl Component for Avatar {
228 fn scope() -> ComponentScope {
229 ComponentScope::Collaboration
230 }
231
232 fn description() -> Option<&'static str> {
233 Some(Avatar::DOCS)
234 }
235
236 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
237 let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
238
239 Some(
240 v_flex()
241 .gap_6()
242 .children(vec![
243 example_group(vec![
244 single_example("Default", Avatar::new(example_avatar).into_any_element()),
245 single_example(
246 "Grayscale",
247 Avatar::new(example_avatar)
248 .grayscale(true)
249 .into_any_element(),
250 ),
251 single_example(
252 "Border",
253 Avatar::new(example_avatar)
254 .border_color(cx.theme().colors().border)
255 .into_any_element(),
256 ).description("Can be used to create visual space by setting the border color to match the background, which creates the appearance of a gap around the avatar."),
257 ]),
258 example_group_with_title(
259 "Indicator Styles",
260 vec![
261 single_example(
262 "Muted",
263 Avatar::new(example_avatar)
264 .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted))
265 .into_any_element(),
266 ).description("Indicates the collaborator's mic is muted."),
267 single_example(
268 "Deafened",
269 Avatar::new(example_avatar)
270 .indicator(AvatarAudioStatusIndicator::new(
271 AudioStatus::Deafened,
272 ))
273 .into_any_element(),
274 ).description("Indicates that both the collaborator's mic and audio are muted."),
275 single_example(
276 "Availability: Free",
277 Avatar::new(example_avatar)
278 .indicator(AvatarAvailabilityIndicator::new(
279 CollaboratorAvailability::Free,
280 ))
281 .into_any_element(),
282 ).description("Indicates that the person is free, usually meaning they are not in a call."),
283 single_example(
284 "Availability: Busy",
285 Avatar::new(example_avatar)
286 .indicator(AvatarAvailabilityIndicator::new(
287 CollaboratorAvailability::Busy,
288 ))
289 .into_any_element(),
290 ).description("Indicates that the person is busy, usually meaning they are in a channel or direct call."),
291 ],
292 ),
293 ])
294 .into_any_element(),
295 )
296 }
297}