dioxus_ui_system/atoms/
toggle.rs1use crate::styles::Style;
8use crate::theme::tokens::Color;
9use crate::theme::{use_style, use_theme};
10use dioxus::prelude::*;
11
12#[derive(Default, Clone, PartialEq, Debug)]
14pub enum ToggleVariant {
15 #[default]
17 Default,
18 Outline,
20 Ghost,
22}
23
24#[derive(Default, Clone, PartialEq, Debug)]
26pub enum ToggleSize {
27 Sm,
29 #[default]
31 Md,
32 Lg,
34}
35
36#[derive(Props, Clone, PartialEq)]
38pub struct ToggleProps {
39 pub children: Element,
41 #[props(default)]
43 pub pressed: bool,
44 #[props(default)]
46 pub on_pressed_change: Option<EventHandler<bool>>,
47 #[props(default)]
49 pub variant: ToggleVariant,
50 #[props(default)]
52 pub size: ToggleSize,
53 #[props(default)]
55 pub disabled: bool,
56 #[props(default)]
58 pub style: Option<String>,
59 #[props(default)]
61 pub class: Option<String>,
62}
63
64#[component]
84pub fn Toggle(props: ToggleProps) -> Element {
85 let _theme = use_theme();
86
87 let mut is_pressed = use_signal(|| props.pressed);
88 let mut is_hovered = use_signal(|| false);
89 let mut is_focused = use_signal(|| false);
90
91 use_effect(move || {
93 is_pressed.set(props.pressed);
94 });
95
96 let pressed = is_pressed();
97 let disabled = props.disabled;
98 let variant = props.variant.clone();
99 let size = props.size.clone();
100
101 let style = use_style(move |t| {
103 let base = Style::new()
104 .inline_flex()
105 .items_center()
106 .justify_center()
107 .gap(&t.spacing, "sm")
108 .rounded(&t.radius, "md")
109 .text(&t.typography, "sm")
110 .font_weight(500)
111 .line_height(1.0)
112 .transition("all 150ms cubic-bezier(0.4, 0, 0.2, 1)")
113 .select_none()
114 .whitespace_nowrap()
115 .cursor(if disabled { "not-allowed" } else { "pointer" });
116
117 let base = if disabled {
119 base.opacity(0.5)
120 } else {
121 base.opacity(1.0)
122 };
123
124 let sized = match size {
126 ToggleSize::Sm => base.px(&t.spacing, "sm").h_px(32),
127 ToggleSize::Md => base.px(&t.spacing, "md").h_px(40),
128 ToggleSize::Lg => base.px(&t.spacing, "lg").h_px(48),
129 };
130
131 let (bg, fg, border) = match variant {
133 ToggleVariant::Default => {
134 if pressed {
135 let bg = if is_hovered() && !disabled {
136 t.colors.primary.darken(0.1)
137 } else {
138 t.colors.primary.clone()
139 };
140 (bg, t.colors.primary_foreground.clone(), None)
141 } else {
142 let bg = if is_hovered() && !disabled {
143 t.colors.muted.darken(0.05)
144 } else {
145 t.colors.muted.clone()
146 };
147 (bg, t.colors.muted_foreground.clone(), None)
148 }
149 }
150 ToggleVariant::Outline => {
151 if pressed {
152 let bg = if is_hovered() && !disabled {
153 t.colors.accent.darken(0.05)
154 } else {
155 t.colors.accent.clone()
156 };
157 let border_color = if is_hovered() && !disabled {
158 t.colors.primary.darken(0.1)
159 } else {
160 t.colors.primary.clone()
161 };
162 (bg, t.colors.foreground.clone(), Some(border_color))
163 } else {
164 let bg = if is_hovered() && !disabled {
165 t.colors.accent.clone()
166 } else {
167 Color::new_rgba(0, 0, 0, 0.0)
168 };
169 (
170 bg,
171 t.colors.foreground.clone(),
172 Some(t.colors.border.clone()),
173 )
174 }
175 }
176 ToggleVariant::Ghost => {
177 if pressed {
178 let bg = if is_hovered() && !disabled {
179 t.colors.accent.darken(0.05)
180 } else {
181 t.colors.accent.clone()
182 };
183 (bg, t.colors.foreground.clone(), None)
184 } else {
185 let bg = if is_hovered() && !disabled {
186 t.colors.accent.clone()
187 } else {
188 Color::new_rgba(0, 0, 0, 0.0)
189 };
190 (bg, t.colors.muted_foreground.clone(), None)
191 }
192 }
193 };
194
195 let mut final_style = sized.bg(&bg).text_color(&fg);
196
197 if let Some(border_color) = border {
199 final_style = final_style.border(1, &border_color);
200 }
201
202 if is_focused() && !disabled {
204 final_style = Style {
205 box_shadow: Some(format!("0 0 0 2px {}", t.colors.ring.to_rgba())),
206 ..final_style
207 };
208 }
209
210 final_style.build()
211 });
212
213 let final_style = if let Some(custom) = &props.style {
215 format!("{} {}", style(), custom)
216 } else {
217 style()
218 };
219
220 let class = props.class.clone().unwrap_or_default();
221
222 let handle_click = move |_| {
223 if !disabled {
224 let new_pressed = !is_pressed();
225 is_pressed.set(new_pressed);
226 if let Some(handler) = &props.on_pressed_change {
227 handler.call(new_pressed);
228 }
229 }
230 };
231
232 rsx! {
233 button {
234 r#type: "button",
235 role: "switch",
236 aria_pressed: "{pressed}",
237 disabled: disabled,
238 style: "{final_style}",
239 class: "{class}",
240 onclick: handle_click,
241 onmouseenter: move |_| if !disabled { is_hovered.set(true) },
242 onmouseleave: move |_| is_hovered.set(false),
243 onfocus: move |_| is_focused.set(true),
244 onblur: move |_| is_focused.set(false),
245 {props.children}
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_toggle_variant_equality() {
256 assert_eq!(ToggleVariant::Default, ToggleVariant::Default);
257 assert_ne!(ToggleVariant::Default, ToggleVariant::Outline);
258 assert_ne!(ToggleVariant::Outline, ToggleVariant::Ghost);
259 }
260
261 #[test]
262 fn test_toggle_size_equality() {
263 assert_eq!(ToggleSize::Md, ToggleSize::Md);
264 assert_ne!(ToggleSize::Sm, ToggleSize::Lg);
265 }
266}