egui_reactor/store.rs
1//! The hook state store: a map from [`egui::Id`] to a slot holding one hook's value.
2
3use std::any::{Any, TypeId};
4use std::cell::{Cell, Ref, RefCell, RefMut};
5use std::collections::{BTreeSet, HashMap};
6use std::panic::Location;
7use std::rc::Rc;
8
9use elsa::{FrozenMap, FrozenVec};
10
11use crate::engine::Tree;
12use crate::engine::lite::LiteTree;
13
14/// A single hook's storage.
15///
16/// Slots are boxed inside a [`FrozenMap`], so a `&Slot` handed out by
17/// [`Store::slot`] stays valid while later hooks insert further slots.
18pub(crate) struct Slot {
19 id: egui::Id,
20 value: RefCell<Box<dyn Any>>,
21 last_visited: Cell<u64>,
22 cleanup: RefCell<Option<Box<dyn FnOnce()>>>,
23 deps_hash: Cell<Option<u64>>,
24 /// `use_memo` values, newest last.
25 ///
26 /// Old entries are kept until the end of the pass because a `&'s T` handed
27 /// out earlier in the same pass may still point at one.
28 memo: FrozenVec<Box<dyn Any>>,
29 /// `use_persisted` bookkeeping: the storage key and how to serialise `T`.
30 persist: Option<(String, ToJson)>,
31 #[allow(dead_code)]
32 location: &'static Location<'static>,
33}
34
35/// Serialise a slot value, given its concrete type at the `use_persisted` call.
36pub(crate) type ToJson = fn(&dyn Any) -> Option<String>;
37
38impl Slot {
39 /// The id this slot is stored under.
40 pub(crate) fn id(&self) -> egui::Id {
41 self.id
42 }
43
44 /// Borrow the slot value as `T`.
45 pub(crate) fn borrow<T: 'static>(&self) -> Ref<'_, T> {
46 Ref::map(self.value.borrow(), |v| {
47 (**v).downcast_ref::<T>().expect("hook slot type mismatch")
48 })
49 }
50
51 /// Mutably borrow the slot value as `T`.
52 pub(crate) fn borrow_mut<T: 'static>(&self) -> RefMut<'_, T> {
53 RefMut::map(self.value.borrow_mut(), |v| {
54 (**v).downcast_mut::<T>().expect("hook slot type mismatch")
55 })
56 }
57
58 /// Mutably borrow the slot value as `T`, or `None` if it is already borrowed.
59 pub(crate) fn try_borrow_mut<T: 'static>(&self) -> Option<RefMut<'_, T>> {
60 let value = self.value.try_borrow_mut().ok()?;
61 Some(RefMut::map(value, |v| {
62 (**v).downcast_mut::<T>().expect("hook slot type mismatch")
63 }))
64 }
65
66 /// The deps hash recorded by the last `use_effect` / `use_memo` run, if any.
67 pub(crate) fn deps_hash(&self) -> Option<u64> {
68 self.deps_hash.get()
69 }
70
71 /// Record the deps hash of the current `use_effect` / `use_memo` run.
72 pub(crate) fn set_deps_hash(&self, hash: u64) {
73 self.deps_hash.set(Some(hash));
74 }
75
76 /// Take the stored cleanup, leaving none behind.
77 pub(crate) fn take_cleanup(&self) -> Option<Box<dyn FnOnce()>> {
78 self.cleanup.borrow_mut().take()
79 }
80
81 /// Store the cleanup to run on the next deps change or on unmount.
82 pub(crate) fn set_cleanup(&self, cleanup: Option<Box<dyn FnOnce()>>) {
83 *self.cleanup.borrow_mut() = cleanup;
84 }
85
86 /// The newest memo value, if `use_memo` ever ran here.
87 pub(crate) fn memo_last(&self) -> Option<&dyn Any> {
88 let len = self.memo.len();
89 (len > 0).then(|| &self.memo[len - 1])
90 }
91
92 /// Append a memo value and borrow it for as long as the slot lives.
93 pub(crate) fn memo_push(&self, value: Box<dyn Any>) -> &dyn Any {
94 self.memo.push_get(value)
95 }
96
97 /// The storage key and serialiser, if this slot came from `use_persisted`.
98 fn persist(&self) -> Option<&(String, ToJson)> {
99 self.persist.as_ref()
100 }
101
102 /// Serialise the current value for persistence, if this slot is persisted.
103 fn to_json(&self) -> Option<(String, String)> {
104 let (key, to_json) = self.persist()?;
105 let value = self.value.try_borrow().ok()?;
106 Some((key.clone(), to_json(&**value)?))
107 }
108
109 /// Drop every memo value but the newest.
110 ///
111 /// Safe only between passes: within a pass, references handed out earlier
112 /// may still point at the older entries.
113 fn prune_memo(&mut self) {
114 let memo = self.memo.as_mut();
115 if memo.len() > 1 {
116 memo.drain(..memo.len() - 1);
117 }
118 }
119}
120
121/// One piece of work queued by `cx.defer` or `update_later`.
122pub(crate) type Deferred = Box<dyn FnOnce(&Store)>;
123
124/// A hook id that was requested twice within one pass.
125///
126/// This means two hooks share an id, usually because a custom hook is missing
127/// `#[hook]` / `hook_scope`, or because a `key` is missing inside a loop.
128#[derive(Clone, Copy, Debug)]
129pub struct Collision {
130 /// The id that was requested twice.
131 pub id: egui::Id,
132 /// The call site that requested it the second time.
133 pub location: &'static Location<'static>,
134}
135
136/// The overlay text shown for one colliding call site.
137fn collision_message(location: &Location<'static>) -> String {
138 format!(
139 "egui-reactor: hook id collision at {}:{}:{}. Wrap custom hooks in #[hook], or add key= inside loops.",
140 location.file(),
141 location.line(),
142 location.column(),
143 )
144}
145
146/// Owns every hook's state, keyed by [`egui::Id`].
147///
148/// The runner (or a test) calls [`Store::begin_pass`] before the tree is drawn
149/// and [`Store::end_pass`] after every guard has been dropped.
150pub struct Store {
151 slots: FrozenMap<egui::Id, Box<Slot>>,
152 pass: Cell<u64>,
153 ctx: egui::Context,
154 collisions: RefCell<Vec<Collision>>,
155 /// The `provide_context` stack: the slot id each type is currently bound to.
156 contexts: RefCell<Vec<(TypeId, egui::Id)>>,
157 /// The `<Suspense>` stack: how many `use_future`s are pending in each open
158 /// boundary, innermost last.
159 suspense: RefCell<Vec<usize>>,
160 /// Work queued by `cx.defer` and `update_later`, applied in `end_pass`.
161 deferred: RefCell<Vec<Deferred>>,
162 /// `use_persisted` values as JSON, keyed by the user's string key.
163 persisted: RefCell<HashMap<String, String>>,
164 /// Every key `use_persisted` has been called with in this process.
165 persisted_keys: RefCell<BTreeSet<String>>,
166 /// One taffy tree per `<View>` root, keyed by the root's layout id.
167 ///
168 /// Here rather than in egui memory: one map lookup per tree per frame with
169 /// no lock, and [`Store::end_pass`] drops a tree an unmounted subtree left
170 /// behind instead of keeping it in egui's `IdTypeMap` forever. Each tree is
171 /// behind its own `Rc<RefCell<..>>` so that a tree opened inside a leaf of
172 /// another one does not run into the outer tree's borrow.
173 trees: RefCell<HashMap<egui::Id, Rc<RefCell<Tree>>>>,
174 /// One lite row per `<VirtualList>` slot, keyed the same way.
175 ///
176 /// A slot that fell back to taffy keeps its (empty) entry here, because
177 /// that is where the "this row is on the taffy path" flag lives; it is what
178 /// makes the fallback stick instead of being decided again every frame.
179 lite_trees: RefCell<HashMap<egui::Id, Rc<RefCell<LiteTree>>>>,
180 /// Draw every row through taffy, whatever its styles say. For the parity
181 /// test, which draws the same row both ways.
182 force_taffy_rows: Cell<bool>,
183 /// How many hidden leaves are being drawn right now (nested leaves count up).
184 hidden: Cell<u32>,
185 warn_on_collision: bool,
186}
187
188/// How many passes a layout tree survives without being drawn.
189///
190/// About two seconds at 60 Hz; long enough for a scroll to bring a slot back,
191/// short enough that an unmounted subtree's trees do not linger. See
192/// `Store::sweep_trees`.
193const TREE_GRACE_PASSES: u64 = 120;
194
195/// The slot id of a `use_persisted` key.
196///
197/// Derived from the key alone, never from the call site: a persisted value has
198/// to survive edits to the source (3.4).
199pub(crate) fn persisted_id(key: &str) -> egui::Id {
200 egui::Id::new(("egui_reactor_persisted", key))
201}
202
203impl Default for Store {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209impl Store {
210 /// Create an empty store.
211 ///
212 /// The [`egui::Context`] used for repaint requests is a placeholder until
213 /// the first [`Store::begin_pass`].
214 pub fn new() -> Self {
215 Self {
216 slots: FrozenMap::new(),
217 pass: Cell::new(0),
218 ctx: egui::Context::default(),
219 collisions: RefCell::new(Vec::new()),
220 contexts: RefCell::new(Vec::new()),
221 suspense: RefCell::new(Vec::new()),
222 deferred: RefCell::new(Vec::new()),
223 persisted: RefCell::new(HashMap::new()),
224 persisted_keys: RefCell::new(BTreeSet::new()),
225 trees: RefCell::new(HashMap::new()),
226 lite_trees: RefCell::new(HashMap::new()),
227 force_taffy_rows: Cell::new(false),
228 hidden: Cell::new(0),
229 warn_on_collision: cfg!(debug_assertions),
230 }
231 }
232
233 /// Start a new pass: bump the pass counter and adopt `ctx` for repaints.
234 pub fn begin_pass(&mut self, ctx: &egui::Context) {
235 self.ctx = ctx.clone();
236 self.pass.set(self.pass.get() + 1);
237 self.collisions.borrow_mut().clear();
238 self.contexts.borrow_mut().clear();
239 self.suspense.borrow_mut().clear();
240 // A panic inside a hidden leaf must not leave the count raised.
241 self.hidden.set(0);
242 }
243
244 /// Mark everything drawn while the guard lives as hidden.
245 ///
246 /// `Cx::leaf` holds one while a `display="none"` leaf draws, so a tree the
247 /// leaf opens over its own `Ui` starts hidden too.
248 pub(crate) fn enter_hidden(&self) -> HiddenGuard<'_> {
249 self.hidden.set(self.hidden.get() + 1);
250 HiddenGuard { store: self }
251 }
252
253 /// Is a hidden leaf being drawn right now?
254 pub fn in_hidden(&self) -> bool {
255 self.hidden.get() > 0
256 }
257
258 /// Finish the pass: apply the deferred queue, then sweep, then warn.
259 ///
260 /// Every [`crate::State`] guard must have been dropped before this is
261 /// called. The deferred queue runs first so that a queued write lands on a
262 /// slot that is still alive, and the collision overlay last so that it is
263 /// painted over the frame it describes.
264 pub fn end_pass(&mut self) {
265 self.run_deferred();
266 self.sweep();
267 self.sweep_trees();
268 self.show_collision_overlay();
269 }
270
271 /// Drop every layout tree that has not been drawn for a while.
272 ///
273 /// A tree belongs to the `<View>` root that opened it, so a subtree that
274 /// unmounted takes its trees with it. It is not dropped the moment it
275 /// misses a pass, though: a `<VirtualList>` slot at the bottom of the
276 /// viewport comes and goes with every fractional scroll, and a tree that
277 /// was dropped in between has to draw its widgets in a sizing pass and ask
278 /// for a discard when it comes back, on every second frame. Keeping a tree
279 /// for [`TREE_GRACE_PASSES`] passes after its last draw costs the memory
280 /// of a few rows' layouts and nothing else.
281 fn sweep_trees(&mut self) {
282 let keep_from = self.pass.get().saturating_sub(TREE_GRACE_PASSES);
283 self.trees
284 .borrow_mut()
285 .retain(|_, tree| tree.borrow().last_visited() >= keep_from);
286 self.lite_trees
287 .borrow_mut()
288 .retain(|_, tree| tree.borrow().last_visited() >= keep_from);
289 }
290
291 /// Apply everything `cx.defer` / `update_later` queued, until nothing is left.
292 fn run_deferred(&mut self) {
293 loop {
294 let batch: Vec<Deferred> = std::mem::take(&mut *self.deferred.borrow_mut());
295 if batch.is_empty() {
296 return;
297 }
298 for f in batch {
299 f(self);
300 }
301 }
302 }
303
304 /// Drop every slot that was not visited this pass and run its cleanup.
305 fn sweep(&mut self) {
306 let pass = self.pass.get();
307 let mut cleanups = Vec::new();
308 let mut saved: Vec<(String, String)> = Vec::new();
309 let map: &mut HashMap<egui::Id, Box<Slot>> = self.slots.as_mut();
310 map.retain(|_, slot| {
311 let alive = slot.last_visited.get() >= pass;
312 if alive {
313 slot.prune_memo();
314 } else {
315 // Unmounted, but a persisted value must still survive to the
316 // next launch, so it is serialised before the slot is dropped.
317 saved.extend(slot.to_json());
318 cleanups.extend(slot.take_cleanup());
319 }
320 alive
321 });
322 if !saved.is_empty() {
323 let mut persisted = self.persisted.borrow_mut();
324 persisted.extend(saved);
325 }
326 // Cleanups are `'static` and cannot reach back into the store, but they
327 // are still run after the map borrow ends, to keep that obvious.
328 for cleanup in cleanups {
329 cleanup();
330 }
331 }
332
333 /// Paint the debug overlay listing this pass's id collisions.
334 fn show_collision_overlay(&self) {
335 if !self.warn_on_collision {
336 return;
337 }
338 let messages: BTreeSet<String> = self
339 .collisions
340 .borrow()
341 .iter()
342 .map(|c| collision_message(c.location))
343 .collect();
344 if messages.is_empty() {
345 return;
346 }
347 egui::Area::new(egui::Id::new("egui_reactor_collision_warning"))
348 .order(egui::Order::Debug)
349 .anchor(egui::Align2::LEFT_TOP, egui::vec2(8.0, 8.0))
350 .show(&self.ctx, |ui| {
351 egui::Frame::popup(ui.style()).show(ui, |ui| {
352 for message in &messages {
353 ui.colored_label(egui::Color32::RED, message);
354 }
355 });
356 });
357 }
358
359 /// Whether id collisions are drawn as an on-screen overlay.
360 ///
361 /// Defaults to `cfg!(debug_assertions)`.
362 pub fn warn_on_collision(&self) -> bool {
363 self.warn_on_collision
364 }
365
366 /// Turn the collision overlay on or off.
367 pub fn set_warn_on_collision(&mut self, warn: bool) {
368 self.warn_on_collision = warn;
369 }
370
371 /// The context repaints are requested on.
372 pub fn ctx(&self) -> &egui::Context {
373 &self.ctx
374 }
375
376 /// The current pass number. Starts at 1 after the first `begin_pass`.
377 pub fn pass(&self) -> u64 {
378 self.pass.get()
379 }
380
381 /// Id collisions recorded during the current pass.
382 pub fn collisions(&self) -> Vec<Collision> {
383 self.collisions.borrow().clone()
384 }
385
386 /// Number of live slots.
387 pub fn len(&self) -> usize {
388 self.slots.len()
389 }
390
391 /// Whether the store holds no slots.
392 pub fn is_empty(&self) -> bool {
393 self.len() == 0
394 }
395
396 /// Number of live layout trees, one per `<View>` root drawn last pass.
397 ///
398 /// For tests: it is how a test asks "did scrolling a list build a tree per
399 /// row?" without reaching into egui memory.
400 pub fn tree_count(&self) -> usize {
401 // A slot that fell back is counted once, by its taffy tree: its lite
402 // entry holds the flag and nothing else.
403 self.trees.borrow().len() + self.lite_row_count()
404 }
405
406 /// How many `<VirtualList>` rows were laid out without taffy last pass.
407 ///
408 /// For `tests/lite_parity.rs`, which has to know that the row it compared
409 /// really took the lite path.
410 #[doc(hidden)]
411 pub fn lite_row_count(&self) -> usize {
412 self.lite_trees
413 .borrow()
414 .values()
415 .filter(|tree| !tree.borrow().fallen_back())
416 .count()
417 }
418
419 /// Draw every `<VirtualList>` row through taffy, whatever its styles allow.
420 ///
421 /// For `tests/lite_parity.rs`, which draws one row both ways and compares
422 /// the rects. Nothing in the library reads it but
423 /// [`crate::Cx::container`].
424 #[doc(hidden)]
425 pub fn force_taffy_rows(&self, force: bool) {
426 self.force_taffy_rows.set(force);
427 }
428
429 /// Whether rows are being forced onto the taffy path.
430 pub(crate) fn taffy_rows_forced(&self) -> bool {
431 self.force_taffy_rows.get()
432 }
433
434 /// The lite row keyed by `id`, created on first use and marked as drawn in
435 /// this pass.
436 pub(crate) fn lite_tree(&self, id: egui::Id) -> Rc<RefCell<LiteTree>> {
437 let pass = self.pass.get();
438 let tree = {
439 let mut trees = self.lite_trees.borrow_mut();
440 Rc::clone(
441 trees
442 .entry(id)
443 .or_insert_with(crate::engine::lite::new_tree),
444 )
445 };
446 tree.borrow_mut().visit(pass);
447 tree
448 }
449
450 /// The layout tree keyed by `id`, created on first use and marked as drawn
451 /// in this pass.
452 pub(crate) fn tree(&self, id: egui::Id) -> Rc<RefCell<Tree>> {
453 let pass = self.pass.get();
454 let tree = {
455 let mut trees = self.trees.borrow_mut();
456 Rc::clone(trees.entry(id).or_insert_with(crate::engine::new_tree))
457 };
458 tree.borrow_mut().visit(pass);
459 tree
460 }
461
462 /// Enter a `<Suspense>` boundary: push a counter of its own.
463 ///
464 /// The stack is cleared by [`Store::begin_pass`], so an early return out of
465 /// a boundary cannot leak a counter into the next pass.
466 pub fn begin_suspense(&self) {
467 self.suspense.borrow_mut().push(0);
468 }
469
470 /// Leave the innermost `<Suspense>` boundary.
471 ///
472 /// Returns how many `use_future`s were pending inside it. Nested boundaries
473 /// keep their own count, so this only reports what nothing inner caught.
474 pub fn end_suspense(&self) -> usize {
475 self.suspense.borrow_mut().pop().unwrap_or(0)
476 }
477
478 /// Count one pending `use_future` against the innermost boundary.
479 ///
480 /// Does nothing outside a boundary: a `use_future` with no `<Suspense>`
481 /// above it just returns `Poll::Pending` to its component.
482 pub fn note_pending(&self) {
483 if let Some(count) = self.suspense.borrow_mut().last_mut() {
484 *count += 1;
485 }
486 }
487
488 /// Queue work to run at the end of the pass.
489 ///
490 /// The closure gets the store back, which is how `update_later` reaches
491 /// its slot without borrowing it across the pass.
492 pub(crate) fn defer_raw(&self, f: Deferred) {
493 self.deferred.borrow_mut().push(f);
494 }
495
496 /// Look up a slot without visiting it.
497 ///
498 /// Used by `use_context`, which reaches a slot some ancestor already
499 /// visited this pass rather than declaring a hook of its own.
500 pub(crate) fn slot_by_id(&self, id: egui::Id) -> Option<&Slot> {
501 self.slots.get(&id)
502 }
503
504 /// Push a context binding for the duration of a subtree.
505 pub(crate) fn push_context(&self, type_id: TypeId, slot: egui::Id) {
506 self.contexts.borrow_mut().push((type_id, slot));
507 }
508
509 /// Pop the most recent context binding.
510 pub(crate) fn pop_context(&self) {
511 self.contexts.borrow_mut().pop();
512 }
513
514 /// The innermost binding for `type_id`, if any.
515 pub(crate) fn lookup_context(&self, type_id: TypeId) -> Option<egui::Id> {
516 self.contexts
517 .borrow()
518 .iter()
519 .rev()
520 .find(|(t, _)| *t == type_id)
521 .map(|(_, id)| *id)
522 }
523
524 /// Get the slot for `id`, creating it with `init` on first visit.
525 ///
526 /// Visiting the same id twice in one pass is a collision and is recorded.
527 pub(crate) fn slot(
528 &self,
529 id: egui::Id,
530 location: &'static Location<'static>,
531 init: impl FnOnce() -> Box<dyn Any>,
532 ) -> &Slot {
533 let pass = self.pass.get();
534 if let Some(slot) = self.slots.get(&id) {
535 if slot.last_visited.get() == pass {
536 self.collisions
537 .borrow_mut()
538 .push(Collision { id, location });
539 log::warn!("egui-reactor: hook id collision at {location} (id {id:?})");
540 }
541 slot.last_visited.set(pass);
542 return slot;
543 }
544 self.slots.insert(
545 id,
546 Box::new(Slot {
547 id,
548 value: RefCell::new(init()),
549 last_visited: Cell::new(pass),
550 cleanup: RefCell::new(None),
551 deps_hash: Cell::new(None),
552 memo: FrozenVec::new(),
553 persist: None,
554 location,
555 }),
556 )
557 }
558
559 /// The JSON `load_persisted` restored for `key`, if any.
560 pub(crate) fn persisted_json(&self, key: &str) -> Option<String> {
561 self.persisted.borrow().get(key).cloned()
562 }
563
564 /// Get the slot behind a `use_persisted` key, creating it with `init`.
565 pub(crate) fn persisted_slot(
566 &self,
567 key: &str,
568 location: &'static Location<'static>,
569 to_json: ToJson,
570 init: impl FnOnce() -> Box<dyn Any>,
571 ) -> &Slot {
572 let id = persisted_id(key);
573 self.persisted_keys.borrow_mut().insert(key.to_owned());
574 let pass = self.pass.get();
575 if let Some(slot) = self.slots.get(&id) {
576 if slot.last_visited.get() == pass {
577 self.collisions
578 .borrow_mut()
579 .push(Collision { id, location });
580 log::warn!("egui-reactor: use_persisted key collision at {location} (key {key:?})");
581 }
582 slot.last_visited.set(pass);
583 return slot;
584 }
585 self.slots.insert(
586 id,
587 Box::new(Slot {
588 id,
589 value: RefCell::new(init()),
590 last_visited: Cell::new(pass),
591 cleanup: RefCell::new(None),
592 deps_hash: Cell::new(None),
593 memo: FrozenVec::new(),
594 persist: Some((key.to_owned(), to_json)),
595 location,
596 }),
597 )
598 }
599
600 /// Restore what [`Store::save_persisted`] produced.
601 ///
602 /// Call this before the first pass; keys that no `use_persisted` asks for
603 /// are kept as they are, so an unused value survives a run that never
604 /// mounted its component.
605 pub fn load_persisted(&mut self, json: &str) {
606 match serde_json::from_str::<HashMap<String, String>>(json) {
607 Ok(map) => *self.persisted.borrow_mut() = map,
608 Err(err) => log::warn!("egui-reactor: could not read persisted state: {err}"),
609 }
610 }
611
612 /// Serialise every `use_persisted` value into one JSON string.
613 ///
614 /// Values whose component is currently mounted are read from their slot;
615 /// unmounted ones come from what the sweep saved.
616 pub fn save_persisted(&self) -> String {
617 {
618 let keys: Vec<String> = self.persisted_keys.borrow().iter().cloned().collect();
619 let mut persisted = self.persisted.borrow_mut();
620 for key in keys {
621 if let Some(slot) = self.slots.get(&persisted_id(&key))
622 && let Some((key, json)) = slot.to_json()
623 {
624 persisted.insert(key, json);
625 }
626 }
627 }
628 serde_json::to_string(&*self.persisted.borrow()).unwrap_or_else(|err| {
629 log::warn!("egui-reactor: could not write persisted state: {err}");
630 String::from("{}")
631 })
632 }
633}
634
635/// What [`Store::enter_hidden`] returns: the count goes back down on drop.
636pub(crate) struct HiddenGuard<'s> {
637 store: &'s Store,
638}
639
640impl Drop for HiddenGuard<'_> {
641 fn drop(&mut self) {
642 self.store.hidden.set(self.store.hidden.get() - 1);
643 }
644}