use alloc::boxed::Box;
use alloc::rc::Rc;
use crate::{Memo, Observable, Reactor, Signal, current, memo_in, untrack};
pub trait WritableObservable: Observable {
fn set(&self, value: Self::Item);
}
impl<T: PartialEq + 'static> WritableObservable for Signal<T> {
fn set(&self, value: T) {
Signal::set(self, value);
}
}
#[track_caller]
pub fn writable<T: PartialEq + 'static>(
get: impl Fn() -> T + 'static,
set: impl Fn(T) + 'static,
) -> Writable<T> {
Writable::new(¤t(), get, set)
}
#[track_caller]
pub fn writable_in<T: PartialEq + 'static>(
reactor: &Reactor,
get: impl Fn() -> T + 'static,
set: impl Fn(T) + 'static,
) -> Writable<T> {
Writable::new(reactor, get, set)
}
pub struct Writable<T> {
memo: Memo<T>,
set: Rc<dyn Fn(T)>,
}
impl<T> Clone for Writable<T> {
fn clone(&self) -> Self {
Self {
memo: self.memo.clone(),
set: Rc::clone(&self.set),
}
}
}
impl<T: PartialEq + 'static> Writable<T> {
#[track_caller]
fn new(reactor: &Reactor, get: impl Fn() -> T + 'static, set: impl Fn(T) + 'static) -> Self {
Self {
memo: memo_in(reactor, get),
set: Rc::from(Box::new(set) as Box<dyn Fn(T)>),
}
}
pub fn as_memo(&self) -> &Memo<T> {
&self.memo
}
}
impl<T: PartialEq + 'static> Observable for Writable<T> {
type Item = T;
fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
self.memo.with(f)
}
fn with_peek<R>(&self, f: impl FnOnce(&T) -> R) -> R {
self.memo.with_peek(f)
}
fn reactor(&self) -> Option<Reactor> {
Some(self.memo.reactor())
}
}
impl<T: PartialEq + 'static> WritableObservable for Writable<T> {
fn set(&self, value: T) {
untrack(|| (self.set)(value));
}
}
impl<T> core::fmt::Debug for Writable<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Writable").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use super::{Writable, WritableObservable, writable_in};
use crate::{Observable, Reactor, memo_in, signal_in};
#[test]
fn writes_translate_upstream_and_reads_stay_derived() {
let reactor = Reactor::new();
let celsius = signal_in(&reactor, 0.0f64);
let fahrenheit = writable_in(
&reactor,
{
let celsius = celsius.clone();
move || celsius.get() * 9.0 / 5.0 + 32.0
},
{
let celsius = celsius.clone();
move |value: f64| {
celsius.set((value - 32.0) * 5.0 / 9.0);
}
},
);
assert_eq!(fahrenheit.get(), 32.0);
fahrenheit.set(212.0);
assert_eq!(celsius.get(), 100.0, "the setter wrote through");
assert_eq!(fahrenheit.get(), 212.0);
celsius.set(0.0);
assert_eq!(fahrenheit.get(), 32.0, "the source still drives the memo");
}
#[test]
fn a_value_identical_round_trip_propagates_nothing() {
let reactor = Reactor::new();
let source = signal_in(&reactor, 10);
let doubled = writable_in(
&reactor,
{
let source = source.clone();
move || source.get() * 2
},
{
let source = source.clone();
move |value: i32| {
source.set(value / 2);
}
},
);
let recomputes = Rc::new(RefCell::new(0));
let downstream = memo_in(&reactor, {
let doubled = doubled.clone();
let recomputes = Rc::clone(&recomputes);
move || {
*recomputes.borrow_mut() += 1;
doubled.get()
}
});
assert_eq!(downstream.get(), 20);
assert_eq!(*recomputes.borrow(), 1);
doubled.set(20);
assert_eq!(downstream.get(), 20);
assert_eq!(*recomputes.borrow(), 1, "no downstream recomputation");
doubled.set(40);
assert_eq!(downstream.get(), 40);
assert_eq!(*recomputes.borrow(), 2);
}
#[test]
fn the_setter_runs_untracked() {
let reactor = Reactor::new();
let source = signal_in(&reactor, 1);
let offset = signal_in(&reactor, 100);
let view = writable_in(
&reactor,
{
let source = source.clone();
move || source.get()
},
{
let source = source.clone();
let offset = offset.clone();
move |value: i32| {
source.set(value + offset.get());
}
},
);
let recomputes = Rc::new(RefCell::new(0));
let observer = memo_in(&reactor, {
let view = view.clone();
let recomputes = Rc::clone(&recomputes);
move || {
*recomputes.borrow_mut() += 1;
view.set(0);
view.get()
}
});
assert_eq!(observer.get(), 100);
assert_eq!(*recomputes.borrow(), 1);
offset.set(500);
assert_eq!(observer.get(), 100);
assert_eq!(
*recomputes.borrow(),
1,
"reads made by the setter are not dependencies"
);
}
#[test]
fn a_lossy_setter_reports_what_the_source_holds() {
let reactor = Reactor::new();
let source = signal_in(&reactor, 0);
let clamped: Writable<i32> = writable_in(
&reactor,
{
let source = source.clone();
move || source.get()
},
{
let source = source.clone();
move |value: i32| {
source.set(value.clamp(0, 10));
}
},
);
clamped.set(50);
assert_eq!(
clamped.get(),
10,
"the next read reports the source, not the assignment"
);
}
#[test]
fn signals_and_writable_computeds_are_interchangeable() {
fn bump(field: &impl WritableObservable<Item = i32>) {
field.set(field.get() + 1);
}
let reactor = Reactor::new();
let count = signal_in(&reactor, 1);
bump(&count);
assert_eq!(count.get(), 2);
let doubled = writable_in(
&reactor,
{
let count = count.clone();
move || count.get() * 2
},
{
let count = count.clone();
move |value: i32| {
count.set(value / 2);
}
},
);
assert_eq!(doubled.get(), 4);
bump(&doubled);
assert_eq!(count.get(), 2, "5 / 2 truncates back onto the source");
assert_eq!(doubled.get(), 4);
}
}