use fluidattacks_designs_domain::icon::{Size as IconSize, Style as IconStyle};
use fluidattacks_designs_domain::link::{
is_external, Variant, DOWNLOAD_ICON, EXTERNAL_ICON, GAP, ICON_MARGIN_X, LINE_HEIGHT,
};
use fluidattacks_designs_domain::typography::{TextSize, FONT_PRIMARY};
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, px, rgb, App, ClickEvent, CursorStyle, ElementId, InteractiveElement as _, IntoElement,
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _,
Window,
};
use crate::icon::Icon;
use crate::theme::ActiveTheme as _;
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum IconPosition {
Left,
#[default]
Right,
}
#[derive(IntoElement)]
pub struct Link {
id: ElementId,
label: SharedString,
href: Option<SharedString>,
variant: Variant,
color: Option<u32>,
download: bool,
icon_position: IconPosition,
on_click: Option<ClickHandler>,
}
impl Link {
#[must_use]
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
label: label.into(),
href: None,
variant: Variant::default(),
color: None,
download: false,
icon_position: IconPosition::default(),
on_click: None,
}
}
#[must_use]
pub fn href(mut self, href: impl Into<SharedString>) -> Self {
self.href = Some(href.into());
self
}
#[must_use]
pub const fn variant(mut self, variant: Variant) -> Self {
self.variant = variant;
self
}
#[must_use]
pub const fn color(mut self, color: u32) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub const fn download(mut self, download: bool) -> Self {
self.download = download;
self
}
#[must_use]
pub const fn icon_position(mut self, icon_position: IconPosition) -> Self {
self.icon_position = icon_position;
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
}
}
fn external_glyph(download: bool, color: u32) -> gpui::Div {
div().mx(px(ICON_MARGIN_X)).child(
Icon::new(if download {
DOWNLOAD_ICON
} else {
EXTERNAL_ICON
})
.style(IconStyle::Light)
.size(IconSize::Sm)
.color(color),
)
}
impl RenderOnce for Link {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let hovered = window.use_keyed_state(self.id.clone(), cx, |_, _| false);
let is_hovered = *hovered.read(cx);
let color = self.color.unwrap_or_else(|| {
let variant = if is_hovered {
self.variant.hover()
} else {
self.variant.color()
};
cx.theme().shade(variant)
});
let glyph = (self.href.as_ref().is_some_and(|href| is_external(href)))
.then(|| external_glyph(self.download, color));
let href = self.href;
let on_click = self.on_click;
let (left_glyph, right_glyph) = match self.icon_position {
IconPosition::Left => (glyph, None),
IconPosition::Right => (None, glyph),
};
div()
.id(self.id)
.flex()
.items_center()
.gap(px(GAP))
.cursor(CursorStyle::PointingHand)
.font_family(FONT_PRIMARY)
.text_size(px(TextSize::Md.px()))
.line_height(px(LINE_HEIGHT))
.underline()
.text_color(rgb(color))
.on_hover(move |now_hovered, hover_window, hover_cx| {
hovered.update(hover_cx, |state, _| *state = *now_hovered);
hover_window.refresh();
})
.on_click(move |event, click_window, click_cx| {
if let Some(href) = href.as_ref() {
click_cx.open_url(href);
}
if let Some(handler) = on_click.as_ref() {
handler(event, click_window, click_cx);
}
})
.when_some(left_glyph, gpui::ParentElement::child)
.child(self.label)
.when_some(right_glyph, gpui::ParentElement::child)
}
}