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