use crate::theme::ActiveTheme as _;
use fluidattacks_designs_domain::tabs::{
active_border, Variant, ACTIVE_BORDER_PX, ACTIVE_BOTTOM_PADDING, BOX_BORDER, BOX_RADIUS, GAP,
ITEM_PADDING, SIDE_PADDING,
};
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, ElementId, InteractiveElement as _, IntoElement,
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _,
Window,
};
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
pub struct TabItem {
id: ElementId,
label: SharedString,
active: bool,
on_click: Option<ClickHandler>,
tag: Option<SharedString>,
sign: Option<crate::NotificationSign>,
tooltip: Option<(ElementId, SharedString)>,
}
impl TabItem {
#[must_use]
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
label: label.into(),
active: false,
on_click: None,
tag: None,
sign: None,
tooltip: None,
}
}
#[must_use]
pub const fn active(mut self, active: bool) -> Self {
self.active = active;
self
}
#[must_use]
pub fn on_click(
mut self,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_click = Some(Box::new(on_click));
self
}
#[must_use]
pub fn tag(mut self, tag: impl Into<SharedString>) -> Self {
self.tag = Some(tag.into());
self
}
#[must_use]
pub const fn notification(mut self, sign: crate::NotificationSign) -> Self {
self.sign = Some(sign);
self
}
#[must_use]
pub fn tooltip(mut self, id: impl Into<ElementId>, tip: impl Into<SharedString>) -> Self {
self.tooltip = Some((id.into(), tip.into()));
self
}
}
#[derive(IntoElement)]
pub struct Tabs {
variant: Variant,
borders: bool,
boxed: bool,
items: Vec<TabItem>,
}
impl Tabs {
#[must_use]
pub const fn new() -> Self {
Self {
variant: Variant::Primary,
borders: true,
boxed: false,
items: Vec::new(),
}
}
#[must_use]
pub const fn variant(mut self, variant: Variant) -> Self {
self.variant = variant;
self
}
#[must_use]
pub const fn borders(mut self, borders: bool) -> Self {
self.borders = borders;
self
}
#[must_use]
pub const fn boxed(mut self, boxed: bool) -> Self {
self.boxed = boxed;
self
}
#[must_use]
pub fn item(mut self, item: TabItem) -> Self {
self.items.push(item);
self
}
#[must_use]
pub fn tab(
self,
id: impl Into<ElementId>,
label: impl Into<SharedString>,
active: bool,
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.item(TabItem::new(id, label).active(active).on_click(on_click))
}
}
impl Default for Tabs {
fn default() -> Self {
Self::new()
}
}
fn item(mut entry: TabItem, variant: Variant, theme: &theme::Colors) -> gpui::AnyElement {
let tooltip = entry.tooltip.take();
let (pad_y, pad_x) = ITEM_PADDING;
let pad_bottom = if entry.active {
ACTIVE_BOTTOM_PADDING
} else {
pad_y
};
let hover_bg = rgb(theme.surface_muted);
let element = div()
.id(entry.id)
.flex()
.items_center()
.gap(px(GAP))
.pt(px(pad_y))
.pb(px(pad_bottom))
.px(px(pad_x))
.font_family(FONT_PRIMARY)
.text_size(px(TextSize::Md.px()))
.whitespace_nowrap()
.text_color(rgb(theme.text_strong))
.cursor_pointer()
.when(entry.active, |el| {
el.border_b(px(ACTIVE_BORDER_PX))
.border_color(rgb(active_border(variant, theme)))
})
.hover(move |style| style.bg(hover_bg))
.when_some(entry.on_click, |el, on_click| el.on_click(on_click))
.child(entry.label)
.when_some(entry.sign, gpui::ParentElement::child)
.when_some(entry.tag, |el, tag| el.child(crate::Tag::new(tag)));
match tooltip {
Some((tooltip_id, tip)) => crate::Tooltip::new(tooltip_id, tip)
.child(element)
.into_any_element(),
None => element.into_any_element(),
}
}
impl RenderOnce for Tabs {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().colors;
let boxed = self.boxed;
let mut bar = div()
.flex()
.items_center()
.gap(px(GAP))
.bg(rgb(theme.surface))
.when(!boxed, gpui::Styled::w_full)
.when(boxed, |el| {
el.border_1()
.border_color(rgb(theme.shade(BOX_BORDER)))
.rounded(px(BOX_RADIUS))
})
.when(self.borders && !boxed, |el| {
el.border_b_1()
.border_color(rgb(theme.border))
.px(px(SIDE_PADDING))
});
for entry in self.items {
bar = bar.child(item(entry, self.variant, &theme));
}
bar
}
}