vizia_core/views/
slider.rs

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