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