use crate::theme::{Theme, ThemeExt};
use gpui::prelude::*;
use gpui::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AvatarSize {
Xs,
Sm,
#[default]
Md,
Lg,
Xl,
Xxl,
}
impl AvatarSize {
fn size(&self) -> Pixels {
match self {
AvatarSize::Xs => px(20.0),
AvatarSize::Sm => px(24.0),
AvatarSize::Md => px(32.0),
AvatarSize::Lg => px(40.0),
AvatarSize::Xl => px(48.0),
AvatarSize::Xxl => px(64.0),
}
}
}
impl From<crate::ComponentSize> for AvatarSize {
fn from(size: crate::ComponentSize) -> Self {
match size {
crate::ComponentSize::Xs => Self::Xs,
crate::ComponentSize::Sm => Self::Sm,
crate::ComponentSize::Md => Self::Md,
crate::ComponentSize::Lg => Self::Lg,
crate::ComponentSize::Xl => Self::Xl,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AvatarShape {
#[default]
Circle,
Square,
}
pub struct Avatar {
name: Option<SharedString>,
src: Option<SharedString>,
size: AvatarSize,
shape: AvatarShape,
status: Option<AvatarStatus>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AvatarStatus {
Online,
Offline,
Away,
Busy,
}
impl AvatarStatus {
fn color(&self, theme: &Theme) -> Rgba {
match self {
AvatarStatus::Online => theme.success,
AvatarStatus::Offline => theme.text_muted,
AvatarStatus::Away => theme.warning,
AvatarStatus::Busy => theme.error,
}
}
}
impl Avatar {
pub fn new() -> Self {
Self {
name: None,
src: None,
size: AvatarSize::default(),
shape: AvatarShape::default(),
status: None,
}
}
pub fn name(mut self, name: impl Into<SharedString>) -> Self {
self.name = Some(name.into());
self
}
pub fn src(mut self, src: impl Into<SharedString>) -> Self {
self.src = Some(src.into());
self
}
pub fn size(mut self, size: AvatarSize) -> Self {
self.size = size;
self
}
pub fn shape(mut self, shape: AvatarShape) -> Self {
self.shape = shape;
self
}
pub fn status(mut self, status: AvatarStatus) -> Self {
self.status = Some(status);
self
}
fn get_initials(&self) -> String {
if let Some(name) = &self.name {
name.split_whitespace()
.filter_map(|word| word.chars().next())
.take(2)
.collect::<String>()
.to_uppercase()
} else {
"?".to_string()
}
}
fn get_bg_color(&self) -> Rgba {
if let Some(name) = &self.name {
let hash: u32 = name.chars().fold(0u32, |acc, c| acc.wrapping_add(c as u32));
let colors = [
rgb(0x007acc), rgb(0x2da44e), rgb(0xd29922), rgb(0xcc3333), rgb(0x8b5cf6), rgb(0x06b6d4), rgb(0xf97316), rgb(0xec4899), ];
colors[(hash as usize) % colors.len()]
} else {
rgb(0x3a3a3a)
}
}
pub fn build_with_theme(self, theme: &Theme) -> Div {
let size = self.size.size();
let initials = self.get_initials();
let bg_color = self.get_bg_color();
let mut avatar = div()
.relative()
.flex()
.items_center()
.justify_center()
.w(size)
.h(size)
.bg(bg_color)
.text_color(theme.text_primary)
.overflow_hidden();
match self.shape {
AvatarShape::Circle => {
avatar = avatar.rounded_full();
}
AvatarShape::Square => {
avatar = avatar.rounded_md();
}
}
avatar = match self.size {
AvatarSize::Xs => avatar.text_xs(),
AvatarSize::Sm => avatar.text_xs(),
AvatarSize::Md => avatar.text_sm(),
AvatarSize::Lg => avatar.text_sm(),
AvatarSize::Xl => avatar,
AvatarSize::Xxl => avatar.text_lg(),
};
if let Some(_src) = self.src {
avatar = avatar.child(initials);
} else {
avatar = avatar.font_weight(FontWeight::SEMIBOLD).child(initials);
}
if let Some(status) = self.status {
let status_size = match self.size {
AvatarSize::Xs | AvatarSize::Sm => px(6.0),
AvatarSize::Md | AvatarSize::Lg => px(8.0),
AvatarSize::Xl | AvatarSize::Xxl => px(10.0),
};
let status_indicator = div()
.absolute()
.bottom_0()
.right_0()
.w(status_size)
.h(status_size)
.rounded_full()
.bg(status.color(theme))
.border_2()
.border_color(theme.background);
avatar = avatar.child(status_indicator);
}
avatar
}
}
impl Default for Avatar {
fn default() -> Self {
Self::new()
}
}
impl RenderOnce for Avatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
self.build_with_theme(&theme)
}
}
impl IntoElement for Avatar {
type Element = gpui::Component<Self>;
fn into_element(self) -> Self::Element {
gpui::Component::new(self)
}
}
pub struct AvatarGroup {
avatars: Vec<Avatar>,
max_display: usize,
size: AvatarSize,
}
impl AvatarGroup {
pub fn new() -> Self {
Self {
avatars: Vec::new(),
max_display: 4,
size: AvatarSize::default(),
}
}
pub fn avatars(mut self, avatars: Vec<Avatar>) -> Self {
self.avatars = avatars;
self
}
pub fn max_display(mut self, max: usize) -> Self {
self.max_display = max;
self
}
pub fn size(mut self, size: AvatarSize) -> Self {
self.size = size;
self
}
pub fn build_with_theme(self, theme: &Theme) -> Div {
let size = self.size.size();
let overlap = size * 0.3;
let mut container = div().flex().items_center();
let display_count = self.avatars.len().min(self.max_display);
let remaining = self.avatars.len().saturating_sub(self.max_display);
for (i, avatar) in self.avatars.into_iter().take(display_count).enumerate() {
let avatar_el = avatar.size(self.size).build_with_theme(theme);
let mut wrapper = div().relative();
if i > 0 {
wrapper = wrapper.ml(-overlap);
}
wrapper = wrapper.child(avatar_el);
container = container.child(wrapper);
}
if remaining > 0 {
container = container.child(
div()
.ml(-overlap)
.flex()
.items_center()
.justify_center()
.w(size)
.h(size)
.rounded_full()
.bg(theme.surface)
.text_color(theme.text_secondary)
.text_xs()
.font_weight(FontWeight::MEDIUM)
.child(format!("+{}", remaining)),
);
}
container
}
}
impl Default for AvatarGroup {
fn default() -> Self {
Self::new()
}
}
impl RenderOnce for AvatarGroup {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
self.build_with_theme(&theme)
}
}
impl IntoElement for AvatarGroup {
type Element = gpui::Component<Self>;
fn into_element(self) -> Self::Element {
gpui::Component::new(self)
}
}