Skip to main content

sequoia_view/
track.rs

1#![forbid(unsafe_code)]
2
3use sequoia_core::{Point, Rect};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum Orientation {
7    #[default]
8    Horizontal,
9    Vertical,
10}
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct Track1D {
14    start: f32,
15    len: f32,
16    pad: f32,
17}
18
19impl Track1D {
20    pub fn new(start: f32, len: f32, pad: f32) -> Self {
21        Self {
22            start,
23            len: len.max(0.0),
24            pad: pad.max(0.0),
25        }
26    }
27
28    pub fn horizontal(rect: Rect, pad: f32) -> Self {
29        Self::new(rect.left(), rect.size.width, pad)
30    }
31
32    pub fn vertical(rect: Rect, pad: f32) -> Self {
33        Self::new(rect.top(), rect.size.height, pad)
34    }
35
36    fn inner(&self) -> f32 {
37        (self.len - self.pad * 2.0).max(1.0)
38    }
39
40    pub fn t_of(&self, pos: f32) -> f32 {
41        if !pos.is_finite() {
42            return 0.0;
43        }
44        ((pos - (self.start + self.pad)) / self.inner()).clamp(0.0, 1.0)
45    }
46
47    pub fn pos_of(&self, t: f32) -> f32 {
48        let t = if t.is_finite() {
49            t.clamp(0.0, 1.0)
50        } else {
51            0.0
52        };
53        self.start + self.pad + t * self.inner()
54    }
55
56    pub fn value_of(&self, pos: f32, min: f32, max: f32) -> f32 {
57        let (lo, hi) = if max >= min { (min, max) } else { (max, min) };
58        lo + self.t_of(pos) * (hi - lo).max(0.0)
59    }
60
61    pub fn pos_of_value(&self, v: f32, min: f32, max: f32) -> f32 {
62        let span = (max - min).abs().max(f32::MIN_POSITIVE);
63        self.pos_of((v - min.min(max)) / span)
64    }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct Pad2D {
69    rect: Rect,
70    pad: f32,
71}
72
73impl Pad2D {
74    pub fn new(rect: Rect, pad: f32) -> Self {
75        Self {
76            rect,
77            pad: pad.max(0.0),
78        }
79    }
80
81    fn track_x(&self) -> Track1D {
82        Track1D::horizontal(self.rect, self.pad)
83    }
84
85    fn track_y(&self) -> Track1D {
86        Track1D::vertical(self.rect, self.pad)
87    }
88
89    pub fn sv_of(&self, p: Point) -> (f32, f32) {
90        let s = self.track_x().t_of(p.x);
91        let v = 1.0 - self.track_y().t_of(p.y);
92        (s, v)
93    }
94
95    pub fn pos_of(&self, s: f32, v: f32) -> Point {
96        Point::new(self.track_x().pos_of(s), self.track_y().pos_of(1.0 - v))
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
105        Rect::from_xywh(x, y, w, h)
106    }
107
108    #[test]
109    fn track_roundtrips_with_knob_pad() {
110        let t = Track1D::horizontal(r(0.0, 0.0, 200.0, 26.0), 7.0);
111        assert!((t.pos_of(0.0) - 7.0).abs() < 1e-4);
112        assert!((t.pos_of(1.0) - 193.0).abs() < 1e-4);
113        assert!((t.t_of(7.0)).abs() < 1e-4);
114        assert!((t.t_of(193.0) - 1.0).abs() < 1e-4);
115
116        assert_eq!(t.t_of(0.0), 0.0);
117        assert_eq!(t.t_of(200.0), 1.0);
118        assert!((t.pos_of(0.5) - 100.0).abs() < 1e-4);
119        assert!((t.t_of(100.0) - 0.5).abs() < 1e-4);
120    }
121
122    #[test]
123    fn track_without_pad_is_full_bleed() {
124        let t = Track1D::horizontal(r(10.0, 0.0, 200.0, 26.0), 0.0);
125        assert!((t.pos_of(0.0) - 10.0).abs() < 1e-4);
126        assert!((t.pos_of(1.0) - 210.0).abs() < 1e-4);
127        assert!((t.t_of(110.0) - 0.5).abs() < 1e-4);
128    }
129
130    #[test]
131    fn track_maps_values_and_survives_garbage() {
132        let t = Track1D::horizontal(r(0.0, 0.0, 200.0, 26.0), 7.0);
133        assert!((t.value_of(7.0, 0.0, 360.0)).abs() < 1e-3);
134        assert!((t.value_of(193.0, 0.0, 360.0) - 360.0).abs() < 1e-3);
135        assert!((t.pos_of_value(180.0, 0.0, 360.0) - t.pos_of(0.5)).abs() < 1e-4);
136        assert_eq!(t.t_of(f32::NAN), 0.0);
137        assert_eq!(t.pos_of(f32::NAN), 7.0);
138
139        let flat = Track1D::horizontal(r(0.0, 0.0, 0.0, 0.0), 7.0);
140        assert!(flat.pos_of(0.5).is_finite());
141        assert!(flat.t_of(0.0).is_finite());
142    }
143
144    #[test]
145    fn pad2d_roundtrips_sv() {
146        let pad = Pad2D::new(r(0.0, 0.0, 200.0, 200.0), 7.0);
147        let c = pad.pos_of(0.5, 0.5);
148        assert!((c.x - 100.0).abs() < 1e-4 && (c.y - 100.0).abs() < 1e-4);
149        let (s, v) = pad.sv_of(c);
150        assert!((s - 0.5).abs() < 1e-4 && (v - 0.5).abs() < 1e-4);
151
152        assert_eq!(pad.sv_of(Point::new(0.0, 0.0)), (0.0, 1.0));
153        assert_eq!(pad.sv_of(Point::new(200.0, 200.0)), (1.0, 0.0));
154    }
155}