//! Fine-grained signal runtime for guiduck: signals, memos, effects, scopes. //! //! All state lives in a thread-local runtime on the UI thread; handles are //! `Copy` 8-byte keys, so closures capture them by value with no clone //! ceremony and widgets store them without lifetimes. Writing a signal is //! immediate, but effects run deferred: the frame scheduler calls //! [`flush_effects`] once per frame, and dependent effects run at most once //! per flush regardless of how many of their sources changed //! (diamond-dependency glitch freedom). //! //! Reading a disposed node panics (use `try_get` to probe); writing one is a //! defined no-op. mod runtime; pub use runtime::{flush_effects, has_pending_effects}; use std::any::Any; use std::marker::PhantomData; use runtime::{Kind, NodeKey, ScopeKey}; /// Marker making handle types `!Send`/`!Sync`: they are only meaningful on /// the thread whose runtime created them. type ThreadBound = PhantomData<(fn() -> T, *mut ())>; /// A writable reactive value. pub struct Signal { key: NodeKey, _marker: ThreadBound, } impl Copy for Signal {} impl Clone for Signal { fn clone(&self) -> Self { *self } } impl Signal { pub fn new(value: T) -> Self { Self { key: runtime::create_node(Kind::Signal, Some(Box::new(value)), None), _marker: PhantomData, } } /// Read the value, registering the running effect or memo (if any) as a /// dependent. pub fn get(self) -> T where T: Clone, { self.with(T::clone) } /// Read without registering a dependency. pub fn get_untracked(self) -> T where T: Clone, { self.with_untracked(T::clone) } /// Read by reference, tracking. pub fn with(self, f: impl FnOnce(&T) -> R) -> R { read(self.key, true, f) } /// Read by reference, untracked. pub fn with_untracked(self, f: impl FnOnce(&T) -> R) -> R { read(self.key, false, f) } /// Read the value if the signal is still alive. pub fn try_get(self) -> Option where T: Clone, { runtime::read(self.key, true, |v| { v.and_then(|v| v.downcast_ref::()).cloned() }) } /// Write a new value; dependents are notified only if it differs from /// the current one. Writing to a disposed signal is a no-op. pub fn set(self, value: T) where T: PartialEq, { let unchanged = runtime::read(self.key, false, |v| match v { Some(v) => v.downcast_ref::() == Some(&value), // Disposed: swallow the write. None => true, }); if !unchanged { runtime::write(self.key, Box::new(value)); } } /// Mutate the value in place. Dependents are always notified, since the /// closure gives no way to detect "no change". pub fn update(self, f: impl FnOnce(&mut T)) { runtime::write_with(self.key, |v| { if let Some(v) = v.downcast_mut::() { f(v); } }); } /// Whether the signal is still alive. pub fn is_alive(self) -> bool { runtime::node_exists(self.key) } /// Dispose the signal explicitly (ahead of its owning scope). Dependents /// stop tracking it; readers see it as dead (`try_get` → `None`). pub fn dispose(self) { runtime::dispose_node(self.key); } /// A read-only handle to the same value: whoever holds it can observe /// but not write. Component props are the canonical use — the owner /// drives the `Signal`, the component's logic sees a [`ReadSignal`]. pub fn read_only(self) -> ReadSignal { ReadSignal(self) } } /// A read-only view of a [`Signal`]: same tracked reads, no `set`/`update`. pub struct ReadSignal(Signal); impl Copy for ReadSignal {} impl Clone for ReadSignal { fn clone(&self) -> Self { *self } } impl ReadSignal { /// Read the value, tracking. pub fn get(self) -> T where T: Clone, { self.0.get() } /// Read without registering a dependency. pub fn get_untracked(self) -> T where T: Clone, { self.0.get_untracked() } /// Read by reference, tracking. pub fn with(self, f: impl FnOnce(&T) -> R) -> R { self.0.with(f) } /// Read the value if the signal is still alive. pub fn try_get(self) -> Option where T: Clone, { self.0.try_get() } } /// A cached derived value, recomputed on demand when a dependency changed. /// Downstream nodes re-run only if the recomputed value actually differs /// (`PartialEq`). pub struct Memo { key: NodeKey, _marker: ThreadBound, } impl Copy for Memo {} impl Clone for Memo { fn clone(&self) -> Self { *self } } impl Memo { pub fn new(mut f: impl FnMut() -> T + 'static) -> Self { let key = runtime::create_and_run( Kind::Memo, Box::new(move |slot: &mut Option>| { let new = f(); match slot.as_mut().and_then(|s| s.downcast_mut::()) { Some(old) if *old == new => false, Some(old) => { *old = new; true } None => { *slot = Some(Box::new(new)); true } } }), ); Self { key, _marker: PhantomData, } } pub fn get(self) -> T where T: Clone, { self.with(T::clone) } pub fn with(self, f: impl FnOnce(&T) -> R) -> R { read(self.key, true, f) } } fn read(key: NodeKey, track: bool, f: impl FnOnce(&T) -> R) -> R { runtime::read(key, track, |v| { let v = v .expect("read of a disposed reactive node") .downcast_ref::() .expect("reactive node holds a different type"); f(v) }) } /// A reactive side effect: runs once at creation (establishing its /// dependencies) and again during [`flush_effects`] whenever a dependency /// changed. pub struct Effect { key: NodeKey, _marker: ThreadBound<()>, } impl Effect { pub fn new(mut f: impl FnMut() + 'static) -> Self { let key = runtime::create_and_run( Kind::Effect, Box::new(move |_| { f(); true }), ); Self { key, _marker: PhantomData, } } /// Stop the effect permanently. pub fn dispose(self) { runtime::dispose_node(self.key); } } /// An ownership scope for reactive nodes. Nodes created while a scope is /// entered are disposed together when the scope is; component instances get /// one scope each, so unmounting tears down all their reactive state. pub struct Scope { key: ScopeKey, _marker: ThreadBound<()>, } impl Scope { /// Create a child of the currently entered scope (or of the root). pub fn new() -> Self { Self { key: runtime::create_scope(), _marker: PhantomData, } } /// Run `f` with this scope as the owner of any nodes it creates. pub fn run(&self, f: impl FnOnce() -> R) -> R { let prev = runtime::enter_scope(self.key); let result = f(); runtime::restore_scope(prev); result } /// Dispose the scope, all nodes it owns, and all descendant scopes. pub fn dispose(self) { runtime::dispose_scope(self.key); } } impl Default for Scope { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests;