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
17#[deprecated(note = "use `produce_state_eq`, which skips no-op writes")]
18pub fn remember_derived<T: Clone + 'static>(
19    key: impl Into<String>,
20    producer: impl Fn() -> T + 'static + Clone,
21) -> std::rc::Rc<crate::Signal<T>> {
22    let key: String = key.into();
23    produce_state(format!("derived:{key}"), producer)
24}
25
26// State holder pattern
27pub trait StateHolder: 'static {
28    type State: Clone;
29    type Event;
30
31    fn initial_state() -> Self::State;
32    fn reduce(state: &Self::State, event: Self::Event) -> Self::State;
33}
34
35/// Lazily produces a Signal<T> (remembered by key) and keeps it up to date
36/// by re-running `producer` under the reactive graph whenever its dependencies change.
37///
38/// - Runs an initial compute immediately to establish dependencies.
39pub fn produce_state<T: Clone + 'static>(
40    key: impl Into<String>,
41    producer: impl Fn() -> T + 'static + Clone,
42) -> Rc<Signal<T>> {
43    produce_state_inner(key.into(), producer, |out, v| out.set(v))
44}
45
46/// Like [`produce_state`], but only writes the output signal when the computed
47/// value actually changed (`T: PartialEq`), skipping invalidations/frame
48/// requests when the derived value is unchanged.
49pub fn produce_state_eq<T: Clone + PartialEq + 'static>(
50    key: impl Into<String>,
51    producer: impl Fn() -> T + 'static + Clone,
52) -> Rc<Signal<T>> {
53    produce_state_inner(key.into(), producer, |out, v| out.set_neq(v))
54}
55
56/// Owner of a `produce_state` observer. Stored in the keyed slot; when the
57/// slot is replaced (type change or key reuse) or explicitly cleared, `Drop`
58/// removes the reactive observer so zombie recomputes cannot accumulate.
59/// Navigation-entry scopes additionally dispose via their own `Scope`, which
60/// is idempotent with this path.
61struct ProduceHandle {
62    obs: reactive::ObserverId,
63}
64
65impl Drop for ProduceHandle {
66    fn drop(&mut self) {
67        reactive::remove_observer(self.obs);
68    }
69}
70
71fn produce_state_inner<T: Clone + 'static>(
72    key: String,
73    producer: impl Fn() -> T + 'static + Clone,
74    write: impl Fn(Signal<T>, T) + 'static + Copy,
75) -> Rc<Signal<T>> {
76    let full_key = format!("produce:{key}");
77    let rc: Rc<(Signal<T>, ProduceHandle)> = remember_with_key(full_key.clone(), || {
78        let out_cell: Rc<RefCell<Option<Signal<T>>>> = Rc::new(RefCell::new(None));
79        let out_cell_c = out_cell.clone();
80        let producer_c = producer.clone();
81        let obs_id = reactive::new_observer(move || {
82            let value = producer_c();
83            let output = {
84                let cell = out_cell_c.borrow();
85                cell.as_ref().cloned()
86            };
87            if let Some(output) = output {
88                write(output, value);
89            } else {
90                let old = {
91                    let mut cell = out_cell_c.borrow_mut();
92                    cell.replace(Signal::new(value))
93                };
94                drop(old);
95            }
96        });
97
98        if let Err(payload) = reactive::try_run_observer_now(obs_id) {
99            reactive::remove_observer(obs_id);
100            std::panic::resume_unwind(payload);
101        }
102        let initialized = out_cell.borrow().is_some();
103        if !initialized {
104            reactive::remove_observer(obs_id);
105        }
106        let out = out_cell
107            .borrow()
108            .as_ref()
109            .cloned()
110            .unwrap_or_else(|| Signal::new(producer()));
111        (out, ProduceHandle { obs: obs_id })
112    });
113    if let Some(scope) = crate::scope::current_scope() {
114        let owner =
115            crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
116        let installed = crate::remember_with_key(
117            format!("produce-cleanup-installed:{full_key}:{owner}"),
118            || RefCell::new(false),
119        );
120        if !*installed.borrow() {
121            *installed.borrow_mut() = true;
122            let key = full_key.clone();
123            let cleanup_owner = owner.clone();
124            let cleanup = crate::Dispose::new(move || {
125                let (_released, removed) =
126                    crate::runtime::release_keyed_owner(&key, &cleanup_owner);
127                drop(removed);
128            });
129            let registered = cleanup.clone();
130            crate::runtime::register_keyed_disposer_for_owner(
131                full_key.clone(),
132                registered.clone(),
133                owner.clone(),
134            );
135            let key = full_key.clone();
136            let scope_owner = owner;
137            scope.add_disposer(move || {
138                crate::runtime::remove_keyed_disposer_for_owner(&key, &registered, &scope_owner);
139            });
140        }
141    }
142    Rc::new(rc.0.clone())
143}
144
145/// Local widget state that drives recomposition on every write.
146///
147/// Unlike [`crate::remember_state`] (a bare `Rc<RefCell<T>>` that never requests
148/// a frame), `Mutable` calls [`request_frame`] on `set`/`update` so async /
149/// timer / layout-callback mutations reliably re-render. Prefer [`Signal`] for
150/// shared/derived state; use `Mutable` for widget-local state that should
151/// always recompose.
152pub struct Mutable<T: 'static> {
153    value: Rc<RefCell<T>>,
154    dependency: Signal<()>,
155}
156
157impl<T: 'static> Clone for Mutable<T> {
158    fn clone(&self) -> Self {
159        Self {
160            value: self.value.clone(),
161            dependency: self.dependency.clone(),
162        }
163    }
164}
165
166impl<T: 'static> Mutable<T> {
167    pub fn new(v: T) -> Self {
168        Self {
169            value: Rc::new(RefCell::new(v)),
170            dependency: Signal::new(()),
171        }
172    }
173
174    fn track_read(&self) {
175        self.dependency.get();
176    }
177
178    fn changed(&self) {
179        reactive::signal_changed(self.dependency.id());
180        crate::signal_fired();
181        request_frame();
182    }
183
184    pub fn get(&self) -> Ref<'_, T> {
185        self.track_read();
186        self.value.borrow()
187    }
188
189    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
190        self.track_read();
191        let value = self.value.borrow();
192        let result = f(&value);
193        drop(value);
194        result
195    }
196
197    pub fn set(&self, v: T) {
198        let old = {
199            let mut value = self.value.borrow_mut();
200            std::mem::replace(&mut *value, v)
201        };
202        drop(old);
203        self.changed();
204    }
205
206    pub fn set_neq(&self, v: T)
207    where
208        T: PartialEq,
209    {
210        let old = {
211            let mut value = self.value.borrow_mut();
212            if *value == v {
213                return;
214            }
215            std::mem::replace(&mut *value, v)
216        };
217        drop(old);
218        self.changed();
219    }
220
221    pub fn update(&self, f: impl FnOnce(&mut T)) {
222        {
223            let mut value = self.value.borrow_mut();
224            f(&mut value);
225        }
226        self.changed();
227    }
228
229    pub fn update_neq(&self, f: impl FnOnce(&mut T))
230    where
231        T: PartialEq + Clone,
232    {
233        let changed = {
234            let mut value = self.value.borrow_mut();
235            let before = value.clone();
236            f(&mut value);
237            *value != before
238        };
239        if changed {
240            self.changed();
241        }
242    }
243
244    pub fn borrow_mut_silent(&self) -> RefMut<'_, T> {
245        self.value.borrow_mut()
246    }
247
248    pub fn as_rc(&self) -> Rc<RefCell<T>> {
249        self.value.clone()
250    }
251}
252
253/// Remember a [`Mutable`] in the current composition slot.
254#[track_caller]
255pub fn remember_mutable<T: 'static>(init: impl FnOnce() -> T) -> Mutable<T> {
256    crate::remember(|| Mutable::new(init())).as_ref().clone()
257}
258
259/// Key-based variant of [`remember_mutable`]. Stable across conditional branches.
260#[track_caller]
261pub fn remember_mutable_with_key<T: 'static>(
262    key: impl Into<String>,
263    init: impl FnOnce() -> T,
264) -> Mutable<T> {
265    remember_with_key(key, || Mutable::new(init()))
266        .as_ref()
267        .clone()
268}
269
270/// Auto-keyed variant of [`remember_mutable`]. Only safe where the number of
271/// calls at this site is fixed across frames; use
272/// [`remember_mutable_with_key`] for list items.
273#[track_caller]
274pub fn remember_mutable_auto<T: 'static>(slot: &str, init: impl FnOnce() -> T) -> Mutable<T> {
275    crate::remember_auto(slot, || Mutable::new(init()))
276        .as_ref()
277        .clone()
278}
279
280/// Remember a reducer-backed local state. Returns a [`Mutable`] snapshot reader
281/// plus a dispatch closure that runs `H::reduce` and writes the result back.
282///
283/// Prefer this for multi-field widget state over many loose `Mutable`s. It keeps
284/// the state shape and all mutations in one place.
285#[track_caller]
286pub fn remember_reducer<H: StateHolder>() -> (Mutable<H::State>, impl Fn(H::Event) + Clone)
287where
288    H::State: 'static,
289    H::Event: 'static,
290{
291    let state = remember_mutable(|| H::initial_state());
292    let dispatch = reducer_dispatch::<H>(state.clone());
293    (state, dispatch)
294}
295
296/// Key-based variant of [`remember_reducer`]. Stable across conditional branches.
297#[track_caller]
298pub fn remember_reducer_with_key<H: StateHolder>(
299    key: impl Into<String>,
300) -> (Mutable<H::State>, impl Fn(H::Event) + Clone)
301where
302    H::State: 'static,
303    H::Event: 'static,
304{
305    let state = remember_mutable_with_key(key, || H::initial_state());
306    let dispatch = reducer_dispatch::<H>(state.clone());
307    (state, dispatch)
308}
309
310/// Auto-keyed variant of [`remember_reducer`]. Only safe where the number of
311/// calls at this site is fixed across frames.
312#[track_caller]
313pub fn remember_reducer_auto<H: StateHolder>(
314    slot: &str,
315) -> (Mutable<H::State>, impl Fn(H::Event) + Clone)
316where
317    H::State: 'static,
318    H::Event: 'static,
319{
320    let state = remember_mutable_auto(slot, || H::initial_state());
321    let dispatch = reducer_dispatch::<H>(state.clone());
322    (state, dispatch)
323}
324
325fn reducer_dispatch<H: StateHolder>(state: Mutable<H::State>) -> impl Fn(H::Event) + Clone
326where
327    H::State: 'static,
328    H::Event: 'static,
329{
330    move |ev: H::Event| {
331        state.update(|s| *s = H::reduce(s, ev));
332    }
333}