freya_components/
slider.rs

1use freya_core::prelude::*;
2use torin::prelude::*;
3
4use crate::{
5    get_theme,
6    theming::component_themes::SliderThemePartial,
7};
8
9#[derive(Debug, Default, PartialEq, Clone, Copy)]
10pub enum SliderStatus {
11    #[default]
12    Idle,
13    Hovering,
14}
15
16/// Slider component.
17///
18/// You must pass a percentage from 0.0 to 100.0 and listen for value changes with `on_moved` and then decide if this changes are applicable,
19/// and if so, apply them.
20///
21/// # Example
22/// ```rust
23/// # use freya::prelude::*;
24/// fn app() -> impl IntoElement {
25///     let mut percentage = use_state(|| 25.0);
26///
27///     Slider::new(move |per| percentage.set(per)).value(percentage())
28/// }
29///
30/// # use freya_testing::prelude::*;
31/// # launch_doc(|| {
32/// #   rect().padding(48.).center().expanded().child(app())
33/// # }, (250., 250.).into(), "./images/gallery_slider.png");
34/// ```
35/// # Preview
36/// ![Slider Preview][slider]
37#[cfg_attr(feature = "docs",
38    doc = embed_doc_image::embed_image!("slider", "images/gallery_slider.png")
39)]
40#[derive(Clone, PartialEq)]
41pub struct Slider {
42    pub(crate) theme: Option<SliderThemePartial>,
43    value: f64,
44    on_moved: EventHandler<f64>,
45    size: Size,
46    direction: Direction,
47    enabled: bool,
48    key: DiffKey,
49}
50
51impl KeyExt for Slider {
52    fn write_key(&mut self) -> &mut DiffKey {
53        &mut self.key
54    }
55}
56
57impl Slider {
58    pub fn new(handler: impl FnMut(f64) + 'static) -> Self {
59        Self {
60            theme: None,
61            value: 0.0,
62            on_moved: EventHandler::new(handler),
63            size: Size::fill(),
64            direction: Direction::Horizontal,
65            enabled: true,
66            key: DiffKey::None,
67        }
68    }
69
70    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
71        self.enabled = enabled.into();
72        self
73    }
74
75    pub fn value(mut self, value: f64) -> Self {
76        self.value = value.clamp(0.0, 100.0);
77        self
78    }
79
80    pub fn theme(mut self, theme: SliderThemePartial) -> Self {
81        self.theme = Some(theme);
82        self
83    }
84
85    pub fn size(mut self, size: Size) -> Self {
86        self.size = size;
87        self
88    }
89
90    pub fn direction(mut self, direction: Direction) -> Self {
91        self.direction = direction;
92        self
93    }
94
95    pub fn key(mut self, key: impl Into<DiffKey>) -> Self {
96        self.key = key.into();
97        self
98    }
99}
100
101impl Render for Slider {
102    fn render(&self) -> impl IntoElement {
103        let theme = get_theme!(&self.theme, slider);
104        let focus = use_focus();
105        let focus_status = use_focus_status(focus);
106        let mut status = use_state(SliderStatus::default);
107        let mut clicking = use_state(|| false);
108        let mut size = use_state(Area::default);
109
110        let enabled = use_reactive(&self.enabled);
111        use_drop(move || {
112            if status() == SliderStatus::Hovering && enabled() {
113                Cursor::set(CursorIcon::default());
114            }
115        });
116
117        let direction_is_vertical = self.direction == Direction::Vertical;
118        let value = self.value.clamp(0.0, 100.0);
119        let on_moved = self.on_moved.clone();
120
121        let on_key_down = {
122            let on_moved = self.on_moved.clone();
123            move |e: Event<KeyboardEventData>| match e.key {
124                Key::ArrowLeft if !direction_is_vertical => {
125                    e.stop_propagation();
126                    on_moved.call((value - 4.0).clamp(0.0, 100.0));
127                }
128                Key::ArrowRight if !direction_is_vertical => {
129                    e.stop_propagation();
130                    on_moved.call((value + 4.0).clamp(0.0, 100.0));
131                }
132                Key::ArrowUp if direction_is_vertical => {
133                    e.stop_propagation();
134                    on_moved.call((value + 4.0).clamp(0.0, 100.0));
135                }
136                Key::ArrowDown if direction_is_vertical => {
137                    e.stop_propagation();
138                    on_moved.call((value - 4.0).clamp(0.0, 100.0));
139                }
140                _ => {}
141            }
142        };
143
144        let on_pointer_enter = move |_| {
145            *status.write() = SliderStatus::Hovering;
146            if enabled() {
147                Cursor::set(CursorIcon::Pointer);
148            } else {
149                Cursor::set(CursorIcon::NotAllowed);
150            }
151        };
152
153        let on_pointer_leave = move |_| {
154            Cursor::set(CursorIcon::default());
155            *status.write() = SliderStatus::Idle;
156        };
157
158        let on_pointer_down = {
159            let on_moved = self.on_moved.clone();
160            move |e: Event<PointerEventData>| {
161                focus.request_focus();
162                clicking.set(true);
163                e.stop_propagation();
164                let coordinates = e.element_location();
165                let percentage = if direction_is_vertical {
166                    let y = coordinates.y - 8.0;
167                    100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
168                } else {
169                    let x = coordinates.x - 8.0;
170                    x / (size.read().width() as f64 - 15.) * 100.0
171                };
172                let percentage = percentage.clamp(0.0, 100.0);
173
174                on_moved.call(percentage);
175            }
176        };
177
178        let on_global_mouse_up = move |_| {
179            clicking.set(false);
180        };
181
182        let on_global_mouse_move = move |e: Event<MouseEventData>| {
183            e.stop_propagation();
184            if *clicking.peek() {
185                let coordinates = e.global_location;
186                let percentage = if direction_is_vertical {
187                    let y = coordinates.y - size.read().min_y() as f64 - 8.0;
188                    100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
189                } else {
190                    let x = coordinates.x - size.read().min_x() as f64 - 8.0;
191                    x / (size.read().width() as f64 - 15.) * 100.0
192                };
193                let percentage = percentage.clamp(0.0, 100.0);
194
195                on_moved.call(percentage);
196            }
197        };
198
199        let border = if focus_status() == FocusStatus::Keyboard {
200            Border::new()
201                .fill(theme.border_fill)
202                .width(2.)
203                .alignment(BorderAlignment::Inner)
204        } else {
205            Border::new()
206                .fill(Color::TRANSPARENT)
207                .width(0.)
208                .alignment(BorderAlignment::Inner)
209        };
210
211        let (
212            slider_width,
213            slider_height,
214            track_width,
215            track_height,
216            thumb_offset_x,
217            thumb_offset_y,
218            thumb_main_align,
219            padding,
220        ) = if direction_is_vertical {
221            (
222                Size::px(6.),
223                self.size.clone(),
224                Size::px(6.),
225                Size::func_data(
226                    move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
227                    &(value as i32),
228                ),
229                -6.,
230                3.,
231                Alignment::end(),
232                (0., 8.),
233            )
234        } else {
235            (
236                self.size.clone(),
237                Size::px(6.),
238                Size::func_data(
239                    move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
240                    &(value as i32),
241                ),
242                Size::px(6.),
243                -3.,
244                -6.,
245                Alignment::start(),
246                (8., 0.),
247            )
248        };
249
250        let thumb = rect()
251            .width(Size::fill())
252            .offset_x(thumb_offset_x)
253            .offset_y(thumb_offset_y)
254            .child(
255                rect()
256                    .width(Size::px(18.))
257                    .height(Size::px(18.))
258                    .corner_radius(50.)
259                    .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
260                    .padding(4.)
261                    .child(
262                        rect()
263                            .width(Size::fill())
264                            .height(Size::fill())
265                            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
266                            .corner_radius(50.),
267                    ),
268            );
269
270        let track = rect()
271            .width(track_width)
272            .height(track_height)
273            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
274            .corner_radius(50.);
275
276        rect()
277            .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
278            .maybe(self.enabled, |rect| {
279                rect.on_key_down(on_key_down)
280                    .on_pointer_down(on_pointer_down)
281                    .on_global_mouse_move(on_global_mouse_move)
282                    .on_global_mouse_up(on_global_mouse_up)
283            })
284            .on_pointer_enter(on_pointer_enter)
285            .on_pointer_leave(on_pointer_leave)
286            .a11y_id(focus.a11y_id())
287            .a11y_focusable(self.enabled)
288            .border(border)
289            .corner_radius(50.)
290            .padding(padding)
291            .child(
292                rect()
293                    .width(slider_width)
294                    .height(slider_height)
295                    .background(theme.background.mul_if(!self.enabled, 0.85))
296                    .corner_radius(50.)
297                    .direction(self.direction)
298                    .main_align(thumb_main_align)
299                    .children(if direction_is_vertical {
300                        vec![thumb.into(), track.into()]
301                    } else {
302                        vec![track.into(), thumb.into()]
303                    }),
304            )
305    }
306
307    fn render_key(&self) -> DiffKey {
308        self.key.clone().or(self.default_key())
309    }
310}