vizia_core/views/
slider.rs

1use std::ops::Range;
2
3use accesskit::ActionData;
4
5use crate::prelude::*;
6
7/// Internal data used by the slider.
8#[derive(Clone, Debug, Default, Data)]
9pub struct SliderDataInternal {
10    /// The orientation of the slider.
11    pub orientation: Orientation,
12    /// The range of the slider.
13    pub range: Range<f32>,
14    /// The step of the slider.
15    pub step: f32,
16    /// How much the slider should change in response to keyboard events.
17    pub keyboard_fraction: f32,
18}
19
20/// The slider control can be used to select from a continuous set of values.
21///
22/// The slider control consists of three main parts, a **thumb** element which can be moved between the extremes of a linear **track**,
23/// and an **active** element which fills the slider to indicate the current value.
24///
25/// # Examples
26///
27/// ## Basic Slider
28/// In the following example, a slider is bound to a value. The `on_change` callback is used to send an event to mutate the
29/// bound value when the slider thumb is moved, or if the track is clicked on.
30/// ```
31/// # use vizia_core::prelude::*;
32/// # use vizia_derive::*;
33/// # let mut cx = &mut Context::default();
34/// # #[derive(Lens, Default)]
35/// # pub struct AppData {
36/// #     value: f32,
37/// # }
38/// # impl Model for AppData {}
39/// # AppData::default().build(cx);
40/// Slider::new(cx, AppData::value)
41///     .on_change(|cx, value| {
42///         debug!("Slider on_change: {}", value);
43///     });
44/// ```
45///
46/// ## Slider with Label
47/// ```
48/// # use vizia_core::prelude::*;
49/// # use vizia_derive::*;
50/// # let mut cx = &mut Context::default();
51/// # #[derive(Lens, Default)]
52/// # pub struct AppData {
53/// #     value: f32,
54/// # }
55/// # impl Model for AppData {}
56/// # AppData::default().build(cx);
57/// HStack::new(cx, |cx|{
58///     Slider::new(cx, AppData::value)
59///         .on_change(|cx, value| {
60///             debug!("Slider on_change: {}", value);
61///         });
62///     Label::new(cx, AppData::value.map(|val| format!("{:.2}", val)));
63/// });
64/// ```
65#[derive(Lens)]
66pub struct Slider<L: Lens> {
67    lens: L,
68    is_dragging: bool,
69    internal: SliderDataInternal,
70    on_change: Option<Box<dyn Fn(&mut EventContext, f32)>>,
71}
72
73impl<L> Slider<L>
74where
75    L: Lens<Target = f32>,
76{
77    /// Creates a new slider bound to the value targeted by the lens.
78    ///
79    /// ```
80    /// # use vizia_core::prelude::*;
81    /// # use vizia_derive::*;
82    /// # let mut cx = &mut Context::default();
83    /// # #[derive(Lens, Default)]
84    /// # pub struct AppData {
85    /// #     value: f32,
86    /// # }
87    /// # impl Model for AppData {}
88    /// # AppData::default().build(cx);
89    /// Slider::new(cx, AppData::value)
90    ///     .on_change(|cx, value| {
91    ///         debug!("Slider on_change: {}", value);
92    ///     });
93    /// ```
94    pub fn new(cx: &mut Context, lens: L) -> Handle<Self> {
95        Self {
96            lens,
97            is_dragging: false,
98
99            internal: SliderDataInternal {
100                orientation: Orientation::Horizontal,
101                range: 0.0..1.0,
102                step: 0.01,
103                keyboard_fraction: 0.1,
104            },
105
106            on_change: None,
107        }
108        .build(cx, move |cx| {
109            Binding::new(cx, Slider::<L>::internal, move |cx, slider_data| {
110                // Track
111                HStack::new(cx, move |cx| {
112                    let slider_data = slider_data.get(cx);
113                    let orientation = slider_data.orientation;
114                    let range = slider_data.range;
115
116                    // Active track
117                    VStack::new(cx, |cx| {
118                        // Thumb
119                        Element::new(cx).class("thumb").bind(lens, move |handle, value| {
120                            let val = value.get(&handle).clamp(range.start, range.end);
121                            let normal_val = (val - range.start) / (range.end - range.start);
122                            if orientation == Orientation::Horizontal {
123                                handle.translate((
124                                    Percentage(100.0 * (1.0 - normal_val)),
125                                    Pixels(0.0),
126                                ));
127                            } else {
128                                handle.translate((
129                                    Pixels(0.0),
130                                    Percentage(-100.0 * (1.0 - normal_val)),
131                                ));
132                            }
133                        });
134                    })
135                    .class("active")
136                    .bind(lens, move |handle, value| {
137                        let val = value.get(&handle).clamp(range.start, range.end);
138                        let normal_val = (val - range.start) / (range.end - range.start);
139
140                        if orientation == Orientation::Horizontal {
141                            handle
142                                .height(Stretch(1.0))
143                                .width(Percentage(normal_val * 100.0))
144                                .layout_type(LayoutType::Row)
145                                .alignment(Alignment::Right);
146                        } else {
147                            handle
148                                .width(Stretch(1.0))
149                                .height(Percentage(normal_val * 100.0))
150                                .layout_type(LayoutType::Column)
151                                .alignment(Alignment::TopCenter);
152                        }
153                    });
154                })
155                .class("track");
156            });
157        })
158        .toggle_class(
159            "vertical",
160            Self::internal.map(|slider_data| slider_data.orientation == Orientation::Vertical),
161        )
162        .role(Role::Slider)
163        .numeric_value(lens.map(|val| (*val as f64 * 100.0).round() / 100.0))
164        .text_value(lens.map(|val| {
165            let v = (*val as f64 * 100.0).round() / 100.0;
166            format!("{}", v)
167        }))
168        .navigable(true)
169    }
170}
171
172impl<L: Lens<Target = f32>> View for Slider<L> {
173    fn element(&self) -> Option<&'static str> {
174        Some("slider")
175    }
176
177    fn accessibility(&self, _cx: &mut AccessContext, node: &mut AccessNode) {
178        node.set_numeric_value_step(self.internal.step as f64);
179        node.set_min_numeric_value(self.internal.range.start as f64);
180        node.set_max_numeric_value(self.internal.range.end as f64);
181    }
182
183    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
184        event.map(|window_event, _| match window_event {
185            WindowEvent::MouseDown(button) if *button == MouseButton::Left => {
186                if !cx.is_disabled() {
187                    self.is_dragging = true;
188                    cx.capture();
189                    cx.focus_with_visibility(false);
190                    cx.with_current(Entity::root(), |cx| {
191                        cx.set_pointer_events(false);
192                    });
193
194                    let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
195                    let thumb_size = match self.internal.orientation {
196                        Orientation::Horizontal => cx.cache.get_width(thumb),
197                        Orientation::Vertical => cx.cache.get_height(thumb),
198                    };
199                    let min = self.internal.range.start;
200                    let max = self.internal.range.end;
201                    let step = self.internal.step;
202
203                    let current = cx.current();
204                    let width = cx.cache.get_width(current);
205                    let height = cx.cache.get_height(current);
206                    let posx = cx.cache.get_posx(current);
207                    let posy = cx.cache.get_posy(current);
208
209                    let mut dx = match self.internal.orientation {
210                        Orientation::Horizontal => {
211                            (cx.mouse.left.pos_down.0 - posx - thumb_size / 2.0)
212                                / (width - thumb_size)
213                        }
214
215                        Orientation::Vertical => {
216                            (height - (cx.mouse.left.pos_down.1 - posy) - thumb_size / 2.0)
217                                / (height - thumb_size)
218                        }
219                    };
220
221                    dx = dx.clamp(0.0, 1.0);
222
223                    let mut val = min + dx * (max - min);
224
225                    val = step * (val / step).ceil();
226                    val = val.clamp(min, max);
227
228                    if let Some(callback) = self.on_change.take() {
229                        (callback)(cx, val);
230
231                        self.on_change = Some(callback);
232                    }
233                }
234            }
235
236            WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
237                self.is_dragging = false;
238                cx.focus_with_visibility(false);
239                cx.release();
240                cx.with_current(Entity::root(), |cx| {
241                    cx.set_pointer_events(true);
242                });
243            }
244
245            WindowEvent::MouseMove(x, y) => {
246                if self.is_dragging {
247                    let thumb = cx.get_entities_by_class("thumb").first().copied().unwrap();
248                    let thumb_size = match self.internal.orientation {
249                        Orientation::Horizontal => cx.cache.get_width(thumb),
250                        Orientation::Vertical => cx.cache.get_height(thumb),
251                    };
252
253                    let min = self.internal.range.start;
254                    let max = self.internal.range.end;
255                    let step = self.internal.step;
256
257                    let current = cx.current();
258                    let width = cx.cache.get_width(current);
259                    let height = cx.cache.get_height(current);
260                    let posx = cx.cache.get_posx(current);
261                    let posy = cx.cache.get_posy(current);
262
263                    let mut dx = match self.internal.orientation {
264                        Orientation::Horizontal => {
265                            (*x - posx - thumb_size / 2.0) / (width - thumb_size)
266                        }
267
268                        Orientation::Vertical => {
269                            (height - (*y - posy) - thumb_size / 2.0) / (height - thumb_size)
270                        }
271                    };
272
273                    dx = dx.clamp(0.0, 1.0);
274
275                    let mut val = min + dx * (max - min);
276
277                    val = step * (val / step).ceil();
278                    val = val.clamp(min, max);
279
280                    if let Some(callback) = &self.on_change {
281                        (callback)(cx, val);
282                    }
283                }
284            }
285
286            WindowEvent::KeyDown(Code::ArrowUp | Code::ArrowRight, _) => {
287                let min = self.internal.range.start;
288                let max = self.internal.range.end;
289                let step = self.internal.step;
290                let mut val = self.lens.get(cx) + step;
291                val = val.clamp(min, max);
292                if let Some(callback) = &self.on_change {
293                    (callback)(cx, val);
294                }
295            }
296
297            WindowEvent::KeyDown(Code::ArrowDown | Code::ArrowLeft, _) => {
298                let min = self.internal.range.start;
299                let max = self.internal.range.end;
300                let step = self.internal.step;
301                let mut val = self.lens.get(cx) - step;
302                val = val.clamp(min, max);
303                if let Some(callback) = &self.on_change {
304                    (callback)(cx, val);
305                }
306            }
307
308            WindowEvent::ActionRequest(action) => match action.action {
309                Action::Increment => {
310                    let min = self.internal.range.start;
311                    let max = self.internal.range.end;
312                    let step = self.internal.step;
313                    let mut val = self.lens.get(cx) + step;
314                    val = step * (val / step).ceil();
315                    val = val.clamp(min, max);
316                    if let Some(callback) = &self.on_change {
317                        (callback)(cx, val);
318                    }
319                }
320
321                Action::Decrement => {
322                    let min = self.internal.range.start;
323                    let max = self.internal.range.end;
324                    let step = self.internal.step;
325                    let mut val = self.lens.get(cx) - step;
326                    val = step * (val / step).ceil();
327                    val = val.clamp(min, max);
328                    if let Some(callback) = &self.on_change {
329                        (callback)(cx, val);
330                    }
331                }
332
333                Action::SetValue => {
334                    if let Some(ActionData::NumericValue(val)) = action.data {
335                        let min = self.internal.range.start;
336                        let max = self.internal.range.end;
337                        let mut v = val as f32;
338                        v = v.clamp(min, max);
339                        if let Some(callback) = &self.on_change {
340                            (callback)(cx, v);
341                        }
342                    }
343                }
344
345                _ => {}
346            },
347
348            _ => {}
349        });
350    }
351}
352
353impl<L: Lens> Handle<'_, Slider<L>> {
354    /// Sets the callback triggered when the slider value is changed.
355    ///
356    /// Takes a closure which triggers when the slider value is changed,
357    /// either by pressing the track or dragging the thumb along the track.
358    ///
359    /// ```
360    /// # use vizia_core::prelude::*;
361    /// # use vizia_derive::*;
362    /// # let mut cx = &mut Context::default();
363    /// # #[derive(Lens, Default)]
364    /// # pub struct AppData {
365    /// #     value: f32,
366    /// # }
367    /// # impl Model for AppData {}
368    /// # AppData::default().build(cx);
369    /// Slider::new(cx, AppData::value)
370    ///     .on_change(|cx, value| {
371    ///         debug!("Slider on_change: {}", value);
372    ///     });
373    /// ```
374    pub fn on_change<F>(self, callback: F) -> Self
375    where
376        F: 'static + Fn(&mut EventContext, f32),
377    {
378        self.modify(|slider| slider.on_change = Some(Box::new(callback)))
379    }
380
381    /// Sets the range of the slider.
382    ///
383    /// If the bound data is outside of the range then the slider will clip to min/max of the range.
384    ///
385    /// ```
386    /// # use vizia_core::prelude::*;
387    /// # use vizia_derive::*;
388    /// # let mut cx = &mut Context::default();
389    /// # #[derive(Lens, Default)]
390    /// # pub struct AppData {
391    /// #     value: f32,
392    /// # }
393    /// # impl Model for AppData {}
394    /// # AppData::default().build(cx);
395    /// Slider::new(cx, AppData::value)
396    ///     .range(-20.0..50.0)
397    ///     .on_change(|cx, value| {
398    ///         debug!("Slider on_change: {}", value);
399    ///     });
400    /// ```
401    pub fn range<U: Into<Range<f32>>>(self, range: impl Res<U>) -> Self {
402        self.bind(range, |handle, range| {
403            let range = range.get(&handle).into();
404            handle.modify(|slider| {
405                slider.internal.range = range;
406            });
407        })
408    }
409
410    /// Sets the orientation of the slider.
411    ///
412    /// ```
413    /// # use vizia_core::prelude::*;
414    /// # use vizia_derive::*;
415    /// # let mut cx = &mut Context::default();
416    /// # #[derive(Lens, Default)]
417    /// # pub struct AppData {
418    /// #     value: f32,
419    /// # }
420    /// # impl Model for AppData {}
421    /// # AppData::default().build(cx);
422    /// Slider::new(cx, AppData::value)
423    ///     .orientation(Orientation::Vertical)
424    ///     .on_change(|cx, value| {
425    ///         debug!("Slider on_change: {}", value);
426    ///     });
427    /// ```
428    pub fn orientation<U: Into<Orientation>>(self, orientation: impl Res<U>) -> Self {
429        self.bind(orientation, |handle, orientation| {
430            let orientation = orientation.get(&handle).into();
431            handle.modify(|slider: &mut Slider<L>| {
432                slider.internal.orientation = orientation;
433            });
434        })
435    }
436
437    /// Set the step value for the slider.
438    ///
439    /// ```
440    /// # use vizia_core::prelude::*;
441    /// # use vizia_derive::*;
442    /// # let mut cx = &mut Context::default();
443    /// # #[derive(Lens, Default)]
444    /// # pub struct AppData {
445    /// #     value: f32,
446    /// # }
447    /// # impl Model for AppData {}
448    /// # AppData::default().build(cx);
449    /// Slider::new(cx, AppData::value)
450    ///     .step(0.1)
451    ///     .on_change(|cx, value| {
452    ///         debug!("Slider on_change: {}", value);
453    ///     });
454    /// ```
455    pub fn step<U: Into<f32>>(self, step: impl Res<U>) -> Self {
456        self.bind(step, |handle, step| {
457            let step = step.get(&handle).into();
458            handle.modify(|slider| {
459                slider.internal.step = step;
460            });
461        })
462    }
463
464    /// Sets the fraction of a slider that a press of an arrow key will change.
465    ///
466    /// ```
467    /// # use vizia_core::prelude::*;
468    /// # use vizia_derive::*;
469    /// # let mut cx = &mut Context::default();
470    /// # #[derive(Lens, Default)]
471    /// # pub struct AppData {
472    /// #     value: f32,
473    /// # }
474    /// # impl Model for AppData {}
475    /// # AppData::default().build(cx);
476    /// Slider::new(cx, AppData::value)
477    ///     .keyboard_fraction(0.05)
478    ///     .on_change(|cx, value| {
479    ///         debug!("Slider on_change: {}", value);
480    ///     });
481    /// ```
482    pub fn keyboard_fraction<U: Into<f32>>(self, keyboard_fraction: impl Res<U>) -> Self {
483        self.bind(keyboard_fraction, |handle, keyboard_fraction| {
484            let keyboard_fraction = keyboard_fraction.get(&handle).into();
485            handle.modify(|slider| {
486                slider.internal.keyboard_fraction = keyboard_fraction;
487            });
488        })
489    }
490}