use fluidattacks_designs_domain::icon::{Size as IconSize, Style as IconStyle};
use fluidattacks_designs_domain::tag::{
colors, padding, radius, Priority, Variant, CLOSE_ICON, CLOSE_PADDING, LINK_GAP, TRUNCATE_AT,
};
use fluidattacks_designs_domain::theme;
use fluidattacks_designs_domain::typography::{TextSize, FONT_PRIMARY};
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, rgb, App, ClickEvent, CursorStyle, ElementId, FontWeight, InteractiveElement as _,
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _,
Styled as _, Window,
};
use crate::icon::Icon;
use crate::link::Link;
use crate::theme::ActiveTheme as _;
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
#[derive(IntoElement)]
pub struct Tag {
label: SharedString,
variant: Variant,
priority: Priority,
icon: Option<(SharedString, IconStyle, Option<u32>)>,
title: SharedString,
filter_values: SharedString,
truncate: bool,
font_size: Option<f32>,
disabled: bool,
on_click: Option<(ElementId, ClickHandler)>,
on_close: Option<(ElementId, ClickHandler)>,
link: Option<(ElementId, SharedString)>,
link_label: SharedString,
}
impl Tag {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
variant: Variant::default(),
priority: Priority::default(),
icon: None,
title: SharedString::default(),
filter_values: SharedString::default(),
truncate: true,
font_size: None,
disabled: false,
on_click: None,
on_close: None,
link: None,
link_label: SharedString::default(),
}
}
#[must_use]
pub const fn variant(mut self, variant: Variant) -> Self {
self.variant = variant;
self
}
#[must_use]
pub const fn priority(mut self, priority: Priority) -> Self {
self.priority = priority;
self
}
#[must_use]
pub fn icon(mut self, name: impl Into<SharedString>) -> Self {
self.icon = Some((name.into(), IconStyle::Light, None));
self
}
#[must_use]
pub fn icon_styled(
mut self,
name: impl Into<SharedString>,
style: IconStyle,
color: Option<u32>,
) -> Self {
self.icon = Some((name.into(), style, color));
self
}
#[must_use]
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
self.title = title.into();
self
}
#[must_use]
pub fn filter_values(mut self, filter_values: impl Into<SharedString>) -> Self {
self.filter_values = filter_values.into();
self
}
#[must_use]
pub const fn truncate(mut self, truncate: bool) -> Self {
self.truncate = truncate;
self
}
#[must_use]
pub const fn font_size(mut self, font_size: f32) -> Self {
self.font_size = Some(font_size);
self
}
#[must_use]
pub const fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
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
}
#[must_use]
pub fn on_close(
mut self,
id: impl Into<ElementId>,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_close = Some((id.into(), Box::new(handler)));
self
}
#[must_use]
pub fn link(mut self, id: impl Into<ElementId>, href: impl Into<SharedString>) -> Self {
self.link = Some((id.into(), href.into()));
self
}
#[must_use]
pub fn link_label(mut self, label: impl Into<SharedString>) -> Self {
self.link_label = label.into();
self
}
fn chip(self, theme: &theme::Colors) -> gpui::Div {
let palette = colors(self.variant, self.priority, theme);
let (pad_y, pad_x) = padding(self.variant);
let label = self.shown_label();
div()
.flex()
.items_center()
.justify_center()
.gap(px(4.0))
.rounded(px(radius(self.variant)))
.py(px(pad_y))
.px(px(pad_x))
.font_family(FONT_PRIMARY)
.text_size(px(self.font_size.unwrap_or(TextSize::Sm.px())))
.line_height(px(18.0))
.whitespace_nowrap()
.text_color(rgb(palette.fg))
.when_some(palette.bg, |el, bg| el.bg(rgb(bg)))
.when_some(palette.border, |el, border| {
el.border_1().border_color(rgb(border))
})
.when_some(self.icon, |el, (name, style, color)| {
el.child(
Icon::new(name)
.style(style)
.size(IconSize::Sm)
.color(color.unwrap_or(palette.fg)),
)
})
.when(!self.title.is_empty(), |el| {
el.child(div().font_weight(FontWeight::BOLD).child(self.title))
})
.child(label)
.when(!self.filter_values.is_empty(), |el| {
el.child(
div()
.font_weight(FontWeight::BOLD)
.child(self.filter_values),
)
})
.when_some(self.on_close, |el, (id, handler)| {
el.child(close_button(id, handler, self.disabled, theme))
})
}
fn shown_label(&self) -> SharedString {
let plain = self.title.is_empty() && self.filter_values.is_empty();
if !(self.truncate && plain) || self.label.chars().count() <= TRUNCATE_AT {
return self.label.clone();
}
let cut: String = self.label.chars().take(TRUNCATE_AT).collect();
format!("{cut}...").into()
}
}
fn close_button(
id: ElementId,
handler: ClickHandler,
disabled: bool,
theme: &theme::Colors,
) -> impl IntoElement {
let glyph_color = if disabled {
theme.disabled.on
} else {
theme.text_strong
};
let close = div()
.id(id)
.flex()
.items_center()
.p(px(CLOSE_PADDING))
.rounded(px(4.0))
.child(
Icon::new(CLOSE_ICON)
.style(IconStyle::Light)
.size(IconSize::Sm)
.color(glyph_color),
);
if disabled {
close.cursor(CursorStyle::OperationNotAllowed)
} else {
let hover_bg = rgb(theme.surface_muted);
close
.cursor_pointer()
.hover(move |style| style.bg(hover_bg))
.on_click(move |event, window, cx| {
cx.stop_propagation();
handler(event, window, cx);
})
}
}
impl RenderOnce for Tag {
fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().colors;
let on_click = self.on_click.take();
let link = self.link.take();
let link_label = core::mem::take(&mut self.link_label);
let chip = self.chip(&theme);
let body = match on_click {
Some((id, handler)) => chip
.id(id)
.cursor_pointer()
.on_click(handler)
.into_any_element(),
None => chip.into_any_element(),
};
match link {
Some((id, href)) => div()
.flex()
.items_center()
.gap(px(LINK_GAP))
.child(body)
.child(Link::new(id, link_label).href(href))
.into_any_element(),
None => body,
}
}
}