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().element_disabled)
95 .with_fallback(|| {
96 h_flex()
97 .size_full()
98 .justify_center()
99 .child(
100 Icon::new(IconName::Person)
101 .color(Color::Muted)
102 .size(IconSize::Small),
103 )
104 .into_any_element()
105 }),
106 )
107 .children(self.indicator.map(|indicator| div().child(indicator)))
108 }
109}
110
111use gpui::AnyView;
112
113/// The audio status of an player, for use in representing
114/// their status visually on their avatar.
115#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
116pub enum AudioStatus {
117 /// The player's microphone is muted.
118 Muted,
119 /// The player's microphone is muted, and collaboration audio is disabled.
120 Deafened,
121}
122
123/// An indicator that shows the audio status of a player.
124#[derive(IntoElement)]
125pub struct AvatarAudioStatusIndicator {
126 audio_status: AudioStatus,
127 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
128}
129
130impl AvatarAudioStatusIndicator {
131 /// Creates a new `AvatarAudioStatusIndicator`
132 pub fn new(audio_status: AudioStatus) -> Self {
133 Self {
134 audio_status,
135 tooltip: None,
136 }
137 }
138
139 /// Sets the tooltip for the indicator.
140 pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
141 self.tooltip = Some(Box::new(tooltip));
142 self
143 }
144}
145
146impl RenderOnce for AvatarAudioStatusIndicator {
147 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
148 let icon_size = IconSize::Indicator;
149
150 let width_in_px = icon_size.rems() * window.rem_size();
151 let padding_x = px(4.);
152
153 div()
154 .absolute()
155 .bottom(rems_from_px(-3.))
156 .right(rems_from_px(-6.))
157 .w(width_in_px + padding_x)
158 .h(icon_size.rems())
159 .child(
160 h_flex()
161 .id("muted-indicator")
162 .justify_center()
163 .px(padding_x)
164 .py(px(2.))
165 .bg(cx.theme().status().error_background)
166 .rounded_sm()
167 .child(
168 Icon::new(match self.audio_status {
169 AudioStatus::Muted => IconName::MicMute,
170 AudioStatus::Deafened => IconName::AudioOff,
171 })
172 .size(icon_size)
173 .color(Color::Error),
174 )
175 .when_some(self.tooltip, |this, tooltip| {
176 this.tooltip(move |window, cx| tooltip(window, cx))
177 }),
178 )
179 }
180}
181
182/// Represents the availability status of a collaborator.
183#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
184pub enum CollaboratorAvailability {
185 Free,
186 Busy,
187}
188
189/// Represents the availability and presence status of a collaborator.
190#[derive(IntoElement)]
191pub struct AvatarAvailabilityIndicator {
192 availability: CollaboratorAvailability,
193 avatar_size: Option<Pixels>,
194}
195
196impl AvatarAvailabilityIndicator {
197 /// Creates a new indicator
198 pub fn new(availability: CollaboratorAvailability) -> Self {
199 Self {
200 availability,
201 avatar_size: None,
202 }
203 }
204
205 /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on.
206 pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
207 self.avatar_size = size.into();
208 self
209 }
210}
211
212impl RenderOnce for AvatarAvailabilityIndicator {
213 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
214 let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size());
215
216 // HACK: non-integer sizes result in oval indicators.
217 let indicator_size = (avatar_size * 0.4).round();
218
219 div()
220 .absolute()
221 .bottom_0()
222 .right_0()
223 .size(indicator_size)
224 .rounded(indicator_size)
225 .bg(match self.availability {
226 CollaboratorAvailability::Free => cx.theme().status().created,
227 CollaboratorAvailability::Busy => cx.theme().status().deleted,
228 })
229 }
230}
231
232// View this component preview using `workspace: open component-preview`
233impl Component for Avatar {
234 fn scope() -> ComponentScope {
235 ComponentScope::Collaboration
236 }
237
238 fn description() -> Option<&'static str> {
239 Some(Avatar::DOCS)
240 }
241
242 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
243 let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
244
245 Some(
246 v_flex()
247 .gap_6()
248 .children(vec![
249 example_group(vec![
250 single_example("Default", Avatar::new(example_avatar).into_any_element()),
251 single_example(
252 "Grayscale",
253 Avatar::new(example_avatar)
254 .grayscale(true)
255 .into_any_element(),
256 ),
257 single_example(
258 "Border",
259 Avatar::new(example_avatar)
260 .border_color(cx.theme().colors().border)
261 .into_any_element(),
262 ).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."),
263 ]),
264 example_group_with_title(
265 "Indicator Styles",
266 vec![
267 single_example(
268 "Muted",
269 Avatar::new(example_avatar)
270 .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted))
271 .into_any_element(),
272 ).description("Indicates the collaborator's mic is muted."),
273 single_example(
274 "Deafened",
275 Avatar::new(example_avatar)
276 .indicator(AvatarAudioStatusIndicator::new(
277 AudioStatus::Deafened,
278 ))
279 .into_any_element(),
280 ).description("Indicates that both the collaborator's mic and audio are muted."),
281 single_example(
282 "Availability: Free",
283 Avatar::new(example_avatar)
284 .indicator(AvatarAvailabilityIndicator::new(
285 CollaboratorAvailability::Free,
286 ))
287 .into_any_element(),
288 ).description("Indicates that the person is free, usually meaning they are not in a call."),
289 single_example(
290 "Availability: Busy",
291 Avatar::new(example_avatar)
292 .indicator(AvatarAvailabilityIndicator::new(
293 CollaboratorAvailability::Busy,
294 ))
295 .into_any_element(),
296 ).description("Indicates that the person is busy, usually meaning they are in a channel or direct call."),
297 ],
298 ),
299 ])
300 .into_any_element(),
301 )
302 }
303}