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 let b_clone = b.clone();
181 let guard_ab = Rc::clone(&syncing);
182 let sub_ab = a.subscribe(move |val| {
183 if !guard_ab.get() {
184 guard_ab.set(true);
185 b_clone.set(val.clone());
186 guard_ab.set(false);
187 }
188 });
189
190 let a_clone = a.clone();
192 let guard_ba = Rc::clone(&syncing);
193 let sub_ba = b.subscribe(move |val| {
194 if !guard_ba.get() {
195 guard_ba.set(true);
196 a_clone.set(val.clone());
197 guard_ba.set(false);
198 }
199 });
200
201 Self {
202 _sub_a_to_b: sub_ab,
203 _sub_b_to_a: sub_ba,
204 _guard: syncing,
205 _phantom: std::marker::PhantomData,
206 }
207 }
208}
209
210impl<T: Clone + PartialEq + 'static> std::fmt::Debug for TwoWayBinding<T> {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 f.debug_struct("TwoWayBinding").finish()
213 }
214}
215
216#[macro_export]
230macro_rules! bind {
231 ($obs:expr) => {
232 $crate::reactive::binding::bind_observable(&$obs)
233 };
234}
235
236#[macro_export]
246macro_rules! bind_map {
247 ($obs:expr, $f:expr) => {
248 $crate::reactive::binding::bind_mapped(&$obs, $f)
249 };
250}
251
252#[macro_export]
263macro_rules! bind_map2 {
264 ($s1:expr, $s2:expr, $f:expr) => {
265 $crate::reactive::binding::bind_mapped2(&$s1, &$s2, $f)
266 };
267}
268
269pub struct BindingScope {
297 subscriptions: Vec<Subscription>,
298}
299
300impl BindingScope {
301 #[must_use]
303 pub fn new() -> Self {
304 Self {
305 subscriptions: Vec::new(),
306 }
307 }
308
309 pub fn hold(&mut self, sub: Subscription) {
312 self.subscriptions.push(sub);
313 }
314
315 pub fn subscribe<T: Clone + PartialEq + 'static>(
319 &mut self,
320 source: &Observable<T>,
321 callback: impl Fn(&T) + 'static,
322 ) -> &mut Self {
323 let sub = source.subscribe(callback);
324 self.subscriptions.push(sub);
325 self
326 }
327
328 pub fn bind<T: Clone + PartialEq + 'static>(&mut self, source: &Observable<T>) -> Binding<T> {
333 bind_observable(source)
334 }
335
336 pub fn bind_map<S: Clone + PartialEq + 'static, T: 'static>(
338 &mut self,
339 source: &Observable<S>,
340 map: impl Fn(&S) -> T + 'static,
341 ) -> Binding<T> {
342 bind_mapped(source, map)
343 }
344
345 #[must_use]
347 pub fn binding_count(&self) -> usize {
348 self.subscriptions.len()
349 }
350
351 #[must_use]
353 pub fn is_empty(&self) -> bool {
354 self.subscriptions.is_empty()
355 }
356
357 pub fn clear(&mut self) {
359 self.subscriptions.clear();
360 }
361}
362
363impl Default for BindingScope {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369impl std::fmt::Debug for BindingScope {
370 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
371 f.debug_struct("BindingScope")
372 .field("binding_count", &self.subscriptions.len())
373 .finish()
374 }
375}
376
377#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn binding_from_observable() {
387 let obs = Observable::new(42);
388 let b = bind_observable(&obs);
389 assert_eq!(b.get(), 42);
390
391 obs.set(100);
392 assert_eq!(b.get(), 100);
393 }
394
395 #[test]
396 fn binding_map() {
397 let count = Observable::new(3);
398 let label = bind_mapped(&count, |c| format!("items: {c}"));
399 assert_eq!(label.get(), "items: 3");
400
401 count.set(7);
402 assert_eq!(label.get(), "items: 7");
403 }
404
405 #[test]
406 fn binding_map2() {
407 let w = Observable::new(10);
408 let h = Observable::new(20);
409 let area = bind_mapped2(&w, &h, |a, b| a * b);
410 assert_eq!(area.get(), 200);
411
412 w.set(5);
413 assert_eq!(area.get(), 100);
414 }
415
416 #[test]
417 fn binding_then_chain() {
418 let obs = Observable::new(5);
419 let doubled = bind_observable(&obs).then(|v| v * 2);
420 assert_eq!(doubled.get(), 10);
421
422 obs.set(3);
423 assert_eq!(doubled.get(), 6);
424 }
425
426 #[test]
427 fn binding_clone_shares_source() {
428 let obs = Observable::new(1);
429 let b1 = bind_observable(&obs);
430 let b2 = b1.clone();
431
432 obs.set(99);
433 assert_eq!(b1.get(), 99);
434 assert_eq!(b2.get(), 99);
435 }
436
437 #[test]
438 fn binding_new_custom() {
439 let counter = Rc::new(Cell::new(0));
440 let c = Rc::clone(&counter);
441 let b = Binding::new(move || {
442 c.set(c.get() + 1);
443 c.get()
444 });
445 assert_eq!(b.get(), 1);
446 assert_eq!(b.get(), 2);
447 }
448
449 #[test]
450 fn bind_macro() {
451 let obs = Observable::new(42);
452 let b = bind!(obs);
453 assert_eq!(b.get(), 42);
454 }
455
456 #[test]
457 fn bind_map_macro() {
458 let obs = Observable::new(5);
459 let b = bind_map!(obs, |v| v * 10);
460 assert_eq!(b.get(), 50);
461 }
462
463 #[test]
464 fn bind_map2_macro() {
465 let a = Observable::new(3);
466 let b = Observable::new(4);
467 let sum = bind_map2!(a, b, |x, y| x + y);
468 assert_eq!(sum.get(), 7);
469 }
470
471 #[test]
474 fn two_way_initial_sync() {
475 let a = Observable::new(10);
476 let b = Observable::new(0);
477 let _binding = TwoWayBinding::new(&a, &b);
478 assert_eq!(b.get(), 10, "b should sync to a's initial value");
479 }
480
481 #[test]
482 fn two_way_a_to_b() {
483 let a = Observable::new(1);
484 let b = Observable::new(0);
485 let _binding = TwoWayBinding::new(&a, &b);
486
487 a.set(42);
488 assert_eq!(b.get(), 42);
489 }
490
491 #[test]
492 fn two_way_b_to_a() {
493 let a = Observable::new(1);
494 let b = Observable::new(0);
495 let _binding = TwoWayBinding::new(&a, &b);
496
497 b.set(99);
498 assert_eq!(a.get(), 99);
499 }
500
501 #[test]
502 fn two_way_no_cycle() {
503 let a = Observable::new(0);
504 let b = Observable::new(0);
505 let _binding = TwoWayBinding::new(&a, &b);
506
507 a.set(5);
509 assert_eq!(a.get(), 5);
510 assert_eq!(b.get(), 5);
511
512 b.set(10);
513 assert_eq!(a.get(), 10);
514 assert_eq!(b.get(), 10);
515 }
516
517 #[test]
518 fn two_way_drop_disconnects() {
519 let a = Observable::new(1);
520 let b = Observable::new(0);
521 {
522 let _binding = TwoWayBinding::new(&a, &b);
523 a.set(5);
524 assert_eq!(b.get(), 5);
525 }
526 a.set(100);
528 assert_eq!(b.get(), 5, "b should not update after binding dropped");
529 }
530
531 #[test]
532 fn two_way_with_strings() {
533 let a = Observable::new(String::from("hello"));
534 let b = Observable::new(String::new());
535 let _binding = TwoWayBinding::new(&a, &b);
536
537 assert_eq!(b.get(), "hello");
538 b.set("world".to_string());
539 assert_eq!(a.get(), "world");
540 }
541
542 #[test]
543 fn multiple_bindings_same_source() {
544 let source = Observable::new(0);
545 let b1 = bind_observable(&source);
546 let b2 = bind_mapped(&source, |v| v * 2);
547 let b3 = bind_mapped(&source, |v| format!("{v}"));
548
549 source.set(5);
550 assert_eq!(b1.get(), 5);
551 assert_eq!(b2.get(), 10);
552 assert_eq!(b3.get(), "5");
553 }
554
555 #[test]
556 fn binding_survives_source_clone() {
557 let source = Observable::new(42);
558 let b = bind_observable(&source);
559
560 let source2 = source.clone();
561 source2.set(99);
562 assert_eq!(
563 b.get(),
564 99,
565 "binding should see changes through cloned observable"
566 );
567 }
568
569 #[test]
572 fn scope_holds_subscriptions() {
573 let obs = Observable::new(0);
574 let seen = Rc::new(Cell::new(0));
575
576 let mut scope = BindingScope::new();
577 let s = Rc::clone(&seen);
578 scope.subscribe(&obs, move |v| s.set(*v));
579 assert_eq!(scope.binding_count(), 1);
580
581 obs.set(42);
582 assert_eq!(seen.get(), 42);
583 }
584
585 #[test]
586 fn scope_drop_releases_subscriptions() {
587 let obs = Observable::new(0);
588 let seen = Rc::new(Cell::new(0));
589
590 {
591 let mut scope = BindingScope::new();
592 let s = Rc::clone(&seen);
593 scope.subscribe(&obs, move |v| s.set(*v));
594 obs.set(1);
595 assert_eq!(seen.get(), 1);
596 }
597
598 obs.set(99);
600 assert_eq!(
601 seen.get(),
602 1,
603 "callback should not fire after scope dropped"
604 );
605 }
606
607 #[test]
608 fn scope_clear_releases() {
609 let obs = Observable::new(0);
610 let seen = Rc::new(Cell::new(0));
611
612 let mut scope = BindingScope::new();
613 let s = Rc::clone(&seen);
614 scope.subscribe(&obs, move |v| s.set(*v));
615 assert_eq!(scope.binding_count(), 1);
616
617 scope.clear();
618 assert_eq!(scope.binding_count(), 0);
619 assert!(scope.is_empty());
620
621 obs.set(42);
622 assert_eq!(seen.get(), 0, "callback should not fire after clear");
623 }
624
625 #[test]
626 fn scope_multiple_subscriptions() {
627 let obs = Observable::new(0);
628 let count = Rc::new(Cell::new(0));
629
630 let mut scope = BindingScope::new();
631 for _ in 0..5 {
632 let c = Rc::clone(&count);
633 scope.subscribe(&obs, move |_| c.set(c.get() + 1));
634 }
635 assert_eq!(scope.binding_count(), 5);
636
637 obs.set(1);
638 assert_eq!(count.get(), 5, "all 5 callbacks should fire");
639 }
640
641 #[test]
642 fn scope_bind_returns_binding() {
643 let obs = Observable::new(42);
644 let mut scope = BindingScope::new();
645 let b = scope.bind(&obs);
646 assert_eq!(b.get(), 42);
647
648 obs.set(7);
649 assert_eq!(b.get(), 7);
650 }
651
652 #[test]
653 fn scope_bind_map() {
654 let obs = Observable::new(3);
655 let mut scope = BindingScope::new();
656 let b = scope.bind_map(&obs, |v| v * 10);
657 assert_eq!(b.get(), 30);
658 }
659
660 #[test]
661 fn scope_reusable_after_clear() {
662 let obs = Observable::new(0);
663 let mut scope = BindingScope::new();
664
665 let seen1 = Rc::new(Cell::new(false));
666 let s1 = Rc::clone(&seen1);
667 scope.subscribe(&obs, move |_| s1.set(true));
668 scope.clear();
669
670 let seen2 = Rc::new(Cell::new(false));
671 let s2 = Rc::clone(&seen2);
672 scope.subscribe(&obs, move |_| s2.set(true));
673
674 obs.set(1);
675 assert!(!seen1.get(), "first subscription should be gone");
676 assert!(seen2.get(), "second subscription should be active");
677 }
678
679 #[test]
680 fn scope_hold_external_subscription() {
681 let obs = Observable::new(0);
682 let seen = Rc::new(Cell::new(0));
683
684 let mut scope = BindingScope::new();
685 let s = Rc::clone(&seen);
686 let sub = obs.subscribe(move |v| s.set(*v));
687 scope.hold(sub);
688
689 obs.set(5);
690 assert_eq!(seen.get(), 5);
691
692 drop(scope);
693 obs.set(99);
694 assert_eq!(
695 seen.get(),
696 5,
697 "held subscription should be released on scope drop"
698 );
699 }
700
701 #[test]
702 fn scope_debug_format() {
703 let mut scope = BindingScope::new();
704 let obs = Observable::new(0);
705 scope.subscribe(&obs, |_| {});
706 scope.subscribe(&obs, |_| {});
707 let debug = format!("{scope:?}");
708 assert!(debug.contains("binding_count: 2"));
709 }
710}