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, inner_slider_width, inner_slider_height) =
207            if direction_is_vertical {
208                (
209                    Size::auto(),
210                    self.size.clone(),
211                    Size::px(6.),
212                    self.size.clone(),
213                )
214            } else {
215                (
216                    self.size.clone(),
217                    Size::auto(),
218                    self.size.clone(),
219                    Size::px(6.),
220                )
221            };
222
223        let track_size = Size::func_data(
224            move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
225            &(value as i32),
226        );
227
228        let (track_width, track_height) = if direction_is_vertical {
229            (Size::px(6.), track_size)
230        } else {
231            (track_size, Size::px(6.))
232        };
233
234        let (thumb_offset_x, thumb_offset_y) = if direction_is_vertical {
235            (-6., 3.)
236        } else {
237            (-3., -6.)
238        };
239
240        let thumb_main_align = if direction_is_vertical {
241            Alignment::end()
242        } else {
243            Alignment::start()
244        };
245
246        let padding = if direction_is_vertical {
247            (0., 8.)
248        } else {
249            (8., 0.)
250        };
251
252        let thumb = rect()
253            .width(Size::fill())
254            .offset_x(thumb_offset_x)
255            .offset_y(thumb_offset_y)
256            .child(
257                rect()
258                    .width(Size::px(18.))
259                    .height(Size::px(18.))
260                    .corner_radius(50.)
261                    .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
262                    .padding(4.)
263                    .child(
264                        rect()
265                            .expanded()
266                            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
267                            .corner_radius(50.),
268                    ),
269            );
270
271        let track = rect()
272            .width(track_width)
273            .height(track_height)
274            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
275            .corner_radius(50.);
276
277        rect()
278            .a11y_id(focus.a11y_id())
279            .a11y_focusable(self.enabled)
280            .a11y_role(AccessibilityRole::Slider)
281            .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
282            .maybe(self.enabled, |rect| {
283                rect.on_key_down(on_key_down)
284                    .on_pointer_down(on_pointer_down)
285                    .on_global_pointer_move(on_global_pointer_move)
286                    .on_global_pointer_press(on_global_pointer_press)
287            })
288            .on_pointer_enter(on_pointer_enter)
289            .on_pointer_leave(on_pointer_leave)
290            .border(border)
291            .corner_radius(50.)
292            .padding(padding)
293            .width(slider_width)
294            .height(slider_height)
295            .child(
296                rect()
297                    .width(inner_slider_width)
298                    .height(inner_slider_height)
299                    .background(theme.background.mul_if(!self.enabled, 0.85))
300                    .corner_radius(50.)
301                    .direction(self.direction)
302                    .main_align(thumb_main_align)
303                    .children(if direction_is_vertical {
304                        vec![thumb.into(), track.into()]
305                    } else {
306                        vec![track.into(), thumb.into()]
307                    }),
308            )
309    }
310
311    fn render_key(&self) -> DiffKey {
312        self.key.clone().or(self.default_key())
313    }
314}