avatar.rs

  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 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}