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