1#![forbid(unsafe_code)]
2
3use std::cell::Cell;
63use std::rc::Rc;
64
65use super::observable::{Observable, Subscription};
66
67pub struct Binding<T> {
76 eval: Rc<dyn Fn() -> T>,
77}
78
79impl<T> Clone for Binding<T> {
80 fn clone(&self) -> Self {
81 Self {
82 eval: Rc::clone(&self.eval),
83 }
84 }
85}
86
87impl<T: std::fmt::Debug + 'static> std::fmt::Debug for Binding<T> {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_struct("Binding")
90 .field("value", &self.get())
91 .finish()
92 }
93}
94
95impl<T: 'static> Binding<T> {
96 pub fn new(f: impl Fn() -> T + 'static) -> Self {
98 Self { eval: Rc::new(f) }
99 }
100
101 #[must_use]
103 pub fn get(&self) -> T {
104 (self.eval)()
105 }
106
107 pub fn then<U: 'static>(self, f: impl Fn(T) -> U + 'static) -> Binding<U> {
109 Binding {
110 eval: Rc::new(move || f((self.eval)())),
111 }
112 }
113}
114
115pub fn bind_observable<T: Clone + PartialEq + 'static>(source: &Observable<T>) -> Binding<T> {
117 let src = source.clone();
118 Binding {
119 eval: Rc::new(move || src.get()),
120 }
121}
122
123pub fn bind_mapped<S: Clone + PartialEq + 'static, T: 'static>(
125 source: &Observable<S>,
126 map: impl Fn(&S) -> T + 'static,
127) -> Binding<T> {
128 let src = source.clone();
129 Binding {
130 eval: Rc::new(move || src.with(|v| map(v))),
131 }
132}
133
134pub fn bind_mapped2<
136 S1: Clone + PartialEq + 'static,
137 S2: Clone + PartialEq + 'static,
138 T: 'static,
139>(
140 s1: &Observable<S1>,
141 s2: &Observable<S2>,
142 map: impl Fn(&S1, &S2) -> T + 'static,
143) -> Binding<T> {
144 let src1 = s1.clone();
145 let src2 = s2.clone();
146 Binding {
147 eval: Rc::new(move || src1.with(|v1| src2.with(|v2| map(v1, v2)))),
148 }
149}
150
151pub struct TwoWayBinding<T: Clone + PartialEq + 'static> {
162 _sub_a_to_b: Subscription,
163 _sub_b_to_a: Subscription,
164 _guard: Rc<Cell<bool>>,
165 _phantom: std::marker::PhantomData<T>,
166}
167
168impl<T: Clone + PartialEq + 'static> TwoWayBinding<T> {
169 pub fn new(a: &Observable<T>, b: &Observable<T>) -> Self {
174 b.set(a.get());
176
177 let syncing = Rc::new(Cell::new(false));
178
179 struct ReentrancyGuard<'a>(&'a Cell<bool>);
180 impl<'a> Drop for ReentrancyGuard<'a> {
181 fn drop(&mut self) {
182 self.0.set(false);
183 }
184 }
185
186 let b_clone = b.clone();
188 let guard_ab = Rc::clone(&syncing);
189 let sub_ab = a.subscribe(move |val| {
190 if !guard_ab.get() {
191 guard_ab.set(true);
192 let _guard = ReentrancyGuard(&guard_ab);
193 b_clone.set(val.clone());
194 }
195 });
196
197 let a_clone = a.clone();
199 let guard_ba = Rc::clone(&syncing);
200 let sub_ba = b.subscribe(move |val| {
201 if !guard_ba.get() {
202 guard_ba.set(true);
203 let _guard = ReentrancyGuard(&guard_ba);
204 a_clone.set(val.clone());
205 }
206 });
207
208 Self {
209 _sub_a_to_b: sub_ab,
210 _sub_b_to_a: sub_ba,
211 _guard: syncing,
212 _phantom: std::marker::PhantomData,
213 }
214 }
215}
216
217impl<T: Clone + PartialEq + 'static> std::fmt::Debug for TwoWayBinding<T> {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 f.debug_struct("TwoWayBinding").finish()
220 }
221}
222
223#[macro_export]
237macro_rules! bind {
238 ($obs:expr) => {
239 $crate::reactive::binding::bind_observable(&$obs)
240 };
241}
242
243#[macro_export]
253macro_rules! bind_map {
254 ($obs:expr, $f:expr) => {
255 $crate::reactive::binding::bind_mapped(&$obs, $f)
256 };
257}
258
259#[macro_export]
270macro_rules! bind_map2 {
271 ($s1:expr, $s2:expr, $f:expr) => {
272 $crate::reactive::binding::bind_mapped2(&$s1, &$s2, $f)
273 };
274}
275
276pub struct BindingScope {
304 subscriptions: Vec<Subscription>,
305}
306
307impl BindingScope {
308 #[must_use]
310 pub fn new() -> Self {
311 Self {
312 subscriptions: Vec::new(),
313 }
314 }
315
316 pub fn hold(&mut self, sub: Subscription) {
319 self.subscriptions.push(sub);
320 }
321
322 pub fn subscribe<T: Clone + PartialEq + 'static>(
326 &mut self,
327 source: &Observable<T>,
328 callback: impl Fn(&T) + 'static,
329 ) -> &mut Self {
330 let sub = source.subscribe(callback);
331 self.subscriptions.push(sub);
332 self
333 }
334
335 pub fn bind<T: Clone + PartialEq + 'static>(&mut self, source: &Observable<T>) -> Binding<T> {
340 bind_observable(source)
341 }
342
343 pub fn bind_map<S: Clone + PartialEq + 'static, T: 'static>(
345 &mut self,
346 source: &Observable<S>,
347 map: impl Fn(&S) -> T + 'static,
348 ) -> Binding<T> {
349 bind_mapped(source, map)
350 }
351
352 #[must_use]
354 pub fn binding_count(&self) -> usize {
355 self.subscriptions.len()
356 }
357
358 #[must_use]
360 pub fn is_empty(&self) -> bool {
361 self.subscriptions.is_empty()
362 }
363
364 pub fn clear(&mut self) {
366 self.subscriptions.clear();
367 }
368}
369
370impl Default for BindingScope {
371 fn default() -> Self {
372 Self::new()
373 }
374}
375
376impl std::fmt::Debug for BindingScope {
377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378 f.debug_struct("BindingScope")
379 .field("binding_count", &self.subscriptions.len())
380 .finish()
381 }
382}
383
384#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn binding_from_observable() {
394 let obs = Observable::new(42);
395 let b = bind_observable(&obs);
396 assert_eq!(b.get(), 42);
397
398 obs.set(100);
399 assert_eq!(b.get(), 100);
400 }
401
402 #[test]
403 fn binding_map() {
404 let count = Observable::new(3);
405 let label = bind_mapped(&count, |c| format!("items: {c}"));
406 assert_eq!(label.get(), "items: 3");
407
408 count.set(7);
409 assert_eq!(label.get(), "items: 7");
410 }
411
412 #[test]
413 fn binding_map2() {
414 let w = Observable::new(10);
415 let h = Observable::new(20);
416 let area = bind_mapped2(&w, &h, |a, b| a * b);
417 assert_eq!(area.get(), 200);
418
419 w.set(5);
420 assert_eq!(area.get(), 100);
421 }
422
423 #[test]
424 fn binding_then_chain() {
425 let obs = Observable::new(5);
426 let doubled = bind_observable(&obs).then(|v| v * 2);
427 assert_eq!(doubled.get(), 10);
428
429 obs.set(3);
430 assert_eq!(doubled.get(), 6);
431 }
432
433 #[test]
434 fn binding_clone_shares_source() {
435 let obs = Observable::new(1);
436 let b1 = bind_observable(&obs);
437 let b2 = b1.clone();
438
439 obs.set(99);
440 assert_eq!(b1.get(), 99);
441 assert_eq!(b2.get(), 99);
442 }
443
444 #[test]
445 fn binding_new_custom() {
446 let counter = Rc::new(Cell::new(0));
447 let c = Rc::clone(&counter);
448 let b = Binding::new(move || {
449 c.set(c.get() + 1);
450 c.get()
451 });
452 assert_eq!(b.get(), 1);
453 assert_eq!(b.get(), 2);
454 }
455
456 #[test]
457 fn bind_macro() {
458 let obs = Observable::new(42);
459 let b = bind!(obs);
460 assert_eq!(b.get(), 42);
461 }
462
463 #[test]
464 fn bind_map_macro() {
465 let obs = Observable::new(5);
466 let b = bind_map!(obs, |v| v * 10);
467 assert_eq!(b.get(), 50);
468 }
469
470 #[test]
471 fn bind_map2_macro() {
472 let a = Observable::new(3);
473 let b = Observable::new(4);
474 let sum = bind_map2!(a, b, |x, y| x + y);
475 assert_eq!(sum.get(), 7);
476 }
477
478 #[test]
481 fn two_way_initial_sync() {
482 let a = Observable::new(10);
483 let b = Observable::new(0);
484 let _binding = TwoWayBinding::new(&a, &b);
485 assert_eq!(b.get(), 10, "b should sync to a's initial value");
486 }
487
488 #[test]
489 fn two_way_a_to_b() {
490 let a = Observable::new(1);
491 let b = Observable::new(0);
492 let _binding = TwoWayBinding::new(&a, &b);
493
494 a.set(42);
495 assert_eq!(b.get(), 42);
496 }
497
498 #[test]
499 fn two_way_b_to_a() {
500 let a = Observable::new(1);
501 let b = Observable::new(0);
502 let _binding = TwoWayBinding::new(&a, &b);
503
504 b.set(99);
505 assert_eq!(a.get(), 99);
506 }
507
508 #[test]
509 fn two_way_no_cycle() {
510 let a = Observable::new(0);
511 let b = Observable::new(0);
512 let _binding = TwoWayBinding::new(&a, &b);
513
514 a.set(5);
516 assert_eq!(a.get(), 5);
517 assert_eq!(b.get(), 5);
518
519 b.set(10);
520 assert_eq!(a.get(), 10);
521 assert_eq!(b.get(), 10);
522 }
523
524 #[test]
525 fn two_way_drop_disconnects() {
526 let a = Observable::new(1);
527 let b = Observable::new(0);
528 {
529 let _binding = TwoWayBinding::new(&a, &b);
530 a.set(5);
531 assert_eq!(b.get(), 5);
532 }
533 a.set(100);
535 assert_eq!(b.get(), 5, "b should not update after binding dropped");
536 }
537
538 #[test]
539 fn two_way_with_strings() {
540 let a = Observable::new(String::from("hello"));
541 let b = Observable::new(String::new());
542 let _binding = TwoWayBinding::new(&a, &b);
543
544 assert_eq!(b.get(), "hello");
545 b.set("world".to_string());
546 assert_eq!(a.get(), "world");
547 }
548
549 #[test]
550 fn multiple_bindings_same_source() {
551 let source = Observable::new(0);
552 let b1 = bind_observable(&source);
553 let b2 = bind_mapped(&source, |v| v * 2);
554 let b3 = bind_mapped(&source, |v| format!("{v}"));
555
556 source.set(5);
557 assert_eq!(b1.get(), 5);
558 assert_eq!(b2.get(), 10);
559 assert_eq!(b3.get(), "5");
560 }
561
562 #[test]
563 fn binding_survives_source_clone() {
564 let source = Observable::new(42);
565 let b = bind_observable(&source);
566
567 let source2 = source.clone();
568 source2.set(99);
569 assert_eq!(
570 b.get(),
571 99,
572 "binding should see changes through cloned observable"
573 );
574 }
575
576 #[test]
579 fn scope_holds_subscriptions() {
580 let obs = Observable::new(0);
581 let seen = Rc::new(Cell::new(0));
582
583 let mut scope = BindingScope::new();
584 let s = Rc::clone(&seen);
585 scope.subscribe(&obs, move |v| s.set(*v));
586 assert_eq!(scope.binding_count(), 1);
587
588 obs.set(42);
589 assert_eq!(seen.get(), 42);
590 }
591
592 #[test]
593 fn scope_drop_releases_subscriptions() {
594 let obs = Observable::new(0);
595 let seen = Rc::new(Cell::new(0));
596
597 {
598 let mut scope = BindingScope::new();
599 let s = Rc::clone(&seen);
600 scope.subscribe(&obs, move |v| s.set(*v));
601 obs.set(1);
602 assert_eq!(seen.get(), 1);
603 }
604
605 obs.set(99);
607 assert_eq!(
608 seen.get(),
609 1,
610 "callback should not fire after scope dropped"
611 );
612 }
613
614 #[test]
615 fn scope_clear_releases() {
616 let obs = Observable::new(0);
617 let seen = Rc::new(Cell::new(0));
618
619 let mut scope = BindingScope::new();
620 let s = Rc::clone(&seen);
621 scope.subscribe(&obs, move |v| s.set(*v));
622 assert_eq!(scope.binding_count(), 1);
623
624 scope.clear();
625 assert_eq!(scope.binding_count(), 0);
626 assert!(scope.is_empty());
627
628 obs.set(42);
629 assert_eq!(seen.get(), 0, "callback should not fire after clear");
630 }
631
632 #[test]
633 fn scope_multiple_subscriptions() {
634 let obs = Observable::new(0);
635 let count = Rc::new(Cell::new(0));
636
637 let mut scope = BindingScope::new();
638 for _ in 0..5 {
639 let c = Rc::clone(&count);
640 scope.subscribe(&obs, move |_| c.set(c.get() + 1));
641 }
642 assert_eq!(scope.binding_count(), 5);
643
644 obs.set(1);
645 assert_eq!(count.get(), 5, "all 5 callbacks should fire");
646 }
647
648 #[test]
649 fn scope_bind_returns_binding() {
650 let obs = Observable::new(42);
651 let mut scope = BindingScope::new();
652 let b = scope.bind(&obs);
653 assert_eq!(b.get(), 42);
654
655 obs.set(7);
656 assert_eq!(b.get(), 7);
657 }
658
659 #[test]
660 fn scope_bind_map() {
661 let obs = Observable::new(3);
662 let mut scope = BindingScope::new();
663 let b = scope.bind_map(&obs, |v| v * 10);
664 assert_eq!(b.get(), 30);
665 }
666
667 #[test]
668 fn scope_reusable_after_clear() {
669 let obs = Observable::new(0);
670 let mut scope = BindingScope::new();
671
672 let seen1 = Rc::new(Cell::new(false));
673 let s1 = Rc::clone(&seen1);
674 scope.subscribe(&obs, move |_| s1.set(true));
675 scope.clear();
676
677 let seen2 = Rc::new(Cell::new(false));
678 let s2 = Rc::clone(&seen2);
679 scope.subscribe(&obs, move |_| s2.set(true));
680
681 obs.set(1);
682 assert!(!seen1.get(), "first subscription should be gone");
683 assert!(seen2.get(), "second subscription should be active");
684 }
685
686 #[test]
687 fn scope_hold_external_subscription() {
688 let obs = Observable::new(0);
689 let seen = Rc::new(Cell::new(0));
690
691 let mut scope = BindingScope::new();
692 let s = Rc::clone(&seen);
693 let sub = obs.subscribe(move |v| s.set(*v));
694 scope.hold(sub);
695
696 obs.set(5);
697 assert_eq!(seen.get(), 5);
698
699 drop(scope);
700 obs.set(99);
701 assert_eq!(
702 seen.get(),
703 5,
704 "held subscription should be released on scope drop"
705 );
706 }
707
708 #[test]
709 fn scope_debug_format() {
710 let mut scope = BindingScope::new();
711 let obs = Observable::new(0);
712 scope.subscribe(&obs, |_| {});
713 scope.subscribe(&obs, |_| {});
714 let debug = format!("{scope:?}");
715 assert!(debug.contains("binding_count: 2"));
716 }
717}