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