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