Skip to main content

oxframe_core/
signal.rs

1use std::cell::{Cell, RefCell};
2use std::fmt;
3use std::rc::{Rc, Weak};
4
5use crate::{Lerp, Seconds, Tween};
6
7/// A reactive value.
8///
9/// A signal either holds a plain value ([`set`](Signal::set)) or is derived
10/// from other signals ([`bind`](Signal::bind)). Derived signals are lazy:
11/// writes only mark dependents dirty, and the closure re-runs on the next
12/// [`get`](Signal::get). Cloning a `Signal` clones the handle, not the value.
13pub struct Signal<T>(Rc<SignalCell<T>>);
14
15struct SignalCell<T> {
16    name: RefCell<Option<Rc<str>>>,
17    value: RefCell<T>,
18    compute: RefCell<Option<Rc<dyn Fn() -> T>>>,
19    dirty: Cell<bool>,
20    computing: Cell<bool>,
21    subscribers: RefCell<Vec<Weak<dyn Observer>>>,
22}
23
24trait Observer {
25    fn invalidate(&self);
26}
27
28thread_local! {
29    /// Derived signals currently being computed, innermost last.
30    static TRACKING: RefCell<Vec<Rc<dyn Observer>>> = const { RefCell::new(Vec::new()) };
31}
32
33impl<T: 'static> Observer for SignalCell<T> {
34    fn invalidate(&self) {
35        if self.compute.borrow().is_some() && !self.dirty.replace(true) {
36            self.notify();
37        }
38    }
39}
40
41impl<T: 'static> SignalCell<T> {
42    fn notify(&self) {
43        // Hot path: most signals (tweened properties) have no dependents.
44        if self.subscribers.borrow().is_empty() {
45            return;
46        }
47        // Collect first: an observer may subscribe while being invalidated.
48        let subs: Vec<_> = {
49            let mut subs = self.subscribers.borrow_mut();
50            subs.retain(|w| w.strong_count() > 0);
51            subs.iter().filter_map(Weak::upgrade).collect()
52        };
53        for s in subs {
54            s.invalidate();
55        }
56    }
57
58    fn display_name(&self) -> String {
59        self.name.borrow().as_deref().map_or_else(
60            || format!("<unnamed {:p}>", self as *const _),
61            str::to_owned,
62        )
63    }
64}
65
66impl<T: Clone + 'static> Signal<T> {
67    /// A signal holding `value`.
68    pub fn new(value: T) -> Self {
69        Self(Rc::new(SignalCell {
70            name: RefCell::new(None),
71            value: RefCell::new(value),
72            compute: RefCell::new(None),
73            dirty: Cell::new(false),
74            computing: Cell::new(false),
75            subscribers: RefCell::new(Vec::new()),
76        }))
77    }
78
79    /// A derived signal. The closure runs lazily on the first `get`.
80    pub fn derived(f: impl Fn() -> T + 'static) -> Self
81    where
82        T: Default,
83    {
84        let s = Self::new(T::default());
85        s.bind(f);
86        s
87    }
88
89    /// Name used in diagnostics (cycle panics, inspector).
90    pub fn with_name(self, name: impl Into<Rc<str>>) -> Self {
91        *self.0.name.borrow_mut() = Some(name.into());
92        self
93    }
94
95    /// Name shown in the editor's inspector, if any.
96    pub fn name(&self) -> Option<Rc<str>> {
97        self.0.name.borrow().clone()
98    }
99
100    /// Current value; a bound signal recomputes only if its inputs changed.
101    ///
102    /// # Panics
103    ///
104    /// In debug builds, if bound signals depend on each other in a cycle
105    /// (release builds return the last value instead).
106    pub fn get(&self) -> T {
107        self.track();
108        let cell = &self.0;
109        if cell.dirty.get() {
110            let compute = cell.compute.borrow().clone();
111            if let Some(f) = compute {
112                if cell.computing.get() {
113                    if cfg!(debug_assertions) {
114                        panic!("signal dependency cycle through `{}`", cell.display_name());
115                    }
116                    return cell.value.borrow().clone();
117                }
118                cell.computing.set(true);
119                let observer: Rc<dyn Observer> = self.0.clone();
120                TRACKING.with(|t| t.borrow_mut().push(observer));
121                let value = f();
122                TRACKING.with(|t| t.borrow_mut().pop());
123                cell.computing.set(false);
124                *cell.value.borrow_mut() = value;
125            }
126            cell.dirty.set(false);
127        }
128        cell.value.borrow().clone()
129    }
130
131    /// Reads without registering a dependency.
132    pub fn peek(&self) -> T {
133        let saved = TRACKING.with(|t| std::mem::take(&mut *t.borrow_mut()));
134        let v = self.get();
135        TRACKING.with(|t| *t.borrow_mut() = saved);
136        v
137    }
138
139    /// Sets a plain value; removes any binding.
140    pub fn set(&self, value: T) {
141        *self.0.compute.borrow_mut() = None;
142        self.0.dirty.set(false);
143        *self.0.value.borrow_mut() = value;
144        self.0.notify();
145    }
146
147    /// Makes this signal derived from `f`.
148    pub fn bind(&self, f: impl Fn() -> T + 'static) {
149        *self.0.compute.borrow_mut() = Some(Rc::new(f));
150        self.0.dirty.set(true);
151        self.0.notify();
152    }
153
154    /// Whether the value comes from [`bind`](Self::bind).
155    pub fn is_bound(&self) -> bool {
156        self.0.compute.borrow().is_some()
157    }
158
159    /// Tweens from the current value to `target` over `duration` seconds.
160    ///
161    /// `f64` is seconds. A theme pace (`Normal`) is a duration too.
162    pub fn to(&self, target: impl Into<T>, duration: impl Seconds) -> Tween<T>
163    where
164        T: Lerp,
165    {
166        Tween::new(self.clone(), target.into(), duration.seconds())
167    }
168
169    fn track(&self) {
170        TRACKING.with(|t| {
171            let t = t.borrow();
172            let Some(observer) = t.last() else { return };
173            let mut subs = self.0.subscribers.borrow_mut();
174            let already = subs
175                .iter()
176                .any(|w| std::ptr::addr_eq(w.as_ptr(), Rc::as_ptr(observer)));
177            if !already {
178                subs.push(Rc::downgrade(observer));
179            }
180        });
181    }
182}
183
184impl<T> Clone for Signal<T> {
185    fn clone(&self) -> Self {
186        Self(self.0.clone())
187    }
188}
189
190impl<T: fmt::Debug> fmt::Debug for Signal<T> {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.debug_struct("Signal")
193            .field("name", &self.0.name.borrow())
194            .field("value", &self.0.value.borrow())
195            .field("bound", &self.0.compute.borrow().is_some())
196            .finish()
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn set_and_get() {
206        let s = Signal::new(1.0);
207        s.set(2.0);
208        assert_eq!(s.get(), 2.0);
209    }
210
211    #[test]
212    fn derived_is_lazy_and_updates() {
213        let runs = Rc::new(Cell::new(0));
214        let a = Signal::new(2.0);
215        let b = {
216            let (a, runs) = (a.clone(), runs.clone());
217            Signal::derived(move || {
218                runs.set(runs.get() + 1);
219                a.get() * 10.0
220            })
221        };
222        assert_eq!(runs.get(), 0, "not computed before first get");
223        assert_eq!(b.get(), 20.0);
224        assert_eq!(b.get(), 20.0);
225        assert_eq!(runs.get(), 1, "cached while clean");
226        a.set(3.0);
227        a.set(4.0);
228        assert_eq!(runs.get(), 1, "writes only mark dirty");
229        assert_eq!(b.get(), 40.0);
230        assert_eq!(runs.get(), 2);
231    }
232
233    #[test]
234    fn chains_propagate() {
235        let a = Signal::new(1.0);
236        let b = {
237            let a = a.clone();
238            Signal::derived(move || a.get() + 1.0)
239        };
240        let c = {
241            let b = b.clone();
242            Signal::derived(move || b.get() * 2.0)
243        };
244        assert_eq!(c.get(), 4.0);
245        a.set(5.0);
246        assert_eq!(c.get(), 12.0);
247    }
248
249    #[test]
250    fn set_unbinds() {
251        let a = Signal::new(1.0);
252        let b = Signal::new(0.0);
253        let a2 = a.clone();
254        b.bind(move || a2.get());
255        assert_eq!(b.get(), 1.0);
256        b.set(7.0);
257        a.set(3.0);
258        assert_eq!(b.get(), 7.0);
259        assert!(!b.is_bound());
260    }
261
262    #[test]
263    #[cfg(debug_assertions)]
264    #[should_panic(expected = "signal dependency cycle through `loop-a`")]
265    fn cycle_panics_with_name() {
266        let a = Signal::new(0.0).with_name("loop-a");
267        let b = Signal::new(0.0).with_name("loop-b");
268        let (a2, b2) = (a.clone(), b.clone());
269        a.bind(move || b2.get() + 1.0);
270        b.bind(move || a2.get() + 1.0);
271        a.get();
272    }
273}