ailloli_ui_runtime/component/
binding.rs

1//! Uniform wrapper for static and reactive widget properties.
2
3use super::{Memo, Signal, State};
4
5/// Bindable widget prop: static value, [`Signal`], or [`Memo`].
6///
7/// Unifies `disabled(true)`, `disabled(signal)`, and `disabled(memo)` without separate DSL methods.
8/// Cloning a static binding clones `T`; cloning reactive variants aliases their
9/// existing `Rc`-backed source. Reads always return an owned value and memos are
10/// evaluated on every read. Signal-backed and transitively reactive memo reads
11/// register the innermost retained Build/Layout/Paint consumer; static values do
12/// not create a dependency. Conditional memo reads observe only sources reached
13/// by the executed branch. The surrounding callback publishes that set only on
14/// success and replaces its previous set atomically. Because the reactive
15/// variants are `Rc`-backed, a binding is UI-thread-local and neither `Send` nor
16/// `Sync`. Subscriptions belong to the mounted consumer rather than the binding:
17/// unmounting removes them while independently owned bindings remain usable.
18///
19/// # Examples
20///
21/// ```
22/// use ailloli_ui_runtime::component::Binding;
23/// let binding: Binding<String> = "label".into();
24/// assert_eq!(binding.read(), "label");
25/// assert!(!binding.is_signal());
26/// ```
27pub enum Binding<T> {
28    /// Owned non-reactive value with constant revision zero.
29    Static(T),
30    /// Aliased mutable signal.
31    Signal(Signal<T>),
32    /// Lazy derived value, possibly carrying a source revision.
33    Memo(Memo<T>),
34}
35
36/// Implements the `Clone` contract for `Binding<T>`.
37impl<T: Clone> Clone for Binding<T> {
38    /// Produces the clone required by the standard cloning contract.
39    fn clone(&self) -> Self {
40        match self {
41            Self::Static(value) => Self::Static(value.clone()),
42            Self::Signal(signal) => Self::Signal(signal.clone()),
43            Self::Memo(memo) => Self::Memo(memo.clone()),
44        }
45    }
46}
47
48/// Implements the `From<T>` contract for `Binding<T>`.
49impl<T> From<T> for Binding<T> {
50    /// Performs the documented infallible conversion.
51    fn from(value: T) -> Self {
52        Self::Static(value)
53    }
54}
55
56/// Implements the `From<Signal<T>>` contract for `Binding<T>`.
57impl<T> From<Signal<T>> for Binding<T> {
58    /// Performs the documented infallible conversion.
59    fn from(value: Signal<T>) -> Self {
60        Self::Signal(value)
61    }
62}
63
64/// Implements the `From<State<T>>` contract for `Binding<T>`.
65impl<T> From<State<T>> for Binding<T> {
66    /// Performs the documented infallible conversion.
67    fn from(value: State<T>) -> Self {
68        Self::Signal(value.into())
69    }
70}
71
72/// Implements the `From<Memo<T>>` contract for `Binding<T>`.
73impl<T> From<Memo<T>> for Binding<T> {
74    /// Performs the documented infallible conversion.
75    fn from(value: Memo<T>) -> Self {
76        Self::Memo(value)
77    }
78}
79
80/// Implements the `From<&str>` contract for `Binding<String>`.
81impl From<&str> for Binding<String> {
82    /// Performs the documented infallible conversion.
83    fn from(value: &str) -> Self {
84        Self::Static(value.to_string())
85    }
86}
87
88/// Implements the `From<&String>` contract for `Binding<String>`.
89impl From<&String> for Binding<String> {
90    /// Performs the documented infallible conversion.
91    fn from(value: &String) -> Self {
92        Self::Static(value.clone())
93    }
94}
95
96/// Provides the operations defined for `Binding<T>`.
97impl<T: Clone> Binding<T> {
98    /// Returns an owned current value.
99    ///
100    /// Static and signal variants clone `T`; a memo evaluates its stored
101    /// computation. Reactive variants are observed by the innermost retained
102    /// build, layout, or paint callback; reads elsewhere are passive. A failed
103    /// surrounding callback does not publish its staged dependency set.
104    ///
105    /// # Panics
106    ///
107    /// Panics if cloning `T` panics, a signal value is already mutably borrowed
108    /// through another alias, or the stored memo computation panics.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use ailloli_ui_runtime::component::{Binding, Memo};
114    /// let static_value = Binding::Static(3);
115    /// let derived = Binding::Memo(Memo::new(|| 4));
116    /// assert_eq!((static_value.read(), derived.read()), (3, 4));
117    /// ```
118    pub fn read(&self) -> T {
119        match self {
120            Binding::Static(v) => v.clone(),
121            Binding::Signal(s) => s.read(),
122            Binding::Memo(m) => m.read(),
123        }
124    }
125}
126
127/// Provides the operations defined for `Binding<T>`.
128impl<T> Binding<T> {
129    /// Returns `true` only for the direct [`Binding::Signal`] variant.
130    ///
131    /// A memo derived from a signal still returns `false`; inspect
132    /// [`Self::revision`] when the distinction of interest is reactivity.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use std::{cell::RefCell, rc::Rc};
138    /// use ailloli_ui_runtime::component::{Binding, Signal};
139    /// let signal = Signal::new(Rc::new(RefCell::new(1)), Rc::new(|| {}));
140    /// let binding: Binding<i32> = signal.into();
141    /// assert!(binding.is_signal());
142    /// ```
143    pub fn is_signal(&self) -> bool {
144        matches!(self, Binding::Signal(_))
145    }
146
147    /// Observed revision of the reactive source used by retained caches.
148    ///
149    /// Static bindings and opaque memos without a reactive source remain at
150    /// revision zero. Memos derived from a [`Signal`] preserve its revision.
151    /// Revisions reserve zero for pristine/opaque sources and wrap from
152    /// `u64::MAX` to one. Signal and derived-memo revision reads observe their
153    /// source unless the runtime explicitly installs an untracked admin scope.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use std::{cell::RefCell, rc::Rc};
159    /// use ailloli_ui_runtime::component::{Binding, Signal};
160    /// let signal = Signal::new(Rc::new(RefCell::new(1)), Rc::new(|| {}));
161    /// let memo_binding: Binding<i32> = signal.map(|value| value + 1).into();
162    /// assert_eq!(memo_binding.revision(), 0);
163    /// signal.set(2);
164    /// assert_eq!(memo_binding.revision(), 1);
165    /// ```
166    pub fn revision(&self) -> u64 {
167        match self {
168            Self::Static(_) => 0,
169            Self::Signal(signal) => signal.revision(),
170            Self::Memo(memo) => memo.revision(),
171        }
172    }
173}