Rework Avatar
indicator to be more general-purpose (#4073)
This PR reworks the way we add indicators to `Avatar`s to make them more general-purpose. Previously we had logic specific to the availability indicator embedded in the `Avatar` component, which made it unwieldy to repurpose for something else. Now the `indicator` is just a slot that we can put anything into. Release Notes: - N/A
This commit is contained in:
parent
d00067cd86
commit
ca4a8b2226
5 changed files with 222 additions and 176 deletions
|
@ -31,8 +31,8 @@ use smallvec::SmallVec;
|
||||||
use std::{mem, sync::Arc};
|
use std::{mem, sync::Arc};
|
||||||
use theme::{ActiveTheme, ThemeSettings};
|
use theme::{ActiveTheme, ThemeSettings};
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label,
|
prelude::*, Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Icon, IconButton,
|
||||||
ListHeader, ListItem, Tooltip,
|
IconName, IconSize, Label, ListHeader, ListItem, Tooltip,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -2000,43 +2000,49 @@ impl CollabPanel {
|
||||||
let busy = contact.busy || calling;
|
let busy = contact.busy || calling;
|
||||||
let user_id = contact.user.id;
|
let user_id = contact.user.id;
|
||||||
let github_login = SharedString::from(contact.user.github_login.clone());
|
let github_login = SharedString::from(contact.user.github_login.clone());
|
||||||
let item =
|
let item = ListItem::new(github_login.clone())
|
||||||
ListItem::new(github_login.clone())
|
.indent_level(1)
|
||||||
.indent_level(1)
|
.indent_step_size(px(20.))
|
||||||
.indent_step_size(px(20.))
|
.selected(is_selected)
|
||||||
.selected(is_selected)
|
.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
|
||||||
.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
|
.child(
|
||||||
.child(
|
h_flex()
|
||||||
h_flex()
|
.w_full()
|
||||||
.w_full()
|
.justify_between()
|
||||||
.justify_between()
|
.child(Label::new(github_login.clone()))
|
||||||
.child(Label::new(github_login.clone()))
|
.when(calling, |el| {
|
||||||
.when(calling, |el| {
|
el.child(Label::new("Calling").color(Color::Muted))
|
||||||
el.child(Label::new("Calling").color(Color::Muted))
|
})
|
||||||
})
|
.when(!calling, |el| {
|
||||||
.when(!calling, |el| {
|
el.child(
|
||||||
el.child(
|
IconButton::new("remove_contact", IconName::Close)
|
||||||
IconButton::new("remove_contact", IconName::Close)
|
.icon_color(Color::Muted)
|
||||||
.icon_color(Color::Muted)
|
.visible_on_hover("")
|
||||||
.visible_on_hover("")
|
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
.on_click(cx.listener({
|
||||||
.on_click(cx.listener({
|
let github_login = github_login.clone();
|
||||||
let github_login = github_login.clone();
|
move |this, _, cx| {
|
||||||
move |this, _, cx| {
|
this.remove_contact(user_id, &github_login, cx);
|
||||||
this.remove_contact(user_id, &github_login, cx);
|
}
|
||||||
}
|
})),
|
||||||
})),
|
)
|
||||||
)
|
}),
|
||||||
}),
|
)
|
||||||
)
|
.start_slot(
|
||||||
.start_slot(
|
// todo handle contacts with no avatar
|
||||||
// todo handle contacts with no avatar
|
Avatar::new(contact.user.avatar_uri.clone())
|
||||||
Avatar::new(contact.user.avatar_uri.clone())
|
.indicator::<AvatarAvailabilityIndicator>(if online {
|
||||||
.availability_indicator(if online { Some(!busy) } else { None }),
|
Some(AvatarAvailabilityIndicator::new(match busy {
|
||||||
)
|
true => ui::Availability::Busy,
|
||||||
.when(online && !busy, |el| {
|
false => ui::Availability::Free,
|
||||||
el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
|
}))
|
||||||
});
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.when(online && !busy, |el| {
|
||||||
|
el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx)))
|
||||||
|
});
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(github_login.clone())
|
.id(github_login.clone())
|
||||||
|
|
|
@ -1,135 +1,5 @@
|
||||||
use crate::prelude::*;
|
mod avatar;
|
||||||
use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled};
|
mod avatar_availability_indicator;
|
||||||
|
|
||||||
/// The shape of an [`Avatar`].
|
pub use avatar::*;
|
||||||
#[derive(Debug, Default, PartialEq, Clone)]
|
pub use avatar_availability_indicator::*;
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
122
crates/ui/src/components/avatar/avatar.rs
Normal file
122
crates/ui/src/components/avatar/avatar.rs
Normal file
|
@ -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 gpui::Render;
|
||||||
use story::Story;
|
use story::Story;
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::Avatar;
|
use crate::Avatar;
|
||||||
|
use crate::{prelude::*, Availability, AvatarAvailabilityIndicator};
|
||||||
|
|
||||||
pub struct AvatarStory;
|
pub struct AvatarStory;
|
||||||
|
|
||||||
|
@ -19,11 +19,11 @@ impl Render for AvatarStory {
|
||||||
))
|
))
|
||||||
.child(
|
.child(
|
||||||
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
|
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
|
||||||
.availability_indicator(true),
|
.indicator(AvatarAvailabilityIndicator::new(Availability::Free)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
|
Avatar::new("https://avatars.githubusercontent.com/u/326587?v=4")
|
||||||
.availability_indicator(false),
|
.indicator(AvatarAvailabilityIndicator::new(Availability::Busy)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue