Skip to main content

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}