ailloli_ui_runtime/component/
state.rs

1//! Ergonomic state handle layered over [`Signal`].
2
3use std::cell::RefCell;
4use std::fmt::Display;
5use std::rc::Rc;
6
7use super::{Memo, Signal};
8
9/// Public DX state handle built on top of the retained runtime `Signal`.
10///
11/// Clones alias one signal value, revision, historical callback, and dynamic
12/// retained consumers. Like [`Signal`], this `Rc`-based type is UI-thread-local
13/// and neither `Send` nor `Sync`. [`Self::new`] starts without an owner callback,
14/// but every successful read during retained Build, Layout, or Paint registers
15/// that exact mounted consumer. Reads outside those callbacks remain passive.
16/// Successful callbacks replace their previous observations atomically, so
17/// conditional reads track only the branch that committed. A failed callback
18/// preserves its previous observations. Unmounting removes the consumer; the
19/// state itself remains usable while another handle owns it.
20///
21/// # Examples
22///
23/// ```
24/// use ailloli_ui_runtime::component::State;
25/// let state = State::new(1);
26/// let alias = state.clone();
27/// alias.set(2);
28/// assert_eq!(state.read(), 2);
29/// ```
30pub struct State<T> {
31    /// Underlying aliased reactive cell.
32    signal: Signal<T>,
33}
34
35/// Implements the `Clone` contract for `State<T>`.
36impl<T> Clone for State<T> {
37    /// Produces the clone required by the standard cloning contract.
38    fn clone(&self) -> Self {
39        Self {
40            signal: self.signal.clone(),
41        }
42    }
43}
44
45/// Provides the operations defined for `State<T>`.
46impl<T: 'static> State<T> {
47    /// Creates standalone state without an initial owner callback.
48    ///
49    /// Mutations advance the hidden source revision and notify retained consumers
50    /// established by later Build/Layout/Paint reads. Before such a read, after
51    /// their unmount, or when accessed only outside retained scopes, the state
52    /// schedules no frame. This differs from state created by a runtime context,
53    /// which also retains its owner-provided historical invalidator for
54    /// compatibility.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use ailloli_ui_runtime::component::State;
60    /// let state = State::new(String::from("ready"));
61    /// assert_eq!(state.read(), "ready");
62    /// ```
63    pub fn new(value: T) -> Self {
64        Self::from_signal(Signal::new(Rc::new(RefCell::new(value)), Rc::new(|| {})))
65    }
66
67    /// Wraps an existing signal while preserving its shared invalidator/revision.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use std::{cell::RefCell, rc::Rc};
73    /// use ailloli_ui_runtime::component::{Signal, State};
74    /// let signal = Signal::new(Rc::new(RefCell::new(1)), Rc::new(|| {}));
75    /// let state = State::from_signal(signal.clone());
76    /// state.set(2);
77    /// assert_eq!((signal.read(), signal.revision()), (2, 1));
78    /// ```
79    pub fn from_signal(signal: Signal<T>) -> Self {
80        Self { signal }
81    }
82
83    /// Replaces the value and notifies the underlying signal's consumers.
84    ///
85    /// Equality is not checked. Borrowing and callback panics follow
86    /// [`Signal::set`].
87    ///
88    /// # Examples
89    ///
90    /// ```
91    /// use ailloli_ui_runtime::component::State;
92    /// let state = State::new(1);
93    /// state.set(4);
94    /// assert_eq!(state.read(), 4);
95    /// ```
96    pub fn set(&self, value: T) {
97        self.signal.set(value);
98    }
99
100    /// Mutates the value in place and notifies the underlying signal's consumers.
101    ///
102    /// The closure is executed synchronously under a mutable `RefCell` borrow;
103    /// panic behavior follows [`Signal::update`].
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use ailloli_ui_runtime::component::State;
109    /// let state = State::new(vec![1]);
110    /// state.update(|items| items.push(2));
111    /// assert_eq!(state.read(), vec![1, 2]);
112    /// ```
113    pub fn update(&self, f: impl FnOnce(&mut T)) {
114        self.signal.update(f);
115    }
116
117    /// Consumes this handle and returns its underlying signal.
118    ///
119    /// Other cloned state handles continue to alias the returned signal.
120    ///
121    /// # Examples
122    ///
123    /// ```
124    /// use ailloli_ui_runtime::component::State;
125    /// let state = State::new(3);
126    /// let alias = state.clone();
127    /// let signal = state.into_signal();
128    /// signal.set(4);
129    /// assert_eq!(alias.read(), 4);
130    /// ```
131    pub fn into_signal(self) -> Signal<T> {
132        self.signal
133    }
134}
135
136/// Provides the operations defined for `State<T>`.
137impl<T: Clone + 'static> State<T> {
138    /// Clones, observes, and returns the current value.
139    ///
140    /// # Panics
141    ///
142    /// Panics if cloning `T` panics or the underlying signal value is already
143    /// mutably borrowed through another alias.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use ailloli_ui_runtime::component::State;
149    /// let state = State::new(String::from("copy"));
150    /// let value: String = state.read();
151    /// assert_eq!(value, "copy");
152    /// ```
153    pub fn read(&self) -> T {
154        self.signal.read()
155    }
156
157    /// Creates a non-caching lazy memo with this state's source revision.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// use ailloli_ui_runtime::component::State;
163    /// let state = State::new(2);
164    /// let squared = state.map(|value| value * value);
165    /// assert_eq!(squared.read(), 4);
166    /// state.set(3);
167    /// assert_eq!(squared.read(), 9);
168    /// ```
169    pub fn map<U: 'static>(&self, f: impl Fn(T) -> U + 'static) -> Memo<U> {
170        self.signal.map(f)
171    }
172
173    /// Creates a lazy string memo using a custom formatter.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use ailloli_ui_runtime::component::State;
179    /// let state = State::new(12);
180    /// assert_eq!(state.to_text_with(|value| format!("{value}%")).read(), "12%");
181    /// ```
182    pub fn to_text_with<F>(&self, format: F) -> Memo<String>
183    where
184        F: Fn(T) -> String + 'static,
185    {
186        self.signal.to_text_with(format)
187    }
188}
189
190impl<T> State<T>
191where
192    T: Clone + Display + 'static,
193{
194    /// Creates a lazy string memo using [`ToString::to_string`].
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use ailloli_ui_runtime::component::State;
200    /// assert_eq!(State::new(12).to_text().read(), "12");
201    /// ```
202    pub fn to_text(&self) -> Memo<String> {
203        self.signal.to_text()
204    }
205}
206
207/// Implements the `From<State<T>>` contract for `Signal<T>`.
208impl<T> From<State<T>> for Signal<T> {
209    /// Performs the documented infallible conversion.
210    fn from(value: State<T>) -> Self {
211        value.signal
212    }
213}