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 hovering = use_state(|| false);
191 let mut pressing = use_state(|| false);
192 let a11y_id = use_a11y();
193 let focus = use_focus(a11y_id);
194
195 let toggled = *self.toggled.read();
196
197 let anim_toggle = use_animation_with_dependencies(
198 &(theme_colors.clone(), theme_layout.clone(), toggled),
199 |conf, (switch_colors, switch_layout, toggled)| {
200 conf.on_creation(OnCreation::Finish);
201 conf.on_change(OnChange::Rerun);
202
203 let value = (
204 AnimNum::new(
205 switch_layout.thumb_offset,
206 switch_layout.toggled_thumb_offset,
207 )
208 .time(300)
209 .function(Function::Expo)
210 .ease(Ease::Out),
211 AnimNum::new(switch_layout.thumb_size, switch_layout.toggled_thumb_size)
212 .time(300)
213 .function(Function::Expo)
214 .ease(Ease::Out),
215 AnimColor::new(switch_colors.background, switch_colors.toggled_background)
216 .time(300)
217 .function(Function::Expo)
218 .ease(Ease::Out),
219 AnimColor::new(
220 switch_colors.thumb_background,
221 switch_colors.toggled_thumb_background,
222 )
223 .time(300)
224 .function(Function::Expo)
225 .ease(Ease::Out),
226 );
227
228 if *toggled {
229 value
230 } else {
231 value.into_reversed()
232 }
233 },
234 );
235
236 let anim_press = use_animation_with_dependencies(&pressing(), move |conf, pressing| {
237 conf.on_creation(OnCreation::Finish);
238 conf.on_change(OnChange::Rerun);
239 let anim = AnimNum::new(0.0, theme_layout.pressed_thumb_size_offset)
240 .time(150)
241 .function(Function::Expo)
242 .ease(Ease::Out);
243 if *pressing {
244 anim
245 } else {
246 anim.into_reversed()
247 }
248 });
249 let press_size = anim_press.get().value();
250
251 let enabled = use_reactive(&self.enabled);
252 let cursor_icon = self.cursor_icon;
253 use_drop(move || {
254 if hovering() && enabled() {
255 Cursor::set(CursorIcon::default());
256 }
257 });
258
259 let border = if focus() == Focus::Keyboard {
260 Border::new()
261 .width(2.)
262 .alignment(BorderAlignment::Inner)
263 .fill(theme_colors.focus_border_fill.mul_if(!self.enabled, 0.9))
264 } else {
265 Border::new()
266 };
267 let (offset_x, size, background, thumb) = anim_toggle.get().value();
268
269 rect()
270 .a11y_id(a11y_id)
271 .a11y_focusable(self.enabled)
272 .a11y_role(AccessibilityRole::Switch)
273 .a11y_builder(|builder| builder.set_toggled(Toggled::from(toggled)))
274 .width(Size::px(theme_layout.width))
275 .height(Size::px(theme_layout.height))
276 .padding(Gaps::new_all(theme_layout.padding))
277 .main_align(Alignment::center())
278 .offset_x(offset_x - press_size / 2.0)
279 .corner_radius(CornerRadius::new_all(50.))
280 .background(background.mul_if(!self.enabled, 0.85))
281 .border(border)
282 .maybe(self.enabled, |rect| {
283 rect.on_press({
284 let on_toggle = self.on_toggle.clone();
285 move |_| {
286 if let Some(on_toggle) = &on_toggle {
287 on_toggle.call(())
288 }
289 a11y_id.request_focus();
290 }
291 })
292 .on_pointer_down(move |e: Event<PointerEventData>| {
293 if matches!(e.data(), PointerEventData::Touch(_)) {
294 pressing.set(true);
295 }
296 })
297 })
298 .on_global_pointer_press(move |_| pressing.set_if_modified(false))
299 .on_pointer_enter(move |_| {
300 hovering.set(true);
301 if enabled() {
302 Cursor::set(cursor_icon);
303 } else {
304 Cursor::set(CursorIcon::NotAllowed);
305 }
306 })
307 .on_pointer_leave(move |_| {
308 if hovering() {
309 Cursor::set(CursorIcon::default());
310 hovering.set(false);
311 }
312 pressing.set_if_modified(false);
313 })
314 .child(
315 rect()
316 .width(Size::px(size + press_size))
317 .height(Size::px(size + press_size))
318 .background(thumb.mul_if(!self.enabled, 0.85))
319 .corner_radius(CornerRadius::new_all(50.)),
320 )
321 }
322
323 fn render_key(&self) -> DiffKey {
324 self.key.clone().or(self.default_key())
325 }
326}