Skip to main content

repose_core/
signal.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3use std::sync::atomic::{AtomicUsize, Ordering};
4
5use crate::reactive;
6
7pub type SubId = usize;
8
9/// Subscriber callback for [`Signal`].
10type SubCallback<T> = Rc<dyn Fn(&T)>;
11
12static NEXT_SIGNAL_ID: AtomicUsize = AtomicUsize::new(1);
13
14pub struct Signal<T: 'static>(Rc<RefCell<Inner<T>>>);
15
16impl<T> Clone for Signal<T> {
17    fn clone(&self) -> Self {
18        Self(self.0.clone())
19    }
20}
21
22struct Inner<T> {
23    id: usize,
24    value: T,
25    subs: Vec<Option<SubCallback<T>>>,
26    free_list: Vec<SubId>,
27}
28
29impl<T> Signal<T> {
30    pub fn new(value: T) -> Self {
31        let id = NEXT_SIGNAL_ID.fetch_add(1, Ordering::Relaxed);
32        Self(Rc::new(RefCell::new(Inner {
33            id,
34            value,
35            subs: Vec::new(),
36            free_list: Vec::new(),
37        })))
38    }
39
40    pub fn id(&self) -> usize {
41        self.0.borrow().id
42    }
43
44    pub fn get(&self) -> T
45    where
46        T: Clone,
47    {
48        let inner = self.0.borrow();
49        reactive::register_signal_read(inner.id);
50        inner.value.clone()
51    }
52
53    /// Read the current value without cloning it, tracking the read in the
54    /// reactive graph. Prefer over `get` for large/expensive-to-clone types.
55    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
56        let inner = self.0.borrow();
57        reactive::register_signal_read(inner.id);
58        f(&inner.value)
59    }
60
61    /// Set the signal value only if it changed, skipping subscribers, the
62    /// reactive graph, and the frame request when the value is unchanged.
63    pub fn set_neq(&self, v: T)
64    where
65        T: PartialEq + Clone,
66    {
67        let id = {
68            let mut inner = self.0.borrow_mut();
69            if inner.value == v {
70                return;
71            }
72            inner.value = v;
73            inner.id
74        };
75        self.notify_and_request_frame(id);
76    }
77
78    /// Set the signal value and notify subscribers + the reactive graph.
79    /// Subscribers observe a snapshot clone, so re-entrant `set`/`update`
80    /// inside a subscriber cannot alias the reference they hold.
81    pub fn set(&self, v: T)
82    where
83        T: Clone,
84    {
85        let id = {
86            let mut inner = self.0.borrow_mut();
87            inner.value = v;
88            inner.id
89        };
90        self.notify_and_request_frame(id);
91    }
92
93    pub fn update<F: FnOnce(&mut T)>(&self, f: F)
94    where
95        T: Clone,
96    {
97        let id = {
98            let mut inner = self.0.borrow_mut();
99            f(&mut inner.value);
100            inner.id
101        };
102        self.notify_and_request_frame(id);
103    }
104
105    fn notify_and_request_frame(&self, id: usize)
106    where
107        T: Clone,
108    {
109        let (cbs, snapshot): (Vec<SubCallback<T>>, T) = {
110            let inner = match self.0.try_borrow() {
111                Ok(b) => b,
112                Err(_) => {
113                    log::warn!("Signal notify: inner already borrowed, skipping notify");
114                    reactive::signal_changed(id);
115                    crate::signal_fired();
116                    crate::request_frame();
117                    return;
118                }
119            };
120            let cbs = inner
121                .subs
122                .iter()
123                .filter_map(|s| s.clone())
124                .collect::<Vec<_>>();
125            let snapshot = inner.value.clone();
126            (cbs, snapshot)
127        };
128        reactive::without_observer(|| {
129            for cb in cbs {
130                let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| cb(&snapshot)));
131                if let Err(e) = res {
132                    let msg = e
133                        .downcast_ref::<String>()
134                        .map(|s| s.as_str())
135                        .or_else(|| e.downcast_ref::<&str>().copied())
136                        .unwrap_or("unknown");
137                    log::error!("Signal subscriber panicked: {msg}");
138                }
139            }
140        });
141
142        reactive::signal_changed(id);
143        crate::signal_fired();
144        crate::request_frame();
145    }
146
147    pub fn subscribe(&self, f: impl Fn(&T) + 'static) -> SubId {
148        let mut inner = self.0.borrow_mut();
149        if let Some(free_id) = inner.free_list.pop() {
150            inner.subs[free_id] = Some(Rc::new(f));
151            free_id
152        } else {
153            inner.subs.push(Some(Rc::new(f)));
154            inner.subs.len() - 1
155        }
156    }
157
158    /// Remove a subscriber by id. Returns true if removed.
159    pub fn unsubscribe(&self, id: SubId) -> bool {
160        let mut inner = self.0.borrow_mut();
161        if id < inner.subs.len() && inner.subs[id].is_some() {
162            inner.subs[id] = None;
163            inner.free_list.push(id);
164            while inner.subs.last().is_some_and(|s| s.is_none()) {
165                let popped = inner.subs.len() - 1;
166                inner.subs.pop();
167                // Remove from free_list if it was the tail we just popped
168                if let Some(pos) = inner.free_list.iter().position(|&x| x == popped) {
169                    inner.free_list.swap_remove(pos);
170                }
171            }
172            true
173        } else {
174            false
175        }
176    }
177
178    /// Subscribe and get a guard that auto-unsubscribes on drop.
179    pub fn subscribe_guard(&self, f: impl Fn(&T) + 'static) -> SubGuard<T> {
180        let id = self.subscribe(f);
181        SubGuard {
182            sig: self.clone(),
183            id,
184        }
185    }
186}
187
188pub fn signal<T>(t: T) -> Signal<T> {
189    Signal::new(t)
190}
191
192/// RAII guard for a Signal subscription. Unsubscribes on drop.
193pub struct SubGuard<T: 'static> {
194    sig: crate::Signal<T>,
195    id: SubId,
196}
197impl<T> Drop for SubGuard<T> {
198    fn drop(&mut self) {
199        let _ = self.sig.unsubscribe(self.id);
200    }
201}