Skip to main content

dioxus_ui_system/atoms/
switch.rs

1//! Switch 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/// Switch properties
10#[derive(Props, Clone, PartialEq)]
11pub struct SwitchProps {
12    /// Whether the switch 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 switch is disabled
19    #[props(default)]
20    pub disabled: bool,
21    /// Switch label
22    #[props(default)]
23    pub label: Option<String>,
24    /// Switch size
25    #[props(default)]
26    pub size: SwitchSize,
27    /// Custom inline styles
28    #[props(default)]
29    pub style: Option<String>,
30    /// Custom class name
31    #[props(default)]
32    pub class: Option<String>,
33}
34
35/// Switch sizes
36#[derive(Default, Clone, PartialEq)]
37pub enum SwitchSize {
38    /// Small switch
39    Sm,
40    /// Medium (default) switch
41    #[default]
42    Md,
43    /// Large switch
44    Lg,
45}
46
47/// Switch atom component
48#[component]
49pub fn Switch(props: SwitchProps) -> Element {
50    let _theme = use_theme();
51    let mut is_checked = use_signal(|| props.checked);
52    let mut is_hovered = use_signal(|| false);
53    let mut is_focused = use_signal(|| false);
54
55    // Sync with prop changes
56    use_effect(move || {
57        is_checked.set(props.checked);
58    });
59
60    let checked = is_checked();
61    let disabled = props.disabled;
62    let cursor_style = if disabled { "not-allowed" } else { "pointer" };
63    let opacity_style = if disabled { "0.5" } else { "1" };
64    let size = props.size.clone();
65
66    // Size configurations
67    let (width, height, thumb_size) = match size {
68        SwitchSize::Sm => (32, 18, 14),
69        SwitchSize::Md => (44, 24, 20),
70        SwitchSize::Lg => (56, 30, 26),
71    };
72
73    let switch_style = use_style(move |t| {
74        let base = Style::new()
75            .w_px(width)
76            .h_px(height)
77            .rounded_full()
78            .relative()
79            .cursor(if disabled { "not-allowed" } else { "pointer" })
80            .transition("all 150ms ease")
81            .border(0, &t.colors.border);
82
83        let styled = if checked {
84            base.bg(&t.colors.primary)
85        } else {
86            base.bg(&t.colors.muted)
87        };
88
89        let styled = if is_hovered() && !disabled {
90            if checked {
91                styled.bg(&t.colors.primary.darken(0.1))
92            } else {
93                styled.bg(&t.colors.muted.darken(0.1))
94            }
95        } else {
96            styled
97        };
98
99        let styled = if is_focused() && !disabled {
100            Style {
101                box_shadow: Some(format!("0 0 0 2px {}", t.colors.ring.to_rgba())),
102                ..styled
103            }
104        } else {
105            styled
106        };
107
108        let styled = if disabled {
109            styled.opacity(0.5)
110        } else {
111            styled
112        };
113
114        styled.build()
115    });
116
117    let _thumb_offset = if checked { width - height + 2 } else { 2 };
118
119    let thumb_style = use_style(move |t| {
120        Style::new()
121            .absolute()
122            .top("2px")
123            .left("{thumb_offset}px")
124            .w_px(thumb_size)
125            .h_px(thumb_size)
126            .rounded_full()
127            .bg(&t.colors.background)
128            .transition("all 150ms ease")
129            .shadow(&t.shadows.sm)
130            .build()
131    });
132
133    let handle_click = move |_| {
134        if !disabled {
135            let new_checked = !is_checked();
136            is_checked.set(new_checked);
137            if let Some(handler) = &props.onchange {
138                handler.call(new_checked);
139            }
140        }
141    };
142
143    let switch_element = rsx! {
144        button {
145            r#type: "button",
146            role: "switch",
147            aria_checked: "{checked}",
148            disabled: disabled,
149            style: "{switch_style} {props.style.clone().unwrap_or_default()}",
150            class: "{props.class.clone().unwrap_or_default()}",
151            onclick: handle_click,
152            onmouseenter: move |_| if !disabled { is_hovered.set(true) },
153            onmouseleave: move |_| is_hovered.set(false),
154            onfocus: move |_| is_focused.set(true),
155            onblur: move |_| is_focused.set(false),
156
157            span {
158                style: "{thumb_style}",
159            }
160        }
161    };
162
163    let label_element = if let Some(label_text) = props.label.clone() {
164        rsx! {
165            label {
166                style: "display: flex; align-items: center; gap: 12px; cursor: {cursor_style};",
167                {switch_element}
168                span {
169                    style: "opacity: {opacity_style};",
170                    "{label_text}"
171                }
172            }
173        }
174    } else {
175        switch_element
176    };
177
178    rsx! {
179        {label_element}
180    }
181}