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                if !e.data().is_primary() {
181                    return;
182                }
183                a11y_id.request_focus();
184                clicking.set(true);
185                e.stop_propagation();
186                let coordinates = e.element_location();
187                on_moved.call(calc_percentage(coordinates.x, coordinates.y));
188            }
189        };
190
191        let on_global_pointer_press = move |_: Event<PointerEventData>| {
192            clicking.set(false);
193        };
194
195        let on_global_pointer_move = move |e: Event<PointerEventData>| {
196            e.stop_propagation();
197            if *clicking.peek() {
198                let coordinates = e.global_location();
199                on_moved.call(calc_percentage(
200                    coordinates.x - size.read().min_x() as f64,
201                    coordinates.y - size.read().min_y() as f64,
202                ));
203            }
204        };
205
206        let on_wheel = {
207            let on_moved = self.on_moved.clone();
208            move |e: Event<WheelEventData>| {
209                if e.delta_y == 0.0 {
210                    return;
211                }
212                e.stop_propagation();
213                on_moved.call((value + e.delta_y * 0.1).clamp(0.0, 100.0));
214            }
215        };
216
217        let border = if focus() == Focus::Keyboard {
218            Border::new()
219                .fill(theme.border_fill)
220                .width(2.)
221                .alignment(BorderAlignment::Inner)
222        } else {
223            Border::new()
224                .fill(Color::TRANSPARENT)
225                .width(0.)
226                .alignment(BorderAlignment::Inner)
227        };
228
229        let (slider_width, slider_height) = if direction_is_vertical {
230            (Size::px(6.), self.size.clone())
231        } else {
232            (self.size.clone(), Size::px(6.))
233        };
234
235        let track_size = Size::func_data(
236            move |ctx| Some(value as f32 / 100. * (ctx.parent - 15.)),
237            &(value as i32),
238        );
239
240        let (track_width, track_height) = if direction_is_vertical {
241            (Size::px(6.), track_size)
242        } else {
243            (track_size, Size::px(6.))
244        };
245
246        let (thumb_offset_x, thumb_offset_y) = if direction_is_vertical {
247            (-6., 3.)
248        } else {
249            (-3., -6.)
250        };
251
252        let thumb_main_align = if direction_is_vertical {
253            Alignment::end()
254        } else {
255            Alignment::start()
256        };
257
258        let padding = if direction_is_vertical {
259            (0., 8.)
260        } else {
261            (8., 0.)
262        };
263
264        let thumb = rect()
265            .width(Size::fill())
266            .offset_x(thumb_offset_x)
267            .offset_y(thumb_offset_y)
268            .child(
269                rect()
270                    .width(Size::px(18.))
271                    .height(Size::px(18.))
272                    .corner_radius(50.)
273                    .background(theme.thumb_background.mul_if(!self.enabled, 0.85))
274                    .padding(4.)
275                    .child(
276                        rect()
277                            .width(Size::fill())
278                            .height(Size::fill())
279                            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
280                            .corner_radius(50.),
281                    ),
282            );
283
284        let track = rect()
285            .width(track_width)
286            .height(track_height)
287            .background(theme.thumb_inner_background.mul_if(!self.enabled, 0.85))
288            .corner_radius(50.);
289
290        rect()
291            .a11y_id(a11y_id)
292            .a11y_focusable(self.enabled)
293            .a11y_role(AccessibilityRole::Slider)
294            .on_sized(move |e: Event<SizedEventData>| size.set(e.area))
295            .maybe(self.enabled, |rect| {
296                rect.on_key_down(on_key_down)
297                    .on_pointer_down(on_pointer_down)
298                    .on_global_pointer_move(on_global_pointer_move)
299                    .on_global_pointer_press(on_global_pointer_press)
300                    .on_wheel(on_wheel)
301            })
302            .on_pointer_enter(on_pointer_enter)
303            .on_pointer_leave(on_pointer_leave)
304            .border(border)
305            .corner_radius(50.)
306            .padding(padding)
307            .child(
308                rect()
309                    .width(slider_width)
310                    .height(slider_height)
311                    .background(theme.background.mul_if(!self.enabled, 0.85))
312                    .corner_radius(50.)
313                    .direction(self.direction)
314                    .main_align(thumb_main_align)
315                    .children(if direction_is_vertical {
316                        vec![thumb.into(), track.into()]
317                    } else {
318                        vec![track.into(), thumb.into()]
319                    }),
320            )
321    }
322
323    fn render_key(&self) -> DiffKey {
324        self.key.clone().or(self.default_key())
325    }
326}