1use 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 cursor_icon: CursorIcon,
103 key: DiffKey,
104}
105
106impl KeyExt for Switch {
107 fn write_key(&mut self) -> &mut DiffKey {
108 &mut self.key
109 }
110}
111
112impl Default for Switch {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118impl Switch {
119 pub fn new() -> Self {
120 Self {
121 toggled: false.into(),
122 on_toggle: None,
123 theme_colors: None,
124 theme_layout: None,
125 layout_variant: SwitchLayoutVariant::Normal,
126 enabled: true,
127 cursor_icon: CursorIcon::default(),
128 key: DiffKey::None,
129 }
130 }
131
132 pub fn toggled(mut self, toggled: impl Into<Readable<bool>>) -> Self {
133 self.toggled = toggled.into();
134 self
135 }
136
137 pub fn on_toggle(mut self, on_toggle: impl Into<EventHandler<()>>) -> Self {
138 self.on_toggle = Some(on_toggle.into());
139 self
140 }
141
142 pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
143 self.enabled = enabled.into();
144 self
145 }
146
147 pub fn layout_variant(mut self, layout_variant: impl Into<SwitchLayoutVariant>) -> Self {
148 self.layout_variant = layout_variant.into();
149 self
150 }
151
152 pub fn theme_colors(mut self, theme: SwitchColorsThemePartial) -> Self {
153 self.theme_colors = Some(theme);
154 self
155 }
156
157 pub fn theme_layout(mut self, theme: SwitchLayoutThemePartial) -> Self {
158 self.theme_layout = Some(theme);
159 self
160 }
161
162 pub fn expanded(self) -> Self {
164 self.layout_variant(SwitchLayoutVariant::Expanded)
165 }
166
167 pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
169 self.cursor_icon = cursor_icon.into();
170 self
171 }
172}
173
174impl Component for Switch {
175 fn render(self: &Switch) -> impl IntoElement {
176 let theme_colors = get_theme!(&self.theme_colors, SwitchColorsThemePreference, "switch");
177 let theme_layout = match self.layout_variant {
178 SwitchLayoutVariant::Normal => get_theme!(
179 &self.theme_layout,
180 SwitchLayoutThemePreference,
181 "switch_layout"
182 ),
183 SwitchLayoutVariant::Expanded => get_theme!(
184 &self.theme_layout,
185 SwitchLayoutThemePreference,
186 "expanded_switch_layout"
187 ),
188 };
189
190 let mut pressing = use_state(|| false);
191 let a11y_id = use_a11y();
192 let focus = use_focus(a11y_id);
193
194 let toggled = *self.toggled.read();
195
196 let anim_toggle = use_animation_with_dependencies(
197 &(theme_colors.clone(), theme_layout.clone(), toggled),
198 |conf, (switch_colors, switch_layout, toggled)| {
199 conf.on_creation(OnCreation::Finish);
200 conf.on_change(OnChange::Rerun);
201
202 let value = (
203 AnimNum::new(
204 switch_layout.thumb_offset,
205 switch_layout.toggled_thumb_offset,
206 )
207 .time(300)
208 .function(Function::Expo)
209 .ease(Ease::Out),
210 AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
211 .time(300)
212 .function(Function::Expo)
213 .ease(Ease::Out),
214 AnimColor::new(switch_colors.background, switch_colors.toggled_background)
215 .time(300)
216 .function(Function::Expo)
217 .ease(Ease::Out),
218 AnimColor::new(
219 switch_colors.thumb_background,
220 switch_colors.toggled_thumb_background,
221 )
222 .time(300)
223 .function(Function::Expo)
224 .ease(Ease::Out),
225 );
226
227 if *toggled {
228 value
229 } else {
230 value.into_reversed()
231 }
232 },
233 );
234
235 let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
236 conf.on_creation(OnCreation::Finish);
237 conf.on_change(OnChange::Rerun);
238 let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
239 .time(150)
240 .function(Function::Expo)
241 .ease(Ease::Out);
242 if *pressing {
243 anim
244 } else {
245 anim.into_reversed()
246 }
247 });
248 let press_size = anim_press.get().value();
249
250 let border = if focus() == Focus::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(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 |event: Event<PressEventData>| {
277 if let Some(on_toggle) = &on_toggle {
278 event.stop_propagation();
279 on_toggle.call(())
280 }
281 a11y_id.request_focus();
282 }
283 })
284 .on_pointer_down(move |e: Event<PointerEventData>| {
285 if matches!(e.data(), PointerEventData::Touch(_)) {
286 pressing.set(true);
287 }
288 })
289 })
290 .on_global_pointer_press(move |_| pressing.set_if_modified(false))
291 .on_pointer_leave(move |_| pressing.set_if_modified(false))
292 .cursor(if self.enabled {
293 self.cursor_icon
294 } else {
295 CursorIcon::NotAllowed
296 })
297 .child(
298 rect()
299 .width(Size::px(size + press_size))
300 .height(Size::px(size + press_size))
301 .background(thumb.mul_if(!self.enabled, 0.85))
302 .corner_radius(CornerRadius::new_all(50.)),
303 )
304 }
305
306 fn render_key(&self) -> DiffKey {
307 self.key.clone().or(self.default_key())
308 }
309}