Skip to main content

sequoia_view/
value.rs

1use sequoia_reactive::{Memo, Signal};
2
3use crate::live::{AnimSpec, Live, SpringSpec};
4
5pub type Value<T> = Live<T>;
6
7pub fn value<T: Clone + PartialEq + 'static>(v: T) -> Value<T> {
8    Live::fixed(v)
9}
10
11pub fn derived<T: Clone + PartialEq + 'static>(f: impl Fn() -> T + 'static) -> Value<T> {
12    Live::derived(f)
13}
14
15pub trait ValueExt<T: Clone + PartialEq + 'static>: Clone {
16    fn value_untracked(&self) -> T;
17
18    /// Derives a new value from this one.
19    ///
20    /// The closure receives `&T`, not `T`, so match it against a reference
21    /// (for example `|v| *v * 2.0` or `|v| v.is_open()`).
22    fn map_value<U: Clone + PartialEq + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Value<U>;
23
24    fn animated(self, spec: impl Into<AnimSpec>) -> Value<T>;
25
26    fn springed(self, spec: SpringSpec) -> Value<T>;
27}
28
29impl<T: Clone + PartialEq + 'static> ValueExt<T> for Signal<T> {
30    fn value_untracked(&self) -> T {
31        self.get_untracked()
32    }
33    fn map_value<U: Clone + PartialEq + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Value<U> {
34        let s = self.clone();
35        Live::derived(move || f(&s.get()))
36    }
37    fn animated(self, spec: impl Into<AnimSpec>) -> Value<T> {
38        Live::of_signal(self).animate(spec)
39    }
40    fn springed(self, spec: SpringSpec) -> Value<T> {
41        Live::of_signal(self).spring(spec)
42    }
43}
44
45impl<T: Clone + PartialEq + 'static> ValueExt<T> for Memo<T> {
46    fn value_untracked(&self) -> T {
47        self.get_untracked()
48    }
49    fn map_value<U: Clone + PartialEq + 'static>(&self, f: impl Fn(&T) -> U + 'static) -> Value<U> {
50        let m = self.clone();
51        Live::derived(move || f(&m.get()))
52    }
53    fn animated(self, spec: impl Into<AnimSpec>) -> Value<T> {
54        Live::of_memo(self).animate(spec)
55    }
56    fn springed(self, spec: SpringSpec) -> Value<T> {
57        Live::of_memo(self).spring(spec)
58    }
59}
60
61pub fn spring_spec(s: sequoia_animation::Spring) -> SpringSpec {
62    let w0 = std::f32::consts::TAU * s.frequency.max(0.0001);
63    let k = w0 * w0;
64    let c = 2.0 * s.damping_ratio.max(0.0) * w0;
65    SpringSpec {
66        stiffness: k,
67        damping: c,
68        mass: 1.0,
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn value_accepts_static_reactive_and_animated() {
78        fn takes_f32(_: impl Into<Value<f32>>) {}
79        let s = Signal::new(0.5f32);
80        takes_f32(0.8);
81        takes_f32(s.clone());
82        takes_f32(s.clone().animated(AnimSpec::SNAPPY));
83        takes_f32(s.clone().springed(SpringSpec::BOUNCY));
84        takes_f32(s.map_value(|v| v * 2.0));
85        takes_f32(value(0.1));
86        takes_f32(derived(|| 0.2));
87    }
88
89    #[test]
90    fn presets_are_sane() {
91        const { assert!(AnimSpec::FAST.duration_secs <= AnimSpec::SMOOTH.duration_secs) };
92        const { assert!(AnimSpec::SMOOTH.duration_secs <= AnimSpec::SLOW.duration_secs) };
93        const { assert!(AnimSpec::SNAPPY.duration_secs <= AnimSpec::GENTLE.duration_secs) };
94        let _ = (
95            AnimSpec::fast(),
96            AnimSpec::smooth(),
97            AnimSpec::snappy(),
98            AnimSpec::gentle(),
99        );
100        let _ = (
101            SpringSpec::snappy(),
102            SpringSpec::bouncy(),
103            SpringSpec::gentle(),
104        );
105    }
106
107    #[test]
108    fn spring_converts_without_nan() {
109        for s in [
110            sequoia_animation::Spring::snappy(),
111            sequoia_animation::Spring::bouncy(),
112            sequoia_animation::Spring::gentle(),
113        ] {
114            let spec = spring_spec(s);
115            assert!(spec.stiffness.is_finite() && spec.stiffness > 0.0);
116            assert!(spec.damping.is_finite() && spec.damping >= 0.0);
117        }
118    }
119
120    #[test]
121    fn value_ext_maps_and_animates() {
122        let s = Signal::new(false);
123        let v = s.map_value(|x| if *x { 1.0f32 } else { 0.6 });
124        assert_eq!(v.target_untracked(), 0.6);
125        s.set(true);
126        assert_eq!(v.target_untracked(), 1.0);
127        assert!(s.value_untracked());
128        let a = s.clone().animated(AnimSpec::SMOOTH);
129        assert!(a.is_animated());
130    }
131}