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}