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#[derive(Clone, PartialEq)]
71pub struct Switch {
72 pub(crate) theme_colors: Option<SwitchColorsThemePartial>,
73 pub(crate) theme_layout: Option<SwitchLayoutThemePartial>,
74 layout_variant: SwitchLayoutVariant,
75 value: WritableSetter<bool>,
76 enabled: bool,
77 cursor_icon: Option<CursorIcon>,
78 key: DiffKey,
79}
80
81impl KeyExt for Switch {
82 fn key_mut(&mut self) -> &mut DiffKey {
83 &mut self.key
84 }
85}
86
87impl Switch {
88 pub fn new(value: impl Into<WritableSetter<bool>>) -> Self {
89 Self {
90 value: value.into(),
91 theme_colors: None,
92 theme_layout: None,
93 layout_variant: SwitchLayoutVariant::Normal,
94 enabled: true,
95 cursor_icon: None,
96 key: DiffKey::None,
97 }
98 }
99
100 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
101 self.enabled = enabled.into();
102 self
103 }
104
105 pub fn layout_variant(mut self, layout_variant: impl Into<SwitchLayoutVariant>) -> Self {
106 self.layout_variant = layout_variant.into();
107 self
108 }
109
110 pub fn theme_colors(mut self, theme: SwitchColorsThemePartial) -> Self {
111 self.theme_colors = Some(theme);
112 self
113 }
114
115 pub fn theme_layout(mut self, theme: SwitchLayoutThemePartial) -> Self {
116 self.theme_layout = Some(theme);
117 self
118 }
119
120 pub fn expanded(self) -> Self {
122 self.layout_variant(SwitchLayoutVariant::Expanded)
123 }
124
125 pub fn cursor(mut self, cursor_icon: impl Into<Option<CursorIcon>>) -> Self {
127 self.cursor_icon = cursor_icon.into();
128 self
129 }
130}
131
132impl Component for Switch {
133 fn render(self: &Switch) -> impl IntoElement {
134 let theme_colors = get_theme!(&self.theme_colors, SwitchColorsThemePreference, "switch");
135 let theme_layout = match self.layout_variant {
136 SwitchLayoutVariant::Normal => get_theme!(
137 &self.theme_layout,
138 SwitchLayoutThemePreference,
139 "switch_layout"
140 ),
141 SwitchLayoutVariant::Expanded => get_theme!(
142 &self.theme_layout,
143 SwitchLayoutThemePreference,
144 "expanded_switch_layout"
145 ),
146 };
147
148 let mut pressing = use_state(|| false);
149 let a11y_id = use_a11y();
150 let focus = use_focus(a11y_id);
151
152 let toggled = *self.value.read();
153
154 let anim_toggle = use_animation_with_dependencies(
155 &(theme_colors.clone(), theme_layout.clone(), toggled),
156 |conf, (switch_colors, switch_layout, toggled)| {
157 conf.on_creation(OnCreation::Finish);
158 conf.on_change(OnChange::Rerun);
159
160 let value = (
161 AnimNum::new(
162 switch_layout.thumb_offset,
163 switch_layout.toggled_thumb_offset,
164 )
165 .time(300)
166 .function(Function::Expo)
167 .ease(Ease::Out),
168 AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
169 .time(300)
170 .function(Function::Expo)
171 .ease(Ease::Out),
172 AnimColor::new(switch_colors.background, switch_colors.toggled_background)
173 .time(300)
174 .function(Function::Expo)
175 .ease(Ease::Out),
176 AnimColor::new(
177 switch_colors.thumb_background,
178 switch_colors.toggled_thumb_background,
179 )
180 .time(300)
181 .function(Function::Expo)
182 .ease(Ease::Out),
183 );
184
185 if *toggled {
186 value
187 } else {
188 value.into_reversed()
189 }
190 },
191 );
192
193 let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
194 conf.on_creation(OnCreation::Finish);
195 conf.on_change(OnChange::Rerun);
196 let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
197 .time(150)
198 .function(Function::Expo)
199 .ease(Ease::Out);
200 if *pressing {
201 anim
202 } else {
203 anim.into_reversed()
204 }
205 });
206 let press_size = anim_press.get().value();
207
208 let border = if focus() == Focus::Keyboard {
209 Border::new()
210 .width(2.)
211 .alignment(BorderAlignment::Inner)
212 .fill(theme_colors.focus_border_fill.mul_if(!self.enabled, 0.9))
213 } else {
214 Border::new()
215 };
216 let (offset_x, size, background, thumb) = anim_toggle.get().value();
217
218 rect()
219 .a11y_id(a11y_id)
220 .a11y_enabled(self.enabled)
221 .a11y_focusable(self.enabled)
222 .a11y_role(AccessibilityRole::Switch)
223 .a11y_builder(|builder| builder.set_toggled(Toggled::from(toggled)))
224 .width(Size::px(theme_layout.width))
225 .height(Size::px(theme_layout.height))
226 .margin(theme_layout.margin)
227 .padding(Gaps::new_all(theme_layout.padding))
228 .main_align(Alignment::center())
229 .offset_x(offset_x - press_size / 2.0)
230 .corner_radius(CornerRadius::new_all(50.))
231 .background(background.mul_if(!self.enabled, 0.85))
232 .border(border)
233 .maybe(self.enabled, |rect| {
234 rect.on_press({
235 let mut value = self.value.clone();
236 move |event: Event<PressEventData>| {
237 event.stop_propagation();
238 let toggled = !*value.peek();
239 value.set(toggled);
240 a11y_id.request_focus();
241 }
242 })
243 .on_pointer_down(move |e: Event<PointerEventData>| {
244 if matches!(e.data(), PointerEventData::Touch(_)) {
245 pressing.set(true);
246 }
247 })
248 })
249 .on_global_pointer_up(move |_| pressing.set_if_modified(false))
250 .on_pointer_leave(move |_| pressing.set_if_modified(false))
251 .cursor(if self.enabled {
252 self.cursor_icon
253 } else {
254 CursorIcon::NotAllowed.into()
255 })
256 .child(
257 rect()
258 .width(Size::px(size + press_size))
259 .height(Size::px(size + press_size))
260 .background(thumb.mul_if(!self.enabled, 0.85))
261 .corner_radius(CornerRadius::new_all(50.)),
262 )
263 }
264
265 fn render_key(&self) -> DiffKey {
266 self.key.clone().or(self.default_key())
267 }
268}