use blinc_core::{Color, State};
use blinc_layout::div::ElementTypeId;
use blinc_layout::element::RenderProps;
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_theme::{ColorToken, ThemeState};
use std::sync::Arc;
use blinc_layout::stateful::{stateful, ButtonState};
use blinc_layout::InstanceKey;
const CHECKMARK_SVG: &str = r#"<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8L6.5 11.5L13 4.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>"#;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CheckboxSize {
Small,
#[default]
Medium,
Large,
}
impl CheckboxSize {
fn size(&self) -> f32 {
match self {
CheckboxSize::Small => 14.0,
CheckboxSize::Medium => 18.0,
CheckboxSize::Large => 22.0,
}
}
fn border_width(&self) -> f32 {
match self {
CheckboxSize::Small => 1.5,
CheckboxSize::Medium => 2.0,
CheckboxSize::Large => 2.0,
}
}
fn checkmark_size(&self) -> f32 {
match self {
CheckboxSize::Small => 10.0,
CheckboxSize::Medium => 12.0,
CheckboxSize::Large => 16.0,
}
}
}
pub struct Checkbox {
inner: Div,
}
impl Checkbox {
pub fn new(checked_state: &State<bool>) -> Self {
Self::with_config(CheckboxConfig::new(checked_state.clone()))
}
fn with_config(config: CheckboxConfig) -> Self {
let theme = ThemeState::get();
let box_size = config.size.size();
let border_width = config.size.border_width();
let checkmark_size = config.size.checkmark_size();
let radius = match config.size {
CheckboxSize::Small => theme.radius(blinc_theme::RadiusToken::Sm) * 0.75,
_ => theme.radius(blinc_theme::RadiusToken::Sm),
};
let checked_bg = config
.checked_color
.unwrap_or_else(|| theme.color(ColorToken::Primary));
let unchecked_bg = config
.unchecked_bg
.unwrap_or_else(|| theme.color(ColorToken::InputBg));
let border = config
.border_color
.unwrap_or_else(|| theme.color(ColorToken::Border));
let hover_border = config
.hover_border_color
.unwrap_or_else(|| theme.color(ColorToken::BorderHover));
let check_mark_color = config
.check_color
.unwrap_or_else(|| theme.color(ColorToken::TextInverse));
let disabled = config.disabled;
let on_change = config.on_change.clone();
let checked_state = config.checked_state.clone();
let checked_state_for_click = config.checked_state.clone();
let mut checkbox = stateful::<ButtonState>()
.deps([checked_state.signal_id()])
.on_state(move |ctx| {
let state = ctx.state();
let is_checked = checked_state.get();
let is_hovered = matches!(state, ButtonState::Hovered | ButtonState::Pressed);
let bg = if is_checked { checked_bg } else { unchecked_bg };
let current_border = if is_hovered && !disabled {
hover_border
} else {
border
};
let scale = if is_hovered && !disabled { 1.05 } else { 1.0 };
let mut visual = div()
.class("cn-checkbox")
.w(box_size)
.h(box_size)
.rounded(radius)
.cursor_pointer()
.items_center()
.justify_center()
.bg(bg)
.border(border_width, current_border)
.transform(blinc_core::Transform::scale(scale, scale));
if is_checked {
visual = visual.class("cn-checkbox--checked");
}
if disabled {
visual = visual.class("cn-checkbox--disabled").opacity(0.5);
}
if is_checked {
visual = visual.child(
svg(CHECKMARK_SVG)
.size(checkmark_size, checkmark_size)
.tint(check_mark_color),
);
}
visual
});
checkbox = checkbox.on_click(move |_| {
if disabled {
return;
}
let current = checked_state_for_click.get();
let new_value = !current;
checked_state_for_click.set(new_value);
if let Some(ref callback) = on_change {
callback(new_value);
}
});
let inner = if let Some(ref label_text) = config.label {
let label_color = if disabled {
theme.color(ColorToken::TextTertiary)
} else {
theme.color(ColorToken::TextPrimary)
};
let checked_state_for_label = config.checked_state.clone();
let on_change_for_label = config.on_change.clone();
div()
.flex_row()
.gap(theme.spacing().space_1)
.items_center()
.cursor_pointer()
.child(checkbox)
.child(text(label_text).size(14.0).color(label_color))
.on_click(move |_| {
if disabled {
return;
}
let current = checked_state_for_label.get();
let new_value = !current;
checked_state_for_label.set(new_value);
if let Some(ref callback) = on_change_for_label {
callback(new_value);
}
})
} else {
div().child(checkbox)
};
Self { inner }
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.class(name);
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
impl ElementBuilder for Checkbox {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> ElementTypeId {
self.inner.element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
#[derive(Clone)]
struct CheckboxConfig {
checked_state: State<bool>,
size: CheckboxSize,
label: Option<String>,
disabled: bool,
checked_color: Option<Color>,
unchecked_bg: Option<Color>,
border_color: Option<Color>,
hover_border_color: Option<Color>,
check_color: Option<Color>,
on_change: Option<Arc<dyn Fn(bool) + Send + Sync>>,
}
impl CheckboxConfig {
fn new(checked_state: State<bool>) -> Self {
Self {
checked_state,
size: CheckboxSize::default(),
label: None,
disabled: false,
checked_color: None,
unchecked_bg: None,
border_color: None,
hover_border_color: None,
check_color: None,
on_change: None,
}
}
}
pub struct CheckboxBuilder {
#[allow(dead_code)]
key: InstanceKey,
config: CheckboxConfig,
built: std::cell::OnceCell<Checkbox>,
}
impl CheckboxBuilder {
#[track_caller]
pub fn new(checked_state: &State<bool>) -> Self {
Self {
key: InstanceKey::new("checkbox"),
config: CheckboxConfig::new(checked_state.clone()),
built: std::cell::OnceCell::new(),
}
}
pub fn with_key(key: impl Into<String>, checked_state: &State<bool>) -> Self {
Self {
key: InstanceKey::explicit(key),
config: CheckboxConfig::new(checked_state.clone()),
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Checkbox {
self.built
.get_or_init(|| Checkbox::with_config(self.config.clone()))
}
pub fn size(mut self, size: CheckboxSize) -> Self {
self.config.size = size;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.config.label = Some(label.into());
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.config.disabled = disabled;
self
}
pub fn checked_color(mut self, color: impl Into<Color>) -> Self {
self.config.checked_color = Some(color.into());
self
}
pub fn unchecked_bg(mut self, color: impl Into<Color>) -> Self {
self.config.unchecked_bg = Some(color.into());
self
}
pub fn border_color(mut self, color: impl Into<Color>) -> Self {
self.config.border_color = Some(color.into());
self
}
pub fn hover_border_color(mut self, color: impl Into<Color>) -> Self {
self.config.hover_border_color = Some(color.into());
self
}
pub fn check_color(mut self, color: impl Into<Color>) -> Self {
self.config.check_color = Some(color.into());
self
}
pub fn on_change<F>(mut self, callback: F) -> Self
where
F: Fn(bool) + Send + Sync + 'static,
{
self.config.on_change = Some(Arc::new(callback));
self
}
pub fn build_component(self) -> Checkbox {
Checkbox::with_config(self.config)
}
}
impl ElementBuilder for CheckboxBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn element_type_id(&self) -> ElementTypeId {
self.get_or_build().element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
pub fn checkbox(state: &State<bool>) -> CheckboxBuilder {
CheckboxBuilder::new(state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checkbox_sizes() {
assert_eq!(CheckboxSize::Small.size(), 14.0);
assert_eq!(CheckboxSize::Medium.size(), 18.0);
assert_eq!(CheckboxSize::Large.size(), 22.0);
}
#[test]
fn test_checkbox_checkmark_sizes() {
assert_eq!(CheckboxSize::Small.checkmark_size(), 10.0);
assert_eq!(CheckboxSize::Medium.checkmark_size(), 12.0);
assert_eq!(CheckboxSize::Large.checkmark_size(), 16.0);
}
}