Skip to main content

telar_reactive_core/
source.rs

1//! Reading a value that may or may not be a signal.
2
3use crate::{Memo, ReadSignal, RwSignal, memo};
4
5/// Anything a derivation — or a widget — can read reactively: either handle on a signal, or a value already
6/// derived once.
7///
8/// One trait rather than a `derive`/`derive_from`/`map` family, because the difference between them was never
9/// about behaviour: a widget reading a service, a read handle, or something derived all want the same thing,
10/// and three spellings of it only meant picking the wrong one and chasing a type error.
11///
12/// A widget that takes `impl Source<Value = T>` instead of `RwSignal<T>` can be fed a derivation. One that
13/// takes the signal cannot, and that is how a catalogue ends up re-implemented next to it — a card wanting a
14/// percentage computed from two services has nothing to hand a widget that insists on a signal it can write.
15pub trait Source {
16    type Value;
17    fn read(&self) -> Self::Value;
18}
19
20impl<T: Clone + 'static> Source for RwSignal<T> {
21    type Value = T;
22    fn read(&self) -> T {
23        self.get()
24    }
25}
26
27impl<T: Clone + 'static> Source for ReadSignal<T> {
28    type Value = T;
29    fn read(&self) -> T {
30        self.get()
31    }
32}
33
34impl<T: Clone + 'static> Source for Memo<T> {
35    type Value = T;
36    fn read(&self) -> T {
37        self.get()
38    }
39}
40
41/// A plain value reads as itself, so a widget taking a [`Source`] still accepts a constant without the caller
42/// wrapping it in a signal that will never change.
43impl Source for f32 {
44    type Value = f32;
45    fn read(&self) -> f32 {
46        *self
47    }
48}
49
50impl Source for bool {
51    type Value = bool;
52    fn read(&self) -> bool {
53        *self
54    }
55}
56
57/// A value derived from another, recomputed when its source moves.
58///
59/// **A derivation is a [`Memo`], never a signal written by an effect.** [`effect`](crate::effect) hands back a
60/// handle whose `Drop` deregisters it, so `let _ = effect(…)` runs exactly once and then stops — the derived
61/// value is seeded correctly and never moves again, which looks like a working widget until you watch it. A
62/// `Memo` is `Rc`-backed and lives as long as the closure reading it, so whatever draws the value is what
63/// keeps the derivation alive, with nothing for a caller to remember.
64pub fn derive<S, U>(source: S, map: impl Fn(S::Value) -> U + 'static) -> Memo<U>
65where
66    S: Source + 'static,
67    U: PartialEq + 'static,
68{
69    memo(move || map(source.read()))
70}
71
72/// [`derive`] over two sources, recomputed when either moves — a label that reads a level and whether it is
73/// charging, and has to follow both.
74pub fn derive_pair<A, B, U>(
75    first: A,
76    second: B,
77    map: impl Fn(A::Value, B::Value) -> U + 'static,
78) -> Memo<U>
79where
80    A: Source + 'static,
81    B: Source + 'static,
82    U: PartialEq + 'static,
83{
84    memo(move || map(first.read(), second.read()))
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::{reset_runtime, signal};
91
92    #[test]
93    fn a_derived_value_follows_its_source() {
94        reset_runtime();
95        let source = signal(2i32);
96        let doubled = derive(source.clone(), |n| n * 2);
97        assert_eq!(doubled.get(), 4, "seeded from the source, not a default");
98        source.set(5);
99        assert_eq!(doubled.get(), 10);
100    }
101
102    #[test]
103    fn a_pair_recomputes_when_either_half_moves() {
104        reset_runtime();
105        let level = signal(10i32);
106        let charging = signal(false);
107        let label = derive_pair(
108            level.read_only(),
109            charging.read_only(),
110            |level, charging| format!("{level}{}", if charging { "+" } else { "" }),
111        );
112        assert_eq!(label.get(), "10");
113        charging.set(true);
114        assert_eq!(label.get(), "10+");
115        level.set(11);
116        assert_eq!(label.get(), "11+");
117    }
118
119    /// The regression this exists for. Deriving through a signal written by an effect seeds correctly and then
120    /// goes dead the moment the handle drops.
121    #[test]
122    fn a_derivation_outlives_the_call_that_made_it() {
123        reset_runtime();
124        let source = signal(1i32);
125        let derived = derive(source.clone(), |n| n * 10);
126        let read: Box<dyn Fn() -> i32> = Box::new(move || derived.get());
127        source.set(7);
128        assert_eq!(read(), 70, "whatever holds the derivation keeps it alive");
129    }
130
131    /// A constant is a source too, so widening a widget's parameter does not cost every caller a signal.
132    #[test]
133    fn a_plain_value_reads_as_itself() {
134        assert_eq!(Source::read(&0.5f32), 0.5);
135        assert!(Source::read(&true));
136    }
137
138    /// A derivation is a source, which is what lets one feed a widget that used to insist on a signal.
139    #[test]
140    fn a_derivation_is_itself_a_source() {
141        reset_runtime();
142        let source = signal(3i32);
143        let once = derive(source.clone(), |n| n + 1);
144        let twice = derive(once, |n| n * 2);
145        assert_eq!(twice.get(), 8);
146        source.set(4);
147        assert_eq!(twice.get(), 10);
148    }
149}