Skip to main content

kui_core/
slider.rs

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