freya_components/
switch.rs1use accesskit::Toggled;
2use freya_animation::prelude::*;
3use freya_core::prelude::*;
4use torin::{
5 alignment::Alignment,
6 gaps::Gaps,
7 size::Size,
8};
9
10use crate::{
11 define_theme,
12 get_theme,
13};
14
15define_theme! {
16 for = Switch;
17 theme_field = theme_colors;
18
19 %[component]
20 pub SwitchColors {
21 %[fields]
22 background: Color,
23 thumb_background: Color,
24 toggled_background: Color,
25 toggled_thumb_background: Color,
26 focus_border_fill: Color,
27 }
28}
29
30define_theme! {
31 for = Switch;
32 theme_field = theme_layout;
33
34 %[component]
35 pub SwitchLayout {
36 %[fields]
37 margin: Gaps,
38 width: f32,
39 height: f32,
40 padding: f32,
41 thumb_size: f32,
42 toggled_thumb_size: f32,
43 pressed_thumb_size_offset: f32,
44 thumb_offset: f32,
45 toggled_thumb_offset: f32,
46 }
47}
48
49#[derive(Clone, PartialEq)]
50pub enum SwitchLayoutVariant {
51 Normal,
52 Expanded,
53}
54
55#[cfg_attr(feature = "docs",
88 doc = embed_doc_image::embed_image!(
89 "gallery_toggled_switch",
90 "images/gallery_toggled_switch.png"
91 ),
92 doc = embed_doc_image::embed_image!("gallery_not_toggled_switch", "images/gallery_not_toggled_switch.png")
93)]
94#[derive(Clone, PartialEq)]
95pub struct Switch {
96 pub(crate) theme_colors: Option<SwitchColorsThemePartial>,
97 pub(crate) theme_layout: Option<SwitchLayoutThemePartial>,
98 layout_variant: SwitchLayoutVariant,
99 toggled: Readable<bool>,
100 on_toggle: Option<EventHandler<()>>,
101 enabled: bool,
102 key: DiffKey,
103}
104
105impl KeyExt for Switch {
106 fn write_key(&mut self) -> &mut DiffKey {
107 &mut self.key
108 }
109}
110
111impl Default for Switch {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117impl Switch {
118 pub fn new() -> Self {
119 Self {
120 toggled: false.into(),
121 on_toggle: None,
122 theme_colors: None,
123 theme_layout: None,
124 layout_variant: SwitchLayoutVariant::Normal,
125 enabled: true,
126 key: DiffKey::None,
127 }
128 }
129
130 pub fn toggled(mut self, toggled: impl Into<Readable<bool>>) -> Self {
131 self.toggled = toggled.into();
132 self
133 }
134
135 pub fn on_toggle(mut self, on_toggle: impl Into<EventHandler<()>>) -> Self {
136 self.on_toggle = Some(on_toggle.into());
137 self
138 }
139
140 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
141 self.enabled = enabled.into();
142 self
143 }
144
145 pub fn layout_variant(mut self, layout_variant: impl Into<SwitchLayoutVariant>) -> Self {
146 self.layout_variant = layout_variant.into();
147 self
148 }
149
150 pub fn theme_colors(mut self, theme: SwitchColorsThemePartial) -> Self {
151 self.theme_colors = Some(theme);
152 self
153 }
154
155 pub fn theme_layout(mut self, theme: SwitchLayoutThemePartial) -> Self {
156 self.theme_layout = Some(theme);
157 self
158 }
159
160 pub fn expanded(self) -> Self {
162 self.layout_variant(SwitchLayoutVariant::Expanded)
163 }
164}
165
166impl Component for Switch {
167 fn render(self: &Switch) -> impl IntoElement {
168 let theme_colors = get_theme!(&self.theme_colors, SwitchColorsThemePreference, "switch");
169 let theme_layout = match self.layout_variant {
170 SwitchLayoutVariant::Normal => get_theme!(
171 &self.theme_layout,
172 SwitchLayoutThemePreference,
173 "switch_layout"
174 ),
175 SwitchLayoutVariant::Expanded => get_theme!(
176 &self.theme_layout,
177 SwitchLayoutThemePreference,
178 "expanded_switch_layout"
179 ),
180 };
181
182 let mut hovering = use_state(|| false);
183 let mut pressing = use_state(|| false);
184 let focus = use_focus();
185 let focus_status = use_focus_status(focus);
186
187 let toggled = *self.toggled.read();
188
189 let anim_toggle = use_animation_with_dependencies(
190 &(theme_colors.clone(), theme_layout.clone(), toggled),
191 |conf, (switch_colors, switch_layout, toggled)| {
192 conf.on_creation(OnCreation::Finish);
193 conf.on_change(OnChange::Rerun);
194
195 let value = (
196 AnimNum::new(
197 switch_layout.thumb_offset,
198 switch_layout.toggled_thumb_offset,
199 )
200 .time(300)
201 .function(Function::Expo)
202 .ease(Ease::Out),
203 AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
204 .time(300)
205 .function(Function::Expo)
206 .ease(Ease::Out),
207 AnimColor::new(switch_colors.background, switch_colors.toggled_background)
208 .time(300)
209 .function(Function::Expo)
210 .ease(Ease::Out),
211 AnimColor::new(
212 switch_colors.thumb_background,
213 switch_colors.toggled_thumb_background,
214 )
215 .time(300)
216 .function(Function::Expo)
217 .ease(Ease::Out),
218 );
219
220 if *toggled {
221 value
222 } else {
223 value.into_reversed()
224 }
225 },
226 );
227
228 let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
229 conf.on_creation(OnCreation::Finish);
230 conf.on_change(OnChange::Rerun);
231 let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
232 .time(150)
233 .function(Function::Expo)
234 .ease(Ease::Out);
235 if *pressing {
236 anim
237 } else {
238 anim.into_reversed()
239 }
240 });
241 let press_size = anim_press.get().value();
242
243 let enabled = use_reactive(&self.enabled);
244 use_drop(move || {
245 if hovering() && enabled() {
246 Cursor::set(CursorIcon::default());
247 }
248 });
249
250 let border = if focus_status() == FocusStatus::Keyboard {
251 Border::new()
252 .width(2.)
253 .alignment(BorderAlignment::Inner)
254 .fill(theme_colors.focus_border_fill.mul_if(!self.enabled, 0.9))
255 } else {
256 Border::new()
257 };
258 let (offset_x, size, background, thumb) = anim_toggle.get().value();
259
260 rect()
261 .a11y_id(focus.a11y_id())
262 .a11y_focusable(self.enabled)
263 .a11y_role(AccessibilityRole::Switch)
264 .a11y_builder(|builder| builder.set_toggled(Toggled::from(toggled)))
265 .width(Size::px(theme_layout.width))
266 .height(Size::px(theme_layout.height))
267 .padding(Gaps::new_all(theme_layout.padding))
268 .main_align(Alignment::center())
269 .offset_x(offset_x - press_size / 2.0)
270 .corner_radius(CornerRadius::new_all(50.))
271 .background(background.mul_if(!self.enabled, 0.85))
272 .border(border)
273 .maybe(self.enabled, |rect| {
274 rect.on_press({
275 let on_toggle = self.on_toggle.clone();
276 move |_| {
277 if let Some(on_toggle) = &on_toggle {
278 on_toggle.call(())
279 }
280 focus.request_focus();
281 }
282 })
283 .on_pointer_down(move |e: Event<PointerEventData>| {
284 if matches!(e.data(), PointerEventData::Touch(_)) {
285 pressing.set(true);
286 }
287 })
288 })
289 .on_global_pointer_press(move |_| pressing.set_if_modified(false))
290 .on_pointer_enter(move |_| {
291 hovering.set(true);
292 if enabled() {
293 Cursor::set(CursorIcon::Pointer);
294 } else {
295 Cursor::set(CursorIcon::NotAllowed);
296 }
297 })
298 .on_pointer_leave(move |_| {
299 if hovering() {
300 Cursor::set(CursorIcon::default());
301 hovering.set(false);
302 }
303 pressing.set_if_modified(false);
304 })
305 .child(
306 rect()
307 .width(Size::px(size + press_size))
308 .height(Size::px(size + press_size))
309 .background(thumb.mul_if(!self.enabled, 0.85))
310 .corner_radius(CornerRadius::new_all(50.)),
311 )
312 }
313
314 fn render_key(&self) -> DiffKey {
315 self.key.clone().or(self.default_key())
316 }
317}