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}