#![forbid(unsafe_code)]
use std::cell::Cell;
use std::rc::Rc;
use super::observable::{Observable, Subscription};
pub struct Binding<T> {
eval: Rc<dyn Fn() -> T>,
}
impl<T> Clone for Binding<T> {
fn clone(&self) -> Self {
Self {
eval: Rc::clone(&self.eval),
}
}
}
impl<T: std::fmt::Debug + 'static> std::fmt::Debug for Binding<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Binding")
.field("value", &self.get())
.finish()
}
}
impl<T: 'static> Binding<T> {
pub fn new(f: impl Fn() -> T + 'static) -> Self {
Self { eval: Rc::new(f) }
}
#[must_use]
pub fn get(&self) -> T {
(self.eval)()
}
pub fn then<U: 'static>(self, f: impl Fn(T) -> U + 'static) -> Binding<U> {
Binding {
eval: Rc::new(move || f((self.eval)())),
}
}
}
pub fn bind_observable<T: Clone + PartialEq + 'static>(source: &Observable<T>) -> Binding<T> {
let src = source.clone();
Binding {
eval: Rc::new(move || src.get()),
}
}
pub fn bind_mapped<S: Clone + PartialEq + 'static, T: 'static>(
source: &Observable<S>,
map: impl Fn(&S) -> T + 'static,
) -> Binding<T> {
let src = source.clone();
Binding {
eval: Rc::new(move || src.with(|v| map(v))),
}
}
pub fn bind_mapped2<
S1: Clone + PartialEq + 'static,
S2: Clone + PartialEq + 'static,
T: 'static,
>(
s1: &Observable<S1>,
s2: &Observable<S2>,
map: impl Fn(&S1, &S2) -> T + 'static,
) -> Binding<T> {
let src1 = s1.clone();
let src2 = s2.clone();
Binding {
eval: Rc::new(move || src1.with(|v1| src2.with(|v2| map(v1, v2)))),
}
}
pub struct TwoWayBinding<T: Clone + PartialEq + 'static> {
_sub_a_to_b: Subscription,
_sub_b_to_a: Subscription,
_guard: Rc<Cell<bool>>,
_phantom: std::marker::PhantomData<T>,
}
impl<T: Clone + PartialEq + 'static> TwoWayBinding<T> {
pub fn new(a: &Observable<T>, b: &Observable<T>) -> Self {
b.set(a.get());
let syncing = Rc::new(Cell::new(false));
struct ReentrancyGuard<'a>(&'a Cell<bool>);
impl<'a> Drop for ReentrancyGuard<'a> {
fn drop(&mut self) {
self.0.set(false);
}
}
let b_clone = b.clone();
let guard_ab = Rc::clone(&syncing);
let sub_ab = a.subscribe(move |val| {
if !guard_ab.get() {
guard_ab.set(true);
let _guard = ReentrancyGuard(&guard_ab);
b_clone.set(val.clone());
}
});
let a_clone = a.clone();
let guard_ba = Rc::clone(&syncing);
let sub_ba = b.subscribe(move |val| {
if !guard_ba.get() {
guard_ba.set(true);
let _guard = ReentrancyGuard(&guard_ba);
a_clone.set(val.clone());
}
});
Self {
_sub_a_to_b: sub_ab,
_sub_b_to_a: sub_ba,
_guard: syncing,
_phantom: std::marker::PhantomData,
}
}
}
impl<T: Clone + PartialEq + 'static> std::fmt::Debug for TwoWayBinding<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TwoWayBinding").finish()
}
}
#[macro_export]
macro_rules! bind {
($obs:expr) => {
$crate::reactive::binding::bind_observable(&$obs)
};
}
#[macro_export]
macro_rules! bind_map {
($obs:expr, $f:expr) => {
$crate::reactive::binding::bind_mapped(&$obs, $f)
};
}
#[macro_export]
macro_rules! bind_map2 {
($s1:expr, $s2:expr, $f:expr) => {
$crate::reactive::binding::bind_mapped2(&$s1, &$s2, $f)
};
}
pub struct BindingScope {
subscriptions: Vec<Subscription>,
}
impl BindingScope {
#[must_use]
pub fn new() -> Self {
Self {
subscriptions: Vec::new(),
}
}
pub fn hold(&mut self, sub: Subscription) {
self.subscriptions.push(sub);
}
pub fn subscribe<T: Clone + PartialEq + 'static>(
&mut self,
source: &Observable<T>,
callback: impl Fn(&T) + 'static,
) -> &mut Self {
let sub = source.subscribe(callback);
self.subscriptions.push(sub);
self
}
pub fn bind<T: Clone + PartialEq + 'static>(&mut self, source: &Observable<T>) -> Binding<T> {
bind_observable(source)
}
pub fn bind_map<S: Clone + PartialEq + 'static, T: 'static>(
&mut self,
source: &Observable<S>,
map: impl Fn(&S) -> T + 'static,
) -> Binding<T> {
bind_mapped(source, map)
}
#[must_use]
pub fn binding_count(&self) -> usize {
self.subscriptions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.subscriptions.is_empty()
}
pub fn clear(&mut self) {
self.subscriptions.clear();
}
}
impl Default for BindingScope {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for BindingScope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BindingScope")
.field("binding_count", &self.subscriptions.len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binding_from_observable() {
let obs = Observable::new(42);
let b = bind_observable(&obs);
assert_eq!(b.get(), 42);
obs.set(100);
assert_eq!(b.get(), 100);
}
#[test]
fn binding_map() {
let count = Observable::new(3);
let label = bind_mapped(&count, |c| format!("items: {c}"));
assert_eq!(label.get(), "items: 3");
count.set(7);
assert_eq!(label.get(), "items: 7");
}
#[test]
fn binding_map2() {
let w = Observable::new(10);
let h = Observable::new(20);
let area = bind_mapped2(&w, &h, |a, b| a * b);
assert_eq!(area.get(), 200);
w.set(5);
assert_eq!(area.get(), 100);
}
#[test]
fn binding_then_chain() {
let obs = Observable::new(5);
let doubled = bind_observable(&obs).then(|v| v * 2);
assert_eq!(doubled.get(), 10);
obs.set(3);
assert_eq!(doubled.get(), 6);
}
#[test]
fn binding_clone_shares_source() {
let obs = Observable::new(1);
let b1 = bind_observable(&obs);
let b2 = b1.clone();
obs.set(99);
assert_eq!(b1.get(), 99);
assert_eq!(b2.get(), 99);
}
#[test]
fn binding_new_custom() {
let counter = Rc::new(Cell::new(0));
let c = Rc::clone(&counter);
let b = Binding::new(move || {
c.set(c.get() + 1);
c.get()
});
assert_eq!(b.get(), 1);
assert_eq!(b.get(), 2);
}
#[test]
fn bind_macro() {
let obs = Observable::new(42);
let b = bind!(obs);
assert_eq!(b.get(), 42);
}
#[test]
fn bind_map_macro() {
let obs = Observable::new(5);
let b = bind_map!(obs, |v| v * 10);
assert_eq!(b.get(), 50);
}
#[test]
fn bind_map2_macro() {
let a = Observable::new(3);
let b = Observable::new(4);
let sum = bind_map2!(a, b, |x, y| x + y);
assert_eq!(sum.get(), 7);
}
#[test]
fn two_way_initial_sync() {
let a = Observable::new(10);
let b = Observable::new(0);
let _binding = TwoWayBinding::new(&a, &b);
assert_eq!(b.get(), 10, "b should sync to a's initial value");
}
#[test]
fn two_way_a_to_b() {
let a = Observable::new(1);
let b = Observable::new(0);
let _binding = TwoWayBinding::new(&a, &b);
a.set(42);
assert_eq!(b.get(), 42);
}
#[test]
fn two_way_b_to_a() {
let a = Observable::new(1);
let b = Observable::new(0);
let _binding = TwoWayBinding::new(&a, &b);
b.set(99);
assert_eq!(a.get(), 99);
}
#[test]
fn two_way_no_cycle() {
let a = Observable::new(0);
let b = Observable::new(0);
let _binding = TwoWayBinding::new(&a, &b);
a.set(5);
assert_eq!(a.get(), 5);
assert_eq!(b.get(), 5);
b.set(10);
assert_eq!(a.get(), 10);
assert_eq!(b.get(), 10);
}
#[test]
fn two_way_drop_disconnects() {
let a = Observable::new(1);
let b = Observable::new(0);
{
let _binding = TwoWayBinding::new(&a, &b);
a.set(5);
assert_eq!(b.get(), 5);
}
a.set(100);
assert_eq!(b.get(), 5, "b should not update after binding dropped");
}
#[test]
fn two_way_with_strings() {
let a = Observable::new(String::from("hello"));
let b = Observable::new(String::new());
let _binding = TwoWayBinding::new(&a, &b);
assert_eq!(b.get(), "hello");
b.set("world".to_string());
assert_eq!(a.get(), "world");
}
#[test]
fn multiple_bindings_same_source() {
let source = Observable::new(0);
let b1 = bind_observable(&source);
let b2 = bind_mapped(&source, |v| v * 2);
let b3 = bind_mapped(&source, |v| format!("{v}"));
source.set(5);
assert_eq!(b1.get(), 5);
assert_eq!(b2.get(), 10);
assert_eq!(b3.get(), "5");
}
#[test]
fn binding_survives_source_clone() {
let source = Observable::new(42);
let b = bind_observable(&source);
let source2 = source.clone();
source2.set(99);
assert_eq!(
b.get(),
99,
"binding should see changes through cloned observable"
);
}
#[test]
fn scope_holds_subscriptions() {
let obs = Observable::new(0);
let seen = Rc::new(Cell::new(0));
let mut scope = BindingScope::new();
let s = Rc::clone(&seen);
scope.subscribe(&obs, move |v| s.set(*v));
assert_eq!(scope.binding_count(), 1);
obs.set(42);
assert_eq!(seen.get(), 42);
}
#[test]
fn scope_drop_releases_subscriptions() {
let obs = Observable::new(0);
let seen = Rc::new(Cell::new(0));
{
let mut scope = BindingScope::new();
let s = Rc::clone(&seen);
scope.subscribe(&obs, move |v| s.set(*v));
obs.set(1);
assert_eq!(seen.get(), 1);
}
obs.set(99);
assert_eq!(
seen.get(),
1,
"callback should not fire after scope dropped"
);
}
#[test]
fn scope_clear_releases() {
let obs = Observable::new(0);
let seen = Rc::new(Cell::new(0));
let mut scope = BindingScope::new();
let s = Rc::clone(&seen);
scope.subscribe(&obs, move |v| s.set(*v));
assert_eq!(scope.binding_count(), 1);
scope.clear();
assert_eq!(scope.binding_count(), 0);
assert!(scope.is_empty());
obs.set(42);
assert_eq!(seen.get(), 0, "callback should not fire after clear");
}
#[test]
fn scope_multiple_subscriptions() {
let obs = Observable::new(0);
let count = Rc::new(Cell::new(0));
let mut scope = BindingScope::new();
for _ in 0..5 {
let c = Rc::clone(&count);
scope.subscribe(&obs, move |_| c.set(c.get() + 1));
}
assert_eq!(scope.binding_count(), 5);
obs.set(1);
assert_eq!(count.get(), 5, "all 5 callbacks should fire");
}
#[test]
fn scope_bind_returns_binding() {
let obs = Observable::new(42);
let mut scope = BindingScope::new();
let b = scope.bind(&obs);
assert_eq!(b.get(), 42);
obs.set(7);
assert_eq!(b.get(), 7);
}
#[test]
fn scope_bind_map() {
let obs = Observable::new(3);
let mut scope = BindingScope::new();
let b = scope.bind_map(&obs, |v| v * 10);
assert_eq!(b.get(), 30);
}
#[test]
fn scope_reusable_after_clear() {
let obs = Observable::new(0);
let mut scope = BindingScope::new();
let seen1 = Rc::new(Cell::new(false));
let s1 = Rc::clone(&seen1);
scope.subscribe(&obs, move |_| s1.set(true));
scope.clear();
let seen2 = Rc::new(Cell::new(false));
let s2 = Rc::clone(&seen2);
scope.subscribe(&obs, move |_| s2.set(true));
obs.set(1);
assert!(!seen1.get(), "first subscription should be gone");
assert!(seen2.get(), "second subscription should be active");
}
#[test]
fn scope_hold_external_subscription() {
let obs = Observable::new(0);
let seen = Rc::new(Cell::new(0));
let mut scope = BindingScope::new();
let s = Rc::clone(&seen);
let sub = obs.subscribe(move |v| s.set(*v));
scope.hold(sub);
obs.set(5);
assert_eq!(seen.get(), 5);
drop(scope);
obs.set(99);
assert_eq!(
seen.get(),
5,
"held subscription should be released on scope drop"
);
}
#[test]
fn scope_debug_format() {
let mut scope = BindingScope::new();
let obs = Observable::new(0);
scope.subscribe(&obs, |_| {});
scope.subscribe(&obs, |_| {});
let debug = format!("{scope:?}");
assert!(debug.contains("binding_count: 2"));
}
}