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            Cursor::set(CursorIcon::Pointer);
146            *status.write() = SliderStatus::Hovering;
147        };
148
149        let on_pointer_leave = move |_| {
150            Cursor::set(CursorIcon::default());
151            *status.write() = SliderStatus::Idle;
152        };
153
154        let on_pointer_down = {
155            let on_moved = self.on_moved.clone();
156            move |e: Event<PointerEventData>| {
157                focus.request_focus();
158                clicking.set(true);
159                e.stop_propagation();
160                let coordinates = e.element_location();
161                let percentage = if direction_is_vertical {
162                    let y = coordinates.y - 8.0;
163                    100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
164                } else {
165                    let x = coordinates.x - 8.0;
166                    x / (size.read().width() as f64 - 15.) * 100.0
167                };
168                let percentage = percentage.clamp(0.0, 100.0);
169
170                on_moved.call(percentage);
171            }
172        };
173
174        let on_global_mouse_up = move |_| {
175            clicking.set(false);
176        };
177
178        let on_global_mouse_move = move |e: Event<MouseEventData>| {
179            e.stop_propagation();
180            if *clicking.peek() {
181                let coordinates = e.global_location;
182                let percentage = if direction_is_vertical {
183                    let y = coordinates.y - size.read().min_y() as f64 - 8.0;
184                    100. - (y / (size.read().height() as f64 - 15.0) * 100.0)
185                } else {
186                    let x = coordinates.x - size.read().min_x() as f64 - 8.0;
187                    x / (size.read().width() as f64 - 15.) * 100.0
188                };
189                let percentage = percentage.clamp(0.0, 100.0);
190
191                on_moved.call(percentage);
192            }
193        };
194
195        let border = if focus_status() == FocusStatus::Keyboard {
196            Border::new()
197                .fill(theme.border_fill)
198                .width(2.)
199                .alignment(BorderAlignment::Inner)
200        } else {
201            Border::new()
202                .fill(Color::TRANSPARENT)
203                .width(0.)
204                .alignment(BorderAlignment::Inner)
205        };
206
207        let (
208            slider_width,
209            slider_height,
210            track_width,
211            track_height,
212            thumb_offset_x,
213            thumb_offset_y,
214            thumb_main_align,
215            padding,
216        ) = if direction_is_vertical {
217            (
218                Size::px(6.),
219                self.size.clone(),
220                Size::px(6.),
221                Size::func_data(
222                    move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
223                    &(value as i32),
224                ),
225                -6.,
226                3.,
227                Alignment::end(),
228                (0., 8.),
229            )
230        } else {
231            (
232                self.size.clone(),
233                Size::px(6.),
234                Size::func_data(
235                    move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
236                    &(value as i32),
237                ),
238                Size::px(6.),
239                -3.,
240                -6.,
241                Alignment::start(),
242                (8., 0.),
243            )
244        };
245
246        let thumb = rect()
247            .width(Size::fill())
248            .offset_x(thumb_offset_x)
249            .offset_y(thumb_offset_y)
250            .child(
251                rect()
252                    .width(Size::px(18.))
253                    .height(Size::px(18.))
254                    .corner_radius(50.)
255                    .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
256                    .padding(4.)
257                    .child(
258                        rect()
259                            .width(Size::fill())
260                            .height(Size::fill())
261                            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
262                            .corner_radius(50.),
263                    ),
264            );
265
266        let track = rect()
267            .width(track_width)
268            .height(track_height)
269            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
270            .corner_radius(50.);
271
272        rect()
273            .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
274            .maybe(self.enabled, |rect| {
275                rect.on_key_down(on_key_down)
276                    .on_pointer_enter(on_pointer_enter)
277                    .on_pointer_leave(on_pointer_leave)
278                    .on_pointer_down(on_pointer_down)
279                    .on_global_mouse_move(on_global_mouse_move)
280                    .on_global_mouse_up(on_global_mouse_up)
281            })
282            .a11y_id(focus.a11y_id())
283            .a11y_focusable(self.enabled)
284            .border(border)
285            .corner_radius(50.)
286            .padding(padding)
287            .child(
288                rect()
289                    .width(slider_width)
290                    .height(slider_height)
291                    .background(theme.background.mul_if(!self.enabled, 0.85))
292                    .corner_radius(50.)
293                    .direction(self.direction)
294                    .main_align(thumb_main_align)
295                    .children(if direction_is_vertical {
296                        vec![thumb.into(), track.into()]
297                    } else {
298                        vec![track.into(), thumb.into()]
299                    }),
300            )
301    }
302
303    fn render_key(&self) -> DiffKey {
304        self.key.clone().or(self.default_key())
305    }
306}