Detailed changes
@@ -31,8 +31,8 @@ use smallvec::SmallVec;
use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use ui::{
- prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
- ListHeader, ListItem, Tooltip,
+ prelude::*, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Icon, IconButton,
+ IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
};
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@@ -2000,43 +2000,49 @@ impl CollabPanel {
let busy = contact.busy || calling;
let user_id = contact.user.id;
let github_login = SharedString::from(contact.user.github_login.clone());
- let item =
- ListItem::new(github_login.clone())
- .indent_level(1)
- .indent_step_size(px(20.))
- .selected(is_selected)
- .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
- .child(
- h_flex()
- .w_full()
- .justify_between()
- .child(Label::new(github_login.clone()))
- .when(calling, |el| {
- el.child(Label::new("Calling").color(Color::Muted))
- })
- .when(!calling, |el| {
- el.child(
- IconButton::new("remove_contact", IconName::Close)
- .icon_color(Color::Muted)
- .visible_on_hover("")
- .tooltip(|cx| Tooltip::text("Remove Contact", cx))
- .on_click(cx.listener({
- let github_login = github_login.clone();
- move |this, _, cx| {
- this.remove_contact(user_id, &github_login, cx);
- }
- })),
- )
- }),
- )
- .start_slot(
- // todo handle contacts with no avatar
- Avatar::new(contact.user.avatar_uri.clone())
- .availability_indicator(if online { Some(!busy) } else { None }),
- )
- .when(online && !busy, |el| {
- el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
- });
+ let item = ListItem::new(github_login.clone())
+ .indent_level(1)
+ .indent_step_size(px(20.))
+ .selected(is_selected)
+ .on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+ .child(
+ h_flex()
+ .w_full()
+ .justify_between()
+ .child(Label::new(github_login.clone()))
+ .when(calling, |el| {
+ el.child(Label::new("Calling").color(Color::Muted))
+ })
+ .when(!calling, |el| {
+ el.child(
+ IconButton::new("remove_contact", IconName::Close)
+ .icon_color(Color::Muted)
+ .visible_on_hover("")
+ .tooltip(|cx| Tooltip::text("Remove Contact", cx))
+ .on_click(cx.listener({
+ let github_login = github_login.clone();
+ move |this, _, cx| {
+ this.remove_contact(user_id, &github_login, cx);
+ }
+ })),
+ )
+ }),
+ )
+ .start_slot(
+ // todo handle contacts with no avatar
+ Avatar::new(contact.user.avatar_uri.clone())
+ .indicator::<AvatarAvailabilityIndicator>(if online {
+ Some(AvatarAvailabilityIndicator::new(match busy {
+ true => ui::Availability::Busy,
+ false => ui::Availability::Free,
+ }))
+ } else {
+ None
+ }),
+ )
+ .when(online && !busy, |el| {
+ el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
+ });
div()
.id(github_login.clone())
@@ -1,135 +1,5 @@
-use crate::prelude::*;
-use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled};
+mod avatar;
+mod avatar_availability_indicator;
-/// The shape of an [`Avatar`].
-#[derive(Debug, Default, PartialEq, Clone)]
-pub enum AvatarShape {
- /// The avatar is shown in a circle.
- #[default]
- Circle,
- /// The avatar is shown in a rectangle with rounded corners.
- RoundedRectangle,
-}
-
-/// An element that renders a user avatar with customizable appearance options.
-///
-/// # Examples
-///
-/// ```
-/// use ui::{Avatar, AvatarShape};
-///
-/// Avatar::new("path/to/image.png")
-/// .shape(AvatarShape::Circle)
-/// .grayscale(true)
-/// .border_color(gpui::red());
-/// ```
-#[derive(IntoElement)]
-pub struct Avatar {
- image: Img,
- size: Option<Pixels>,
- border_color: Option<Hsla>,
- is_available: Option<bool>,
-}
-
-impl RenderOnce for Avatar {
- fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
- if self.image.style().corner_radii.top_left.is_none() {
- self = self.shape(AvatarShape::Circle);
- }
-
- let size = self.size.unwrap_or_else(|| cx.rem_size());
-
- div()
- .size(size + px(2.))
- .map(|mut div| {
- div.style().corner_radii = self.image.style().corner_radii.clone();
- div
- })
- .when_some(self.border_color, |this, color| {
- this.border().border_color(color)
- })
- .child(
- self.image
- .size(size)
- .bg(cx.theme().colors().ghost_element_background),
- )
- .children(self.is_available.map(|is_free| {
- // HACK: non-integer sizes result in oval indicators.
- let indicator_size = (size * 0.4).round();
-
- div()
- .absolute()
- .z_index(1)
- .bg(if is_free {
- cx.theme().status().created
- } else {
- cx.theme().status().deleted
- })
- .size(indicator_size)
- .rounded(indicator_size)
- .bottom_0()
- .right_0()
- }))
- }
-}
-
-impl Avatar {
- pub fn new(src: impl Into<ImageSource>) -> Self {
- Avatar {
- image: img(src),
- is_available: None,
- border_color: None,
- size: None,
- }
- }
-
- /// Sets the shape of the avatar image.
- ///
- /// This method allows the shape of the avatar to be specified using a [`Shape`].
- /// It modifies the corner radius of the image to match the specified shape.
- ///
- /// # Examples
- ///
- /// ```
- /// use ui::{Avatar, AvatarShape};
- ///
- /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
- /// ```
- pub fn shape(mut self, shape: AvatarShape) -> Self {
- self.image = match shape {
- AvatarShape::Circle => self.image.rounded_full(),
- AvatarShape::RoundedRectangle => self.image.rounded_md(),
- };
- self
- }
-
- /// Applies a grayscale filter to the avatar image.
- ///
- /// # Examples
- ///
- /// ```
- /// use ui::{Avatar, AvatarShape};
- ///
- /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
- /// ```
- pub fn grayscale(mut self, grayscale: bool) -> Self {
- self.image = self.image.grayscale(grayscale);
- self
- }
-
- pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
- self.border_color = Some(color.into());
- self
- }
-
- pub fn availability_indicator(mut self, is_available: impl Into<Option<bool>>) -> Self {
- self.is_available = is_available.into();
- self
- }
-
- /// Size overrides the avatar size. By default they are 1rem.
- pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
- self.size = size.into();
- self
- }
-}
+pub use avatar::*;
+pub use avatar_availability_indicator::*;
@@ -0,0 +1,122 @@
+use crate::prelude::*;
+use gpui::{img, AnyElement, Hsla, ImageSource, Img, IntoElement, Styled};
+
+/// The shape of an [`Avatar`].
+#[derive(Debug, Default, PartialEq, Clone)]
+pub enum AvatarShape {
+ /// The avatar is shown in a circle.
+ #[default]
+ Circle,
+ /// The avatar is shown in a rectangle with rounded corners.
+ RoundedRectangle,
+}
+
+/// An element that renders a user avatar with customizable appearance options.
+///
+/// # Examples
+///
+/// ```
+/// use ui::{Avatar, AvatarShape};
+///
+/// Avatar::new("path/to/image.png")
+/// .shape(AvatarShape::Circle)
+/// .grayscale(true)
+/// .border_color(gpui::red());
+/// ```
+#[derive(IntoElement)]
+pub struct Avatar {
+ image: Img,
+ size: Option<Pixels>,
+ border_color: Option<Hsla>,
+ indicator: Option<AnyElement>,
+}
+
+impl Avatar {
+ pub fn new(src: impl Into<ImageSource>) -> Self {
+ Avatar {
+ image: img(src),
+ size: None,
+ border_color: None,
+ indicator: None,
+ }
+ }
+
+ /// Sets the shape of the avatar image.
+ ///
+ /// This method allows the shape of the avatar to be specified using a [`Shape`].
+ /// It modifies the corner radius of the image to match the specified shape.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ui::{Avatar, AvatarShape};
+ ///
+ /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle);
+ /// ```
+ pub fn shape(mut self, shape: AvatarShape) -> Self {
+ self.image = match shape {
+ AvatarShape::Circle => self.image.rounded_full(),
+ AvatarShape::RoundedRectangle => self.image.rounded_md(),
+ };
+ self
+ }
+
+ /// Applies a grayscale filter to the avatar image.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use ui::{Avatar, AvatarShape};
+ ///
+ /// let avatar = Avatar::new("path/to/image.png").grayscale(true);
+ /// ```
+ pub fn grayscale(mut self, grayscale: bool) -> Self {
+ self.image = self.image.grayscale(grayscale);
+ self
+ }
+
+ pub fn border_color(mut self, color: impl Into<Hsla>) -> Self {
+ self.border_color = Some(color.into());
+ self
+ }
+
+ /// Size overrides the avatar size. By default they are 1rem.
+ pub fn size(mut self, size: impl Into<Option<Pixels>>) -> Self {
+ self.size = size.into();
+ self
+ }
+
+ pub fn indicator<E: IntoElement>(mut self, indicator: impl Into<Option<E>>) -> Self {
+ self.indicator = indicator.into().map(IntoElement::into_any_element);
+ self
+ }
+}
+
+impl RenderOnce for Avatar {
+ fn render(mut self, cx: &mut WindowContext) -> impl IntoElement {
+ if self.image.style().corner_radii.top_left.is_none() {
+ self = self.shape(AvatarShape::Circle);
+ }
+
+ let size = self.size.unwrap_or_else(|| cx.rem_size());
+
+ div()
+ .size(size + px(2.))
+ .map(|mut div| {
+ div.style().corner_radii = self.image.style().corner_radii.clone();
+ div
+ })
+ .when_some(self.border_color, |this, color| {
+ this.border().border_color(color)
+ })
+ .child(
+ self.image
+ .size(size)
+ .bg(cx.theme().colors().ghost_element_background),
+ )
+ .children(
+ self.indicator
+ .map(|indicator| div().z_index(1).child(indicator)),
+ )
+ }
+}
@@ -0,0 +1,48 @@
+use crate::prelude::*;
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+pub enum Availability {
+ Free,
+ Busy,
+}
+
+#[derive(IntoElement)]
+pub struct AvatarAvailabilityIndicator {
+ availability: Availability,
+ avatar_size: Option<Pixels>,
+}
+
+impl AvatarAvailabilityIndicator {
+ pub fn new(availability: Availability) -> Self {
+ Self {
+ availability,
+ avatar_size: None,
+ }
+ }
+
+ /// Sets the size of the [`Avatar`] this indicator appears on.
+ pub fn avatar_size(mut self, size: impl Into<Option<Pixels>>) -> Self {
+ self.avatar_size = size.into();
+ self
+ }
+}
+
+impl RenderOnce for AvatarAvailabilityIndicator {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let avatar_size = self.avatar_size.unwrap_or_else(|| cx.rem_size());
+
+ // HACK: non-integer sizes result in oval indicators.
+ let indicator_size = (avatar_size * 0.4).round();
+
+ div()
+ .absolute()
+ .bottom_0()
+ .right_0()
+ .size(indicator_size)
+ .rounded(indicator_size)
+ .bg(match self.availability {
+ Availability::Free => cx.theme().status().created,
+ Availability::Busy => cx.theme().status().deleted,
+ })
+ }
+}
@@ -1,8 +1,8 @@
use gpui::Render;
use story::Story;
-use crate::prelude::*;
use crate::Avatar;
+use crate::{prelude::*, Availability, AvatarAvailabilityIndicator};
pub struct AvatarStory;
@@ -19,11 +19,11 @@ impl Render for AvatarStory {
))
.child(
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
- .availability_indicator(true),
+ .indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
)
.child(
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
- .availability_indicator(false),
+ .indicator(AvatarAvailabilityIndicator::new(Availability::Busy)),
)
}
}