use fluidattacks_designs_domain::checkbox::{
ring_color, tones, BOX_MARGIN_RIGHT, BOX_RADIUS, BOX_SIZE, GLYPH, GLYPH_COLOR, GLYPH_SIZE,
GLYPH_STYLE, LABEL_COLOR, LABEL_DISABLED_COLOR, LABEL_LINE_HEIGHT, LABEL_SIZE, LABEL_WEIGHT,
RING, TOOLTIP_GAP, TOOLTIP_GLYPH, TOOLTIP_GLYPH_SIZE, TOOLTIP_GLYPH_STYLE, TOOLTIP_PLACE,
};
use fluidattacks_designs_domain::typography::FONT_PRIMARY;
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, point, px, rgb, rgb_to_hsla, App, BoxShadow, Div, ElementId, FontWeight,
InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, SharedString,
StatefulInteractiveElement as _, Styled as _, Window,
};
use crate::theme::{ActiveTheme as _, Theme};
use crate::{Icon, Tooltip};
type ChangeHandler = Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>;
#[derive(IntoElement)]
pub struct Checkbox {
id: ElementId,
checked: bool,
label: Option<SharedString>,
disabled: bool,
tooltip: Option<(ElementId, SharedString)>,
on_change: Option<ChangeHandler>,
}
impl Checkbox {
pub fn new(id: impl Into<ElementId>, checked: bool) -> Self {
Self {
id: id.into(),
checked,
label: None,
disabled: false,
tooltip: None,
on_change: None,
}
}
#[must_use]
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub const fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
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
}
#[must_use]
pub fn on_change(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Box::new(handler));
self
}
}
fn ring(checked: bool, theme: &Theme) -> BoxShadow {
BoxShadow {
color: rgb_to_hsla(rgb(theme.shade(ring_color(checked)))),
offset: point(px(0.0), px(0.0)),
blur_radius: px(0.0),
spread_radius: px(RING),
inset: false,
}
}
fn square(checked: bool, disabled: bool, theme: &Theme) -> Div {
let painted = tones(checked, disabled);
let ring = ring(checked, theme);
div()
.flex()
.flex_none()
.items_center()
.justify_center()
.size(px(BOX_SIZE))
.mr(px(BOX_MARGIN_RIGHT))
.rounded(px(BOX_RADIUS))
.border_1()
.border_color(rgb(theme.shade(painted.border)))
.bg(rgb(theme.shade(painted.bg)))
.when(!disabled, |el| {
el.hover(move |style| style.shadow(vec![ring]))
})
.when(checked, |el| {
el.child(
Icon::new(GLYPH)
.style(GLYPH_STYLE)
.size_px(GLYPH_SIZE)
.color(theme.shade(GLYPH_COLOR)),
)
})
}
fn tip(id: ElementId, tooltip: SharedString) -> Tooltip {
Tooltip::new(id, tooltip).place(TOOLTIP_PLACE).child(
Icon::new(TOOLTIP_GLYPH)
.style(TOOLTIP_GLYPH_STYLE)
.size(TOOLTIP_GLYPH_SIZE),
)
}
impl RenderOnce for Checkbox {
fn render(self, _window: &mut Window, app: &mut App) -> impl IntoElement {
let theme = app.theme();
let Self {
id,
checked,
label,
disabled,
tooltip,
on_change,
} = self;
let handler = on_change.filter(|_| !disabled);
let next = !checked;
let text = if disabled {
LABEL_DISABLED_COLOR
} else {
LABEL_COLOR
};
let control = div()
.id(id)
.flex()
.items_center()
.font_family(FONT_PRIMARY)
.text_size(px(LABEL_SIZE.px()))
.line_height(px(LABEL_LINE_HEIGHT))
.font_weight(FontWeight(f32::from(LABEL_WEIGHT.value())))
.text_color(rgb(theme.shade(text)))
.when(disabled, gpui::Styled::cursor_not_allowed)
.when(!disabled, gpui::Styled::cursor_pointer)
.child(square(checked, disabled, theme))
.when_some(label, gpui::ParentElement::child)
.when_some(handler, |el, handler| {
el.on_click(move |_, window, cx| handler(&next, window, cx))
});
div()
.flex()
.items_start()
.gap(px(TOOLTIP_GAP))
.child(control)
.when_some(tooltip, |el, (tip_id, tooltip)| {
el.child(tip(tip_id, tooltip))
})
}
}