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}