mirror of
https://github.com/Zenithsiz/dynatos.git
synced 2026-02-07 20:51:04 +00:00
Added Signal, Effect and WeakEffect primitives to dynatos-reactive.
This commit is contained in:
parent
ecd5254f6f
commit
b1967cddef
83
Cargo.lock
generated
83
Cargo.lock
generated
@ -2,6 +2,16 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "duplicate"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de78e66ac9061e030587b2a2e75cc88f22304913c907b11307bca737141230cb"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynatos"
|
||||
version = "0.1.0"
|
||||
@ -9,3 +19,76 @@ version = "0.1.0"
|
||||
[[package]]
|
||||
name = "dynatos-reactive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"duplicate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
@ -8,3 +8,5 @@ resolver = "2"
|
||||
# Workspace members
|
||||
dynatos = { path = "dynatos" }
|
||||
dynatos-reactive = { path = "dynatos-reactive" }
|
||||
|
||||
duplicate = "1.0.0"
|
||||
|
||||
@ -4,3 +4,5 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
duplicate = { workspace = true }
|
||||
|
||||
121
dynatos-reactive/src/effect.rs
Normal file
121
dynatos-reactive/src/effect.rs
Normal file
@ -0,0 +1,121 @@
|
||||
//! Effect
|
||||
//!
|
||||
//! An effect is a function that is re-run whenever
|
||||
//! one of it's dependencies changes.
|
||||
|
||||
// Imports
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
hash::Hash,
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
/// Effect stack
|
||||
static EFFECT_STACK: RefCell<Vec<Effect>> = RefCell::new(vec![]);
|
||||
}
|
||||
|
||||
/// Effect inner
|
||||
struct Inner {
|
||||
/// Effect runner
|
||||
run: Box<dyn Fn()>,
|
||||
}
|
||||
|
||||
/// Effect
|
||||
#[derive(Clone)]
|
||||
pub struct Effect {
|
||||
/// Inner
|
||||
inner: Rc<RefCell<Inner>>,
|
||||
}
|
||||
|
||||
impl Effect {
|
||||
/// Creates a new computed effect.
|
||||
///
|
||||
/// Runs the effect once to gather dependencies.
|
||||
pub fn new<F>(run: F) -> Self
|
||||
where
|
||||
F: Fn() + 'static,
|
||||
{
|
||||
// Create the effect
|
||||
let inner = Inner { run: Box::new(run) };
|
||||
let effect = Self {
|
||||
inner: Rc::new(RefCell::new(inner)),
|
||||
};
|
||||
|
||||
// And run it once to gather dependencies.
|
||||
effect.run();
|
||||
|
||||
effect
|
||||
}
|
||||
|
||||
/// Downgrades this effect
|
||||
pub fn downgrade(&self) -> WeakEffect {
|
||||
WeakEffect {
|
||||
inner: Rc::downgrade(&self.inner),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the current running effect
|
||||
pub fn running() -> Option<Self> {
|
||||
EFFECT_STACK.with_borrow(|effects| effects.last().cloned())
|
||||
}
|
||||
|
||||
/// Runs the effect
|
||||
pub fn run(&self) {
|
||||
// Push the effect, run the closure and pop it
|
||||
EFFECT_STACK.with_borrow_mut(|effects| effects.push(self.clone()));
|
||||
|
||||
// Then run it
|
||||
let inner = self.inner.borrow();
|
||||
let run = inner.run.as_ref();
|
||||
run();
|
||||
|
||||
// And finally pop the effect from the stack
|
||||
EFFECT_STACK
|
||||
.with_borrow_mut(|effects| effects.pop())
|
||||
.expect("Missing added effect");
|
||||
}
|
||||
}
|
||||
|
||||
/// Weak effect
|
||||
///
|
||||
/// Used to break ownership between a signal and it's subscribers
|
||||
#[derive(Clone)]
|
||||
pub struct WeakEffect {
|
||||
/// Inner
|
||||
inner: Weak<RefCell<Inner>>,
|
||||
}
|
||||
|
||||
impl WeakEffect {
|
||||
/// Upgrades this effect
|
||||
pub fn upgrade(&self) -> Option<Effect> {
|
||||
self.inner.upgrade().map(|inner| Effect { inner })
|
||||
}
|
||||
|
||||
/// Runs this effect, if it exists.
|
||||
///
|
||||
/// Returns if the effect still existed
|
||||
pub fn try_run(&self) -> bool {
|
||||
// Try to upgrade, else return that it was missing
|
||||
let Some(effect) = self.upgrade() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
effect.run();
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for WeakEffect {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Weak::ptr_eq(&self.inner, &other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for WeakEffect {}
|
||||
|
||||
impl Hash for WeakEffect {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.inner.as_ptr().hash(state);
|
||||
}
|
||||
}
|
||||
@ -1 +1,11 @@
|
||||
//! Reactivity for [`dynatos`]
|
||||
|
||||
// Modules
|
||||
pub mod effect;
|
||||
pub mod signal;
|
||||
|
||||
// Exports
|
||||
pub use self::{
|
||||
effect::{Effect, WeakEffect},
|
||||
signal::Signal,
|
||||
};
|
||||
|
||||
147
dynatos-reactive/src/signal.rs
Normal file
147
dynatos-reactive/src/signal.rs
Normal file
@ -0,0 +1,147 @@
|
||||
//! Signal
|
||||
//!
|
||||
//! A read-write value that automatically updates
|
||||
//! any subscribers when changed.
|
||||
|
||||
// Imports
|
||||
use {
|
||||
crate::{Effect, WeakEffect},
|
||||
std::{cell::RefCell, collections::HashSet, 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>>>,
|
||||
}
|
||||
|
||||
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)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the inner value
|
||||
#[track_caller]
|
||||
pub fn get(&self) -> T
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
if let Some(effect) = Effect::running() {
|
||||
self.add_subscriber(effect);
|
||||
}
|
||||
|
||||
self.inner
|
||||
.try_borrow()
|
||||
.expect("Cannot get signal value while updating")
|
||||
.value
|
||||
}
|
||||
|
||||
/// Calls `f` with the inner value
|
||||
#[track_caller]
|
||||
pub fn with<O, F>(&self, f: F) -> O
|
||||
where
|
||||
F: FnOnce(&T) -> O,
|
||||
{
|
||||
let inner = self.inner.try_borrow().expect("Cannot use signal value while updating");
|
||||
f(&inner.value)
|
||||
}
|
||||
|
||||
/// Sets the inner value.
|
||||
///
|
||||
/// Updates all subscribers.
|
||||
///
|
||||
/// Returns the previous value
|
||||
pub fn set(&self, new_value: T) -> T {
|
||||
self.update(|value| mem::replace(value, new_value))
|
||||
}
|
||||
|
||||
/// Updates the value in-place.
|
||||
///
|
||||
/// Updates all subscribers
|
||||
#[track_caller]
|
||||
pub fn update<O, F>(&self, f: F) -> O
|
||||
where
|
||||
F: FnOnce(&mut T) -> O,
|
||||
{
|
||||
// Update the value and get the output
|
||||
let output = {
|
||||
let mut inner = self
|
||||
.inner
|
||||
.try_borrow_mut()
|
||||
.expect("Cannot update signal value while using it");
|
||||
f(&mut inner.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);
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// 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> Clone for Signal<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: Rc::clone(&self.inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user