ailloli_ui_runtime/component/signal.rs
1//! Single-threaded reactive values and lazy derived computations.
2
3use std::cell::RefCell;
4use std::fmt::Display;
5use std::rc::Rc;
6
7use super::reactive::ReactiveSource;
8
9/// Mutable UI-local reactive cell with retained dependency observation.
10///
11/// Clones alias the same [`RefCell`], source revision, historical invalidation
12/// callback, and dynamic retained consumers. A successful read during Build,
13/// Layout, or Paint records the innermost exact consumer; a read outside such a
14/// callback is passive. After a retained callback succeeds, its newly observed
15/// sources replace its previous sources as one update. This makes conditional
16/// reads follow the branch that actually committed; a panic keeps the previous
17/// observations. Unmounting a consumer removes its observations while leaving
18/// externally owned signals readable and mutable.
19///
20/// Mutations commit the value and revision, snapshot live retained consumers,
21/// release all value and subscriber borrows, notify those consumers, and then
22/// invoke the historical callback. Do not rely on ordering among retained
23/// consumers. Notification is synchronous and reentrant; if a retained consumer
24/// panics, later consumers and the historical callback are not invoked for that
25/// mutation. The `Rc`-based handle is neither `Send` nor `Sync` and must remain
26/// on its owning UI thread.
27///
28/// # Examples
29///
30/// ```
31/// use std::{cell::RefCell, rc::Rc};
32/// use ailloli_ui_runtime::component::Signal;
33/// let signal = Signal::new(Rc::new(RefCell::new(1)), Rc::new(|| {}));
34/// let alias = signal.clone();
35/// alias.set(2);
36/// assert_eq!(signal.read(), 2);
37/// assert_eq!(signal.revision(), 1);
38/// ```
39pub struct Signal<T> {
40 /// Aliased mutable value.
41 value: Rc<RefCell<T>>,
42 /// Shared revision, historical invalidator, and retained subscribers.
43 source: Rc<ReactiveSource>,
44}
45
46/// Implements the `Clone` contract for `Signal<T>`.
47impl<T> Clone for Signal<T> {
48 /// Produces the clone required by the standard cloning contract.
49 fn clone(&self) -> Self {
50 Self {
51 value: self.value.clone(),
52 source: self.source.clone(),
53 }
54 }
55}
56
57/// Provides the operations defined for `Signal<T>`.
58impl<T> Signal<T> {
59 /// Creates a signal around caller-owned storage and a historical callback.
60 ///
61 /// The initial revision is zero. Multiple calls using the same `value` do
62 /// not share revisions unless the resulting [`Signal`] itself is cloned.
63 /// The callback is the historical, owner-provided invalidator: it may capture
64 /// UI scheduling state and runs synchronously after exact retained consumers
65 /// on every successful mutation. It remains installed even when no retained
66 /// consumer has observed the signal. Reads establish dynamic dependencies
67 /// independently of this callback.
68 ///
69 /// # Examples
70 ///
71 /// ```
72 /// use std::{cell::{Cell, RefCell}, rc::Rc};
73 /// use ailloli_ui_runtime::component::Signal;
74 /// let calls = Rc::new(Cell::new(0));
75 /// let observed = calls.clone();
76 /// let signal = Signal::new(Rc::new(RefCell::new("a")), Rc::new(move || observed.set(observed.get() + 1)));
77 /// signal.set("b");
78 /// assert_eq!(calls.get(), 1);
79 /// ```
80 pub fn new(value: Rc<RefCell<T>>, invalidate: Rc<dyn Fn()>) -> Self {
81 Self {
82 value,
83 source: ReactiveSource::new(invalidate),
84 }
85 }
86
87 /// Creates a signal with an explicitly shared reactive source.
88 ///
89 /// This crate-internal constructor lets state slots recreate handles that
90 /// observe one revision. Public callers obtain the same sharing behavior by
91 /// cloning a signal.
92 ///
93 /// # Examples
94 ///
95 /// ```
96 /// use std::{cell::RefCell, rc::Rc};
97 /// use ailloli_ui_runtime::component::Signal;
98 /// let signal = Signal::new(Rc::new(RefCell::new(0)), Rc::new(|| {}));
99 /// let alias = signal.clone();
100 /// signal.set(1);
101 /// assert_eq!(alias.revision(), 1);
102 /// ```
103 pub(crate) fn with_source(value: Rc<RefCell<T>>, source: Rc<ReactiveSource>) -> Self {
104 Self { value, source }
105 }
106
107 /// Replaces the value, advances revision, then notifies all consumers.
108 ///
109 /// Equality is not checked, so setting an equal value still invalidates.
110 /// Revisions advance from zero to one and wrap from `u64::MAX` back to one;
111 /// compare them for inequality, not ordering.
112 ///
113 /// # Panics
114 ///
115 /// Panics on a conflicting `RefCell` borrow or when a notification callback
116 /// panics. In the callback case, the new value and revision remain committed.
117 /// Retained consumers run before the historical callback; a retained panic
118 /// stops the remaining notification sequence. All signal borrows are released
119 /// before callbacks run, so a callback may read or mutate the signal again.
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use std::{cell::RefCell, rc::Rc};
125 /// use ailloli_ui_runtime::component::Signal;
126 /// let signal = Signal::new(Rc::new(RefCell::new(3)), Rc::new(|| {}));
127 /// signal.set(3);
128 /// assert_eq!(signal.read(), 3);
129 /// assert_eq!(signal.revision(), 1);
130 /// ```
131 pub fn set(&self, value: T) {
132 *self.value.borrow_mut() = value;
133 self.source.bump_revision();
134 self.source.notify();
135 }
136
137 /// Mutates the value in place, advances revision, then notifies consumers.
138 ///
139 /// The closure is called exactly once under a mutable `RefCell` borrow.
140 /// This method does not detect whether the closure actually changed `T`.
141 ///
142 /// # Panics
143 ///
144 /// Panics on conflicting borrowing. If `f` panics, its partial mutation is
145 /// retained but revision and notification are skipped. A notification panic
146 /// propagates after mutation and revision commit, with the value borrow gone.
147 ///
148 /// # Examples
149 ///
150 /// ```
151 /// use std::{cell::RefCell, rc::Rc};
152 /// use ailloli_ui_runtime::component::Signal;
153 /// let signal = Signal::new(Rc::new(RefCell::new(vec![1])), Rc::new(|| {}));
154 /// signal.update(|items| items.push(2));
155 /// assert_eq!(signal.read(), vec![1, 2]);
156 /// ```
157 pub fn update(&self, f: impl FnOnce(&mut T)) {
158 {
159 let mut value = self.value.borrow_mut();
160 f(&mut value);
161 }
162 self.source.bump_revision();
163 self.source.notify();
164 }
165
166 /// Returns and observes the shared mutation revision without borrowing `T`.
167 ///
168 /// Zero means no successful mutating method has completed. The revision is
169 /// nonzero thereafter but can repeat after `u64` wraparound. In a retained
170 /// observation scope this registers the same source as [`Self::read`].
171 ///
172 /// # Examples
173 ///
174 /// ```
175 /// use std::{cell::RefCell, rc::Rc};
176 /// use ailloli_ui_runtime::component::Signal;
177 /// let signal = Signal::new(Rc::new(RefCell::new(0)), Rc::new(|| {}));
178 /// assert_eq!(signal.revision(), 0);
179 /// signal.set(1);
180 /// assert_eq!(signal.revision(), 1);
181 /// ```
182 pub fn revision(&self) -> u64 {
183 let revision = self.source.revision();
184 self.source.observe();
185 revision
186 }
187}
188
189/// Provides the operations defined for `Signal<T>`.
190impl<T: PartialEq> Signal<T> {
191 /// Replaces a small comparable value only when it actually changed.
192 ///
193 /// This avoids needless invalidations for scalar/domain revision signals;
194 /// it is not intended as a substitute for retained models or for comparing
195 /// complete trees.
196 ///
197 /// A `false` result performs no invalidation and does not advance revision.
198 /// A `true` result commits the value before synchronously invalidating.
199 ///
200 /// # Panics
201 ///
202 /// Panics on a conflicting `RefCell` borrow, a panicking equality operation,
203 /// or a panicking invalidation callback.
204 ///
205 /// # Examples
206 ///
207 /// ```
208 /// use std::{cell::RefCell, rc::Rc};
209 /// use ailloli_ui_runtime::component::Signal;
210 /// let signal = Signal::new(Rc::new(RefCell::new(4)), Rc::new(|| {}));
211 /// assert!(!signal.set_if_changed(4));
212 /// assert!(signal.set_if_changed(5));
213 /// assert_eq!((signal.read(), signal.revision()), (5, 1));
214 /// ```
215 pub fn set_if_changed(&self, value: T) -> bool {
216 let mut current = self.value.borrow_mut();
217 if *current == value {
218 return false;
219 }
220 *current = value;
221 drop(current);
222 self.source.bump_revision();
223 self.source.notify();
224 true
225 }
226}
227
228/// Provides the operations defined for `Signal<T>`.
229impl<T: Clone> Signal<T> {
230 /// Clones, observes, and returns the current value.
231 ///
232 /// # Panics
233 ///
234 /// A successful read is attributed only to the innermost retained build,
235 /// layout, or paint callback. Outside those callbacks it schedules no future
236 /// frame. Panics when the value is already mutably borrowed through another
237 /// alias.
238 ///
239 /// # Examples
240 ///
241 /// ```
242 /// use std::{cell::RefCell, rc::Rc};
243 /// use ailloli_ui_runtime::component::Signal;
244 /// let signal = Signal::new(Rc::new(RefCell::new(String::from("hello"))), Rc::new(|| {}));
245 /// let copy = signal.read();
246 /// assert_eq!(copy, "hello");
247 /// ```
248 pub fn read(&self) -> T {
249 let value = self.value.borrow().clone();
250 self.source.observe();
251 value
252 }
253
254 /// Creates a lazy derived value whose revision mirrors this signal.
255 ///
256 /// No value is cached: every [`Memo::read`] clones the current `T` and calls
257 /// `f`. The mapping itself installs no additional historical callback.
258 /// Reading the memo transitively observes this signal in the active retained
259 /// consumer scope.
260 ///
261 /// # Examples
262 ///
263 /// ```
264 /// use std::{cell::RefCell, rc::Rc};
265 /// use ailloli_ui_runtime::component::Signal;
266 /// let signal = Signal::new(Rc::new(RefCell::new(2)), Rc::new(|| {}));
267 /// let doubled = signal.map(|value| value * 2);
268 /// assert_eq!(doubled.read(), 4);
269 /// signal.set(3);
270 /// assert_eq!((doubled.read(), doubled.revision()), (6, 1));
271 /// ```
272 pub fn map<U: 'static>(&self, f: impl Fn(T) -> U + 'static) -> Memo<U>
273 where
274 T: 'static,
275 {
276 let signal = self.clone();
277 let revision_signal = self.clone();
278
279 Memo::with_revision(
280 move || {
281 let value = signal.read();
282 f(value)
283 },
284 move || revision_signal.revision(),
285 )
286 }
287}
288
289impl<T> Signal<T>
290where
291 T: Clone + Display + 'static,
292{
293 /// Creates a lazy string memo using [`ToString::to_string`].
294 ///
295 /// # Examples
296 ///
297 /// ```
298 /// use std::{cell::RefCell, rc::Rc};
299 /// use ailloli_ui_runtime::component::Signal;
300 /// let signal = Signal::new(Rc::new(RefCell::new(42)), Rc::new(|| {}));
301 /// assert_eq!(signal.to_text().read(), "42");
302 /// ```
303 pub fn to_text(&self) -> Memo<String> {
304 self.map(|value| value.to_string())
305 }
306}
307
308impl<T> Signal<T>
309where
310 T: Clone + 'static,
311{
312 /// Creates a lazy string memo using a custom owned-value formatter.
313 ///
314 /// # Examples
315 ///
316 /// ```
317 /// use std::{cell::RefCell, rc::Rc};
318 /// use ailloli_ui_runtime::component::Signal;
319 /// let signal = Signal::new(Rc::new(RefCell::new(3)), Rc::new(|| {}));
320 /// assert_eq!(signal.to_text_with(|value| format!("#{value}")).read(), "#3");
321 /// ```
322 pub fn to_text_with<F>(&self, format: F) -> Memo<String>
323 where
324 F: Fn(T) -> String + 'static,
325 {
326 self.map(format)
327 }
328}
329
330/// Provides the operations defined for `Signal<T>`.
331impl<T: Default> Signal<T> {
332 /// Replaces the current value with [`Default`] and returns the old value.
333 ///
334 /// Revision and invalidation always occur, even when the old value already
335 /// equaled its default.
336 ///
337 /// # Panics
338 ///
339 /// Panics on conflicting borrowing or a panicking invalidation callback.
340 ///
341 /// # Examples
342 ///
343 /// ```
344 /// use std::{cell::RefCell, rc::Rc};
345 /// use ailloli_ui_runtime::component::Signal;
346 /// let signal = Signal::new(Rc::new(RefCell::new(String::from("take"))), Rc::new(|| {}));
347 /// assert_eq!(signal.take(), "take");
348 /// assert_eq!(signal.take(), "");
349 /// assert_eq!(signal.revision(), 2);
350 /// ```
351 pub fn take(&self) -> T {
352 let old = {
353 let mut value = self.value.borrow_mut();
354 std::mem::take(&mut *value)
355 };
356 self.source.bump_revision();
357 self.source.notify();
358 old
359 }
360}
361
362/// Derived read-only value computed from signals or other memos.
363///
364/// A memo stores functions, not a cached value. Every [`Self::read`] evaluates
365/// its closure again. Clones share the same closures through [`Rc`] and are
366/// neither `Send` nor `Sync`. Revision reporting is advisory: [`Self::new`]
367/// always reports zero, while memos derived through [`Signal::map`] propagate
368/// their source revision.
369///
370/// A memo is dynamically reactive when its computation reads a [`Signal`] or
371/// [`super::State`] during a retained build, layout, or paint callback, even if
372/// its advisory revision is zero. Only sources reached by the executed branch
373/// are observed. After the surrounding callback succeeds, that conditional set
374/// atomically replaces the callback's previous observations; a panic leaves the
375/// previous set active. Reads outside retained callbacks are passive.
376///
377/// # Examples
378///
379/// ```
380/// use std::{cell::Cell, rc::Rc};
381/// use ailloli_ui_runtime::component::Memo;
382/// let calls = Rc::new(Cell::new(0));
383/// let observed = calls.clone();
384/// let memo = Memo::new(move || { observed.set(observed.get() + 1); 7 });
385/// assert_eq!((memo.read(), memo.read()), (7, 7));
386/// assert_eq!(calls.get(), 2);
387/// ```
388pub struct Memo<T> {
389 /// Lazy computation invoked on every read.
390 read_fn: Rc<dyn Fn() -> T>,
391 /// Advisory source revision computation.
392 revision_fn: Rc<dyn Fn() -> u64>,
393}
394
395/// Implements the `Clone` contract for `Memo<T>`.
396impl<T> Clone for Memo<T> {
397 /// Produces the clone required by the standard cloning contract.
398 fn clone(&self) -> Self {
399 Self {
400 read_fn: self.read_fn.clone(),
401 revision_fn: self.revision_fn.clone(),
402 }
403 }
404}
405
406/// Provides the operations defined for `Memo<T>`.
407impl<T> Memo<T> {
408 /// Creates an opaque lazy computation with constant revision zero.
409 ///
410 /// `read_fn` is not evaluated during construction and its outputs are not
411 /// cached. If it reads a signal during a retained callback, that signal is
412 /// observed dynamically even though [`Self::revision`] remains zero. Other
413 /// mutable external state can change without notifying the retained runtime.
414 ///
415 /// # Examples
416 ///
417 /// ```
418 /// use ailloli_ui_runtime::component::Memo;
419 /// let memo = Memo::new(|| 21 * 2);
420 /// assert_eq!(memo.read(), 42);
421 /// assert_eq!(memo.revision(), 0);
422 /// ```
423 pub fn new(read_fn: impl Fn() -> T + 'static) -> Self {
424 Self {
425 read_fn: Rc::new(read_fn),
426 revision_fn: Rc::new(|| 0),
427 }
428 }
429
430 /// Creates a memo with separate lazy value and revision functions.
431 fn with_revision(
432 read_fn: impl Fn() -> T + 'static,
433 revision_fn: impl Fn() -> u64 + 'static,
434 ) -> Self {
435 Self {
436 read_fn: Rc::new(read_fn),
437 revision_fn: Rc::new(revision_fn),
438 }
439 }
440
441 /// Evaluates and returns the current derived value.
442 ///
443 /// # Panics
444 ///
445 /// Any panic from the stored computation propagates to the caller.
446 ///
447 /// # Examples
448 ///
449 /// ```
450 /// use ailloli_ui_runtime::component::Memo;
451 /// let memo = Memo::new(|| String::from("fresh"));
452 /// assert_eq!(memo.read(), "fresh");
453 /// ```
454 pub fn read(&self) -> T {
455 (self.read_fn)()
456 }
457
458 /// Evaluates the memo's advisory revision function.
459 ///
460 /// A standalone memo returns zero. Derived signal memos return the source's
461 /// nonzero-after-change wrapping revision. This call does not read the value,
462 /// but a signal-derived revision function observes that signal when invoked
463 /// during a retained callback.
464 ///
465 /// # Examples
466 ///
467 /// ```
468 /// use ailloli_ui_runtime::component::Memo;
469 /// let memo = Memo::new(|| 1);
470 /// assert_eq!(memo.revision(), 0);
471 /// ```
472 pub fn revision(&self) -> u64 {
473 (self.revision_fn)()
474 }
475
476 /// Lazily maps this memo while preserving its revision function.
477 ///
478 /// Neither the source nor result is cached; each result read first evaluates
479 /// the source and then `f`.
480 ///
481 /// # Examples
482 ///
483 /// ```
484 /// use ailloli_ui_runtime::component::Memo;
485 /// let source = Memo::new(|| 5);
486 /// let label = source.map(|value| format!("value={value}"));
487 /// assert_eq!(label.read(), "value=5");
488 /// assert_eq!(label.revision(), 0);
489 /// ```
490 pub fn map<U: 'static>(&self, f: impl Fn(T) -> U + 'static) -> Memo<U>
491 where
492 T: 'static,
493 {
494 let memo = self.clone();
495 let revision_memo = self.clone();
496
497 Memo::with_revision(
498 move || {
499 let value = memo.read();
500 f(value)
501 },
502 move || revision_memo.revision(),
503 )
504 }
505}
506
507impl<T> Memo<T>
508where
509 T: Display + 'static,
510{
511 /// Lazily formats each source value through [`ToString::to_string`].
512 ///
513 /// # Examples
514 ///
515 /// ```
516 /// use ailloli_ui_runtime::component::Memo;
517 /// assert_eq!(Memo::new(|| 8).to_text().read(), "8");
518 /// ```
519 pub fn to_text(&self) -> Memo<String> {
520 self.map(|value| value.to_string())
521 }
522}
523
524impl<T> Memo<T>
525where
526 T: 'static,
527{
528 /// Lazily formats each source value with `format`.
529 ///
530 /// # Examples
531 ///
532 /// ```
533 /// use ailloli_ui_runtime::component::Memo;
534 /// let text = Memo::new(|| 8).to_text_with(|value| format!("{value}px"));
535 /// assert_eq!(text.read(), "8px");
536 /// ```
537 pub fn to_text_with<F>(&self, format: F) -> Memo<String>
538 where
539 F: Fn(T) -> String + 'static,
540 {
541 self.map(format)
542 }
543}