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>;
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}