Skip to main content

guise/reactive/
binding.rs

1//! `Binding` — a two-way connection to a value (SwiftUI's `Binding`).
2//!
3//! A [`Signal`] is the store; a `Binding` is the connection: a getter and a
4//! setter over `App`. Components accept one through `.bind(...)` (controlled
5//! builders) or `X::bind(entity, &signal, cx)` (stateful entities), so a value
6//! flows both ways without hand-written change handlers.
7//!
8//! ```ignore
9//! // Whole signal as a binding:
10//! let dark = use_state(cx, false);
11//! Switch::new("dark-mode").bind(dark.binding());
12//!
13//! // One field of a struct signal (a lens):
14//! let settings = use_state(cx, Settings::default());
15//! Slider::bind(&volume_slider, &settings, cx); // entities bind to signals
16//! Checkbox::new("mute").bind(settings.lens(|s| s.muted, |s, v| s.muted = v));
17//! ```
18
19use std::rc::Rc;
20
21use gpui::App;
22
23use super::signal::Signal;
24
25type Getter<T> = Rc<dyn Fn(&App) -> T>;
26type Setter<T> = Rc<dyn Fn(&mut App, T)>;
27
28/// A two-way connection to a value: a getter and a setter over `App`.
29///
30/// Cheap to clone (both accessors are `Rc`-shared) and `'static`, so it can be
31/// captured by element closures. Build one from a [`Signal`] with
32/// [`Signal::binding`] or [`Signal::lens`], or from raw accessors with
33/// [`Binding::new`].
34pub struct Binding<T> {
35  get: Getter<T>,
36  set: Setter<T>,
37}
38
39impl<T> Clone for Binding<T> {
40  fn clone(&self) -> Self {
41    Binding {
42      get: self.get.clone(),
43      set: self.set.clone(),
44    }
45  }
46}
47
48impl<T: 'static> Binding<T> {
49  /// Create a binding from a getter and a setter.
50  pub fn new(get: impl Fn(&App) -> T + 'static, set: impl Fn(&mut App, T) + 'static) -> Self {
51    Binding {
52      get: Rc::new(get),
53      set: Rc::new(set),
54    }
55  }
56
57  /// Read the current value.
58  pub fn get(&self, cx: &App) -> T {
59    (self.get)(cx)
60  }
61
62  /// Write a new value.
63  pub fn set(&self, cx: &mut App, value: T) {
64    (self.set)(cx, value)
65  }
66
67  /// Bidirectional transform (e.g. `Binding<f64>` <-> `Binding<String>`):
68  /// `from` converts on read, `into` converts back on write.
69  pub fn map<U: 'static>(
70    &self,
71    from: impl Fn(T) -> U + 'static,
72    into: impl Fn(U) -> T + 'static,
73  ) -> Binding<U> {
74    let get = self.get.clone();
75    let set = self.set.clone();
76    Binding {
77      get: Rc::new(move |cx| from(get(cx))),
78      set: Rc::new(move |cx, value| set(cx, into(value))),
79    }
80  }
81
82  /// Read-only binding over a fixed value; writes are a no-op. Useful for
83  /// disabled or demo states.
84  pub fn constant(value: T) -> Self
85  where
86    T: Clone,
87  {
88    Binding {
89      get: Rc::new(move |_cx| value.clone()),
90      set: Rc::new(|_cx, _value| {}),
91    }
92  }
93}
94
95impl<T: Clone + PartialEq + 'static> Signal<T> {
96  /// The whole signal as a binding. Writes go through
97  /// [`Signal::set_if_changed`], so equal values skip the notify.
98  pub fn binding(&self) -> Binding<T> {
99    let read = self.clone();
100    let write = self.clone();
101    Binding::new(
102      move |cx| read.get(cx),
103      move |cx, value| write.set_if_changed(cx, value),
104    )
105  }
106}
107
108impl<T: 'static> Signal<T> {
109  /// Project a field: signal of a struct -> binding of one field (a lens).
110  /// Writes that leave the field unchanged skip the notify.
111  pub fn lens<U: Clone + PartialEq + 'static>(
112    &self,
113    get: impl Fn(&T) -> U + 'static,
114    set: impl Fn(&mut T, U) + 'static,
115  ) -> Binding<U> {
116    let get = Rc::new(get);
117    let project = get.clone();
118    let read = self.clone();
119    let write = self.clone();
120    Binding::new(
121      move |cx| project(read.read(cx)),
122      move |cx, value| {
123        if get(write.read(cx)) == value {
124          return;
125        }
126        write.update(cx, |slot| set(slot, value));
127      },
128    )
129  }
130}
131
132#[cfg(test)]
133mod tests {
134  use super::*;
135
136  #[test]
137  fn clone_shares_the_accessors() {
138    let binding = Binding::new(|_cx| 1i32, |_cx, _value| {});
139    let clone = binding.clone();
140    assert_eq!(Rc::strong_count(&binding.get), 2);
141    assert_eq!(Rc::strong_count(&clone.set), 2);
142  }
143
144  #[test]
145  fn map_shares_the_source_accessors() {
146    let binding = Binding::new(|_cx| 1.5f64, |_cx, _value| {});
147    let _mapped: Binding<String> = binding.map(|v| v.to_string(), |s| s.parse().unwrap_or(0.0));
148    // `map` captures the source's accessors rather than copying values.
149    assert_eq!(Rc::strong_count(&binding.get), 2);
150    assert_eq!(Rc::strong_count(&binding.set), 2);
151  }
152}