Skip to main content

dioxus_ui_system/atoms/
checkbox.rs

1//! Checkbox atom component
2//!
3//! A control that allows the user to toggle between checked and not checked.
4
5use crate::styles::Style;
6use crate::theme::{use_style, use_theme};
7use dioxus::prelude::*;
8
9/// Checkbox properties
10#[derive(Props, Clone, PartialEq)]
11pub struct CheckboxProps {
12    /// Whether the checkbox is checked
13    #[props(default)]
14    pub checked: bool,
15    /// Callback when checked state changes
16    #[props(default)]
17    pub onchange: Option<EventHandler<bool>>,
18    /// Whether the checkbox is disabled
19    #[props(default)]
20    pub disabled: bool,
21    /// Checkbox label
22    #[props(default)]
23    pub label: Option<String>,
24    /// Custom inline styles
25    #[props(default)]
26    pub style: Option<String>,
27    /// Custom class name
28    #[props(default)]
29    pub class: Option<String>,
30}
31
32/// Checkbox atom component
33#[component]
34pub fn Checkbox(props: CheckboxProps) -> Element {
35    let _theme = use_theme();
36    let mut is_checked = use_signal(|| props.checked);
37    let mut is_hovered = use_signal(|| false);
38    let mut is_focused = use_signal(|| false);
39
40    // Sync with prop changes
41    use_effect(move || {
42        is_checked.set(props.checked);
43    });
44
45    let checked = is_checked();
46    let disabled = props.disabled;
47    let cursor_style = if disabled { "not-allowed" } else { "pointer" };
48    let opacity_style = if disabled { "0.5" } else { "1" };
49
50    let checkbox_style = use_style(move |t| {
51        let base = Style::new()
52            .w_px(20)
53            .h_px(20)
54            .rounded(&t.radius, "sm")
55            .border(1, &t.colors.border)
56            .flex()
57            .items_center()
58            .justify_center()
59            .cursor("pointer")
60            .transition("all 150ms ease");
61
62        let styled = if checked {
63            base.bg(&t.colors.primary).border_color(&t.colors.primary)
64        } else {
65            base.bg(&t.colors.background)
66        };
67
68        let styled = if is_hovered() && !disabled && !checked {
69            styled.border_color(&t.colors.primary)
70        } else {
71            styled
72        };
73
74        let styled = if is_focused() && !disabled {
75            Style {
76                box_shadow: Some(format!("0 0 0 2px {}", t.colors.ring.to_rgba())),
77                ..styled
78            }
79        } else {
80            styled
81        };
82
83        let styled = if disabled {
84            styled.opacity(0.5)
85        } else {
86            styled
87        };
88
89        styled.build()
90    });
91
92    let checkmark_style = use_style(|t| {
93        Style::new()
94            .w_px(12)
95            .h_px(12)
96            .text_color(&t.colors.primary_foreground)
97            .build()
98    });
99
100    let handle_click = move |_| {
101        if !disabled {
102            let new_checked = !is_checked();
103            is_checked.set(new_checked);
104            if let Some(handler) = &props.onchange {
105                handler.call(new_checked);
106            }
107        }
108    };
109
110    let checkbox_element = rsx! {
111        button {
112            r#type: "button",
113            role: "checkbox",
114            aria_checked: "{checked}",
115            disabled: disabled,
116            style: "{checkbox_style} {props.style.clone().unwrap_or_default()}",
117            class: "{props.class.clone().unwrap_or_default()}",
118            onclick: handle_click,
119            onmouseenter: move |_| if !disabled { is_hovered.set(true) },
120            onmouseleave: move |_| is_hovered.set(false),
121            onfocus: move |_| is_focused.set(true),
122            onblur: move |_| is_focused.set(false),
123
124            if checked {
125                // Checkmark icon
126                svg {
127                    view_box: "0 0 24 24",
128                    fill: "none",
129                    stroke: "currentColor",
130                    stroke_width: "3",
131                    stroke_linecap: "round",
132                    stroke_linejoin: "round",
133                    style: "{checkmark_style}",
134                    polyline { points: "20 6 9 17 4 12" }
135                }
136            }
137        }
138    };
139
140    let label_element = if let Some(label_text) = props.label.clone() {
141        rsx! {
142            label {
143                style: "display: flex; align-items: center; gap: 8px; cursor: {cursor_style}; opacity: {opacity_style};",
144                {checkbox_element}
145                span { "{label_text}" }
146            }
147        }
148    } else {
149        checkbox_element
150    };
151
152    rsx! {
153        {label_element}
154    }
155}