kas_widgets/
slider.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! `Slider` control
7
8use super::{GripMsg, GripPart};
9use kas::event::FocusSource;
10use kas::messages::{DecrementStep, IncrementStep, SetValueF64};
11use kas::prelude::*;
12use kas::theme::Feature;
13use std::fmt::Debug;
14use std::ops::{Add, RangeInclusive, Sub};
15
16/// Requirements on type used by [`Slider`]
17///
18/// Implementations are provided for standard float and integer types.
19pub trait SliderValue:
20    Copy
21    + Debug
22    + PartialOrd
23    + Add<Output = Self>
24    + Sub<Output = Self>
25    + Cast<f64>
26    + ConvApprox<f64>
27    + 'static
28{
29    /// The default step size (usually 1)
30    fn default_step() -> Self;
31}
32
33impl SliderValue for f64 {
34    fn default_step() -> Self {
35        1.0
36    }
37}
38
39impl SliderValue for f32 {
40    fn default_step() -> Self {
41        1.0
42    }
43}
44
45macro_rules! impl_slider_ty {
46    ($ty:ty) => {
47        impl SliderValue for $ty {
48            fn default_step() -> Self {
49                1
50            }
51        }
52    };
53    ($ty:ty, $($tt:ty),*) => {
54        impl_slider_ty!($ty);
55        impl_slider_ty!($($tt),*);
56    };
57}
58impl_slider_ty!(i8, i16, i32, i64, i128, isize);
59impl_slider_ty!(u8, u16, u32, u64, u128, usize);
60
61#[impl_self]
62mod Slider {
63    /// A slider
64    ///
65    /// Sliders allow user input of a value from a fixed range.
66    ///
67    /// ### Messages
68    ///
69    /// [`SetValueF64`] may be used to set the input value.
70    ///
71    /// [`IncrementStep`] and [`DecrementStep`] change the value by one step.
72    #[autoimpl(Debug ignore self.state_fn, self.on_move)]
73    #[widget]
74    pub struct Slider<A, T: SliderValue, D: Directional = Direction> {
75        core: widget_core!(),
76        direction: D,
77        // Terminology assumes vertical orientation:
78        range: (T, T),
79        step: T,
80        value: T,
81        #[widget(&())]
82        grip: GripPart,
83        state_fn: Box<dyn Fn(&ConfigCx, &A) -> T>,
84        on_move: Option<Box<dyn Fn(&mut EventCx, &A, T)>>,
85    }
86
87    impl Self
88    where
89        D: Default,
90    {
91        /// Construct a slider
92        ///
93        /// Values vary within the given `range`, increasing in the given
94        /// `direction`. The default step size is
95        /// 1 for common types (see [`SliderValue::default_step`]).
96        ///
97        /// The slider's current value is set by `state_fn` on update.
98        ///
99        /// To make the slider interactive, assign an event handler with
100        /// [`Self::with`] or [`Self::with_msg`].
101        #[inline]
102        pub fn new(
103            range: RangeInclusive<T>,
104            state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
105        ) -> Self {
106            Slider::new_dir(range, state_fn, D::default())
107        }
108    }
109    impl<A, T: SliderValue> Slider<A, T, kas::dir::Left> {
110        /// Construct with fixed direction
111        #[inline]
112        pub fn left(
113            range: RangeInclusive<T>,
114            state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
115        ) -> Self {
116            Slider::new(range, state_fn)
117        }
118    }
119    impl<A, T: SliderValue> Slider<A, T, kas::dir::Right> {
120        /// Construct with fixed direction
121        #[inline]
122        pub fn right(
123            range: RangeInclusive<T>,
124            state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
125        ) -> Self {
126            Slider::new(range, state_fn)
127        }
128    }
129    impl<A, T: SliderValue> Slider<A, T, kas::dir::Up> {
130        /// Construct with fixed direction
131        #[inline]
132        pub fn up(
133            range: RangeInclusive<T>,
134            state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
135        ) -> Self {
136            Slider::new(range, state_fn)
137        }
138    }
139
140    impl<A, T: SliderValue> Slider<A, T, kas::dir::Down> {
141        /// Construct with fixed direction
142        #[inline]
143        pub fn down(
144            range: RangeInclusive<T>,
145            state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
146        ) -> Self {
147            Slider::new(range, state_fn)
148        }
149    }
150
151    impl Self {
152        /// Construct a slider with given direction
153        ///
154        /// Values vary within the given `range`, increasing in the given
155        /// `direction`. The default step size is
156        /// 1 for common types (see [`SliderValue::default_step`]).
157        ///
158        /// The slider's current value is set by `state_fn` on update.
159        ///
160        /// To make the slider interactive, assign an event handler with
161        /// [`Self::with`] or [`Self::with_msg`].
162        #[inline]
163        pub fn new_dir(
164            range: RangeInclusive<T>,
165            state_fn: impl Fn(&ConfigCx, &A) -> T + 'static,
166            direction: D,
167        ) -> Self {
168            assert!(!range.is_empty());
169            let value = *range.start();
170            Slider {
171                core: Default::default(),
172                direction,
173                range: range.into_inner(),
174                step: T::default_step(),
175                value,
176                grip: GripPart::new(),
177                state_fn: Box::new(state_fn),
178                on_move: None,
179            }
180        }
181
182        /// Send the message generated by `f` on movement
183        #[inline]
184        #[must_use]
185        pub fn with_msg<M>(self, f: impl Fn(T) -> M + 'static) -> Self
186        where
187            M: std::fmt::Debug + 'static,
188        {
189            self.with(move |cx, _, state| cx.push(f(state)))
190        }
191
192        /// Call the handler `f` on movement
193        #[inline]
194        #[must_use]
195        pub fn with(mut self, f: impl Fn(&mut EventCx, &A, T) + 'static) -> Self {
196            debug_assert!(self.on_move.is_none());
197            self.on_move = Some(Box::new(f));
198            self
199        }
200
201        /// Get the slider's direction
202        #[inline]
203        pub fn direction(&self) -> Direction {
204            self.direction.as_direction()
205        }
206
207        /// Set the step size
208        #[inline]
209        #[must_use]
210        pub fn with_step(mut self, step: T) -> Self {
211            self.step = step;
212            self
213        }
214
215        /// Set value and update grip
216        ///
217        /// Returns `true` if, after clamping to the supported range, `value`
218        /// differs from the existing value.
219        #[allow(clippy::neg_cmp_op_on_partial_ord)]
220        fn set_value(&mut self, cx: &mut EventState, value: T) -> bool {
221            let value = if !(value >= self.range.0) {
222                self.range.0
223            } else if !(value <= self.range.1) {
224                self.range.1
225            } else {
226                value
227            };
228
229            if value == self.value {
230                return false;
231            }
232
233            self.value = value;
234            self.grip.set_offset(cx, self.offset());
235            true
236        }
237
238        // translate value to offset in local coordinates
239        fn offset(&self) -> Offset {
240            let a = self.value - self.range.0;
241            let b = self.range.1 - self.range.0;
242            let max_offset = self.grip.max_offset();
243            let mut frac = a.cast() / b.cast();
244            assert!((0.0..=1.0).contains(&frac));
245            if self.direction.is_reversed() {
246                frac = 1.0 - frac;
247            }
248            match self.direction.is_vertical() {
249                false => Offset((max_offset.0 as f64 * frac).cast_floor(), 0),
250                true => Offset(0, (max_offset.1 as f64 * frac).cast_floor()),
251            }
252        }
253
254        fn apply_grip_offset(&mut self, cx: &mut EventCx, data: &A, offset: Offset) {
255            let b = self.range.1 - self.range.0;
256            let max_offset = self.grip.max_offset();
257            let (offset, max) = match self.direction.is_vertical() {
258                false => (offset.0, max_offset.0),
259                true => (offset.1, max_offset.1),
260            };
261            let mut a = (b.cast() * (offset as f64 / max as f64))
262                .round()
263                .cast_approx();
264            if self.direction.is_reversed() {
265                a = b - a;
266            }
267            if self.set_value(cx, a + self.range.0) {
268                if let Some(ref f) = self.on_move {
269                    f(cx, data, self.value);
270                }
271            }
272        }
273    }
274
275    impl Layout for Self {
276        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
277            let _ = self.grip.size_rules(sizer.re(), axis);
278            sizer.feature(Feature::Slider(self.direction()), axis)
279        }
280
281        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, hints: AlignHints) {
282            let align = match self.direction.is_vertical() {
283                false => AlignPair::new(Align::Stretch, hints.vert.unwrap_or(Align::Center)),
284                true => AlignPair::new(hints.horiz.unwrap_or(Align::Center), Align::Stretch),
285            };
286            let mut rect = cx.align_feature(Feature::Slider(self.direction()), rect, align);
287            widget_set_rect!(rect);
288            self.grip.set_track(rect);
289
290            // Set the grip size (we could instead call set_size but the widget
291            // model requires we call set_rect anyway):
292            rect.size
293                .set_component(self.direction, cx.size_cx().grip_len());
294            self.grip.set_rect(cx, rect, AlignHints::NONE);
295            // Correct the position:
296            self.grip.set_offset(cx, self.offset());
297        }
298
299        fn draw(&self, mut draw: DrawCx) {
300            let dir = self.direction.as_direction();
301            draw.slider(self.rect(), &self.grip, dir);
302        }
303    }
304
305    impl Tile for Self {
306        fn navigable(&self) -> bool {
307            true
308        }
309
310        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
311            Role::Slider {
312                min: self.range.0.cast(),
313                max: self.range.1.cast(),
314                step: self.step.cast(),
315                value: self.value.cast(),
316                direction: self.direction.as_direction(),
317            }
318        }
319
320        fn probe(&self, coord: Coord) -> Id {
321            if self.on_move.is_some() {
322                if let Some(id) = self.grip.try_probe(coord) {
323                    return id;
324                }
325            }
326            self.id()
327        }
328    }
329
330    impl Events for Self {
331        const REDRAW_ON_MOUSE_OVER: bool = true;
332
333        type Data = A;
334
335        fn update(&mut self, cx: &mut ConfigCx, data: &A) {
336            let v = (self.state_fn)(cx, data);
337            self.set_value(cx, v);
338        }
339
340        fn handle_event(&mut self, cx: &mut EventCx, data: &A, event: Event) -> IsUsed {
341            if self.on_move.is_none() {
342                return Unused;
343            }
344
345            match event {
346                Event::Command(cmd, code) => {
347                    let rev = self.direction.is_reversed();
348                    let value = match cmd {
349                        Command::Left | Command::Up => match rev {
350                            false => self.value - self.step,
351                            true => self.value + self.step,
352                        },
353                        Command::Right | Command::Down => match rev {
354                            false => self.value + self.step,
355                            true => self.value - self.step,
356                        },
357                        Command::PageUp | Command::PageDown => {
358                            // Generics makes this easier than constructing a literal and multiplying!
359                            let mut x = self.step + self.step;
360                            x = x + x;
361                            x = x + x;
362                            x = x + x;
363                            match rev == (cmd == Command::PageDown) {
364                                false => self.value + x,
365                                true => self.value - x,
366                            }
367                        }
368                        Command::Home => self.range.0,
369                        Command::End => self.range.1,
370                        _ => return Unused,
371                    };
372
373                    cx.depress_with_key(&self, code);
374
375                    if self.set_value(cx, value) {
376                        if let Some(ref f) = self.on_move {
377                            f(cx, data, self.value);
378                        }
379                    }
380                }
381                Event::PressStart(press) => {
382                    let offset = self.grip.handle_press_on_track(cx, &press);
383                    self.apply_grip_offset(cx, data, offset);
384                }
385                _ => return Unused,
386            }
387            Used
388        }
389
390        fn handle_messages(&mut self, cx: &mut EventCx, data: &A) {
391            if self.on_move.is_none() {
392                return;
393            }
394
395            match cx.try_pop() {
396                Some(GripMsg::PressStart) => {
397                    cx.request_nav_focus(self.id(), FocusSource::Synthetic)
398                }
399                Some(GripMsg::PressMove(pos)) => {
400                    self.apply_grip_offset(cx, data, pos);
401                }
402                Some(GripMsg::PressEnd(_)) => (),
403                None => {
404                    let mut new_value = None;
405                    if let Some(SetValueF64(v)) = cx.try_pop() {
406                        new_value = v
407                            .try_cast_approx()
408                            .map_err(|err| log::warn!("Slider failed to handle SetValueF64: {err}"))
409                            .ok();
410                    } else if let Some(IncrementStep) = cx.try_pop() {
411                        new_value = Some(self.value + self.step);
412                    } else if let Some(DecrementStep) = cx.try_pop() {
413                        new_value = Some(self.value - self.step);
414                    }
415
416                    if let Some(value) = new_value
417                        && self.set_value(cx, value)
418                        && let Some(ref f) = self.on_move
419                    {
420                        f(cx, data, self.value);
421                    }
422                }
423            }
424        }
425    }
426}