Skip to main content

vizia_core/views/
slider.rs

1use std::ops::Range;
2
3use crate::prelude::*;
4use accesskit::ActionData;
5
6/// Internal events for the slider view.
7pub(crate) enum SliderEvent {
8    Increment,
9    Decrement,
10    SetMin,
11    SetMax,
12    ResetDefault,
13}
14
15/// The slider control can be used to select from a continuous set of values.
16///
17/// The slider control consists of three main parts, a **thumb** element which can be moved between the extremes of a linear **track**,
18/// and a **range** element which fills the slider to indicate the current value.
19///
20/// # Examples
21///
22/// ## Basic Slider
23/// In the following example, a slider reads from a value source. The `on_change` callback is used
24/// to update that value when the slider thumb is moved, or if the track is clicked on.
25/// ```
26/// # use vizia_core::prelude::*;
27///
28/// # let mut cx = &mut Context::default();
29/// # #[derive(Default)]
30/// # pub struct AppData {
31/// #     value: f32,
32/// # }
33/// # impl Model for AppData {}
34/// # let value = Signal::new(0.5);
35/// Slider::new(cx, value)
36///     .on_change(|cx, value| {
37///         let _ = (cx, value);
38///     });
39/// ```
40///
41/// ## Slider with Label
42/// ```
43/// # use vizia_core::prelude::*;
44///
45/// # let mut cx = &mut Context::default();
46/// # #[derive(Default)]
47/// # pub struct AppData {
48/// #     value: f32,
49/// # }
50/// # impl Model for AppData {}
51/// # let value = Signal::new(0.5);
52/// HStack::new(cx, |cx|{
53///     Slider::new(cx, value)
54///         .on_change(|cx, value| {
55///             let _ = (cx, value);
56///         });
57///     Label::new(cx, value.map(|val| format!("{:.2}", val)));
58/// });
59/// ```
60pub struct Slider<S> {
61    value: S,
62    is_dragging: bool,
63    /// The orientation of the slider.
64    orientation: Signal<Orientation>,
65    /// The range of the slider.
66    range: Signal<Range<f32>>,
67    /// The step of the slider.
68    step: Signal<f32>,
69    /// The value that the slider resets to when double-clicking the thumb.
70    default_value: Signal<f32>,
71    on_change: Option<Box<dyn Fn(&mut EventContext, f32)>>,
72}
73
74impl<S> Slider<S>
75where
76    S: SignalGet<f32> + SignalMap<f32> + Copy + 'static,
77{
78    /// Creates a new slider from the provided value source.
79    ///
80    /// ```
81    /// # use vizia_core::prelude::*;
82    ///
83    /// # let mut cx = &mut Context::default();
84    /// # #[derive(Default)]
85    /// # pub struct AppData {
86    /// #     value: f32,
87    /// # }
88    /// # impl Model for AppData {}
89    /// # let value = Signal::new(0.5);
90    /// Slider::new(cx, value)
91    ///     .on_change(|cx, value| {
92    ///         let _ = (cx, value);
93    ///     });
94    /// ```
95    pub fn new(cx: &mut Context, value: S) -> Handle<Self> {
96        let range = Signal::new(0.0..1.0);
97        let orientation = Signal::new(Orientation::Horizontal);
98        let step = Signal::new(0.01);
99        let default_value = Signal::new(value.get());
100
101        Self { value, is_dragging: false, orientation, range, step, default_value, on_change: None }
102            .build(cx, move |cx| {
103                Keymap::from(vec![
104                    (
105                        KeyChord::new(Modifiers::empty(), Code::ArrowUp),
106                        KeymapEntry::new("Increment", |cx| cx.emit(SliderEvent::Increment)),
107                    ),
108                    (
109                        KeyChord::new(Modifiers::empty(), Code::ArrowRight),
110                        KeymapEntry::new("Increment", |cx| cx.emit(SliderEvent::Increment)),
111                    ),
112                    (
113                        KeyChord::new(Modifiers::empty(), Code::ArrowDown),
114                        KeymapEntry::new("Decrement", |cx| cx.emit(SliderEvent::Decrement)),
115                    ),
116                    (
117                        KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
118                        KeymapEntry::new("Decrement", |cx| cx.emit(SliderEvent::Decrement)),
119                    ),
120                    (
121                        KeyChord::new(Modifiers::empty(), Code::Home),
122                        KeymapEntry::new("Set Min", |cx| cx.emit(SliderEvent::SetMin)),
123                    ),
124                    (
125                        KeyChord::new(Modifiers::empty(), Code::End),
126                        KeymapEntry::new("Set Max", |cx| cx.emit(SliderEvent::SetMax)),
127                    ),
128                ])
129                .build(cx);
130
131                // Track
132                HStack::new(cx, move |cx| {
133                    let active_normalized = Memo::new(move |_| {
134                        let active_range = range.get();
135                        let val = value.get().clamp(active_range.start, active_range.end);
136                        (val - active_range.start) / (active_range.end - active_range.start)
137                    });
138
139                    let active_width = Memo::new(move |_| {
140                        let normal_val = active_normalized.get();
141                        if orientation.get() == Orientation::Horizontal {
142                            Percentage(normal_val * 100.0)
143                        } else {
144                            Stretch(1.0)
145                        }
146                    });
147
148                    let active_height = Memo::new(move |_| {
149                        let normal_val = active_normalized.get();
150                        if orientation.get() == Orientation::Horizontal {
151                            Stretch(1.0)
152                        } else {
153                            Percentage(normal_val * 100.0)
154                        }
155                    });
156
157                    // Range track
158                    VStack::new(cx, move |cx| {
159                        let dir = cx.environment().direction;
160
161                        let thumb_translate: Memo<Translate> = Memo::new(move |_| {
162                            let thumb_range = range.get();
163                            let val = value.get().clamp(thumb_range.start, thumb_range.end);
164                            let normal_val =
165                                (val - thumb_range.start) / (thumb_range.end - thumb_range.start);
166                            // Todo: Find a way to react to local direction rather than global direction.
167                            // Currently not possible because local direction is a style property
168                            // that gets resolved after bindings.
169                            // Ideally we need a way to do the translation in css which means changing
170                            // a css variable in rust code that gets used in the stylesheet to do the translation
171                            // rather than doing it here in code.
172                            let is_rtl = dir.get() == Direction::RightToLeft;
173                            if orientation.get() == Orientation::Horizontal {
174                                if is_rtl {
175                                    (Percentage(-100.0 * (1.0 - normal_val)), Pixels(0.0)).into()
176                                } else {
177                                    (Percentage(100.0 * (1.0 - normal_val)), Pixels(0.0)).into()
178                                }
179                            } else {
180                                (Pixels(0.0), Percentage(-100.0 * (1.0 - normal_val))).into()
181                            }
182                        });
183
184                        // Thumb
185                        Element::new(cx).class("thumb").translate(thumb_translate);
186                    })
187                    .class("range")
188                    .width(active_width)
189                    .height(active_height)
190                    .layout_type(orientation.map(|o| {
191                        if *o == Orientation::Horizontal {
192                            LayoutType::Row
193                        } else {
194                            LayoutType::Column
195                        }
196                    }))
197                    .alignment(orientation.map(|o| {
198                        if *o == Orientation::Horizontal {
199                            Alignment::Right
200                        } else {
201                            Alignment::TopCenter
202                        }
203                    }));
204                })
205                .class("track");
206            })
207            .orientation(orientation)
208            .role(Role::Slider)
209            .numeric_value(value.map(|v| (*v as f64 * 100.0).round() / 100.0))
210            .text_value(value.map(|v| format!("{}", (*v as f64 * 100.0).round() / 100.0)))
211            .navigable(true)
212    }
213}
214
215impl<S> View for Slider<S>
216where
217    S: SignalGet<f32> + 'static,
218{
219    fn element(&self) -> Option<&'static str> {
220        Some("slider")
221    }
222
223    fn accessibility(&self, _cx: &mut AccessContext, node: &mut AccessNode) {
224        node.set_numeric_value_step(self.step.get() as f64);
225        node.set_min_numeric_value(self.range.get().start as f64);
226        node.set_max_numeric_value(self.range.get().end as f64);
227    }
228
229    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
230        event.map(|slider_event, _| match slider_event {
231            SliderEvent::Increment => {
232                let min = self.range.get().start;
233                let max = self.range.get().end;
234                let step = self.step.get();
235                let mut val = self.value.get() + step;
236                val = val.clamp(min, max);
237                if let Some(callback) = &self.on_change {
238                    (callback)(cx, val);
239                }
240            }
241
242            SliderEvent::Decrement => {
243                let min = self.range.get().start;
244                let max = self.range.get().end;
245                let step = self.step.get();
246                let mut val = self.value.get() - step;
247                val = val.clamp(min, max);
248                if let Some(callback) = &self.on_change {
249                    (callback)(cx, val);
250                }
251            }
252
253            SliderEvent::SetMin => {
254                if let Some(callback) = &self.on_change {
255                    (callback)(cx, self.range.get().start);
256                }
257            }
258
259            SliderEvent::SetMax => {
260                if let Some(callback) = &self.on_change {
261                    (callback)(cx, self.range.get().end);
262                }
263            }
264
265            SliderEvent::ResetDefault => {
266                let min = self.range.get().start;
267                let max = self.range.get().end;
268                let val = self.default_value.get().clamp(min, max);
269                if let Some(callback) = &self.on_change {
270                    (callback)(cx, val);
271                }
272            }
273        });
274
275        event.map(|window_event, meta| match window_event {
276            WindowEvent::MouseDown(button) if *button == MouseButton::Left => {
277                if !cx.is_disabled() {
278                    self.is_dragging = true;
279                    cx.capture();
280                    cx.focus_with_visibility(false);
281                    cx.with_current(Entity::root(), |cx| {
282                        cx.set_pointer_events(false);
283                    });
284
285                    let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
286                    let current = cx.current();
287                    let bounds = cx.transformed_bounds(current);
288                    let thumb_bounds = cx.transformed_bounds(thumb);
289                    let thumb_size = match self.orientation.get() {
290                        Orientation::Horizontal => thumb_bounds.width(),
291                        Orientation::Vertical => thumb_bounds.height(),
292                    };
293                    let min = self.range.get().start;
294                    let max = self.range.get().end;
295                    let step = self.step.get();
296
297                    let is_rtl = matches!(
298                        cx.style.direction.get(current).copied(),
299                        Some(Direction::RightToLeft)
300                    );
301
302                    let mut dx = match self.orientation.get() {
303                        Orientation::Horizontal => {
304                            let span = (bounds.width() - thumb_size).max(f32::EPSILON);
305                            let raw_dx =
306                                (cx.mouse.left.pos_down.0 - bounds.left() - thumb_size / 2.0)
307                                    / span;
308                            if is_rtl { 1.0 - raw_dx } else { raw_dx }
309                        }
310
311                        Orientation::Vertical => {
312                            let span = (bounds.height() - thumb_size).max(f32::EPSILON);
313                            (bounds.height()
314                                - (cx.mouse.left.pos_down.1 - bounds.top())
315                                - thumb_size / 2.0)
316                                / span
317                        }
318                    };
319
320                    dx = dx.clamp(0.0, 1.0);
321
322                    let mut val = min + dx * (max - min);
323
324                    val = step * (val / step).ceil();
325                    val = val.clamp(min, max);
326
327                    if let Some(callback) = self.on_change.take() {
328                        (callback)(cx, val);
329
330                        self.on_change = Some(callback);
331                    }
332                }
333            }
334
335            WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
336                self.is_dragging = false;
337                cx.focus_with_visibility(false);
338                cx.release();
339                cx.with_current(Entity::root(), |cx| {
340                    cx.set_pointer_events(true);
341                });
342            }
343
344            WindowEvent::MouseMove(x, y) => {
345                if self.is_dragging {
346                    let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
347                    let current = cx.current();
348                    let bounds = cx.transformed_bounds(current);
349                    let thumb_bounds = cx.transformed_bounds(thumb);
350                    let thumb_size = match self.orientation.get() {
351                        Orientation::Horizontal => thumb_bounds.width(),
352                        Orientation::Vertical => thumb_bounds.height(),
353                    };
354
355                    let min = self.range.get().start;
356                    let max = self.range.get().end;
357                    let step = self.step.get();
358
359                    let is_rtl = matches!(
360                        cx.style.direction.get(current).copied(),
361                        Some(Direction::RightToLeft)
362                    );
363
364                    let mut dx = match self.orientation.get() {
365                        Orientation::Horizontal => {
366                            let span = (bounds.width() - thumb_size).max(f32::EPSILON);
367                            let raw_dx = (*x - bounds.left() - thumb_size / 2.0) / span;
368                            if is_rtl { 1.0 - raw_dx } else { raw_dx }
369                        }
370
371                        Orientation::Vertical => {
372                            let span = (bounds.height() - thumb_size).max(f32::EPSILON);
373                            (bounds.height() - (*y - bounds.top()) - thumb_size / 2.0) / span
374                        }
375                    };
376
377                    dx = dx.clamp(0.0, 1.0);
378
379                    let mut val = min + dx * (max - min);
380
381                    val = step * (val / step).ceil();
382                    val = val.clamp(min, max);
383
384                    if let Some(callback) = &self.on_change {
385                        (callback)(cx, val);
386                    }
387                }
388            }
389
390            WindowEvent::MouseDoubleClick(button) if *button == MouseButton::Left => {
391                let is_thumb_target = cx
392                    .get_entities_by_class("thumb")
393                    .first()
394                    .copied()
395                    .map(|thumb| thumb == meta.target)
396                    .unwrap_or(false);
397
398                if is_thumb_target {
399                    cx.focus_with_visibility(false);
400                    cx.release();
401                    cx.with_current(Entity::root(), |cx| {
402                        cx.set_pointer_events(true);
403                    });
404                    self.is_dragging = false;
405                    cx.emit(SliderEvent::ResetDefault);
406                }
407            }
408
409            WindowEvent::ActionRequest(action) => match action.action {
410                Action::Increment => {
411                    let min = self.range.get().start;
412                    let max = self.range.get().end;
413                    let step = self.step.get();
414                    let mut val = self.value.get() + step;
415                    val = step * (val / step).ceil();
416                    val = val.clamp(min, max);
417                    if let Some(callback) = &self.on_change {
418                        (callback)(cx, val);
419                    }
420                }
421
422                Action::Decrement => {
423                    let min = self.range.get().start;
424                    let max = self.range.get().end;
425                    let step = self.step.get();
426                    let mut val = self.value.get() - step;
427                    val = step * (val / step).ceil();
428                    val = val.clamp(min, max);
429                    if let Some(callback) = &self.on_change {
430                        (callback)(cx, val);
431                    }
432                }
433
434                Action::SetValue => {
435                    if let Some(ActionData::NumericValue(val)) = action.data {
436                        let min = self.range.get().start;
437                        let max = self.range.get().end;
438                        let mut v = val as f32;
439                        v = v.clamp(min, max);
440                        if let Some(callback) = &self.on_change {
441                            (callback)(cx, v);
442                        }
443                    }
444                }
445
446                _ => {}
447            },
448
449            _ => {}
450        });
451    }
452}
453
454pub trait SliderModifiers: Sized {
455    /// Sets the callback triggered when the slider value is changed.
456    ///
457    /// Takes a closure which triggers when the slider value is changed,
458    /// either by pressing the track or dragging the thumb along the track.
459    ///
460    /// ```
461    /// # use vizia_core::prelude::*;
462    ///
463    /// # let mut cx = &mut Context::default();
464    /// # #[derive(Default)]
465    /// # pub struct AppData {
466    /// #     value: f32,
467    /// # }
468    /// # impl Model for AppData {}
469    /// # let value = Signal::new(0.5);
470    /// Slider::new(cx, value)
471    ///     .on_change(|cx, value| {
472    ///         let _ = (cx, value);
473    ///     });
474    /// ```
475    fn on_change<F>(self, callback: F) -> Self
476    where
477        F: 'static + Fn(&mut EventContext, f32);
478
479    /// Sets the range of the slider.
480    ///
481    /// If the source value is outside of the range then the slider will clip to min/max of the range.
482    ///
483    /// ```
484    /// # use vizia_core::prelude::*;
485    ///
486    /// # let mut cx = &mut Context::default();
487    /// # #[derive(Default)]
488    /// # pub struct AppData {
489    /// #     value: f32,
490    /// # }
491    /// # impl Model for AppData {}
492    /// # let value = Signal::new(0.5);
493    /// Slider::new(cx, value)
494    ///     .range(-20.0..50.0)
495    ///     .on_change(|cx, value| {
496    ///         let _ = (cx, value);
497    ///     });
498    /// ```
499    fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self;
500
501    /// Sets the orientation of the slider to vertical.
502    ///
503    /// ```
504    /// # use vizia_core::prelude::*;
505    ///
506    /// # let mut cx = &mut Context::default();
507    /// # #[derive(Default)]
508    /// # pub struct AppData {
509    /// #     value: f32,
510    /// # }
511    /// # impl Model for AppData {}
512    /// # let value = Signal::new(0.5);
513    /// Slider::new(cx, value)
514    ///     .vertical(true)
515    ///     .on_change(|cx, value| {
516    ///         let _ = (cx, value);
517    ///     });
518    /// ```
519    fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self;
520
521    /// Set the step value for the slider.
522    ///
523    /// ```
524    /// # use vizia_core::prelude::*;
525    ///
526    /// # let mut cx = &mut Context::default();
527    /// # #[derive(Default)]
528    /// # pub struct AppData {
529    /// #     value: f32,
530    /// # }
531    /// # impl Model for AppData {}
532    /// # let value = Signal::new(0.5);
533    /// Slider::new(cx, value)
534    ///     .step(0.1_f32)
535    ///     .on_change(|cx, value| {
536    ///         let _ = (cx, value);
537    ///     });
538    /// ```
539    fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self;
540
541    /// Sets the value that the slider resets to when the thumb is double-clicked.
542    fn default_value<U: Into<f32> + Clone + 'static>(
543        self,
544        default_value: impl Res<U> + 'static,
545    ) -> Self;
546}
547
548impl<S> SliderModifiers for Handle<'_, Slider<S>>
549where
550    S: SignalGet<f32> + 'static,
551{
552    fn on_change<F>(self, callback: F) -> Self
553    where
554        F: 'static + Fn(&mut EventContext, f32),
555    {
556        self.modify(|slider| slider.on_change = Some(Box::new(callback)))
557    }
558
559    fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self {
560        let range = range.to_signal(self.cx);
561        self.bind(range, move |handle| {
562            let range = range.get();
563            let range = range.into();
564            handle.modify(|slider| {
565                slider.range.set(range);
566            });
567        })
568    }
569
570    fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self {
571        let vertical = vertical.to_signal(self.cx);
572        self.bind(vertical, move |handle| {
573            let vertical = vertical.get().into();
574
575            let orientation =
576                if vertical { Orientation::Vertical } else { Orientation::Horizontal };
577            handle.modify(|slider| {
578                slider.orientation.set(orientation);
579            });
580        })
581    }
582
583    fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self {
584        let step = step.to_signal(self.cx);
585        self.bind(step, move |handle| {
586            let step = step.get();
587            let step = step.into();
588            handle.modify(|slider| {
589                slider.step.set(step);
590            });
591        })
592    }
593
594    fn default_value<U: Into<f32> + Clone + 'static>(
595        self,
596        default_value: impl Res<U> + 'static,
597    ) -> Self {
598        let default_value = default_value.to_signal(self.cx);
599        self.bind(default_value, move |handle| {
600            let default_value = default_value.get().into();
601            handle.modify(|slider| {
602                slider.default_value.set(default_value);
603            });
604        })
605    }
606}