use std::cell::Cell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use super::computed::Computed;
use super::effect::Effect;
use super::signal::Signal;
pub type BindingId = u64;
static BINDING_COUNTER: AtomicU64 = AtomicU64::new(1);
fn next_binding_id() -> BindingId {
BINDING_COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindingDirection {
OneWay,
TwoWay,
}
pub trait Binding {
fn id(&self) -> BindingId;
fn direction(&self) -> BindingDirection;
fn is_active(&self) -> bool;
fn dispose(&self);
}
pub trait PropertySink<T> {
fn set_value(&self, value: &T);
}
impl<T, F: Fn(&T)> PropertySink<T> for F {
fn set_value(&self, value: &T) {
self(value);
}
}
pub struct OneWayBinding<T: Clone + 'static> {
id: BindingId,
effect: Effect,
_source: Signal<T>,
}
impl<T: Clone + 'static> OneWayBinding<T> {
pub fn new(source: &Signal<T>, sink: impl PropertySink<T> + 'static) -> Self {
let id = next_binding_id();
let source_clone = source.clone();
let effect = Effect::new({
let sig = source.clone();
move || {
let value = sig.get();
sink.set_value(&value);
}
});
source.subscribe(effect.as_subscriber());
Self {
id,
effect,
_source: source_clone,
}
}
}
impl<T: Clone + 'static> Binding for OneWayBinding<T> {
fn id(&self) -> BindingId {
self.id
}
fn direction(&self) -> BindingDirection {
BindingDirection::OneWay
}
fn is_active(&self) -> bool {
self.effect.is_active()
}
fn dispose(&self) {
self.effect.dispose();
}
}
pub struct TwoWayBinding<T: Clone + 'static> {
id: BindingId,
effect: Effect,
source: Signal<T>,
updating: Rc<Cell<bool>>,
}
impl<T: Clone + 'static> TwoWayBinding<T> {
pub fn new(source: &Signal<T>, sink: impl PropertySink<T> + 'static) -> Self {
let id = next_binding_id();
let updating = Rc::new(Cell::new(false));
let effect = Effect::new({
let sig = source.clone();
let guard = Rc::clone(&updating);
move || {
if guard.get() {
return;
}
let value = sig.get();
sink.set_value(&value);
}
});
source.subscribe(effect.as_subscriber());
Self {
id,
effect,
source: source.clone(),
updating,
}
}
pub fn write_back(&self, value: T) {
if !self.effect.is_active() {
return;
}
self.updating.set(true);
self.source.set(value);
self.updating.set(false);
}
}
impl<T: Clone + 'static> Binding for TwoWayBinding<T> {
fn id(&self) -> BindingId {
self.id
}
fn direction(&self) -> BindingDirection {
BindingDirection::TwoWay
}
fn is_active(&self) -> bool {
self.effect.is_active()
}
fn dispose(&self) {
self.effect.dispose();
}
}
pub struct BindingExpression<S: Clone + 'static, T: Clone + 'static> {
id: BindingId,
effect: Effect,
_source: Signal<S>,
_computed: Computed<T>,
}
impl<S: Clone + 'static, T: Clone + 'static> BindingExpression<S, T> {
pub fn new(
source: &Signal<S>,
transform: impl Fn(&S) -> T + 'static,
sink: impl PropertySink<T> + 'static,
) -> Self {
let id = next_binding_id();
let computed = Computed::new({
let sig = source.clone();
move || {
let v = sig.get();
transform(&v)
}
});
source.subscribe(computed.as_subscriber());
let effect = Effect::new({
let comp = computed.clone();
move || {
let value = comp.get();
sink.set_value(&value);
}
});
computed.subscribe(effect.as_subscriber());
Self {
id,
effect,
_source: source.clone(),
_computed: computed,
}
}
}
impl<S: Clone + 'static, T: Clone + 'static> Binding for BindingExpression<S, T> {
fn id(&self) -> BindingId {
self.id
}
fn direction(&self) -> BindingDirection {
BindingDirection::OneWay
}
fn is_active(&self) -> bool {
self.effect.is_active()
}
fn dispose(&self) {
self.effect.dispose();
}
}
pub struct BindingScope {
bindings: Vec<Box<dyn Binding>>,
}
impl BindingScope {
#[must_use]
pub fn new() -> Self {
Self {
bindings: Vec::new(),
}
}
pub fn bind<T: Clone + 'static>(
&mut self,
source: &Signal<T>,
sink: impl PropertySink<T> + 'static,
) -> BindingId {
let binding = OneWayBinding::new(source, sink);
let id = binding.id();
self.bindings.push(Box::new(binding));
id
}
pub fn bind_two_way<T: Clone + 'static>(
&mut self,
source: &Signal<T>,
sink: impl PropertySink<T> + 'static,
) -> (TwoWayBinding<T>, BindingId) {
let binding = TwoWayBinding::new(source, sink);
let id = binding.id();
let caller_binding = TwoWayBinding {
id: binding.id,
effect: binding.effect.clone(),
source: binding.source.clone(),
updating: Rc::clone(&binding.updating),
};
self.bindings.push(Box::new(binding));
(caller_binding, id)
}
pub fn bind_expression<S: Clone + 'static, T: Clone + 'static>(
&mut self,
source: &Signal<S>,
transform: impl Fn(&S) -> T + 'static,
sink: impl PropertySink<T> + 'static,
) -> BindingId {
let binding = BindingExpression::new(source, transform, sink);
let id = binding.id();
self.bindings.push(Box::new(binding));
id
}
pub fn binding_count(&self) -> usize {
self.bindings.len()
}
pub fn is_binding_active(&self, id: BindingId) -> bool {
self.bindings
.iter()
.find(|b| b.id() == id)
.is_some_and(|b| b.is_active())
}
}
impl Default for BindingScope {
fn default() -> Self {
Self::new()
}
}
impl Drop for BindingScope {
fn drop(&mut self) {
for binding in &self.bindings {
binding.dispose();
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::reactive::batch::batch;
use std::cell::RefCell;
#[test]
fn one_way_pushes_initial_value() {
let sig = Signal::new(42);
let output = Rc::new(Cell::new(0));
let _binding = OneWayBinding::new(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
assert_eq!(output.get(), 42);
}
#[test]
fn one_way_pushes_on_change() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
let _binding = OneWayBinding::new(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
sig.set(10);
assert_eq!(output.get(), 10);
sig.set(20);
assert_eq!(output.get(), 20);
}
#[test]
fn one_way_stops_after_dispose() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
let binding = OneWayBinding::new(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
sig.set(5);
assert_eq!(output.get(), 5);
binding.dispose();
assert!(!binding.is_active());
sig.set(99);
assert_eq!(output.get(), 5); }
#[test]
fn one_way_direction() {
let sig = Signal::new(0);
let binding = OneWayBinding::new(&sig, |_: &i32| {});
assert_eq!(binding.direction(), BindingDirection::OneWay);
}
#[test]
fn one_way_unique_ids() {
let sig = Signal::new(0);
let a = OneWayBinding::new(&sig, |_: &i32| {});
let b = OneWayBinding::new(&sig, |_: &i32| {});
assert_ne!(a.id(), b.id());
}
#[test]
fn one_way_with_string_sink() {
let sig = Signal::new(String::from("hello"));
let output = Rc::new(RefCell::new(String::new()));
let _binding = OneWayBinding::new(&sig, {
let out = Rc::clone(&output);
move |v: &String| *out.borrow_mut() = v.clone()
});
assert_eq!(*output.borrow(), "hello");
sig.set("world".into());
assert_eq!(*output.borrow(), "world");
}
#[test]
fn two_way_forward_push() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
let _binding = TwoWayBinding::new(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
sig.set(42);
assert_eq!(output.get(), 42);
}
#[test]
fn two_way_write_back() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
let binding = TwoWayBinding::new(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
binding.write_back(99);
assert_eq!(sig.get(), 99);
}
#[test]
fn two_way_loop_guard() {
let sig = Signal::new(0);
let push_count = Rc::new(Cell::new(0u32));
let binding = TwoWayBinding::new(&sig, {
let count = Rc::clone(&push_count);
move |_: &i32| count.set(count.get() + 1)
});
assert_eq!(push_count.get(), 1);
binding.write_back(42);
assert_eq!(push_count.get(), 1); assert_eq!(sig.get(), 42);
sig.set(100);
assert_eq!(push_count.get(), 2);
}
#[test]
fn two_way_disposed_write_back_ignored() {
let sig = Signal::new(0);
let binding = TwoWayBinding::new(&sig, |_: &i32| {});
binding.dispose();
binding.write_back(42);
assert_eq!(sig.get(), 0);
}
#[test]
fn two_way_direction() {
let sig = Signal::new(0);
let binding = TwoWayBinding::new(&sig, |_: &i32| {});
assert_eq!(binding.direction(), BindingDirection::TwoWay);
}
#[test]
fn expression_transforms_value() {
let sig = Signal::new(3);
let output = Rc::new(RefCell::new(String::new()));
let _binding = BindingExpression::new(&sig, |v: &i32| format!("Count: {v}"), {
let out = Rc::clone(&output);
move |v: &String| *out.borrow_mut() = v.clone()
});
assert_eq!(*output.borrow(), "Count: 3");
sig.set(7);
assert_eq!(*output.borrow(), "Count: 7");
}
#[test]
fn expression_stops_after_dispose() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
let binding = BindingExpression::new(&sig, |v: &i32| v * 10, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
sig.set(5);
assert_eq!(output.get(), 50);
binding.dispose();
sig.set(99);
assert_eq!(output.get(), 50); }
#[test]
fn expression_direction() {
let sig = Signal::new(0);
let binding = BindingExpression::new(&sig, |v: &i32| *v, |_: &i32| {});
assert_eq!(binding.direction(), BindingDirection::OneWay);
}
#[test]
fn expression_type_conversion() {
let sig = Signal::new(42i32);
let output = Rc::new(Cell::new(0.0f64));
let _binding = BindingExpression::new(&sig, |v: &i32| *v as f64 * 1.5, {
let out = Rc::clone(&output);
move |v: &f64| out.set(*v)
});
assert!((output.get() - 63.0).abs() < f64::EPSILON);
sig.set(10);
assert!((output.get() - 15.0).abs() < f64::EPSILON);
}
#[test]
fn scope_bind_creates_one_way() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
let mut scope = BindingScope::new();
let id = scope.bind(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
assert_eq!(scope.binding_count(), 1);
assert!(scope.is_binding_active(id));
sig.set(10);
assert_eq!(output.get(), 10);
}
#[test]
fn scope_bind_two_way() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
let mut scope = BindingScope::new();
let (two_way, id) = scope.bind_two_way(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
assert_eq!(scope.binding_count(), 1);
assert!(scope.is_binding_active(id));
sig.set(10);
assert_eq!(output.get(), 10);
two_way.write_back(50);
assert_eq!(sig.get(), 50);
}
#[test]
fn scope_bind_expression() {
let sig = Signal::new(5);
let output = Rc::new(RefCell::new(String::new()));
let mut scope = BindingScope::new();
let id = scope.bind_expression(&sig, |v: &i32| format!("val={v}"), {
let out = Rc::clone(&output);
move |v: &String| *out.borrow_mut() = v.clone()
});
assert!(scope.is_binding_active(id));
assert_eq!(*output.borrow(), "val=5");
sig.set(10);
assert_eq!(*output.borrow(), "val=10");
}
#[test]
fn scope_disposes_bindings_on_drop() {
let sig = Signal::new(0);
let output = Rc::new(Cell::new(0));
{
let mut scope = BindingScope::new();
scope.bind(&sig, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
sig.set(5);
assert_eq!(output.get(), 5);
}
sig.set(99);
assert_eq!(output.get(), 5); }
#[test]
fn scope_multiple_bindings() {
let a = Signal::new(0);
let b = Signal::new(0);
let out_a = Rc::new(Cell::new(0));
let out_b = Rc::new(Cell::new(0));
let mut scope = BindingScope::new();
scope.bind(&a, {
let out = Rc::clone(&out_a);
move |v: &i32| out.set(*v)
});
scope.bind(&b, {
let out = Rc::clone(&out_b);
move |v: &i32| out.set(*v)
});
assert_eq!(scope.binding_count(), 2);
a.set(10);
b.set(20);
assert_eq!(out_a.get(), 10);
assert_eq!(out_b.get(), 20);
}
#[test]
fn scope_is_binding_active_returns_false_for_unknown_id() {
let scope = BindingScope::new();
assert!(!scope.is_binding_active(99999));
}
#[test]
fn binding_with_batch() {
let sig = Signal::new(0);
let push_count = Rc::new(Cell::new(0u32));
let _binding = OneWayBinding::new(&sig, {
let count = Rc::clone(&push_count);
move |_: &i32| count.set(count.get() + 1)
});
assert_eq!(push_count.get(), 1);
batch(|| {
sig.set(1);
sig.set(2);
sig.set(3);
});
assert_eq!(push_count.get(), 2);
}
#[test]
fn binding_expression_with_batch() {
let sig = Signal::new(0);
let transform_count = Rc::new(Cell::new(0u32));
let output = Rc::new(Cell::new(0));
let _binding = BindingExpression::new(
&sig,
{
let count = Rc::clone(&transform_count);
move |v: &i32| {
count.set(count.get() + 1);
v * 2
}
},
{
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
},
);
assert_eq!(transform_count.get(), 1);
assert_eq!(output.get(), 0);
batch(|| {
sig.set(5);
sig.set(10);
});
assert_eq!(output.get(), 20);
}
#[test]
fn two_way_binding_round_trip() {
let model = Signal::new(String::from("initial"));
let view = Rc::new(RefCell::new(String::new()));
let binding = TwoWayBinding::new(&model, {
let view = Rc::clone(&view);
move |v: &String| *view.borrow_mut() = v.clone()
});
assert_eq!(*view.borrow(), "initial");
model.set("forward".into());
assert_eq!(*view.borrow(), "forward");
binding.write_back("reverse".into());
assert_eq!(model.get(), "reverse");
model.set("final".into());
assert_eq!(*view.borrow(), "final");
}
#[test]
fn binding_scope_with_mixed_types() {
let count = Signal::new(0);
let name = Signal::new(String::from("test"));
let out_count = Rc::new(Cell::new(0));
let out_name = Rc::new(RefCell::new(String::new()));
let mut scope = BindingScope::new();
scope.bind(&count, {
let out = Rc::clone(&out_count);
move |v: &i32| out.set(*v)
});
scope.bind(&name, {
let out = Rc::clone(&out_name);
move |v: &String| *out.borrow_mut() = v.clone()
});
count.set(42);
name.set("hello".into());
assert_eq!(out_count.get(), 42);
assert_eq!(*out_name.borrow(), "hello");
}
#[test]
fn chained_one_way_bindings() {
let source = Signal::new(1);
let middle = Signal::new(0);
let output = Rc::new(Cell::new(0));
let _binding1 = OneWayBinding::new(&source, {
let mid = middle.clone();
move |v: &i32| mid.set(*v * 2)
});
let _binding2 = OneWayBinding::new(&middle, {
let out = Rc::clone(&output);
move |v: &i32| out.set(*v)
});
assert_eq!(middle.get(), 2);
assert_eq!(output.get(), 2);
source.set(5);
assert_eq!(middle.get(), 10);
assert_eq!(output.get(), 10);
}
#[test]
fn scope_default_is_empty() {
let scope = BindingScope::default();
assert_eq!(scope.binding_count(), 0);
}
#[test]
fn disposed_binding_not_active_in_scope() {
let sig = Signal::new(0);
let mut scope = BindingScope::new();
let id = scope.bind(&sig, |_: &i32| {});
assert!(scope.is_binding_active(id));
drop(scope);
}
#[test]
fn stress_many_bindings() {
let sig = Signal::new(0);
let count = Rc::new(Cell::new(0u32));
let mut scope = BindingScope::new();
for _ in 0..50 {
scope.bind(&sig, {
let count = Rc::clone(&count);
move |_: &i32| count.set(count.get() + 1)
});
}
assert_eq!(count.get(), 50);
sig.set(1);
assert_eq!(count.get(), 100);
drop(scope);
sig.set(2);
assert_eq!(count.get(), 100);
}
}