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 thumb_size = match self.orientation.get() {
287                        Orientation::Horizontal => cx.cache.get_width(thumb),
288                        Orientation::Vertical => cx.cache.get_height(thumb),
289                    };
290                    let min = self.range.get().start;
291                    let max = self.range.get().end;
292                    let step = self.step.get();
293
294                    let current = cx.current();
295                    let width = cx.cache.get_width(current);
296                    let height = cx.cache.get_height(current);
297                    let posx = cx.cache.get_posx(current);
298                    let posy = cx.cache.get_posy(current);
299
300                    let is_rtl = matches!(
301                        cx.style.direction.get(current).copied(),
302                        Some(Direction::RightToLeft)
303                    );
304
305                    let mut dx = match self.orientation.get() {
306                        Orientation::Horizontal => {
307                            let raw_dx = (cx.mouse.left.pos_down.0 - posx - thumb_size / 2.0)
308                                / (width - thumb_size);
309                            if is_rtl { 1.0 - raw_dx } else { raw_dx }
310                        }
311
312                        Orientation::Vertical => {
313                            (height - (cx.mouse.left.pos_down.1 - posy) - thumb_size / 2.0)
314                                / (height - thumb_size)
315                        }
316                    };
317
318                    dx = dx.clamp(0.0, 1.0);
319
320                    let mut val = min + dx * (max - min);
321
322                    val = step * (val / step).ceil();
323                    val = val.clamp(min, max);
324
325                    if let Some(callback) = self.on_change.take() {
326                        (callback)(cx, val);
327
328                        self.on_change = Some(callback);
329                    }
330                }
331            }
332
333            WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
334                self.is_dragging = false;
335                cx.focus_with_visibility(false);
336                cx.release();
337                cx.with_current(Entity::root(), |cx| {
338                    cx.set_pointer_events(true);
339                });
340            }
341
342            WindowEvent::MouseMove(x, y) => {
343                if self.is_dragging {
344                    let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
345                    let thumb_size = match self.orientation.get() {
346                        Orientation::Horizontal => cx.cache.get_width(thumb),
347                        Orientation::Vertical => cx.cache.get_height(thumb),
348                    };
349
350                    let min = self.range.get().start;
351                    let max = self.range.get().end;
352                    let step = self.step.get();
353
354                    let current = cx.current();
355                    let width = cx.cache.get_width(current);
356                    let height = cx.cache.get_height(current);
357                    let posx = cx.cache.get_posx(current);
358                    let posy = cx.cache.get_posy(current);
359
360                    let is_rtl = matches!(
361                        cx.style.direction.get(current).copied(),
362                        Some(Direction::RightToLeft)
363                    );
364
365                    let mut dx = match self.orientation.get() {
366                        Orientation::Horizontal => {
367                            let raw_dx = (*x - posx - thumb_size / 2.0) / (width - thumb_size);
368                            if is_rtl { 1.0 - raw_dx } else { raw_dx }
369                        }
370
371                        Orientation::Vertical => {
372                            (height - (*y - posy) - thumb_size / 2.0) / (height - thumb_size)
373                        }
374                    };
375
376                    dx = dx.clamp(0.0, 1.0);
377
378                    let mut val = min + dx * (max - min);
379
380                    val = step * (val / step).ceil();
381                    val = val.clamp(min, max);
382
383                    if let Some(callback) = &self.on_change {
384                        (callback)(cx, val);
385                    }
386                }
387            }
388
389            WindowEvent::MouseDoubleClick(button) if *button == MouseButton::Left => {
390                let is_thumb_target = cx
391                    .get_entities_by_class("thumb")
392                    .first()
393                    .copied()
394                    .map(|thumb| thumb == meta.target)
395                    .unwrap_or(false);
396
397                if is_thumb_target {
398                    cx.focus_with_visibility(false);
399                    cx.release();
400                    cx.with_current(Entity::root(), |cx| {
401                        cx.set_pointer_events(true);
402                    });
403                    self.is_dragging = false;
404                    cx.emit(SliderEvent::ResetDefault);
405                }
406            }
407
408            WindowEvent::ActionRequest(action) => match action.action {
409                Action::Increment => {
410                    let min = self.range.get().start;
411                    let max = self.range.get().end;
412                    let step = self.step.get();
413                    let mut val = self.value.get() + step;
414                    val = step * (val / step).ceil();
415                    val = val.clamp(min, max);
416                    if let Some(callback) = &self.on_change {
417                        (callback)(cx, val);
418                    }
419                }
420
421                Action::Decrement => {
422                    let min = self.range.get().start;
423                    let max = self.range.get().end;
424                    let step = self.step.get();
425                    let mut val = self.value.get() - step;
426                    val = step * (val / step).ceil();
427                    val = val.clamp(min, max);
428                    if let Some(callback) = &self.on_change {
429                        (callback)(cx, val);
430                    }
431                }
432
433                Action::SetValue => {
434                    if let Some(ActionData::NumericValue(val)) = action.data {
435                        let min = self.range.get().start;
436                        let max = self.range.get().end;
437                        let mut v = val as f32;
438                        v = v.clamp(min, max);
439                        if let Some(callback) = &self.on_change {
440                            (callback)(cx, v);
441                        }
442                    }
443                }
444
445                _ => {}
446            },
447
448            _ => {}
449        });
450    }
451}
452
453pub trait SliderModifiers: Sized {
454    /// Sets the callback triggered when the slider value is changed.
455    ///
456    /// Takes a closure which triggers when the slider value is changed,
457    /// either by pressing the track or dragging the thumb along the track.
458    ///
459    /// ```
460    /// # use vizia_core::prelude::*;
461    ///
462    /// # let mut cx = &mut Context::default();
463    /// # #[derive(Default)]
464    /// # pub struct AppData {
465    /// #     value: f32,
466    /// # }
467    /// # impl Model for AppData {}
468    /// # let value = Signal::new(0.5);
469    /// Slider::new(cx, value)
470    ///     .on_change(|cx, value| {
471    ///         let _ = (cx, value);
472    ///     });
473    /// ```
474    fn on_change<F>(self, callback: F) -> Self
475    where
476        F: 'static + Fn(&mut EventContext, f32);
477
478    /// Sets the range of the slider.
479    ///
480    /// If the source value is outside of the range then the slider will clip to min/max of the range.
481    ///
482    /// ```
483    /// # use vizia_core::prelude::*;
484    ///
485    /// # let mut cx = &mut Context::default();
486    /// # #[derive(Default)]
487    /// # pub struct AppData {
488    /// #     value: f32,
489    /// # }
490    /// # impl Model for AppData {}
491    /// # let value = Signal::new(0.5);
492    /// Slider::new(cx, value)
493    ///     .range(-20.0..50.0)
494    ///     .on_change(|cx, value| {
495    ///         let _ = (cx, value);
496    ///     });
497    /// ```
498    fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self;
499
500    /// Sets the orientation of the slider to vertical.
501    ///
502    /// ```
503    /// # use vizia_core::prelude::*;
504    ///
505    /// # let mut cx = &mut Context::default();
506    /// # #[derive(Default)]
507    /// # pub struct AppData {
508    /// #     value: f32,
509    /// # }
510    /// # impl Model for AppData {}
511    /// # let value = Signal::new(0.5);
512    /// Slider::new(cx, value)
513    ///     .vertical(true)
514    ///     .on_change(|cx, value| {
515    ///         let _ = (cx, value);
516    ///     });
517    /// ```
518    fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self;
519
520    /// Set the step value for the slider.
521    ///
522    /// ```
523    /// # use vizia_core::prelude::*;
524    ///
525    /// # let mut cx = &mut Context::default();
526    /// # #[derive(Default)]
527    /// # pub struct AppData {
528    /// #     value: f32,
529    /// # }
530    /// # impl Model for AppData {}
531    /// # let value = Signal::new(0.5);
532    /// Slider::new(cx, value)
533    ///     .step(0.1_f32)
534    ///     .on_change(|cx, value| {
535    ///         let _ = (cx, value);
536    ///     });
537    /// ```
538    fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self;
539
540    /// Sets the value that the slider resets to when the thumb is double-clicked.
541    fn default_value<U: Into<f32> + Clone + 'static>(
542        self,
543        default_value: impl Res<U> + 'static,
544    ) -> Self;
545}
546
547impl<S> SliderModifiers for Handle<'_, Slider<S>>
548where
549    S: SignalGet<f32> + 'static,
550{
551    fn on_change<F>(self, callback: F) -> Self
552    where
553        F: 'static + Fn(&mut EventContext, f32),
554    {
555        self.modify(|slider| slider.on_change = Some(Box::new(callback)))
556    }
557
558    fn range<U: Into<Range<f32>> + Clone + 'static>(self, range: impl Res<U> + 'static) -> Self {
559        let range = range.to_signal(self.cx);
560        self.bind(range, move |handle| {
561            let range = range.get();
562            let range = range.into();
563            handle.modify(|slider| {
564                slider.range.set(range);
565            });
566        })
567    }
568
569    fn vertical<U: Into<bool> + Clone + 'static>(self, vertical: impl Res<U> + 'static) -> Self {
570        let vertical = vertical.to_signal(self.cx);
571        self.bind(vertical, move |handle| {
572            let vertical = vertical.get().into();
573
574            let orientation =
575                if vertical { Orientation::Vertical } else { Orientation::Horizontal };
576            handle.modify(|slider| {
577                slider.orientation.set(orientation);
578            });
579        })
580    }
581
582    fn step<U: Into<f32> + Clone + 'static>(self, step: impl Res<U> + 'static) -> Self {
583        let step = step.to_signal(self.cx);
584        self.bind(step, move |handle| {
585            let step = step.get();
586            let step = step.into();
587            handle.modify(|slider| {
588                slider.step.set(step);
589            });
590        })
591    }
592
593    fn default_value<U: Into<f32> + Clone + 'static>(
594        self,
595        default_value: impl Res<U> + 'static,
596    ) -> Self {
597        let default_value = default_value.to_signal(self.cx);
598        self.bind(default_value, move |handle| {
599            let default_value = default_value.get().into();
600            handle.modify(|slider| {
601                slider.default_value.set(default_value);
602            });
603        })
604    }
605}