lib.rs raw

//! 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<T> = PhantomData<(fn() -> T, *mut ())>;

/// A writable reactive value.
pub struct Signal<T> {
    key: NodeKey,
    _marker: ThreadBound<T>,
}

impl<T> Copy for Signal<T> {}
impl<T> Clone for Signal<T> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<T: 'static> Signal<T> {
    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<R>(self, f: impl FnOnce(&T) -> R) -> R {
        read(self.key, true, f)
    }

    /// Read by reference, untracked.
    pub fn with_untracked<R>(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<T>
    where
        T: Clone,
    {
        runtime::read(self.key, true, |v| {
            v.and_then(|v| v.downcast_ref::<T>()).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::<T>() == 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::<T>() {
                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<T> {
        ReadSignal(self)
    }
}

/// A read-only view of a [`Signal`]: same tracked reads, no `set`/`update`.
pub struct ReadSignal<T>(Signal<T>);

impl<T> Copy for ReadSignal<T> {}
impl<T> Clone for ReadSignal<T> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<T: 'static> ReadSignal<T> {
    /// 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<R>(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<T>
    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<T> {
    key: NodeKey,
    _marker: ThreadBound<T>,
}

impl<T> Copy for Memo<T> {}
impl<T> Clone for Memo<T> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<T: PartialEq + 'static> Memo<T> {
    pub fn new(mut f: impl FnMut() -> T + 'static) -> Self {
        let key = runtime::create_and_run(
            Kind::Memo,
            Box::new(move |slot: &mut Option<Box<dyn Any>>| {
                let new = f();
                match slot.as_mut().and_then(|s| s.downcast_mut::<T>()) {
                    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<R>(self, f: impl FnOnce(&T) -> R) -> R {
        read(self.key, true, f)
    }
}

fn read<T: 'static, R>(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::<T>()
            .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<R>(&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;