use std::sync::Arc;
use blinc_core::{Color, State};
use blinc_theme::{ColorToken, ThemeState};
use crate::css_parser::{active_stylesheet, ElementState, Stylesheet};
use crate::div::{div, ElementBuilder};
use crate::element::RenderProps;
use crate::element_style::ElementStyle;
use crate::key::InstanceKey;
use crate::stateful::{stateful_with_key, ButtonState};
use crate::svg::svg;
use crate::text::text;
use crate::tree::{LayoutNodeId, LayoutTree};
#[derive(Clone)]
pub struct CheckboxConfig {
checked: State<bool>,
pub disabled: bool,
pub label: Option<String>,
pub size: f32,
pub gap: f32,
pub unchecked_bg: Option<Color>,
pub checked_bg: Option<Color>,
pub hover_tint: f32,
pub check_color: Option<Color>,
pub label_color: Option<Color>,
pub label_font_size: f32,
pub corner_radius: f32,
pub border_color: Option<Color>,
pub border_width: f32,
pub disabled_opacity: f32,
pub css_element_id: Option<String>,
pub on_change: Option<Arc<dyn Fn(bool) + Send + Sync>>,
}
impl CheckboxConfig {
fn new(checked: State<bool>) -> Self {
Self {
checked,
disabled: false,
label: None,
size: 20.0,
gap: 8.0,
unchecked_bg: None,
checked_bg: None,
hover_tint: 0.1,
check_color: None,
label_color: None,
label_font_size: 14.0,
corner_radius: 4.0,
border_color: None,
border_width: 2.0,
disabled_opacity: 0.5,
css_element_id: None,
on_change: None,
}
}
}
struct ResolvedColors {
unchecked_bg: Color,
checked_bg: Color,
check_color: Color,
label_color: Color,
border_color: Color,
}
impl ResolvedColors {
fn from_config(config: &CheckboxConfig, theme: &ThemeState) -> Self {
Self {
unchecked_bg: config.unchecked_bg.unwrap_or(Color::TRANSPARENT),
checked_bg: config
.checked_bg
.unwrap_or_else(|| theme.color(ColorToken::Primary)),
check_color: config
.check_color
.unwrap_or_else(|| theme.color(ColorToken::TextInverse)),
label_color: config
.label_color
.unwrap_or_else(|| theme.color(ColorToken::TextPrimary)),
border_color: config
.border_color
.unwrap_or_else(|| theme.color(ColorToken::BorderSecondary)),
}
}
}
fn lighten(color: Color, amount: f32) -> Color {
Color::rgba(
(color.r + amount).min(1.0),
(color.g + amount).min(1.0),
(color.b + amount).min(1.0),
color.a,
)
}
fn apply_css_overrides_checkbox(
colors: &mut ResolvedColors,
cfg: &mut CheckboxConfig,
stylesheet: &Stylesheet,
element_id: &str,
is_checked: bool,
is_hovered: bool,
is_disabled: bool,
) {
if let Some(base) = stylesheet.get(element_id) {
apply_style_to_checkbox(colors, cfg, base, is_checked);
}
if is_checked {
if let Some(s) = stylesheet.get_with_state(element_id, ElementState::Checked) {
apply_style_to_checkbox(colors, cfg, s, is_checked);
}
}
if is_hovered {
if let Some(s) = stylesheet.get_with_state(element_id, ElementState::Hover) {
apply_style_to_checkbox(colors, cfg, s, is_checked);
}
}
if is_disabled {
if let Some(s) = stylesheet.get_with_state(element_id, ElementState::Disabled) {
apply_style_to_checkbox(colors, cfg, s, is_checked);
}
}
}
fn apply_style_to_checkbox(
colors: &mut ResolvedColors,
cfg: &mut CheckboxConfig,
style: &ElementStyle,
is_checked: bool,
) {
if let Some(blinc_core::Brush::Solid(color)) = style.background {
if is_checked {
colors.checked_bg = color;
} else {
colors.unchecked_bg = color;
}
}
if let Some(color) = style.border_color {
colors.border_color = color;
}
if let Some(w) = style.border_width {
cfg.border_width = w;
}
if let Some(cr) = style.corner_radius {
cfg.corner_radius = cr.top_left;
}
if let Some(opacity) = style.opacity {
cfg.disabled_opacity = opacity;
}
if let Some(color) = style.accent_color {
colors.check_color = color;
}
if let Some(color) = style.text_color {
colors.label_color = color;
}
if let Some(size) = style.font_size {
cfg.label_font_size = size;
}
}
pub struct Checkbox {
inner: crate::div::Div,
}
impl Checkbox {
fn with_config(instance_key: &InstanceKey, config: CheckboxConfig) -> Self {
let checked_state = config.checked.clone();
let checked_for_click = config.checked.clone();
let on_change = config.on_change.clone();
let disabled = config.disabled;
let key = instance_key.get().to_string();
let css_element_id = config.css_element_id.clone();
let label_text = config.label.clone();
let gap = config.gap;
let mut checkbox = stateful_with_key::<ButtonState>(&key)
.deps([checked_state.signal_id()])
.on_state(move |ctx| {
let button_state = ctx.state();
let is_hovered =
matches!(button_state, ButtonState::Hovered | ButtonState::Pressed);
let is_checked = checked_state.get();
let is_disabled = config.disabled;
let theme = ThemeState::get();
let mut colors = ResolvedColors::from_config(&config, theme);
let mut cfg = config.clone();
if let Some(ref element_id) = css_element_id {
if let Some(stylesheet) = active_stylesheet() {
apply_css_overrides_checkbox(
&mut colors,
&mut cfg,
&stylesheet,
element_id,
is_checked,
is_hovered,
is_disabled,
);
}
}
let bg = if is_checked {
if is_hovered && !is_disabled {
lighten(colors.checked_bg, cfg.hover_tint)
} else {
colors.checked_bg
}
} else if is_hovered && !is_disabled {
lighten(colors.unchecked_bg, cfg.hover_tint)
} else {
colors.unchecked_bg
};
let icon_size = cfg.size * 0.7;
div()
.w(cfg.size)
.h(cfg.size)
.bg(bg)
.rounded(cfg.corner_radius)
.border(cfg.border_width, colors.border_color)
.items_center()
.justify_center()
.when(is_checked, |d| {
let checkmark_svg = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{icon_size}" height="{icon_size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>"#
);
d.child(svg(&checkmark_svg).size(icon_size, icon_size).color(colors.check_color).internal())
})
.when(!is_checked, |d| {
d.child(div().w(icon_size).h(icon_size))
})
.when(is_disabled, |d| d.opacity(cfg.disabled_opacity))
});
checkbox = checkbox.on_click(move |_| {
if disabled {
return;
}
let current = checked_for_click.get();
checked_for_click.set(!current);
if let Some(ref handler) = on_change {
handler(!current);
}
});
let inner = if let Some(ref label) = label_text {
let theme = ThemeState::get();
let label_color = if disabled {
theme.color(ColorToken::TextTertiary)
} else {
theme.color(ColorToken::TextPrimary)
};
div()
.flex_row()
.gap(gap)
.items_center()
.cursor_pointer()
.child(checkbox)
.child(text(label).size(14.0).color(label_color))
} else {
div()
.flex_row()
.gap(gap)
.items_center()
.cursor_pointer()
.child(checkbox)
};
Self { inner }
}
}
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) -> crate::div::ElementTypeId {
self.inner.element_type_id()
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("checkbox")
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn event_handlers(&self) -> Option<&crate::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub struct CheckboxBuilder {
key: InstanceKey,
config: CheckboxConfig,
built: std::cell::OnceCell<Checkbox>,
}
impl CheckboxBuilder {
#[track_caller]
pub fn new(checked: &State<bool>) -> Self {
Self {
key: InstanceKey::new("checkbox"),
config: CheckboxConfig::new(checked.clone()),
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Checkbox {
self.built
.get_or_init(|| Checkbox::with_config(&self.key, self.config.clone()))
}
pub fn id(mut self, id: &str) -> Self {
self.config.css_element_id = Some(id.to_string());
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.config.label = Some(label.into());
self
}
pub fn checkbox_size(mut self, size: f32) -> Self {
self.config.size = size;
self
}
pub fn checked_bg(mut self, color: impl Into<Color>) -> Self {
self.config.checked_bg = 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 check_color(mut self, color: impl Into<Color>) -> Self {
self.config.check_color = Some(color.into());
self
}
pub fn label_color(mut self, color: impl Into<Color>) -> Self {
self.config.label_color = Some(color.into());
self
}
pub fn label_font_size(mut self, size: f32) -> Self {
self.config.label_font_size = size;
self
}
pub fn rounded(mut self, radius: f32) -> Self {
self.config.corner_radius = radius;
self
}
pub fn border_color(mut self, color: impl Into<Color>) -> Self {
self.config.border_color = Some(color.into());
self
}
pub fn border_width(mut self, width: f32) -> Self {
self.config.border_width = width;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.config.disabled = disabled;
self
}
pub fn on_change<F>(mut self, handler: F) -> Self
where
F: Fn(bool) + Send + Sync + 'static,
{
self.config.on_change = Some(Arc::new(handler));
self
}
}
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) -> crate::div::ElementTypeId {
self.get_or_build().element_type_id()
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("checkbox")
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn event_handlers(&self) -> Option<&crate::event_handler::EventHandlers> {
self.get_or_build().event_handlers()
}
fn element_id(&self) -> Option<&str> {
self.get_or_build().element_id()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
#[track_caller]
pub fn checkbox(checked: &State<bool>) -> CheckboxBuilder {
CheckboxBuilder::new(checked)
}
#[track_caller]
pub fn checkbox_labeled(checked: &State<bool>, label: impl Into<String>) -> CheckboxBuilder {
CheckboxBuilder::new(checked).label(label)
}