Skip to main content

egui_reactor/
dispatch.rs

1//! [`Dispatch`] and `use_reducer`: the way to change state from outside a pass.
2
3use std::any::Any;
4use std::panic::Location;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use crate::cx::{Cx, location_key};
8use crate::state::State;
9
10/// A `Clone + Send` sender into one `use_reducer`'s message queue.
11///
12/// This is the only hook handle that outlives a pass, so it is what a
13/// background thread (and, later, `use_future`) holds to report back.
14pub struct Dispatch<M> {
15    queue: Arc<Mutex<Vec<M>>>,
16    ctx: egui::Context,
17}
18
19impl<M> Clone for Dispatch<M> {
20    fn clone(&self) -> Self {
21        Self {
22            queue: Arc::clone(&self.queue),
23            ctx: self.ctx.clone(),
24        }
25    }
26}
27
28impl<M> std::fmt::Debug for Dispatch<M> {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("Dispatch")
31            .field("pending", &self.queue.lock().map(|q| q.len()).unwrap_or(0))
32            .finish()
33    }
34}
35
36impl<M> Dispatch<M> {
37    fn new(queue: Arc<Mutex<Vec<M>>>, ctx: egui::Context) -> Self {
38        Self { queue, ctx }
39    }
40
41    /// Queue a message and ask for a repaint.
42    ///
43    /// The reducer runs the next time the hook is visited, which the repaint
44    /// request guarantees will happen.
45    pub fn send(&self, msg: M) {
46        lock(&self.queue).push(msg);
47        self.ctx.request_repaint();
48    }
49}
50
51/// Lock the queue, ignoring poisoning: a panicking reducer must not wedge the app.
52fn lock<M>(queue: &Mutex<Vec<M>>) -> MutexGuard<'_, Vec<M>> {
53    queue.lock().unwrap_or_else(|e| e.into_inner())
54}
55
56/// Hold `S` in the store and fold queued messages into it with `reducer`.
57///
58/// Messages are applied *when the hook is visited*, not at the end of the
59/// pass. That keeps `reducer` an ordinary closure (a pass-end reducer would
60/// have to be stored, hence `'static`) and saves a frame of latency for
61/// messages that arrived from another thread.
62#[track_caller]
63pub fn use_reducer<'s, S: 'static, M: Send + 'static>(
64    cx: &mut Cx<'s, '_>,
65    mut reducer: impl FnMut(&mut S, M),
66    init: impl FnOnce() -> S,
67) -> (State<'s, S>, Dispatch<M>) {
68    let location = Location::caller();
69    let store = cx.store;
70    let id = cx.scope_id().with(location_key(location));
71
72    // The queue lives in a slot of its own so that the state slot holds a bare
73    // `S`, which is what `State` and `update_later` expect to downcast to.
74    let queue_slot = store.slot(id.with("__egui_reactor_reducer_queue"), location, || {
75        Box::new(Arc::new(Mutex::new(Vec::<M>::new()))) as Box<dyn Any>
76    });
77    let queue: Arc<Mutex<Vec<M>>> = Arc::clone(&queue_slot.borrow::<Arc<Mutex<Vec<M>>>>());
78
79    let slot = store.slot(id, location, || Box::new(init()) as Box<dyn Any>);
80    let mut state = State::new(store, slot, location);
81
82    // Draining here is also what makes a second pass a no-op: the queue is
83    // already empty when the hook is visited again in the same frame.
84    let pending: Vec<M> = std::mem::take(&mut *lock(&queue));
85    for msg in pending {
86        reducer(&mut state, msg);
87    }
88
89    let dispatch = Dispatch::new(queue, store.ctx().clone());
90    (state, dispatch)
91}