Skip to main content

egui_reactor/
state.rs

1//! [`State`], the hook guard, and [`Handle`], its `Copy` accessor.
2
3use std::cell::RefMut;
4use std::marker::PhantomData;
5use std::ops::{Deref, DerefMut};
6use std::panic::Location;
7
8use crate::store::{Slot, Store};
9
10/// Queue a write to the slot `id` for the end of the pass.
11///
12/// The closure is `'static`: it outlives the component body, so it cannot hold
13/// the slot reference and has to look the slot up again when it runs.
14fn queue_update<T: 'static>(store: &Store, id: egui::Id, f: impl FnOnce(&mut T) + 'static) {
15    store.defer_raw(Box::new(move |store| {
16        // The slot is gone if the component unmounted; drop the write.
17        let Some(slot) = store.slot_by_id(id) else {
18            return;
19        };
20        f(&mut slot.borrow_mut::<T>());
21        store.ctx().request_repaint();
22    }));
23}
24
25/// A guard over one `use_state` slot.
26///
27/// Derefs to `T`, so `*count += 1` works. The value lives in the store, so
28/// nothing is written back on drop; the only thing drop does is request a
29/// repaint if the value was mutably dereferenced.
30pub struct State<'s, T: 'static> {
31    // `Option` so that `into_handle` can release the `RefCell` borrow without
32    // moving out of a type that implements `Drop` (which would need `unsafe`).
33    inner: Option<RefMut<'s, T>>,
34    slot: &'s Slot,
35    store: &'s Store,
36    dirty: bool,
37}
38
39impl<'s, T: 'static> State<'s, T> {
40    pub(crate) fn new(
41        store: &'s Store,
42        slot: &'s Slot,
43        location: &'static Location<'static>,
44    ) -> Self {
45        // A slot that is already borrowed means two hooks share one id and the
46        // first guard is still alive. Report that instead of the bare
47        // `RefCell` "already mutably borrowed" panic.
48        let inner = slot.try_borrow_mut::<T>().unwrap_or_else(|| {
49            panic!(
50                "egui-reactor: hook id collision at {location}: the same hook \
51                 slot is already borrowed in this pass. Wrap custom hooks in \
52                 #[hook] (hook_scope) or add key= inside loops."
53            )
54        });
55        Self {
56            inner: Some(inner),
57            slot,
58            store,
59            dirty: false,
60        }
61    }
62
63    /// Consume the guard and return a `Copy` [`Handle`] to the same slot.
64    ///
65    /// This releases the guard's borrow. There is deliberately no
66    /// `handle(&self)`: using a `Handle` while the guard is alive would
67    /// double-borrow the same `RefCell` and panic.
68    pub fn into_handle(mut self) -> Handle<'s, T> {
69        self.inner = None;
70        Handle {
71            slot: self.slot,
72            store: self.store,
73            _t: PhantomData,
74        }
75        // `self` is dropped here, requesting a repaint if it was mutated.
76    }
77
78    /// Hand `&mut T` to a widget that writes into it directly.
79    ///
80    /// Unlike `&mut *state` this does *not* mark the state dirty, so a bound
81    /// widget does not ask for a repaint on every single frame. That is safe
82    /// because the widget only changes the value in response to input, and
83    /// input makes egui repaint anyway. This is what the `bind` prop of
84    /// `TextEdit`, `Checkbox`, `Slider` and `ComboBox` expects.
85    pub fn bind(&mut self) -> &mut T {
86        self.inner.as_mut().expect("state guard already released")
87    }
88
89    /// Queue a write for the end of the pass and request a repaint.
90    ///
91    /// This is the way out of "the loop borrows the state, so the handler
92    /// inside it cannot": `todos.update_later(move |t| t.remove(i))`. The
93    /// closure is `'static`, so captures need `move`.
94    ///
95    /// Calling this while the guard is alive is fine: the write happens long
96    /// after the guard is gone.
97    pub fn update_later(&self, f: impl FnOnce(&mut T) + 'static) {
98        queue_update(self.store, self.slot.id(), f);
99    }
100}
101
102impl<T: 'static> Deref for State<'_, T> {
103    type Target = T;
104
105    fn deref(&self) -> &T {
106        self.inner.as_ref().expect("state guard already released")
107    }
108}
109
110impl<T: 'static> DerefMut for State<'_, T> {
111    fn deref_mut(&mut self) -> &mut T {
112        self.dirty = true;
113        self.inner.as_mut().expect("state guard already released")
114    }
115}
116
117impl<T: 'static> Drop for State<'_, T> {
118    fn drop(&mut self) {
119        if self.dirty {
120            self.store.ctx().request_repaint();
121        }
122    }
123}
124
125/// A `Copy` accessor to one hook slot.
126///
127/// Unlike [`State`] it borrows the slot only for the duration of each call, so
128/// it can be stored in a struct or handed to a child component.
129pub struct Handle<'s, T: 'static> {
130    slot: &'s Slot,
131    store: &'s Store,
132    _t: PhantomData<fn() -> T>,
133}
134
135impl<T: 'static> Clone for Handle<'_, T> {
136    fn clone(&self) -> Self {
137        *self
138    }
139}
140
141impl<T: 'static> Copy for Handle<'_, T> {}
142
143impl<T: 'static> Handle<'_, T> {
144    /// Read a clone of the value.
145    pub fn get(&self) -> T
146    where
147        T: Clone,
148    {
149        self.slot.borrow::<T>().clone()
150    }
151
152    /// Read the value through a closure.
153    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
154        f(&self.slot.borrow::<T>())
155    }
156
157    /// Replace the value and request a repaint.
158    pub fn set(&self, value: T) {
159        *self.slot.borrow_mut::<T>() = value;
160        self.store.ctx().request_repaint();
161    }
162
163    /// The store id of the slot this handle points at.
164    pub(crate) fn slot_id(&self) -> egui::Id {
165        self.slot.id()
166    }
167
168    /// Mutate the value in place and request a repaint.
169    pub fn update<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
170        let r = f(&mut self.slot.borrow_mut::<T>());
171        self.store.ctx().request_repaint();
172        r
173    }
174
175    /// Queue a write for the end of the pass and request a repaint.
176    ///
177    /// See [`State::update_later`].
178    pub fn update_later(&self, f: impl FnOnce(&mut T) + 'static) {
179        queue_update(self.store, self.slot.id(), f);
180    }
181}
182
183impl<'s, T: 'static> Handle<'s, T> {
184    pub(crate) fn new(store: &'s Store, slot: &'s Slot) -> Self {
185        Self {
186            slot,
187            store,
188            _t: PhantomData,
189        }
190    }
191}