egui_reactor/future.rs
1//! `use_future` and [`spawn`]: work that spans frames.
2
3use std::any::Any;
4use std::cell::Cell;
5use std::future::Future;
6use std::hash::Hash;
7use std::panic::Location;
8use std::sync::{Arc, Mutex, MutexGuard};
9use std::task::Poll;
10
11use crate::cx::{Cx, location_key};
12use crate::hooks::deps_hash;
13
14/// A future [`use_future`] and [`spawn`] can run.
15///
16/// Native polls it on a thread of its own, so it has to be `Send`; wasm polls it
17/// on the one browser thread, so it does not. That is the only difference
18/// between the platforms, and it lives here so that user code names one trait.
19#[cfg(not(target_arch = "wasm32"))]
20pub trait SpawnFuture<T>: Future<Output = T> + Send + 'static {}
21
22#[cfg(not(target_arch = "wasm32"))]
23impl<T, F: Future<Output = T> + Send + 'static> SpawnFuture<T> for F {}
24
25/// A future [`use_future`] and [`spawn`] can run.
26///
27/// See the native version: on wasm the future need not be `Send`.
28#[cfg(target_arch = "wasm32")]
29pub trait SpawnFuture<T>: Future<Output = T> + 'static {}
30
31#[cfg(target_arch = "wasm32")]
32impl<T, F: Future<Output = T> + 'static> SpawnFuture<T> for F {}
33
34/// The one place that knows how a future is actually run.
35mod task {
36 /// Run `fut` on a thread of its own.
37 ///
38 /// One thread per future, no pool: this is meant for futures that wait
39 /// (HTTP, file IO). A future that burns CPU should hand that off itself.
40 #[cfg(not(target_arch = "wasm32"))]
41 pub(crate) fn spawn(fut: impl Future<Output = ()> + Send + 'static) {
42 if let Err(err) = std::thread::Builder::new()
43 .name(String::from("egui-reactor-future"))
44 .spawn(move || pollster::block_on(fut))
45 {
46 // The future is dropped and the hook stays `Pending`. Losing a
47 // thread is not worth taking the app down for.
48 log::error!("egui-reactor: could not spawn a thread for a future: {err}");
49 }
50 }
51
52 /// Run `fut` on the browser's event loop.
53 #[cfg(target_arch = "wasm32")]
54 pub(crate) fn spawn(fut: impl Future<Output = ()> + 'static) {
55 wasm_bindgen_futures::spawn_local(fut);
56 }
57}
58
59/// Run a future to completion and throw its result away.
60///
61/// This is the imperative counterpart of [`use_future`]: start something from an
62/// event handler and report back with a
63/// [`Dispatch`](crate::Dispatch), which is `Send` and requests a repaint.
64///
65/// ```no_run
66/// # use egui_reactor::prelude::*;
67/// # fn demo(dispatch: Dispatch<u32>) {
68/// spawn(async move { dispatch.send(41) });
69/// # }
70/// ```
71pub fn spawn(fut: impl SpawnFuture<()>) {
72 task::spawn(fut);
73}
74
75/// Where a running future leaves its result for the next visit.
76///
77/// The cell is shared with the future, so it outlives the slot: a result that
78/// arrives after the component unmounted is written to a cell nobody reads.
79struct Inbox<T> {
80 /// `(generation, value)`: which launch produced the result, and the result.
81 cell: Arc<Mutex<Option<(u64, T)>>>,
82 /// The generation of the most recent launch.
83 generation: Cell<u64>,
84}
85
86/// Lock the inbox, ignoring poisoning: a panicking future must not wedge the app.
87fn lock<T>(cell: &Mutex<Option<(u64, T)>>) -> MutexGuard<'_, Option<(u64, T)>> {
88 cell.lock().unwrap_or_else(|e| e.into_inner())
89}
90
91/// Run the future `f` builds, and report where it has got to.
92///
93/// `f` is called at the call site whenever the hash of `deps` changes (and on
94/// the first visit), so it can read locals and `State` guards while it builds
95/// the future. The future itself is `'static`, so it has to own what it needs.
96///
97/// The result lands on the visit *after* the future finished; finishing asks for
98/// a repaint, so that visit happens without the user touching anything. Changing
99/// `deps` builds a new future and drops whatever the old one eventually returns;
100/// it does not stop the old one, because a future cannot be stopped.
101///
102/// Like [`use_memo`](crate::use_memo) the reference borrows the store rather
103/// than the `Cx`, so it can be read next to a `State` guard. A child component
104/// waits with a `let`-`else`, which is where React would throw:
105///
106/// ```no_run
107/// # use egui_reactor::prelude::*;
108/// # fn load(url: String) -> impl Future<Output = String> + Send + 'static { async { url } }
109/// #[component]
110/// fn Body(cx: &mut Cx, url: &str) {
111/// let text = use_future(cx, url, || load(url.to_owned()));
112/// let Poll::Ready(text) = text else { return };
113/// cx.ui().label(text.clone());
114/// }
115/// ```
116///
117/// While the hook is `Pending` it counts towards the nearest `<Suspense>`
118/// boundary, which draws its fallback instead of its children.
119#[track_caller]
120pub fn use_future<'s, D, T, F>(cx: &mut Cx<'s, '_>, deps: D, f: impl FnOnce() -> F) -> &'s Poll<T>
121where
122 D: Hash,
123 T: Send + 'static,
124 F: SpawnFuture<T>,
125{
126 let location = Location::caller();
127 let store = cx.store;
128 let id = cx.scope_id().with(location_key(location));
129
130 // Two slots, as in `use_reducer`: the state slot keeps the deps hash and the
131 // `Poll` values, the inbox slot the cell the running future writes into.
132 let inbox_slot = store.slot(id.with("__egui_reactor_future_inbox"), location, || {
133 Box::new(Inbox::<T> {
134 cell: Arc::new(Mutex::new(None)),
135 generation: Cell::new(0),
136 }) as Box<dyn Any>
137 });
138 let slot = store.slot(id, location, || Box::new(()) as Box<dyn Any>);
139
140 let hash = deps_hash(&deps);
141 if slot.deps_hash() != Some(hash) {
142 let (cell, generation) = {
143 let inbox = inbox_slot.borrow::<Inbox<T>>();
144 inbox.generation.set(inbox.generation.get() + 1);
145 (Arc::clone(&inbox.cell), inbox.generation.get())
146 };
147 let fut = f();
148 let ctx = store.ctx().clone();
149 // The launch is only ever `Pending` here: the result is picked up on the
150 // next visit, which the repaint request below guarantees will come.
151 slot.memo_push(Box::new(Poll::<T>::Pending) as Box<dyn Any>);
152 slot.set_deps_hash(hash);
153 task::spawn(async move {
154 let value = fut.await;
155 {
156 let mut inbox = lock(&cell);
157 // Two futures can finish in either order between two visits, so
158 // an older one must not overwrite a newer one's result.
159 let newer = inbox.as_ref().is_none_or(|(seen, _)| generation > *seen);
160 if newer {
161 *inbox = Some((generation, value));
162 }
163 }
164 ctx.request_repaint();
165 });
166 } else {
167 let arrived = {
168 let inbox = inbox_slot.borrow::<Inbox<T>>();
169 // Taking unconditionally is what drops a stale result: its deps are
170 // gone and the current future is still on its way.
171 let taken = lock(&inbox.cell).take();
172 taken.filter(|(generation, _)| *generation == inbox.generation.get())
173 };
174 if let Some((_, value)) = arrived {
175 slot.memo_push(Box::new(Poll::Ready(value)) as Box<dyn Any>);
176 }
177 }
178
179 let value = slot
180 .memo_last()
181 .expect("use_future pushes a Poll on every launch")
182 .downcast_ref::<Poll<T>>()
183 .expect("future slot type mismatch");
184 if value.is_pending() {
185 store.note_pending();
186 }
187 value
188}