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, signal};
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: Signal<T> = signal(producer());
78        let out_clone = out.clone();
79
80        let obs_id = reactive::new_observer({
81            let producer = producer.clone();
82            move || {
83                let v = producer();
84                write(out_clone.clone(), v);
85            }
86        });
87
88        // Establish initial deps and value
89        reactive::run_observer_now(obs_id);
90
91        (out, ProduceHandle { obs: obs_id })
92    });
93    if let Some(scope) = crate::scope::current_scope() {
94        let obs = rc.1.obs;
95        scope.memo(&format!("produce-cleanup:{full_key}"), || {
96            let fk = full_key.clone();
97            scope.add_disposer(move || {
98                reactive::remove_observer(obs);
99                crate::runtime::COMPOSER.with(|c| match c.try_borrow_mut() {
100                    Ok(mut c) => {
101                        c.keyed_slots.remove(&fk);
102                    }
103                    Err(_) => {
104                        log::error!(
105                            "produce_state: composer busy during unmount cleanup for '{fk}'; observer removed but slot retained"
106                        );
107                    }
108                });
109            });
110            ()
111        });
112    }
113    Rc::new(rc.0.clone())
114}
115
116/// Local widget state that drives recomposition on every write.
117///
118/// Unlike [`crate::remember_state`] (a bare `Rc<RefCell<T>>` that never requests
119/// a frame), `Mutable` calls [`request_frame`] on `set`/`update` so async /
120/// timer / layout-callback mutations reliably re-render. Prefer [`Signal`] for
121/// shared/derived state; use `Mutable` for widget-local state that should
122/// always recompose.
123pub struct Mutable<T: 'static>(Rc<RefCell<T>>);
124
125// Manual impl: `#[derive(Clone)]` would require `T: Clone`, but `Rc<RefCell<T>>`
126// is unconditionally cloneable and local widget state must not need `T: Clone`.
127impl<T: 'static> Clone for Mutable<T> {
128    fn clone(&self) -> Self {
129        Self(self.0.clone())
130    }
131}
132
133impl<T: 'static> Mutable<T> {
134    pub fn new(v: T) -> Self {
135        Self(Rc::new(RefCell::new(v)))
136    }
137
138    pub fn get(&self) -> Ref<'_, T> {
139        self.0.borrow()
140    }
141
142    /// Read the current value without holding the borrow across the closure.
143    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
144        f(&*self.0.borrow())
145    }
146
147    pub fn set(&self, v: T) {
148        *self.0.borrow_mut() = v;
149        crate::signal_fired();
150        request_frame();
151    }
152
153    /// Like [`set`], but skips the frame request + signal when the value is
154    /// unchanged (`T: PartialEq`).
155    pub fn set_neq(&self, v: T)
156    where
157        T: PartialEq,
158    {
159        {
160            let mut b = self.0.borrow_mut();
161            if *b == v {
162                return;
163            }
164            *b = v;
165        }
166        crate::signal_fired();
167        request_frame();
168    }
169
170    pub fn update(&self, f: impl FnOnce(&mut T)) {
171        f(&mut *self.0.borrow_mut());
172        crate::signal_fired();
173        request_frame();
174    }
175
176    /// Like [`update`], but only requests a frame + fires the signal when the
177    /// value actually changed (`T: PartialEq + Clone`).
178    pub fn update_neq(&self, f: impl FnOnce(&mut T))
179    where
180        T: PartialEq + Clone,
181    {
182        let changed = {
183            let mut b = self.0.borrow_mut();
184            let before = (*b).clone();
185            f(&mut *b);
186            *b != before
187        };
188        if changed {
189            crate::signal_fired();
190            request_frame();
191        }
192    }
193
194    /// Escape hatch when batching many writes. Call [`request_frame`] yourself.
195    pub fn borrow_mut_silent(&self) -> RefMut<'_, T> {
196        self.0.borrow_mut()
197    }
198
199    pub fn as_rc(&self) -> Rc<RefCell<T>> {
200        self.0.clone()
201    }
202}
203
204/// Remember a [`Mutable`] in the current composition slot.
205#[track_caller]
206pub fn remember_mutable<T: 'static>(init: impl FnOnce() -> T) -> Mutable<T> {
207    crate::remember(|| Mutable::new(init())).as_ref().clone()
208}
209
210/// Key-based variant of [`remember_mutable`]. Stable across conditional branches.
211#[track_caller]
212pub fn remember_mutable_with_key<T: 'static>(
213    key: impl Into<String>,
214    init: impl FnOnce() -> T,
215) -> Mutable<T> {
216    remember_with_key(key, || Mutable::new(init()))
217        .as_ref()
218        .clone()
219}
220
221/// Remember a reducer-backed local state. Returns a [`Mutable`] snapshot reader
222/// plus a dispatch closure that runs `H::reduce` and writes the result back.
223///
224/// Prefer this for multi-field widget state over many loose `Mutable`s. It keeps
225/// the state shape and all mutations in one place.
226#[track_caller]
227pub fn remember_reducer<H: StateHolder>() -> (Mutable<H::State>, impl Fn(H::Event) + Clone)
228where
229    H::State: 'static,
230    H::Event: 'static,
231{
232    let state = remember_mutable(|| H::initial_state());
233    let dispatch = {
234        let state = state.clone();
235        move |ev: H::Event| {
236            state.update(|s| *s = H::reduce(s, ev));
237        }
238    };
239    (state, dispatch)
240}
241
242/// Key-based variant of [`remember_reducer`]. Stable across conditional branches.
243#[track_caller]
244pub fn remember_reducer_with_key<H: StateHolder>(
245    key: impl Into<String>,
246) -> (Mutable<H::State>, impl Fn(H::Event) + Clone)
247where
248    H::State: 'static,
249    H::Event: 'static,
250{
251    let state = remember_mutable_with_key(key, || H::initial_state());
252    let dispatch = {
253        let state = state.clone();
254        move |ev: H::Event| {
255            state.update(|s| *s = H::reduce(s, ev));
256        }
257    };
258    (state, dispatch)
259}