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