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}