Added new reactivity primitive Trigger.

`Signal<T>` now uses `T` underneath.
This commit is contained in:
Filipe Rodrigues 2024-02-05 19:52:25 +00:00
parent d5c96a117f
commit bee58c76a2
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
3 changed files with 130 additions and 56 deletions

View File

@ -3,12 +3,14 @@
// Modules
pub mod effect;
pub mod signal;
pub mod trigger;
pub mod with_default;
// Exports
pub use self::{
effect::{Effect, WeakEffect},
signal::Signal,
trigger::Trigger,
with_default::{SignalWithDefault, WithDefault},
};

View File

@ -5,53 +5,27 @@
// Imports
use {
crate::{Effect, SignalGet, SignalReplace, SignalSet, SignalUpdate, SignalWith, WeakEffect},
std::{cell::RefCell, collections::HashSet, fmt, mem, rc::Rc},
crate::{Effect, SignalGet, SignalReplace, SignalSet, SignalUpdate, SignalWith, Trigger, WeakEffect},
std::{cell::RefCell, fmt, mem, rc::Rc},
};
/// Signal inner
struct Inner<T> {
/// Value
value: T,
/// Subscribers
subscribers: HashSet<WeakEffect>,
}
/// Signal
pub struct Signal<T> {
/// Inner
inner: Rc<RefCell<Inner<T>>>,
/// Value
value: Rc<RefCell<T>>,
/// Trigger
trigger: Trigger,
}
impl<T> Signal<T> {
/// Creates a new signal
pub fn new(value: T) -> Self {
let inner = Inner {
value,
subscribers: HashSet::new(),
};
Self {
inner: Rc::new(RefCell::new(inner)),
value: Rc::new(RefCell::new(value)),
trigger: Trigger::new(),
}
}
/// Explicitly adds a subscriber to this signal.
///
/// Returns if the subscriber already existed.
pub fn add_subscriber<S: IntoSubscriber>(&self, subscriber: S) -> bool {
let mut inner = self.inner.borrow_mut();
let new_effect = inner.subscribers.insert(subscriber.into_subscriber());
!new_effect
}
/// Removes a subscriber from this signal.
///
/// Returns if the subscriber existed
pub fn remove_subscriber<S: IntoSubscriber>(&self, subscriber: S) -> bool {
let mut inner = self.inner.borrow_mut();
inner.subscribers.remove(&subscriber.into_subscriber())
}
}
impl<T> SignalGet for Signal<T>
@ -73,11 +47,11 @@ impl<T> SignalWith for Signal<T> {
F: FnOnce(&Self::Value) -> O,
{
if let Some(effect) = Effect::running() {
self.add_subscriber(effect);
self.trigger.add_subscriber(effect);
}
let inner = self.inner.try_borrow().expect("Cannot use signal value while updating");
f(&inner.value)
let value = self.value.try_borrow().expect("Cannot use signal value while updating");
f(&value)
}
}
@ -102,24 +76,15 @@ impl<T> SignalUpdate for Signal<T> {
{
// Update the value and get the output
let output = {
let mut inner = self
.inner
let mut value = self
.value
.try_borrow_mut()
.expect("Cannot update signal value while using it");
f(&mut inner.value)
f(&mut value)
};
// Then update all subscribers, removing any stale ones.
// Note: Since running the effect will add subscribers, we can't keep
// the inner borrow active, so we gather all dependencies before-hand.
// However, we can remove subscribers in between running effects, so we
// don't need to wait for that.
let subscribers = self.inner.borrow().subscribers.iter().cloned().collect::<Vec<_>>();
for subscriber in subscribers {
if !subscriber.try_run() {
self.remove_subscriber(subscriber);
}
}
// Then trigger our trigger
self.trigger.trigger();
output
}
@ -128,16 +93,15 @@ impl<T> SignalUpdate for Signal<T> {
impl<T> Clone for Signal<T> {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
value: Rc::clone(&self.value),
trigger: self.trigger.clone(),
}
}
}
impl<T: fmt::Debug> fmt::Debug for Signal<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Signal")
.field("value", &self.inner.borrow().value)
.finish()
f.debug_struct("Signal").field("value", &*self.value.borrow()).finish()
}
}

View File

@ -0,0 +1,108 @@
//! Trigger
//!
//! A reactivity primitive that allows re-running
//! any subscribers.
// Imports
use {
crate::{Effect, WeakEffect},
std::{cell::RefCell, collections::HashSet, fmt, rc::Rc},
};
/// Trigger inner
struct Inner {
/// Subscribers
subscribers: HashSet<WeakEffect>,
}
/// Trigger
pub struct Trigger {
/// Inner
inner: Rc<RefCell<Inner>>,
}
impl Trigger {
/// Creates a new trigger
pub fn new() -> Self {
let inner = Inner {
subscribers: HashSet::new(),
};
Self {
inner: Rc::new(RefCell::new(inner)),
}
}
/// Adds a subscriber to this trigger.
///
/// Returns if the subscriber already existed.
pub fn add_subscriber<S: IntoSubscriber>(&self, subscriber: S) -> bool {
let mut inner = self.inner.borrow_mut();
let new_effect = inner.subscribers.insert(subscriber.into_subscriber());
!new_effect
}
/// Removes a subscriber from this trigger.
///
/// Returns if the subscriber existed
pub fn remove_subscriber<S: IntoSubscriber>(&self, subscriber: S) -> bool {
let mut inner = self.inner.borrow_mut();
inner.subscribers.remove(&subscriber.into_subscriber())
}
/// Triggers this trigger.
///
/// Re-runs all subscribers.
pub fn trigger(&self) {
// Run all subscribers, and remove any empty ones
// Note: Since running the subscriber might add subscribers, we can't keep
// the inner borrow active, so we gather all dependencies before-hand.
// However, we can remove subscribers in between running effects, so we
// don't need to wait for that.
// TODO: Have a 2nd field `to_add_subscribers` where subscribers are added if
// the main field is locked, and after this loop move any subscribers from
// it to the main field?
let subscribers = self.inner.borrow().subscribers.iter().cloned().collect::<Vec<_>>();
for subscriber in subscribers {
if !subscriber.try_run() {
self.remove_subscriber(subscriber);
}
}
}
}
impl Default for Trigger {
fn default() -> Self {
Self::new()
}
}
impl Clone for Trigger {
fn clone(&self) -> Self {
Self {
inner: Rc::clone(&self.inner),
}
}
}
impl fmt::Debug for Trigger {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Trigger").finish_non_exhaustive()
}
}
/// Types that may be converted into a subscriber
pub trait IntoSubscriber {
fn into_subscriber(self) -> WeakEffect;
}
#[duplicate::duplicate_item(
T body;
[ Effect ] [ self.downgrade() ];
[ &'_ Effect ] [ self.downgrade() ];
[ WeakEffect ] [ self ];
)]
impl IntoSubscriber for T {
fn into_subscriber(self) -> WeakEffect {
body
}
}