Skip to main content

kui_core/
slider.rs

1//! A slider's arithmetic: what value a pointer position, an arrow, a Page
2//! key or Home / End means on a node whose role is `Slider` and that
3//! declared `on_change`.
4//!
5//! The core proposes a value and never applies it: the `change` event
6//! carries the number, and nothing moves until the view declares it as
7//! `value_now`. Nothing here is retained past a drag. The stock control is
8//! [`widgets::slider`](crate::widgets::slider); this module is what it and
9//! a custom slider share.
10//!
11//! Numbers are worked in `f64` from the declared `f32`s read back through
12//! their shortest decimal spelling, and a result is rounded to the
13//! decimals the range and step are written in, so a step of `0.1` from
14//! `0` is `0.3` on the wire and not `0.30000001192092896`.
15
16use crate::geom::{Rect, Vec2};
17use crate::input::UiEvent;
18use crate::key::Key;
19use crate::spec::AccessSpec;
20use crate::tree::OriginId;
21use crate::value::Value;
22
23/// A slider's range and step, as the view declared them. The ARIA
24/// defaults stand in for an unset end: 0 and 100.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct SliderRange {
27    pub min: f64,
28    pub max: f64,
29    pub step: f64,
30    /// Decimals the results are rounded to: the most the declared numbers
31    /// are written with.
32    decimals: usize,
33}
34
35/// One keyboard (or assistive-technology) move of a slider.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum SliderMove {
38    /// One step up (`1`) or down (`-1`): an arrow, Increment / Decrement.
39    Step(i32),
40    /// Ten steps: PageUp / PageDown.
41    Page(i32),
42    /// The range's start: Home.
43    Home,
44    /// The range's end: End.
45    End,
46}
47
48/// An `f32` as the decimal it was written as: `0.1f32` is `0.1`, not
49/// `0.10000000149011612`.
50pub(crate) fn exact(v: f32) -> f64 {
51    format!("{v}").parse().unwrap_or(v as f64)
52}
53
54/// How many decimals `v`'s shortest spelling has.
55fn decimals_of(v: f64) -> usize {
56    let s = format!("{v}");
57    s.split_once('.').map_or(0, |(_, frac)| frac.len())
58}
59
60impl SliderRange {
61    /// The range a node declares, or `None` when it cannot be one: an end
62    /// that is not finite, or a max not past the min.
63    pub fn of(ax: &AccessSpec) -> Option<Self> {
64        let min = exact(ax.value_min.unwrap_or(0.0));
65        let max = exact(ax.value_max.unwrap_or(100.0));
66        if !(min.is_finite() && max.is_finite()) || max <= min {
67            return None;
68        }
69        let step = match ax.value_step {
70            Some(s) => exact(s),
71            None => (max - min) / 100.0,
72        };
73        // A hundredth of a range like 0..1 is 0.01, which has two
74        // decimals; one of 0..7 is 0.07. The step's own spelling says
75        // what the grid is.
76        let decimals = decimals_of(min)
77            .max(decimals_of(max))
78            .max(decimals_of(step))
79            .min(9);
80        Some(SliderRange {
81            min,
82            max,
83            step,
84            decimals,
85        })
86    }
87
88    /// `v` on the step grid from `min`, inside the range. The top of the
89    /// range is always reachable even when the step does not divide it.
90    pub fn snap(&self, v: f64) -> f64 {
91        let v = v.clamp(self.min, self.max);
92        let k = ((v - self.min) / self.step).round();
93        let snapped = (self.min + k * self.step).min(self.max);
94        let snapped = if self.max - v < (v - snapped).abs() {
95            self.max
96        } else {
97            snapped
98        };
99        self.round(snapped)
100    }
101
102    fn round(&self, v: f64) -> f64 {
103        format!("{:.*}", self.decimals, v).parse().unwrap_or(v)
104    }
105
106    /// The value a fraction of the track means, snapped.
107    pub fn at_fraction(&self, f: f64) -> f64 {
108        self.snap(self.min + f.clamp(0.0, 1.0) * (self.max - self.min))
109    }
110
111    /// Where one move from `now` lands, snapped and clamped. `now` outside
112    /// the range moves from the end it is past.
113    pub fn moved(&self, now: Option<f32>, mv: SliderMove) -> f64 {
114        let now = now.map_or(self.min, exact).clamp(self.min, self.max);
115        match mv {
116            SliderMove::Step(n) => self.snap(now + n as f64 * self.step),
117            SliderMove::Page(n) => self.snap(now + 10.0 * n as f64 * self.step),
118            SliderMove::Home => self.min,
119            SliderMove::End => self.max,
120        }
121    }
122}
123
124/// A slider's track as the pointer reads it: the node's content box along
125/// its main axis, and the range it maps onto. Registered on the node's
126/// hit region when it declared `on_change` (`HitRegion::slider`).
127#[derive(Clone, Debug, PartialEq)]
128pub struct SliderTrack {
129    /// Where the track starts and how long it is, logical px along its
130    /// axis: the content box, so a stock slider's padding — half its thumb
131    /// — keeps the thumb's centre under the pointer at both ends.
132    pub start: f32,
133    pub len: f32,
134    /// A column slider runs bottom to top, its maximum at the top.
135    pub vertical: bool,
136    pub range: SliderRange,
137    pub tag: Value,
138}
139
140impl SliderTrack {
141    /// The track of a node laid out at `rect` with `padding`, running
142    /// along its main axis.
143    pub fn new(
144        rect: Rect,
145        padding: crate::geom::Edges,
146        vertical: bool,
147        range: SliderRange,
148        tag: Value,
149    ) -> Self {
150        let (start, len) = if vertical {
151            (rect.y + padding.t, rect.h - padding.y())
152        } else {
153            (rect.x + padding.l, rect.w - padding.x())
154        };
155        SliderTrack {
156            start,
157            len: len.max(0.0),
158            vertical,
159            range,
160            tag,
161        }
162    }
163
164    /// The value under `p`, snapped.
165    pub fn value_at(&self, p: Vec2) -> f64 {
166        if self.len <= 0.0 {
167            return self.range.min;
168        }
169        let along = if self.vertical { p.y } else { p.x };
170        let f = ((along - self.start) / self.len) as f64;
171        self.range
172            .at_fraction(if self.vertical { 1.0 - f } else { f })
173    }
174}
175
176/// `{kind="change", value, phase, tag}` on the slider.
177pub fn change_event(origin: OriginId, key: Key, value: f64, phase: &str, tag: &Value) -> UiEvent {
178    let payload = Value::map([
179        ("kind", Value::str("change")),
180        ("value", Value::Float(value)),
181        ("phase", Value::str(phase)),
182    ]);
183    UiEvent::on(origin, key, payload).tagged(Some(tag))
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::geom::Edges;
190
191    fn range(min: f32, max: f32, step: Option<f32>) -> SliderRange {
192        SliderRange::of(&AccessSpec {
193            value_min: Some(min),
194            value_max: Some(max),
195            value_step: step,
196            ..AccessSpec::EMPTY
197        })
198        .unwrap()
199    }
200
201    #[test]
202    fn a_step_lands_on_the_decimal_it_names() {
203        let r = range(0.0, 1.0, Some(0.1));
204        assert_eq!(r.moved(Some(0.2), SliderMove::Step(1)), 0.3);
205        assert_eq!(r.moved(Some(0.3), SliderMove::Step(-1)), 0.2);
206        assert_eq!(r.moved(Some(0.95), SliderMove::Step(1)), 1.0);
207        assert_eq!(r.moved(Some(0.0), SliderMove::Step(-1)), 0.0);
208    }
209
210    #[test]
211    fn the_default_step_is_a_hundredth_and_the_ends_are_aria_s() {
212        let r = SliderRange::of(&AccessSpec::EMPTY).unwrap();
213        assert_eq!((r.min, r.max, r.step), (0.0, 100.0, 1.0));
214        assert_eq!(r.moved(Some(41.0), SliderMove::Page(1)), 51.0);
215        assert_eq!(r.moved(None, SliderMove::End), 100.0);
216        assert_eq!(r.moved(Some(3.0), SliderMove::Home), 0.0);
217    }
218
219    #[test]
220    fn a_step_that_does_not_divide_the_range_still_reaches_the_top() {
221        let r = range(0.0, 10.0, Some(3.0));
222        assert_eq!(r.snap(9.9), 10.0);
223        assert_eq!(r.snap(8.0), 9.0);
224        assert_eq!(r.moved(Some(9.0), SliderMove::Step(1)), 10.0);
225    }
226
227    #[test]
228    fn a_range_that_is_not_one_is_none() {
229        let flat = AccessSpec {
230            value_min: Some(5.0),
231            value_max: Some(5.0),
232            ..AccessSpec::EMPTY
233        };
234        assert!(SliderRange::of(&flat).is_none());
235    }
236
237    #[test]
238    fn the_pointer_reads_the_content_box_and_a_column_runs_upward() {
239        let r = range(0.0, 100.0, Some(10.0));
240        let pad = Edges {
241            l: 8.0,
242            r: 8.0,
243            t: 8.0,
244            b: 8.0,
245        };
246        let t = SliderTrack::new(Rect::new(0.0, 0.0, 116.0, 16.0), pad, false, r, Value::Null);
247        assert_eq!(t.value_at(Vec2::new(8.0, 8.0)), 0.0);
248        assert_eq!(t.value_at(Vec2::new(58.0, 8.0)), 50.0);
249        assert_eq!(t.value_at(Vec2::new(-40.0, 8.0)), 0.0);
250        assert_eq!(t.value_at(Vec2::new(500.0, 8.0)), 100.0);
251        let v = SliderTrack::new(Rect::new(0.0, 0.0, 16.0, 116.0), pad, true, r, Value::Null);
252        assert_eq!(v.value_at(Vec2::new(8.0, 8.0)), 100.0);
253        assert_eq!(v.value_at(Vec2::new(8.0, 108.0)), 0.0);
254    }
255}