Skip to main content

saorsa_core/reactive/
binding.rs

1//! Data binding primitives for connecting signals to widget properties.
2//!
3//! Bindings bridge the reactive system and the widget layer. A binding
4//! watches a signal (or computed expression) and pushes its value to a
5//! [`PropertySink`] whenever the source changes.
6//!
7//! Three binding flavours are provided:
8//!
9//! - [`OneWayBinding`]: source signal → sink (read-only push)
10//! - [`TwoWayBinding`]: source signal ↔ sink (bidirectional, with loop guard)
11//! - [`BindingExpression`]: source signal → transform → sink
12
13use std::cell::Cell;
14use std::rc::Rc;
15use std::sync::atomic::{AtomicU64, Ordering};
16
17use super::computed::Computed;
18use super::effect::Effect;
19use super::signal::Signal;
20
21// ---------------------------------------------------------------------------
22// BindingId
23// ---------------------------------------------------------------------------
24
25/// Unique identifier for a binding instance.
26pub type BindingId = u64;
27
28static BINDING_COUNTER: AtomicU64 = AtomicU64::new(1);
29
30/// Generate the next unique binding ID.
31fn next_binding_id() -> BindingId {
32    BINDING_COUNTER.fetch_add(1, Ordering::Relaxed)
33}
34
35// ---------------------------------------------------------------------------
36// BindingDirection
37// ---------------------------------------------------------------------------
38
39/// Direction in which data flows through a binding.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum BindingDirection {
42    /// Data flows from source to sink only.
43    OneWay,
44    /// Data flows in both directions (source ↔ sink).
45    TwoWay,
46}
47
48// ---------------------------------------------------------------------------
49// Binding trait
50// ---------------------------------------------------------------------------
51
52/// A type-erased binding that can be stored and managed in a collection.
53pub trait Binding {
54    /// Unique identifier for this binding.
55    fn id(&self) -> BindingId;
56
57    /// Direction of data flow.
58    fn direction(&self) -> BindingDirection;
59
60    /// Whether this binding is still active.
61    fn is_active(&self) -> bool;
62
63    /// Permanently deactivate this binding.
64    fn dispose(&self);
65}
66
67// ---------------------------------------------------------------------------
68// PropertySink
69// ---------------------------------------------------------------------------
70
71/// A target that can receive property values from a binding.
72///
73/// Implementations typically wrap a widget property setter or a
74/// `Rc<RefCell<T>>` shared cell.
75pub trait PropertySink<T> {
76    /// Push a new value to the sink.
77    fn set_value(&self, value: &T);
78}
79
80/// Blanket implementation: any `Fn(&T)` is a `PropertySink<T>`.
81impl<T, F: Fn(&T)> PropertySink<T> for F {
82    fn set_value(&self, value: &T) {
83        self(value);
84    }
85}
86
87// ---------------------------------------------------------------------------
88// OneWayBinding
89// ---------------------------------------------------------------------------
90
91/// A one-way binding that pushes signal changes to a property sink.
92///
93/// When the source signal changes, the binding reads the new value
94/// and calls [`PropertySink::set_value`] on the sink.
95///
96/// # Examples
97///
98/// ```ignore
99/// let count = Signal::new(0);
100/// let display = Rc::new(RefCell::new(String::new()));
101///
102/// let binding = OneWayBinding::new(&count, {
103///     let display = display.clone();
104///     move |v: &i32| *display.borrow_mut() = format!("{v}")
105/// });
106///
107/// count.set(42);
108/// assert_eq!(*display.borrow(), "42");
109/// ```
110pub struct OneWayBinding<T: Clone + 'static> {
111    id: BindingId,
112    effect: Effect,
113    /// Keep the signal alive for the binding's lifetime.
114    _source: Signal<T>,
115}
116
117impl<T: Clone + 'static> OneWayBinding<T> {
118    /// Create a new one-way binding from `source` to `sink`.
119    ///
120    /// The sink is called immediately with the current value, and
121    /// again whenever the source signal changes.
122    pub fn new(source: &Signal<T>, sink: impl PropertySink<T> + 'static) -> Self {
123        let id = next_binding_id();
124        let source_clone = source.clone();
125
126        let effect = Effect::new({
127            let sig = source.clone();
128            move || {
129                let value = sig.get();
130                sink.set_value(&value);
131            }
132        });
133
134        // Subscribe the effect to source changes.
135        source.subscribe(effect.as_subscriber());
136
137        Self {
138            id,
139            effect,
140            _source: source_clone,
141        }
142    }
143}
144
145impl<T: Clone + 'static> Binding for OneWayBinding<T> {
146    fn id(&self) -> BindingId {
147        self.id
148    }
149
150    fn direction(&self) -> BindingDirection {
151        BindingDirection::OneWay
152    }
153
154    fn is_active(&self) -> bool {
155        self.effect.is_active()
156    }
157
158    fn dispose(&self) {
159        self.effect.dispose();
160    }
161}
162
163// ---------------------------------------------------------------------------
164// TwoWayBinding
165// ---------------------------------------------------------------------------
166
167/// A two-way binding between a signal and a property sink.
168///
169/// The forward direction works like [`OneWayBinding`]: signal changes
170/// are pushed to the sink. The reverse direction is handled by calling
171/// [`TwoWayBinding::write_back`], which updates the signal.
172///
173/// An internal loop guard prevents infinite ping-pong when a write-back
174/// triggers a forward push that would trigger another write-back.
175///
176/// # Examples
177///
178/// ```ignore
179/// let model = Signal::new(String::from("hello"));
180/// let display = Rc::new(RefCell::new(String::new()));
181///
182/// let binding = TwoWayBinding::new(&model, {
183///     let display = display.clone();
184///     move |v: &String| *display.borrow_mut() = v.clone()
185/// });
186///
187/// // Forward: model → display
188/// model.set("world".into());
189/// assert_eq!(*display.borrow(), "world");
190///
191/// // Reverse: display → model
192/// binding.write_back("reverse".into());
193/// assert_eq!(model.get(), "reverse");
194/// ```
195pub struct TwoWayBinding<T: Clone + 'static> {
196    id: BindingId,
197    effect: Effect,
198    source: Signal<T>,
199    /// Guard to prevent infinite update loops.
200    updating: Rc<Cell<bool>>,
201}
202
203impl<T: Clone + 'static> TwoWayBinding<T> {
204    /// Create a new two-way binding between `source` and `sink`.
205    ///
206    /// Forward direction runs immediately and on every source change.
207    pub fn new(source: &Signal<T>, sink: impl PropertySink<T> + 'static) -> Self {
208        let id = next_binding_id();
209        let updating = Rc::new(Cell::new(false));
210
211        let effect = Effect::new({
212            let sig = source.clone();
213            let guard = Rc::clone(&updating);
214            move || {
215                if guard.get() {
216                    return;
217                }
218                let value = sig.get();
219                sink.set_value(&value);
220            }
221        });
222
223        source.subscribe(effect.as_subscriber());
224
225        Self {
226            id,
227            effect,
228            source: source.clone(),
229            updating,
230        }
231    }
232
233    /// Write a value back from the sink to the source signal.
234    ///
235    /// Uses the loop guard to prevent the forward effect from
236    /// re-pushing the same value to the sink.
237    pub fn write_back(&self, value: T) {
238        if !self.effect.is_active() {
239            return;
240        }
241
242        self.updating.set(true);
243        self.source.set(value);
244        self.updating.set(false);
245    }
246}
247
248impl<T: Clone + 'static> Binding for TwoWayBinding<T> {
249    fn id(&self) -> BindingId {
250        self.id
251    }
252
253    fn direction(&self) -> BindingDirection {
254        BindingDirection::TwoWay
255    }
256
257    fn is_active(&self) -> bool {
258        self.effect.is_active()
259    }
260
261    fn dispose(&self) {
262        self.effect.dispose();
263    }
264}
265
266// ---------------------------------------------------------------------------
267// BindingExpression
268// ---------------------------------------------------------------------------
269
270/// A binding that transforms the source value before pushing to the sink.
271///
272/// Uses a [`Computed`] internally so the transform result is cached
273/// and only re-evaluated when the source changes.
274///
275/// # Examples
276///
277/// ```ignore
278/// let count = Signal::new(3);
279/// let label = Rc::new(RefCell::new(String::new()));
280///
281/// let binding = BindingExpression::new(
282///     &count,
283///     |v: &i32| format!("Count: {v}"),
284///     {
285///         let label = label.clone();
286///         move |v: &String| *label.borrow_mut() = v.clone()
287///     },
288/// );
289///
290/// count.set(7);
291/// assert_eq!(*label.borrow(), "Count: 7");
292/// ```
293pub struct BindingExpression<S: Clone + 'static, T: Clone + 'static> {
294    id: BindingId,
295    effect: Effect,
296    /// Keep source and computed alive.
297    _source: Signal<S>,
298    _computed: Computed<T>,
299}
300
301impl<S: Clone + 'static, T: Clone + 'static> BindingExpression<S, T> {
302    /// Create a binding that transforms source values via `transform`
303    /// before pushing to `sink`.
304    pub fn new(
305        source: &Signal<S>,
306        transform: impl Fn(&S) -> T + 'static,
307        sink: impl PropertySink<T> + 'static,
308    ) -> Self {
309        let id = next_binding_id();
310
311        // Create a computed that applies the transform.
312        let computed = Computed::new({
313            let sig = source.clone();
314            move || {
315                let v = sig.get();
316                transform(&v)
317            }
318        });
319        source.subscribe(computed.as_subscriber());
320
321        // Effect reads the computed and pushes to sink.
322        let effect = Effect::new({
323            let comp = computed.clone();
324            move || {
325                let value = comp.get();
326                sink.set_value(&value);
327            }
328        });
329        computed.subscribe(effect.as_subscriber());
330
331        Self {
332            id,
333            effect,
334            _source: source.clone(),
335            _computed: computed,
336        }
337    }
338}
339
340impl<S: Clone + 'static, T: Clone + 'static> Binding for BindingExpression<S, T> {
341    fn id(&self) -> BindingId {
342        self.id
343    }
344
345    fn direction(&self) -> BindingDirection {
346        BindingDirection::OneWay
347    }
348
349    fn is_active(&self) -> bool {
350        self.effect.is_active()
351    }
352
353    fn dispose(&self) {
354        self.effect.dispose();
355    }
356}
357
358// ---------------------------------------------------------------------------
359// BindingScope
360// ---------------------------------------------------------------------------
361
362/// A scope that owns bindings and disposes them on drop.
363///
364/// `BindingScope` provides a convenient way to manage the lifetime
365/// of multiple bindings. When the scope is dropped, all owned bindings
366/// are disposed.
367///
368/// # Examples
369///
370/// ```ignore
371/// let count = Signal::new(0);
372/// let display = Rc::new(RefCell::new(String::new()));
373///
374/// {
375///     let mut scope = BindingScope::new();
376///     scope.bind(&count, {
377///         let display = display.clone();
378///         move |v: &i32| *display.borrow_mut() = format!("{v}")
379///     });
380///     count.set(5);
381///     assert_eq!(*display.borrow(), "5");
382/// }
383/// // Scope dropped — binding disposed.
384/// count.set(99);
385/// // display still shows "5" because the binding is gone.
386/// ```
387pub struct BindingScope {
388    bindings: Vec<Box<dyn Binding>>,
389}
390
391impl BindingScope {
392    /// Create a new empty binding scope.
393    #[must_use]
394    pub fn new() -> Self {
395        Self {
396            bindings: Vec::new(),
397        }
398    }
399
400    /// Create a one-way binding from `source` to `sink`.
401    ///
402    /// Returns the binding ID.
403    pub fn bind<T: Clone + 'static>(
404        &mut self,
405        source: &Signal<T>,
406        sink: impl PropertySink<T> + 'static,
407    ) -> BindingId {
408        let binding = OneWayBinding::new(source, sink);
409        let id = binding.id();
410        self.bindings.push(Box::new(binding));
411        id
412    }
413
414    /// Create a two-way binding between `source` and `sink`.
415    ///
416    /// Returns the `TwoWayBinding` (for calling `write_back`) and the binding ID.
417    pub fn bind_two_way<T: Clone + 'static>(
418        &mut self,
419        source: &Signal<T>,
420        sink: impl PropertySink<T> + 'static,
421    ) -> (TwoWayBinding<T>, BindingId) {
422        let binding = TwoWayBinding::new(source, sink);
423        let id = binding.id();
424
425        // We need a second instance for the caller — clone the internals.
426        let caller_binding = TwoWayBinding {
427            id: binding.id,
428            effect: binding.effect.clone(),
429            source: binding.source.clone(),
430            updating: Rc::clone(&binding.updating),
431        };
432
433        self.bindings.push(Box::new(binding));
434        (caller_binding, id)
435    }
436
437    /// Create a binding expression (source → transform → sink).
438    ///
439    /// Returns the binding ID.
440    pub fn bind_expression<S: Clone + 'static, T: Clone + 'static>(
441        &mut self,
442        source: &Signal<S>,
443        transform: impl Fn(&S) -> T + 'static,
444        sink: impl PropertySink<T> + 'static,
445    ) -> BindingId {
446        let binding = BindingExpression::new(source, transform, sink);
447        let id = binding.id();
448        self.bindings.push(Box::new(binding));
449        id
450    }
451
452    /// Get the number of bindings in this scope.
453    pub fn binding_count(&self) -> usize {
454        self.bindings.len()
455    }
456
457    /// Check if a specific binding is still active.
458    pub fn is_binding_active(&self, id: BindingId) -> bool {
459        self.bindings
460            .iter()
461            .find(|b| b.id() == id)
462            .is_some_and(|b| b.is_active())
463    }
464}
465
466impl Default for BindingScope {
467    fn default() -> Self {
468        Self::new()
469    }
470}
471
472impl Drop for BindingScope {
473    fn drop(&mut self) {
474        for binding in &self.bindings {
475            binding.dispose();
476        }
477    }
478}
479
480// ---------------------------------------------------------------------------
481// Tests
482// ---------------------------------------------------------------------------
483
484#[cfg(test)]
485#[allow(clippy::unwrap_used)]
486mod tests {
487    use super::*;
488    use crate::reactive::batch::batch;
489    use std::cell::RefCell;
490
491    // -- OneWayBinding --
492
493    #[test]
494    fn one_way_pushes_initial_value() {
495        let sig = Signal::new(42);
496        let output = Rc::new(Cell::new(0));
497
498        let _binding = OneWayBinding::new(&sig, {
499            let out = Rc::clone(&output);
500            move |v: &i32| out.set(*v)
501        });
502
503        assert_eq!(output.get(), 42);
504    }
505
506    #[test]
507    fn one_way_pushes_on_change() {
508        let sig = Signal::new(0);
509        let output = Rc::new(Cell::new(0));
510
511        let _binding = OneWayBinding::new(&sig, {
512            let out = Rc::clone(&output);
513            move |v: &i32| out.set(*v)
514        });
515
516        sig.set(10);
517        assert_eq!(output.get(), 10);
518
519        sig.set(20);
520        assert_eq!(output.get(), 20);
521    }
522
523    #[test]
524    fn one_way_stops_after_dispose() {
525        let sig = Signal::new(0);
526        let output = Rc::new(Cell::new(0));
527
528        let binding = OneWayBinding::new(&sig, {
529            let out = Rc::clone(&output);
530            move |v: &i32| out.set(*v)
531        });
532
533        sig.set(5);
534        assert_eq!(output.get(), 5);
535
536        binding.dispose();
537        assert!(!binding.is_active());
538
539        sig.set(99);
540        assert_eq!(output.get(), 5); // Unchanged.
541    }
542
543    #[test]
544    fn one_way_direction() {
545        let sig = Signal::new(0);
546        let binding = OneWayBinding::new(&sig, |_: &i32| {});
547        assert_eq!(binding.direction(), BindingDirection::OneWay);
548    }
549
550    #[test]
551    fn one_way_unique_ids() {
552        let sig = Signal::new(0);
553        let a = OneWayBinding::new(&sig, |_: &i32| {});
554        let b = OneWayBinding::new(&sig, |_: &i32| {});
555        assert_ne!(a.id(), b.id());
556    }
557
558    #[test]
559    fn one_way_with_string_sink() {
560        let sig = Signal::new(String::from("hello"));
561        let output = Rc::new(RefCell::new(String::new()));
562
563        let _binding = OneWayBinding::new(&sig, {
564            let out = Rc::clone(&output);
565            move |v: &String| *out.borrow_mut() = v.clone()
566        });
567
568        assert_eq!(*output.borrow(), "hello");
569
570        sig.set("world".into());
571        assert_eq!(*output.borrow(), "world");
572    }
573
574    // -- TwoWayBinding --
575
576    #[test]
577    fn two_way_forward_push() {
578        let sig = Signal::new(0);
579        let output = Rc::new(Cell::new(0));
580
581        let _binding = TwoWayBinding::new(&sig, {
582            let out = Rc::clone(&output);
583            move |v: &i32| out.set(*v)
584        });
585
586        sig.set(42);
587        assert_eq!(output.get(), 42);
588    }
589
590    #[test]
591    fn two_way_write_back() {
592        let sig = Signal::new(0);
593        let output = Rc::new(Cell::new(0));
594
595        let binding = TwoWayBinding::new(&sig, {
596            let out = Rc::clone(&output);
597            move |v: &i32| out.set(*v)
598        });
599
600        binding.write_back(99);
601        assert_eq!(sig.get(), 99);
602    }
603
604    #[test]
605    fn two_way_loop_guard() {
606        let sig = Signal::new(0);
607        let push_count = Rc::new(Cell::new(0u32));
608
609        let binding = TwoWayBinding::new(&sig, {
610            let count = Rc::clone(&push_count);
611            move |_: &i32| count.set(count.get() + 1)
612        });
613
614        // Initial push.
615        assert_eq!(push_count.get(), 1);
616
617        // Write-back should NOT trigger the forward push
618        // (loop guard prevents it).
619        binding.write_back(42);
620        assert_eq!(push_count.get(), 1); // Still 1.
621        assert_eq!(sig.get(), 42);
622
623        // Regular forward push still works.
624        sig.set(100);
625        assert_eq!(push_count.get(), 2);
626    }
627
628    #[test]
629    fn two_way_disposed_write_back_ignored() {
630        let sig = Signal::new(0);
631        let binding = TwoWayBinding::new(&sig, |_: &i32| {});
632
633        binding.dispose();
634        binding.write_back(42);
635
636        // Signal unchanged because binding is disposed.
637        assert_eq!(sig.get(), 0);
638    }
639
640    #[test]
641    fn two_way_direction() {
642        let sig = Signal::new(0);
643        let binding = TwoWayBinding::new(&sig, |_: &i32| {});
644        assert_eq!(binding.direction(), BindingDirection::TwoWay);
645    }
646
647    // -- BindingExpression --
648
649    #[test]
650    fn expression_transforms_value() {
651        let sig = Signal::new(3);
652        let output = Rc::new(RefCell::new(String::new()));
653
654        let _binding = BindingExpression::new(&sig, |v: &i32| format!("Count: {v}"), {
655            let out = Rc::clone(&output);
656            move |v: &String| *out.borrow_mut() = v.clone()
657        });
658
659        assert_eq!(*output.borrow(), "Count: 3");
660
661        sig.set(7);
662        assert_eq!(*output.borrow(), "Count: 7");
663    }
664
665    #[test]
666    fn expression_stops_after_dispose() {
667        let sig = Signal::new(0);
668        let output = Rc::new(Cell::new(0));
669
670        let binding = BindingExpression::new(&sig, |v: &i32| v * 10, {
671            let out = Rc::clone(&output);
672            move |v: &i32| out.set(*v)
673        });
674
675        sig.set(5);
676        assert_eq!(output.get(), 50);
677
678        binding.dispose();
679        sig.set(99);
680        assert_eq!(output.get(), 50); // Unchanged.
681    }
682
683    #[test]
684    fn expression_direction() {
685        let sig = Signal::new(0);
686        let binding = BindingExpression::new(&sig, |v: &i32| *v, |_: &i32| {});
687        assert_eq!(binding.direction(), BindingDirection::OneWay);
688    }
689
690    #[test]
691    fn expression_type_conversion() {
692        let sig = Signal::new(42i32);
693        let output = Rc::new(Cell::new(0.0f64));
694
695        let _binding = BindingExpression::new(&sig, |v: &i32| *v as f64 * 1.5, {
696            let out = Rc::clone(&output);
697            move |v: &f64| out.set(*v)
698        });
699
700        assert!((output.get() - 63.0).abs() < f64::EPSILON);
701
702        sig.set(10);
703        assert!((output.get() - 15.0).abs() < f64::EPSILON);
704    }
705
706    // -- BindingScope --
707
708    #[test]
709    fn scope_bind_creates_one_way() {
710        let sig = Signal::new(0);
711        let output = Rc::new(Cell::new(0));
712
713        let mut scope = BindingScope::new();
714        let id = scope.bind(&sig, {
715            let out = Rc::clone(&output);
716            move |v: &i32| out.set(*v)
717        });
718
719        assert_eq!(scope.binding_count(), 1);
720        assert!(scope.is_binding_active(id));
721
722        sig.set(10);
723        assert_eq!(output.get(), 10);
724    }
725
726    #[test]
727    fn scope_bind_two_way() {
728        let sig = Signal::new(0);
729        let output = Rc::new(Cell::new(0));
730
731        let mut scope = BindingScope::new();
732        let (two_way, id) = scope.bind_two_way(&sig, {
733            let out = Rc::clone(&output);
734            move |v: &i32| out.set(*v)
735        });
736
737        assert_eq!(scope.binding_count(), 1);
738        assert!(scope.is_binding_active(id));
739
740        // Forward.
741        sig.set(10);
742        assert_eq!(output.get(), 10);
743
744        // Reverse.
745        two_way.write_back(50);
746        assert_eq!(sig.get(), 50);
747    }
748
749    #[test]
750    fn scope_bind_expression() {
751        let sig = Signal::new(5);
752        let output = Rc::new(RefCell::new(String::new()));
753
754        let mut scope = BindingScope::new();
755        let id = scope.bind_expression(&sig, |v: &i32| format!("val={v}"), {
756            let out = Rc::clone(&output);
757            move |v: &String| *out.borrow_mut() = v.clone()
758        });
759
760        assert!(scope.is_binding_active(id));
761        assert_eq!(*output.borrow(), "val=5");
762
763        sig.set(10);
764        assert_eq!(*output.borrow(), "val=10");
765    }
766
767    #[test]
768    fn scope_disposes_bindings_on_drop() {
769        let sig = Signal::new(0);
770        let output = Rc::new(Cell::new(0));
771
772        {
773            let mut scope = BindingScope::new();
774            scope.bind(&sig, {
775                let out = Rc::clone(&output);
776                move |v: &i32| out.set(*v)
777            });
778
779            sig.set(5);
780            assert_eq!(output.get(), 5);
781        }
782        // Scope dropped.
783
784        sig.set(99);
785        assert_eq!(output.get(), 5); // Binding disposed, no update.
786    }
787
788    #[test]
789    fn scope_multiple_bindings() {
790        let a = Signal::new(0);
791        let b = Signal::new(0);
792        let out_a = Rc::new(Cell::new(0));
793        let out_b = Rc::new(Cell::new(0));
794
795        let mut scope = BindingScope::new();
796        scope.bind(&a, {
797            let out = Rc::clone(&out_a);
798            move |v: &i32| out.set(*v)
799        });
800        scope.bind(&b, {
801            let out = Rc::clone(&out_b);
802            move |v: &i32| out.set(*v)
803        });
804
805        assert_eq!(scope.binding_count(), 2);
806
807        a.set(10);
808        b.set(20);
809        assert_eq!(out_a.get(), 10);
810        assert_eq!(out_b.get(), 20);
811    }
812
813    #[test]
814    fn scope_is_binding_active_returns_false_for_unknown_id() {
815        let scope = BindingScope::new();
816        assert!(!scope.is_binding_active(99999));
817    }
818
819    // -- Integration tests --
820
821    #[test]
822    fn binding_with_batch() {
823        let sig = Signal::new(0);
824        let push_count = Rc::new(Cell::new(0u32));
825
826        let _binding = OneWayBinding::new(&sig, {
827            let count = Rc::clone(&push_count);
828            move |_: &i32| count.set(count.get() + 1)
829        });
830
831        assert_eq!(push_count.get(), 1); // Initial.
832
833        batch(|| {
834            sig.set(1);
835            sig.set(2);
836            sig.set(3);
837        });
838
839        // Should push only once after batch.
840        assert_eq!(push_count.get(), 2);
841    }
842
843    #[test]
844    fn binding_expression_with_batch() {
845        let sig = Signal::new(0);
846        let transform_count = Rc::new(Cell::new(0u32));
847        let output = Rc::new(Cell::new(0));
848
849        let _binding = BindingExpression::new(
850            &sig,
851            {
852                let count = Rc::clone(&transform_count);
853                move |v: &i32| {
854                    count.set(count.get() + 1);
855                    v * 2
856                }
857            },
858            {
859                let out = Rc::clone(&output);
860                move |v: &i32| out.set(*v)
861            },
862        );
863
864        // Initial transform + push.
865        assert_eq!(transform_count.get(), 1);
866        assert_eq!(output.get(), 0);
867
868        batch(|| {
869            sig.set(5);
870            sig.set(10);
871        });
872
873        // Transform called once more after batch.
874        assert_eq!(output.get(), 20);
875    }
876
877    #[test]
878    fn two_way_binding_round_trip() {
879        let model = Signal::new(String::from("initial"));
880        let view = Rc::new(RefCell::new(String::new()));
881
882        let binding = TwoWayBinding::new(&model, {
883            let view = Rc::clone(&view);
884            move |v: &String| *view.borrow_mut() = v.clone()
885        });
886
887        // Forward: model → view.
888        assert_eq!(*view.borrow(), "initial");
889
890        model.set("forward".into());
891        assert_eq!(*view.borrow(), "forward");
892
893        // Reverse: view → model.
894        binding.write_back("reverse".into());
895        assert_eq!(model.get(), "reverse");
896
897        // Forward again after reverse.
898        model.set("final".into());
899        assert_eq!(*view.borrow(), "final");
900    }
901
902    #[test]
903    fn binding_scope_with_mixed_types() {
904        let count = Signal::new(0);
905        let name = Signal::new(String::from("test"));
906
907        let out_count = Rc::new(Cell::new(0));
908        let out_name = Rc::new(RefCell::new(String::new()));
909
910        let mut scope = BindingScope::new();
911
912        scope.bind(&count, {
913            let out = Rc::clone(&out_count);
914            move |v: &i32| out.set(*v)
915        });
916
917        scope.bind(&name, {
918            let out = Rc::clone(&out_name);
919            move |v: &String| *out.borrow_mut() = v.clone()
920        });
921
922        count.set(42);
923        name.set("hello".into());
924
925        assert_eq!(out_count.get(), 42);
926        assert_eq!(*out_name.borrow(), "hello");
927    }
928
929    #[test]
930    fn chained_one_way_bindings() {
931        let source = Signal::new(1);
932        let middle = Signal::new(0);
933        let output = Rc::new(Cell::new(0));
934
935        // source → middle
936        let _binding1 = OneWayBinding::new(&source, {
937            let mid = middle.clone();
938            move |v: &i32| mid.set(*v * 2)
939        });
940
941        // middle → output
942        let _binding2 = OneWayBinding::new(&middle, {
943            let out = Rc::clone(&output);
944            move |v: &i32| out.set(*v)
945        });
946
947        // Initial: source=1, middle=2, output=2.
948        assert_eq!(middle.get(), 2);
949        assert_eq!(output.get(), 2);
950
951        source.set(5);
952        assert_eq!(middle.get(), 10);
953        assert_eq!(output.get(), 10);
954    }
955
956    #[test]
957    fn scope_default_is_empty() {
958        let scope = BindingScope::default();
959        assert_eq!(scope.binding_count(), 0);
960    }
961
962    #[test]
963    fn disposed_binding_not_active_in_scope() {
964        let sig = Signal::new(0);
965        let mut scope = BindingScope::new();
966
967        let id = scope.bind(&sig, |_: &i32| {});
968        assert!(scope.is_binding_active(id));
969
970        // Drop the scope to dispose.
971        drop(scope);
972
973        // After drop we can't check scope, but the binding's effect is gone.
974        // This test verifies that dispose is called on drop.
975    }
976
977    #[test]
978    fn stress_many_bindings() {
979        let sig = Signal::new(0);
980        let count = Rc::new(Cell::new(0u32));
981        let mut scope = BindingScope::new();
982
983        for _ in 0..50 {
984            scope.bind(&sig, {
985                let count = Rc::clone(&count);
986                move |_: &i32| count.set(count.get() + 1)
987            });
988        }
989
990        // 50 initial pushes.
991        assert_eq!(count.get(), 50);
992
993        sig.set(1);
994        // 50 more pushes.
995        assert_eq!(count.get(), 100);
996
997        drop(scope);
998        sig.set(2);
999        // No more pushes.
1000        assert_eq!(count.get(), 100);
1001    }
1002}