use crate::icon::Icon;
use crate::theme::ActiveTheme as _;
use fluidattacks_designs_domain::icon::Size as IconSize;
use fluidattacks_designs_domain::typography::{TextSize, FONT_PRIMARY};
use fluidattacks_designs_domain::{avatar, button, theme};
use gpui::{
div, px, rgb, App, ClickEvent, Div, ElementId, FontWeight, InteractiveElement as _,
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
Styled as _, Window,
};
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
#[derive(IntoElement)]
pub struct Avatar {
user_name: SharedString,
show_username: bool,
show_icon: bool,
on_click: Option<(ElementId, ClickHandler)>,
}
impl Avatar {
pub fn new(user_name: impl Into<SharedString>) -> Self {
Self {
user_name: user_name.into(),
show_username: false,
show_icon: false,
on_click: None,
}
}
#[must_use]
pub const fn show_username(mut self, show: bool) -> Self {
self.show_username = show;
self
}
#[must_use]
pub const fn show_icon(mut self, show: bool) -> Self {
self.show_icon = show;
self
}
#[must_use]
pub fn on_click(
mut self,
id: impl Into<ElementId>,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some((id.into(), Box::new(handler)));
self
}
}
fn circle(user_name: &str, theme: &theme::Colors) -> Div {
let letter = avatar::initial(user_name).unwrap_or('?');
div()
.flex()
.items_center()
.justify_center()
.size(px(avatar::SIZE))
.rounded_full()
.bg(rgb(theme.shade(avatar::BG)))
.text_color(rgb(theme.shade(avatar::FG)))
.font_family(FONT_PRIMARY)
.text_size(px(avatar::FONT_SIZE))
.line_height(px(avatar::LINE_HEIGHT))
.font_weight(FontWeight(f32::from(avatar::FONT_WEIGHT)))
.child(SharedString::from(letter.to_string()))
}
fn username_label(
avatar_el: Div,
user_name: &str,
interactive: bool,
theme: &theme::Colors,
) -> Div {
let Some(first) = avatar::first_name(user_name) else {
return avatar_el;
};
let label = if interactive {
first.to_owned()
} else {
format!("Hi {first}!")
};
avatar_el.child(
div()
.font_family(FONT_PRIMARY)
.text_size(px(TextSize::Md.px()))
.line_height(px(TextSize::Md.line_height_px()))
.text_color(rgb(theme.text_strong))
.child(SharedString::from(label)),
)
}
impl RenderOnce for Avatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().colors;
let interactive = self.on_click.is_some();
let mut base = div()
.flex()
.items_center()
.gap(px(8.0))
.p(px(8.0))
.child(circle(&self.user_name, &theme));
if self.show_username {
base = username_label(base, &self.user_name, interactive, &theme);
}
if self.show_icon {
base = base.child(
Icon::new("ellipsis-vertical")
.size(IconSize::Xs)
.color(theme.text_strong),
);
}
match self.on_click {
Some((id, handler)) => base
.id(id)
.rounded(px(button::spacing(button::Variant::Ghost)))
.cursor_pointer()
.hover(move |style| style.bg(rgb(theme.surface_muted)))
.on_click(handler)
.into_any_element(),
None => base.into_any_element(),
}
}
}