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