dioxus_ui_system/atoms/
switch.rs1use crate::styles::Style;
6use crate::theme::{use_style, use_theme};
7use dioxus::prelude::*;
8
9#[derive(Props, Clone, PartialEq)]
11pub struct SwitchProps {
12 #[props(default)]
14 pub checked: bool,
15 #[props(default)]
17 pub onchange: Option<EventHandler<bool>>,
18 #[props(default)]
20 pub disabled: bool,
21 #[props(default)]
23 pub label: Option<String>,
24 #[props(default)]
26 pub size: SwitchSize,
27 #[props(default)]
29 pub style: Option<String>,
30 #[props(default)]
32 pub class: Option<String>,
33}
34
35#[derive(Default, Clone, PartialEq)]
37pub enum SwitchSize {
38 Sm,
40 #[default]
42 Md,
43 Lg,
45}
46
47#[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 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 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}