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}