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