Skip to main content

repose_core/
state.rs

1use std::any::Any;
2use std::cell::{Ref, RefCell, RefMut};
3use std::rc::Rc;
4
5use crate::{Signal, reactive, remember_with_key, request_frame};
6
7#[allow(dead_code)]
8pub struct MutableState<T: Clone + 'static> {
9    inner: Signal<T>,
10    saver: Option<Box<dyn StateSaver<T>>>,
11}
12pub trait StateSaver<T>: 'static {
13    fn save(&self, value: &T) -> Box<dyn Any>;
14    fn restore(&self, saved: &dyn Any) -> Option<T>;
15}
16
17pub fn remember_derived<T: Clone + 'static>(
18    key: impl Into<String>,
19    producer: impl Fn() -> T + 'static + Clone,
20) -> std::rc::Rc<crate::Signal<T>> {
21    let key: String = key.into();
22    produce_state(format!("derived:{key}"), producer)
23}
24
25// State holder pattern
26pub trait StateHolder: 'static {
27    type State: Clone;
28    type Event;
29
30    fn initial_state() -> Self::State;
31    fn reduce(state: &Self::State, event: Self::Event) -> Self::State;
32}
33
34/// Lazily produces a Signal<T> (remembered by key) and keeps it up to date
35/// by re-running `producer` under the reactive graph whenever its dependencies change.
36///
37/// - Runs an initial compute immediately to establish dependencies.
38pub fn produce_state<T: Clone + 'static>(
39    key: impl Into<String>,
40    producer: impl Fn() -> T + 'static + Clone,
41) -> Rc<Signal<T>> {
42    produce_state_inner(key.into(), producer, |out, v| out.set(v))
43}
44
45/// Like [`produce_state`], but only writes the output signal when the computed
46/// value actually changed (`T: PartialEq`), skipping invalidations/frame
47/// requests when the derived value is unchanged.
48pub fn produce_state_eq<T: Clone + PartialEq + 'static>(
49    key: impl Into<String>,
50    producer: impl Fn() -> T + 'static + Clone,
51) -> Rc<Signal<T>> {
52    produce_state_inner(key.into(), producer, |out, v| out.set_neq(v))
53}
54
55/// Owner of a `produce_state` observer. Stored in the keyed slot; when the
56/// slot is replaced (type change or key reuse) or explicitly cleared, `Drop`
57/// removes the reactive observer so zombie recomputes cannot accumulate.
58/// Navigation-entry scopes additionally dispose via their own `Scope`, which
59/// is idempotent with this path.
60struct ProduceHandle {
61    obs: reactive::ObserverId,
62}
63
64impl Drop for ProduceHandle {
65    fn drop(&mut self) {
66        reactive::remove_observer(self.obs);
67    }
68}
69
70fn produce_state_inner<T: Clone + 'static>(
71    key: String,
72    producer: impl Fn() -> T + 'static + Clone,
73    write: impl Fn(Signal<T>, T) + 'static + Copy,
74) -> Rc<Signal<T>> {
75    let full_key = format!("produce:{key}");
76    let rc: Rc<(Signal<T>, ProduceHandle)> = remember_with_key(full_key.clone(), || {
77        let out_cell: Rc<RefCell<Option<Signal<T>>>> = Rc::new(RefCell::new(None));
78        let out_cell_c = out_cell.clone();
79        let producer_c = producer.clone();
80        let obs_id = reactive::new_observer(move || {
81            let v = producer_c();
82            if let Some(out) = out_cell_c.borrow().as_ref() {
83                write(out.clone(), v);
84            } else {
85                *out_cell_c.borrow_mut() = Some(Signal::new(v));
86            }
87        });
88
89        reactive::run_observer_now(obs_id);
90
91        let out = out_cell.borrow().as_ref().cloned().unwrap_or_else(|| {
92            Signal::new(producer())
93        });
94        (out, ProduceHandle { obs: obs_id })
95    });
96    if let Some(scope) = crate::scope::current_scope() {
97        let obs = rc.1.obs;
98        scope.memo(&format!("produce-cleanup:{full_key}"), || {
99            let fk = full_key.clone();
100            scope.add_disposer(move || {
101                reactive::remove_observer(obs);
102                crate::runtime::COMPOSER.with(|c| match c.try_borrow_mut() {
103                    Ok(mut c) => {
104                        c.keyed_slots.remove(&fk);
105                    }
106                    Err(_) => {
107                        log::error!(
108                            "produce_state: composer busy during unmount cleanup for '{fk}'; observer removed but slot retained"
109                        );
110                    }
111                });
112            });
113            ()
114        });
115    }
116    Rc::new(rc.0.clone())
117}
118
119/// Local widget state that drives recomposition on every write.
120///
121/// Unlike [`crate::remember_state`] (a bare `Rc<RefCell<T>>` that never requests
122/// a frame), `Mutable` calls [`request_frame`] on `set`/`update` so async /
123/// timer / layout-callback mutations reliably re-render. Prefer [`Signal`] for
124/// shared/derived state; use `Mutable` for widget-local state that should
125/// always recompose.
126pub struct Mutable<T: 'static>(Rc<RefCell<T>>);
127
128// Manual impl: `#[derive(Clone)]` would require `T: Clone`, but `Rc<RefCell<T>>`
129// is unconditionally cloneable and local widget state must not need `T: Clone`.
130impl<T: 'static> Clone for Mutable<T> {
131    fn clone(&self) -> Self {
132        Self(self.0.clone())
133    }
134}
135
136impl<T: 'static> Mutable<T> {
137    pub fn new(v: T) -> Self {
138        Self(Rc::new(RefCell::new(v)))
139    }
140
141    pub fn get(&self) -> Ref<'_, T> {
142        self.0.borrow()
143    }
144
145    /// Read the current value without holding the borrow across the closure.
146    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
147        f(&*self.0.borrow())
148    }
149
150    pub fn set(&self, v: T) {
151        *self.0.borrow_mut() = v;
152        crate::signal_fired();
153        request_frame();
154    }
155
156    /// Unconditional write + frame request. Prefer `set_neq`/`update_neq` for
157    /// UI state where equality is cheap - `set`/`update` always invalidate.
158
159    /// Like [`set`], but skips the frame request + signal when the value is
160    /// unchanged (`T: PartialEq`).
161    pub fn set_neq(&self, v: T)
162    where
163        T: PartialEq,
164    {
165        {
166            let mut b = self.0.borrow_mut();
167            if *b == v {
168                return;
169            }
170            *b = v;
171        }
172        crate::signal_fired();
173        request_frame();
174    }
175
176    pub fn update(&self, f: impl FnOnce(&mut T)) {
177        f(&mut *self.0.borrow_mut());
178        crate::signal_fired();
179        request_frame();
180    }
181
182    /// Like [`update`], but only requests a frame + fires the signal when the
183    /// value actually changed (`T: PartialEq + Clone`).
184    pub fn update_neq(&self, f: impl FnOnce(&mut T))
185    where
186        T: PartialEq + Clone,
187    {
188        let changed = {
189            let mut b = self.0.borrow_mut();
190            let before = (*b).clone();
191            f(&mut *b);
192            *b != before
193        };
194        if changed {
195            crate::signal_fired();
196            request_frame();
197        }
198    }
199
200    /// Escape hatch when batching many writes. Call [`request_frame`] yourself.
201    pub fn borrow_mut_silent(&self) -> RefMut<'_, T> {
202        self.0.borrow_mut()
203    }
204
205    pub fn as_rc(&self) -> Rc<RefCell<T>> {
206        self.0.clone()
207    }
208}
209
210/// Remember a [`Mutable`] in the current composition slot.
211#[track_caller]
212pub fn remember_mutable<T: 'static>(init: impl FnOnce() -> T) -> Mutable<T> {
213    crate::remember(|| Mutable::new(init())).as_ref().clone()
214}
215
216/// Key-based variant of [`remember_mutable`]. Stable across conditional branches.
217#[track_caller]
218pub fn remember_mutable_with_key<T: 'static>(
219    key: impl Into<String>,
220    init: impl FnOnce() -> T,
221) -> Mutable<T> {
222    remember_with_key(key, || Mutable::new(init()))
223        .as_ref()
224        .clone()
225}
226
227/// Remember a reducer-backed local state. Returns a [`Mutable`] snapshot reader
228/// plus a dispatch closure that runs `H::reduce` and writes the result back.
229///
230/// Prefer this for multi-field widget state over many loose `Mutable`s. It keeps
231/// the state shape and all mutations in one place.
232#[track_caller]
233pub fn remember_reducer<H: StateHolder>() -> (Mutable<H::State>, impl Fn(H::Event) + Clone)
234where
235    H::State: 'static,
236    H::Event: 'static,
237{
238    let state = remember_mutable(|| H::initial_state());
239    let dispatch = {
240        let state = state.clone();
241        move |ev: H::Event| {
242            state.update(|s| *s = H::reduce(s, ev));
243        }
244    };
245    (state, dispatch)
246}
247
248/// Key-based variant of [`remember_reducer`]. Stable across conditional branches.
249#[track_caller]
250pub fn remember_reducer_with_key<H: StateHolder>(
251    key: impl Into<String>,
252) -> (Mutable<H::State>, impl Fn(H::Event) + Clone)
253where
254    H::State: 'static,
255    H::Event: 'static,
256{
257    let state = remember_mutable_with_key(key, || H::initial_state());
258    let dispatch = {
259        let state = state.clone();
260        move |ev: H::Event| {
261            state.update(|s| *s = H::reduce(s, ev));
262        }
263    };
264    (state, dispatch)
265}