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. Takes anything that converts,
140    /// like [`Signal::to`]: `text.set("Done")` on a `Signal<String>`.
141    pub fn set(&self, value: impl Into<T>) {
142        *self.0.compute.borrow_mut() = None;
143        self.0.dirty.set(false);
144        *self.0.value.borrow_mut() = value.into();
145        self.0.notify();
146    }
147
148    /// Makes this signal derived from `f`.
149    pub fn bind(&self, f: impl Fn() -> T + 'static) {
150        *self.0.compute.borrow_mut() = Some(Rc::new(f));
151        self.0.dirty.set(true);
152        self.0.notify();
153    }
154
155    /// Whether the value comes from [`bind`](Self::bind).
156    pub fn is_bound(&self) -> bool {
157        self.0.compute.borrow().is_some()
158    }
159
160    /// Tweens from the current value to `target` over `duration` seconds.
161    ///
162    /// `f64` is seconds. A theme pace (`Normal`) is a duration too.
163    pub fn to(&self, target: impl Into<T>, duration: impl Seconds) -> Tween<T>
164    where
165        T: Lerp,
166    {
167        Tween::new(self.clone(), target.into(), duration.seconds())
168    }
169
170    fn track(&self) {
171        TRACKING.with(|t| {
172            let t = t.borrow();
173            let Some(observer) = t.last() else { return };
174            let mut subs = self.0.subscribers.borrow_mut();
175            let already = subs
176                .iter()
177                .any(|w| std::ptr::addr_eq(w.as_ptr(), Rc::as_ptr(observer)));
178            if !already {
179                subs.push(Rc::downgrade(observer));
180            }
181        });
182    }
183}
184
185impl<T> Clone for Signal<T> {
186    fn clone(&self) -> Self {
187        Self(self.0.clone())
188    }
189}
190
191impl<T: fmt::Debug> fmt::Debug for Signal<T> {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.debug_struct("Signal")
194            .field("name", &self.0.name.borrow())
195            .field("value", &self.0.value.borrow())
196            .field("bound", &self.0.compute.borrow().is_some())
197            .finish()
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn set_and_get() {
207        let s = Signal::new(1.0);
208        s.set(2.0);
209        assert_eq!(s.get(), 2.0);
210    }
211
212    #[test]
213    fn derived_is_lazy_and_updates() {
214        let runs = Rc::new(Cell::new(0));
215        let a = Signal::new(2.0);
216        let b = {
217            let (a, runs) = (a.clone(), runs.clone());
218            Signal::derived(move || {
219                runs.set(runs.get() + 1);
220                a.get() * 10.0
221            })
222        };
223        assert_eq!(runs.get(), 0, "not computed before first get");
224        assert_eq!(b.get(), 20.0);
225        assert_eq!(b.get(), 20.0);
226        assert_eq!(runs.get(), 1, "cached while clean");
227        a.set(3.0);
228        a.set(4.0);
229        assert_eq!(runs.get(), 1, "writes only mark dirty");
230        assert_eq!(b.get(), 40.0);
231        assert_eq!(runs.get(), 2);
232    }
233
234    #[test]
235    fn chains_propagate() {
236        let a = Signal::new(1.0);
237        let b = {
238            let a = a.clone();
239            Signal::derived(move || a.get() + 1.0)
240        };
241        let c = {
242            let b = b.clone();
243            Signal::derived(move || b.get() * 2.0)
244        };
245        assert_eq!(c.get(), 4.0);
246        a.set(5.0);
247        assert_eq!(c.get(), 12.0);
248    }
249
250    #[test]
251    fn set_unbinds() {
252        let a = Signal::new(1.0);
253        let b = Signal::new(0.0);
254        let a2 = a.clone();
255        b.bind(move || a2.get());
256        assert_eq!(b.get(), 1.0);
257        b.set(7.0);
258        a.set(3.0);
259        assert_eq!(b.get(), 7.0);
260        assert!(!b.is_bound());
261    }
262
263    #[test]
264    #[cfg(debug_assertions)]
265    #[should_panic(expected = "signal dependency cycle through `loop-a`")]
266    fn cycle_panics_with_name() {
267        let a = Signal::new(0.0).with_name("loop-a");
268        let b = Signal::new(0.0).with_name("loop-b");
269        let (a2, b2) = (a.clone(), b.clone());
270        a.bind(move || b2.get() + 1.0);
271        b.bind(move || a2.get() + 1.0);
272        a.get();
273    }
274}