freya-core 0.4.0-rc.7

Reactivity runtime, tree management, accessibility integration, rendering pipeline and more, for Freya
Documentation
use std::{
    cell::RefCell,
    hash::{
        Hash,
        Hasher,
    },
    rc::Rc,
};

use futures_channel::mpsc::UnboundedSender;
use generational_box::GenerationalBox;
use rustc_hash::FxHashSet;

use crate::{
    current_context::CurrentContext,
    notify::Notify,
    runner::Message,
    scope_id::ScopeId,
};

pub(crate) struct Inner {
    self_rc: Option<ReactiveContext>,
    update: Rc<dyn Fn()>,
    subscriptions: Vec<Rc<RefCell<FxHashSet<ReactiveContext>>>>,
}

impl Drop for Inner {
    fn drop(&mut self) {
        let Some(self_rc) = self.self_rc.take() else {
            return;
        };
        for subscription in self.subscriptions.drain(..) {
            subscription.borrow_mut().remove(&self_rc);
        }
    }
}

#[derive(Clone)]
pub struct ReactiveContext {
    pub(crate) inner: GenerationalBox<Inner>,
}

impl Hash for ReactiveContext {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.inner.id().hash(state);
    }
}

impl PartialEq for ReactiveContext {
    fn eq(&self, other: &Self) -> bool {
        self.inner.ptr_eq(&other.inner)
    }
}

impl Eq for ReactiveContext {}

impl ReactiveContext {
    pub(crate) fn new_for_scope(
        sender: UnboundedSender<Message>,
        scope_id: ScopeId,
        writer: &dyn Fn(Inner) -> GenerationalBox<Inner>,
    ) -> Self {
        let rc = Self {
            inner: (writer)(Inner {
                self_rc: None,

                update: Rc::new(move || {
                    sender
                        .unbounded_send(Message::MarkScopeAsDirty(scope_id))
                        .unwrap();
                }),
                subscriptions: Vec::default(),
            }),
        };

        rc.inner.write().self_rc = Some(rc.clone());

        rc
    }

    pub fn new_for_task() -> (Notify, Self) {
        let notify = Notify::default();

        let rc = CurrentContext::with(|ctx| {
            let owner = ctx
                .scopes_storages
                .borrow()
                .get(&ctx.scope_id)
                .map(|s| s.owner.clone())
                .unwrap();
            let notify = notify.clone();
            Self {
                inner: owner.insert(Inner {
                    self_rc: None,
                    update: Rc::new(move || {
                        notify.notify();
                    }),
                    subscriptions: Vec::default(),
                }),
            }
        });

        rc.inner.write().self_rc = Some(rc.clone());

        (notify, rc)
    }

    pub fn run<T>(new_context: Self, run: impl FnOnce() -> T) -> T {
        for subscription in new_context.inner.write().subscriptions.drain(..) {
            subscription.borrow_mut().remove(&new_context);
        }
        REACTIVE_CONTEXTS_STACK.with_borrow_mut(|context| context.push(new_context));
        let res = run();
        REACTIVE_CONTEXTS_STACK.with_borrow_mut(|context| context.pop());

        res
    }

    pub fn try_current() -> Option<Self> {
        REACTIVE_CONTEXTS_STACK.with_borrow(|contexts| contexts.last().cloned())
    }

    pub fn current() -> Self {
        REACTIVE_CONTEXTS_STACK.with_borrow(|contexts| contexts.last().cloned().expect("Your trying to access a Freya reactive context outside of Freya, you might be in a separate thread or async task that is not integrated with Freya."))
    }

    pub fn notify(&self) -> bool {
        if let Ok(inner) = self.inner.try_write() {
            (inner.update)();
            true
        } else {
            false
        }
    }

    pub fn subscribe(&mut self, subscribers: &Rc<RefCell<FxHashSet<ReactiveContext>>>) {
        subscribers.borrow_mut().insert(self.clone());
        self.inner.write().subscriptions.push(subscribers.clone())
    }
}

thread_local! {
    static REACTIVE_CONTEXTS_STACK: RefCell<Vec<ReactiveContext>> = const { RefCell::new(Vec::new()) }
}