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