Skip to main content

freya_components/
slider.rs

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