windjammer_ui/
reactivity.rs1use std::cell::RefCell;
29use std::collections::{HashMap, HashSet};
30use std::rc::Rc;
31use std::sync::atomic::{AtomicUsize, Ordering};
32
33pub type SignalId = usize;
35
36pub type EffectId = usize;
38
39thread_local! {
41 static REACTIVE_CONTEXT: RefCell<ReactiveContext> = RefCell::new(ReactiveContext::new());
42 static EFFECT_REGISTRY: RefCell<HashMap<EffectId, EffectHandle>> = RefCell::new(HashMap::new());
43}
44
45struct ReactiveContext {
47 current_effect: Option<EffectId>,
49 cleanups: HashMap<EffectId, Vec<Box<dyn Fn()>>>,
51}
52
53impl ReactiveContext {
54 fn new() -> Self {
55 Self {
56 current_effect: None,
57 cleanups: HashMap::new(),
58 }
59 }
60}
61
62struct EffectHandle {
64 f: Rc<dyn Fn()>,
65}
66
67#[derive(Clone)]
69pub struct Signal<T: Clone> {
70 id: SignalId,
71 value: Rc<RefCell<T>>,
72 subscribers: Rc<RefCell<HashSet<EffectId>>>,
73}
74
75impl<T: Clone> Signal<T> {
76 pub fn new(value: T) -> Self {
78 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
79 Self {
80 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
81 value: Rc::new(RefCell::new(value)),
82 subscribers: Rc::new(RefCell::new(HashSet::new())),
83 }
84 }
85
86 pub fn get(&self) -> T {
88 REACTIVE_CONTEXT.with(|ctx| {
90 let ctx = ctx.borrow();
91 if let Some(effect_id) = ctx.current_effect {
92 self.subscribers.borrow_mut().insert(effect_id);
93 }
94 });
95
96 self.value.borrow().clone()
97 }
98
99 pub fn get_untracked(&self) -> T {
101 self.value.borrow().clone()
102 }
103
104 pub fn set(&self, value: T) {
106 *self.value.borrow_mut() = value;
107 self.notify();
108 }
109
110 pub fn update<F>(&self, f: F)
112 where
113 F: FnOnce(&mut T),
114 {
115 f(&mut self.value.borrow_mut());
116 self.notify();
117 }
118
119 fn notify(&self) {
121 let subscribers = self.subscribers.borrow().clone();
122 for effect_id in subscribers {
123 EFFECT_REGISTRY.with(|registry| {
124 if let Some(effect) = registry.borrow().get(&effect_id) {
125 (effect.f)();
126 }
127 });
128 }
129
130 #[cfg(target_arch = "wasm32")]
132 {
133 crate::app_reactive::trigger_rerender();
134 }
135
136 #[cfg(all(not(target_arch = "wasm32"), feature = "desktop"))]
138 {
139 crate::desktop_app_context::trigger_repaint();
140 }
141 }
142
143 pub fn id(&self) -> SignalId {
145 self.id
146 }
147}
148
149impl<T: Clone + std::fmt::Debug> std::fmt::Debug for Signal<T> {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("Signal")
152 .field("id", &self.id)
153 .field("value", &self.get_untracked())
154 .finish()
155 }
156}
157
158#[derive(Clone)]
160pub struct Computed<T: Clone> {
161 signal: Signal<T>,
162 _effect_id: EffectId,
163}
164
165impl<T: Clone + 'static> Computed<T> {
166 pub fn new<F>(compute: F) -> Self
168 where
169 F: Fn() -> T + 'static,
170 {
171 let compute = Rc::new(compute);
172
173 let initial_value = compute();
175 let signal = Signal::new(initial_value);
176
177 let signal_clone = signal.clone();
179 let compute_clone = compute.clone();
180 let effect_id = Effect::new(move || {
181 let new_value = compute_clone();
182 *signal_clone.value.borrow_mut() = new_value;
184 });
185
186 Self {
187 signal,
188 _effect_id: effect_id,
189 }
190 }
191
192 pub fn get(&self) -> T {
194 self.signal.get()
195 }
196
197 pub fn get_untracked(&self) -> T {
199 self.signal.get_untracked()
200 }
201}
202
203impl<T: Clone + std::fmt::Debug + 'static> std::fmt::Debug for Computed<T> {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 f.debug_struct("Computed")
206 .field("value", &self.get_untracked())
207 .finish()
208 }
209}
210
211pub struct Effect {
213 _id: EffectId,
214}
215
216impl Effect {
217 #[allow(clippy::new_ret_no_self)]
219 pub fn new<F>(f: F) -> EffectId
220 where
221 F: Fn() + 'static,
222 {
223 static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
224 let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
225 let f: Rc<dyn Fn()> = Rc::new(f);
226
227 EFFECT_REGISTRY.with(|registry| {
229 registry
230 .borrow_mut()
231 .insert(id, EffectHandle { f: f.clone() });
232 });
233
234 Self::run_effect(id, f.clone());
236
237 id
238 }
239
240 fn run_effect(id: EffectId, f: Rc<dyn Fn()>) {
242 REACTIVE_CONTEXT.with(|ctx| {
243 let prev_effect = ctx.borrow().current_effect;
245
246 ctx.borrow_mut().current_effect = Some(id);
248
249 f();
251
252 ctx.borrow_mut().current_effect = prev_effect;
254 });
255 }
256
257 pub fn dispose(id: EffectId) {
259 REACTIVE_CONTEXT.with(|ctx| {
261 if let Some(cleanups) = ctx.borrow_mut().cleanups.remove(&id) {
262 for cleanup in cleanups {
263 cleanup();
264 }
265 }
266 });
267
268 EFFECT_REGISTRY.with(|registry| {
270 registry.borrow_mut().remove(&id);
271 });
272 }
273}
274
275pub fn on_cleanup<F>(cleanup: F)
277where
278 F: Fn() + 'static,
279{
280 REACTIVE_CONTEXT.with(|ctx| {
281 let mut ctx = ctx.borrow_mut();
282 if let Some(effect_id) = ctx.current_effect {
283 ctx.cleanups
284 .entry(effect_id)
285 .or_insert_with(Vec::new)
286 .push(Box::new(cleanup));
287 }
288 });
289}
290
291pub fn create_effect<F>(f: F) -> EffectId
293where
294 F: Fn() + 'static,
295{
296 Effect::new(f)
297}
298
299pub fn create_computed<T, F>(compute: F) -> Computed<T>
301where
302 T: Clone + 'static,
303 F: Fn() -> T + 'static,
304{
305 Computed::new(compute)
306}
307
308pub fn create_signal<T: Clone>(value: T) -> Signal<T> {
310 Signal::new(value)
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn test_signal_basic() {
319 let count = Signal::new(0);
320 assert_eq!(count.get(), 0);
321
322 count.set(5);
323 assert_eq!(count.get(), 5);
324
325 count.update(|c| *c += 10);
326 assert_eq!(count.get(), 15);
327 }
328
329 #[test]
330 fn test_signal_effect() {
331 let count = Signal::new(0);
332 let result = Rc::new(RefCell::new(0));
333
334 let result_clone = result.clone();
335 let count_clone = count.clone();
336 Effect::new(move || {
337 *result_clone.borrow_mut() = count_clone.get() * 2;
338 });
339
340 assert_eq!(*result.borrow(), 0);
341
342 count.set(5);
343 assert_eq!(*result.borrow(), 10);
344
345 count.set(10);
346 assert_eq!(*result.borrow(), 20);
347 }
348
349 #[test]
350 fn test_computed() {
351 let count = Signal::new(5);
352 let count_clone = count.clone();
353 let doubled = Computed::new(move || count_clone.get() * 2);
354
355 assert_eq!(doubled.get(), 10);
356
357 count.set(10);
358 assert_eq!(doubled.get(), 20);
359
360 count.set(7);
361 assert_eq!(doubled.get(), 14);
362 }
363
364 #[test]
365 fn test_multiple_dependencies() {
366 let a = Signal::new(2);
367 let b = Signal::new(3);
368
369 let a_clone = a.clone();
370 let b_clone = b.clone();
371 let sum = Computed::new(move || a_clone.get() + b_clone.get());
372
373 assert_eq!(sum.get(), 5);
374
375 a.set(10);
376 assert_eq!(sum.get(), 13);
377
378 b.set(7);
379 assert_eq!(sum.get(), 17);
380 }
381
382 #[test]
383 fn test_effect_runs_immediately() {
384 let ran = Rc::new(RefCell::new(false));
385 let ran_clone = ran.clone();
386
387 Effect::new(move || {
388 *ran_clone.borrow_mut() = true;
389 });
390
391 assert!(*ran.borrow());
392 }
393
394 #[test]
395 fn test_untracked_read() {
396 let count = Signal::new(0);
397 let effect_count = Rc::new(RefCell::new(0));
398
399 let effect_count_clone = effect_count.clone();
400 let count_clone = count.clone();
401 Effect::new(move || {
402 let _ = count_clone.get_untracked();
404 *effect_count_clone.borrow_mut() += 1;
405 });
406
407 assert_eq!(*effect_count.borrow(), 1); count.set(5);
410 assert_eq!(*effect_count.borrow(), 1); }
412
413 #[test]
414 fn test_nested_effects() {
415 let count = Signal::new(0);
416 let doubled = Rc::new(RefCell::new(0));
417 let quadrupled = Rc::new(RefCell::new(0));
418
419 let doubled_clone = doubled.clone();
421 let count_clone = count.clone();
422 Effect::new(move || {
423 *doubled_clone.borrow_mut() = count_clone.get() * 2;
424 });
425
426 let quadrupled_clone = quadrupled.clone();
428 let doubled_clone2 = doubled.clone();
429 Effect::new(move || {
430 *quadrupled_clone.borrow_mut() = *doubled_clone2.borrow() * 2;
431 });
432
433 assert_eq!(*doubled.borrow(), 0);
434 assert_eq!(*quadrupled.borrow(), 0);
435
436 count.set(5);
437 assert_eq!(*doubled.borrow(), 10);
438 }
441}