Skip to main content

cliffy_core/
behavior.rs

1//! Behavior - Time-varying values backed by geometric algebra
2//!
3//! A `Behavior<T>` represents a value that can change over time.
4//! Internally, all values are stored as GA3 multivectors, enabling
5//! geometric transformations as the foundation for state updates.
6//!
7//! This is inspired by Conal Elliott's classical FRP semantics where
8//! `Behavior a = Time -> a` - a function from time to a value.
9
10use crate::geometric::{FromGeometric, IntoGeometric, GA3};
11use std::cell::RefCell;
12use std::rc::Rc;
13
14/// A subscription handle that can be used to unsubscribe
15#[allow(clippy::type_complexity)]
16pub struct Subscription {
17    #[allow(dead_code)] // Reserved for debugging
18    id: usize,
19    unsubscribe: Rc<RefCell<Option<Box<dyn Fn()>>>>,
20}
21
22impl Subscription {
23    /// Unsubscribe from updates
24    pub fn unsubscribe(self) {
25        if let Some(unsub) = self.unsubscribe.borrow_mut().take() {
26            unsub();
27        }
28    }
29}
30
31/// A time-varying value backed by geometric algebra
32///
33/// `Behavior<T>` wraps any value type `T` and stores it internally as a
34/// GA3 multivector. This enables:
35///
36/// - Reactive updates with subscriber notification
37/// - Derived behaviors via `map` and `combine`
38/// - Geometric transformations under the hood
39///
40/// # Example
41///
42/// ```rust
43/// use cliffy_core::{behavior, Behavior};
44///
45/// let count = behavior(0);
46/// assert_eq!(count.sample(), 0);
47///
48/// count.update(|n| n + 1);
49/// assert_eq!(count.sample(), 1);
50/// ```
51#[allow(clippy::type_complexity)]
52pub struct Behavior<T> {
53    /// Internal geometric state
54    state: Rc<RefCell<GA3>>,
55
56    /// Cached value (for types that can't be reconstructed from GA3)
57    cache: Rc<RefCell<T>>,
58
59    /// Subscribers to notify on update
60    subscribers: Rc<RefCell<Vec<(usize, Box<dyn Fn(&T)>)>>>,
61
62    /// Next subscriber ID
63    next_id: Rc<RefCell<usize>>,
64}
65
66impl<T> Clone for Behavior<T> {
67    fn clone(&self) -> Self {
68        Self {
69            state: Rc::clone(&self.state),
70            cache: Rc::clone(&self.cache),
71            subscribers: Rc::clone(&self.subscribers),
72            next_id: Rc::clone(&self.next_id),
73        }
74    }
75}
76
77impl<T: IntoGeometric + FromGeometric + Clone + 'static> Behavior<T> {
78    /// Create a new behavior with an initial value
79    pub fn new(initial: T) -> Self {
80        let mv = initial.clone().into_geometric();
81        Self {
82            state: Rc::new(RefCell::new(mv)),
83            cache: Rc::new(RefCell::new(initial)),
84            subscribers: Rc::new(RefCell::new(Vec::new())),
85            next_id: Rc::new(RefCell::new(0)),
86        }
87    }
88
89    /// Sample the current value
90    pub fn sample(&self) -> T {
91        self.cache.borrow().clone()
92    }
93
94    /// Update the value using a transformation function
95    ///
96    /// The function receives the current value and returns the new value.
97    /// All subscribers are notified after the update.
98    pub fn update<F>(&self, f: F)
99    where
100        F: FnOnce(T) -> T,
101    {
102        let current = self.cache.borrow().clone();
103        let new_value = f(current);
104        let new_mv = new_value.clone().into_geometric();
105
106        *self.state.borrow_mut() = new_mv;
107        *self.cache.borrow_mut() = new_value;
108
109        // Notify subscribers
110        let cache = self.cache.borrow();
111        for (_, callback) in self.subscribers.borrow().iter() {
112            callback(&cache);
113        }
114    }
115
116    /// Set the value directly
117    pub fn set(&self, value: T) {
118        self.update(|_| value);
119    }
120
121    /// Subscribe to value changes
122    ///
123    /// Returns a `Subscription` that can be used to unsubscribe.
124    pub fn subscribe<F>(&self, callback: F) -> Subscription
125    where
126        F: Fn(&T) + 'static,
127    {
128        let id = {
129            let mut next = self.next_id.borrow_mut();
130            let id = *next;
131            *next += 1;
132            id
133        };
134
135        self.subscribers.borrow_mut().push((id, Box::new(callback)));
136
137        let subscribers = Rc::clone(&self.subscribers);
138        let unsubscribe = Rc::new(RefCell::new(Some(Box::new(move || {
139            subscribers.borrow_mut().retain(|(i, _)| *i != id);
140        }) as Box<dyn Fn()>)));
141
142        Subscription { id, unsubscribe }
143    }
144
145    /// Create a derived behavior by mapping a function over this behavior
146    ///
147    /// The derived behavior will automatically update when this behavior changes.
148    pub fn map<U, F>(&self, f: F) -> Behavior<U>
149    where
150        U: IntoGeometric + FromGeometric + Clone + 'static,
151        F: Fn(T) -> U + 'static,
152    {
153        let initial = f(self.sample());
154        let derived = Behavior::new(initial);
155
156        // Subscribe to changes and update derived
157        let derived_clone = derived.clone();
158        self.subscribe(move |value| {
159            let new_value = f(value.clone());
160            derived_clone.set(new_value);
161        });
162
163        derived
164    }
165
166    /// Combine two behaviors into a new behavior
167    pub fn combine<U, V, F>(&self, other: &Behavior<U>, f: F) -> Behavior<V>
168    where
169        U: IntoGeometric + FromGeometric + Clone + 'static,
170        V: IntoGeometric + FromGeometric + Clone + 'static,
171        F: Fn(T, U) -> V + Clone + 'static,
172    {
173        let initial = f(self.sample(), other.sample());
174        let combined = Behavior::new(initial);
175
176        // Subscribe to self
177        let combined_clone = combined.clone();
178        let other_clone = other.clone();
179        let f_clone = f.clone();
180        self.subscribe(move |a| {
181            let b = other_clone.sample();
182            combined_clone.set(f_clone(a.clone(), b));
183        });
184
185        // Subscribe to other
186        let combined_clone = combined.clone();
187        let self_clone = self.clone();
188        other.subscribe(move |b| {
189            let a = self_clone.sample();
190            combined_clone.set(f(a, b.clone()));
191        });
192
193        combined
194    }
195
196    /// Get the internal geometric state (for advanced users)
197    pub fn geometric_state(&self) -> GA3 {
198        self.state.borrow().clone()
199    }
200
201    /// Apply a geometric transformation directly (for advanced users)
202    pub fn apply_geometric<F>(&self, transform: F)
203    where
204        F: FnOnce(&GA3) -> GA3,
205    {
206        let current = self.state.borrow().clone();
207        let new_mv = transform(&current);
208        *self.state.borrow_mut() = new_mv.clone();
209        *self.cache.borrow_mut() = T::from_geometric(&new_mv);
210
211        // Notify subscribers
212        let cache = self.cache.borrow();
213        for (_, callback) in self.subscribers.borrow().iter() {
214            callback(&cache);
215        }
216    }
217}
218
219/// Convenience function to create a behavior
220///
221/// # Example
222///
223/// ```rust
224/// use cliffy_core::behavior;
225///
226/// let count = behavior(0);
227/// let name = behavior("Alice".to_string());
228/// let active = behavior(true);
229/// ```
230pub fn behavior<T: IntoGeometric + FromGeometric + Clone + 'static>(initial: T) -> Behavior<T> {
231    Behavior::new(initial)
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use std::cell::Cell;
238
239    #[test]
240    fn test_behavior_new_and_sample() {
241        let b = behavior(42i32);
242        assert_eq!(b.sample(), 42);
243    }
244
245    #[test]
246    fn test_behavior_update() {
247        let b = behavior(0i32);
248        b.update(|n| n + 1);
249        assert_eq!(b.sample(), 1);
250
251        b.update(|n| n * 2);
252        assert_eq!(b.sample(), 2);
253    }
254
255    #[test]
256    fn test_behavior_set() {
257        let b = behavior(0i32);
258        b.set(100);
259        assert_eq!(b.sample(), 100);
260    }
261
262    #[test]
263    fn test_behavior_subscribe() {
264        let b = behavior(0i32);
265        let called = Rc::new(Cell::new(0));
266        let called_clone = Rc::clone(&called);
267
268        let _sub = b.subscribe(move |_value| {
269            called_clone.set(called_clone.get() + 1);
270        });
271
272        b.update(|n| n + 1);
273        assert_eq!(called.get(), 1);
274
275        b.update(|n| n + 1);
276        assert_eq!(called.get(), 2);
277    }
278
279    #[test]
280    fn test_behavior_unsubscribe() {
281        let b = behavior(0i32);
282        let called = Rc::new(Cell::new(0));
283        let called_clone = Rc::clone(&called);
284
285        let sub = b.subscribe(move |_value| {
286            called_clone.set(called_clone.get() + 1);
287        });
288
289        b.update(|n| n + 1);
290        assert_eq!(called.get(), 1);
291
292        sub.unsubscribe();
293
294        b.update(|n| n + 1);
295        assert_eq!(called.get(), 1); // Should not increase
296    }
297
298    #[test]
299    fn test_behavior_map() {
300        let count = behavior(5i32);
301        let doubled = count.map(|n| n * 2);
302
303        assert_eq!(doubled.sample(), 10);
304
305        count.update(|n| n + 1);
306        assert_eq!(doubled.sample(), 12);
307    }
308
309    #[test]
310    fn test_behavior_combine() {
311        let a = behavior(10i32);
312        let b = behavior(20i32);
313        let sum = a.combine(&b, |x, y| x + y);
314
315        assert_eq!(sum.sample(), 30);
316
317        a.update(|n| n + 5);
318        assert_eq!(sum.sample(), 35);
319
320        b.update(|n| n + 10);
321        assert_eq!(sum.sample(), 45);
322    }
323
324    #[test]
325    fn test_behavior_with_string() {
326        let name = behavior("Alice".to_string());
327        assert_eq!(name.sample(), "Alice");
328
329        name.set("Bob".to_string());
330        assert_eq!(name.sample(), "Bob");
331    }
332
333    #[test]
334    fn test_behavior_with_bool() {
335        let active = behavior(false);
336        assert!(!active.sample());
337
338        active.update(|_| true);
339        assert!(active.sample());
340    }
341
342    #[test]
343    fn test_behavior_clone_shares_state() {
344        let a = behavior(0i32);
345        let b = a.clone();
346
347        a.update(|n| n + 1);
348        assert_eq!(b.sample(), 1);
349    }
350}