egui_async/bind.rs
1//! Core state management for asynchronous operations.
2//!
3//! This module provides the `Bind` struct, which is the heart of `egui-async`. It acts as a
4//! state machine to manage the lifecycle of a `Future`, from initiation to completion, and
5//! holds the resulting data or error.
6use std::{fmt::Debug, future::Future};
7
8use atomic_float::AtomicF64;
9use tokio::sync::oneshot;
10use tracing::warn;
11
12/// The `egui` time of the current frame, updated by `ContextExt::loop_handle`.
13pub static CURR_FRAME: AtomicF64 = AtomicF64::new(0.0);
14/// The `egui` time of the previous frame, updated by `ContextExt::loop_handle`.
15pub static LAST_FRAME: AtomicF64 = AtomicF64::new(0.0);
16
17/// A lazily initialized Tokio runtime for executing async tasks on non-WASM targets.
18#[cfg(not(target_family = "wasm"))]
19pub static ASYNC_RUNTIME: std::sync::LazyLock<tokio::runtime::Runtime> =
20 std::sync::LazyLock::new(|| {
21 tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime.")
22 });
23
24/// A global holder for the `egui::Context`, used to request repaints from background tasks.
25///
26/// This is initialized once by `egui::ContextExt::loop_handle`.
27#[cfg(feature = "egui")]
28pub static CTX: std::sync::OnceLock<egui::Context> = std::sync::OnceLock::new();
29
30/// Represents the execution state of an asynchronous operation managed by `Bind`.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
32pub enum State {
33 /// No operation is running, and no data is available from a previous run.
34 #[default]
35 Idle,
36 /// An operation is currently in-flight.
37 Pending,
38 /// An operation has completed, and its result (success or error) is available.
39 Finished,
40}
41
42/// Represents the detailed state of a `Bind`, including available data.
43pub enum StateWithData<'a, T, E> {
44 /// No operation is running.
45 Idle,
46 /// An operation is currently in-flight.
47 Pending,
48 /// An operation has completed with a successful result.
49 Finished(&'a T),
50 /// An operation has completed with an error.
51 Failed(&'a E),
52}
53
54/// A state manager for a single asynchronous operation, designed for use with `egui`.
55///
56/// `Bind` tracks the lifecycle of a `Future` and stores its `Result<T, E>`. It acts as a
57/// bridge between the immediate-mode UI and the background async task, ensuring the UI
58/// can react to changes in state (e.g., show a spinner while `Pending`, display the
59/// result when `Finished`, or show an error).
60pub struct Bind<T, E> {
61 /// The `egui` time of the most recent frame where this `Bind` was polled.
62 drawn_time_last: f64,
63 /// The `egui` time of the second most recent frame where this `Bind` was polled.
64 drawn_time_prev: f64,
65
66 /// The result of the completed async operation. `None` if the task is not `Finished`.
67 pub(crate) data: Option<Result<T, E>>,
68 /// The receiving end of a one-shot channel used to get the result from the background task.
69 /// This is `Some` only when the state is `Pending`.
70 recv: Option<oneshot::Receiver<Result<T, E>>>,
71
72 /// The current execution state of the async operation.
73 pub(crate) state: State,
74 /// The `egui` time when the most recent operation was started.
75 last_start_time: f64,
76 /// The `egui` time when the most recent operation was completed.
77 last_complete_time: f64,
78
79 /// If `true`, the `data` from a `Finished` state is preserved even if the `Bind` instance
80 /// is not polled for one or more frames. If `false`, the data is cleared.
81 retain: bool,
82
83 /// A counter for how many times an async operation has been started.
84 times_executed: usize,
85}
86
87impl<T, E> Debug for Bind<T, E> {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 let mut out = f.debug_struct("Bind");
90 let mut out = out
91 .field("state", &self.state)
92 .field("retain", &self.retain)
93 .field("drawn_time_last", &self.drawn_time_last)
94 .field("drawn_time_prev", &self.drawn_time_prev)
95 .field("last_start_time", &self.last_start_time)
96 .field("last_complete_time", &self.last_complete_time)
97 .field("times_executed", &self.times_executed);
98
99 // Avoid printing the full data/recv content for cleaner debug output.
100 if self.data.is_some() {
101 out = out.field("data", &"Some(...)");
102 } else {
103 out = out.field("data", &"None");
104 }
105
106 if self.recv.is_some() {
107 out = out.field("recv", &"Some(...)");
108 } else {
109 out = out.field("recv", &"None");
110 }
111
112 out.finish()
113 }
114}
115
116impl<T: 'static, E: 'static> Default for Bind<T, E> {
117 /// Creates a default `Bind` instance in an `Idle` state.
118 ///
119 /// The `retain` flag is set to `false`. This implementation does not require `T` or `E`
120 /// to implement `Default`.
121 fn default() -> Self {
122 Self::new(false)
123 }
124}
125
126/// A trait alias for `Send` on native targets.
127///
128/// On WASM, this trait has no bounds, allowing non-`Send` types to be used in `Bind`
129/// since WASM is single-threaded.
130#[cfg(not(target_family = "wasm"))]
131pub trait MaybeSend: Send {}
132#[cfg(not(target_family = "wasm"))]
133impl<T: Send> MaybeSend for T {}
134
135/// A trait alias with no bounds on WASM targets.
136///
137/// This allows `Bind` to work with `!Send` futures and data types in a single-threaded
138/// web environment.
139#[cfg(target_family = "wasm")]
140pub trait MaybeSend {}
141#[cfg(target_family = "wasm")]
142impl<T> MaybeSend for T {}
143
144impl<T: 'static, E: 'static> Bind<T, E> {
145 /// Creates a new `Bind` instance with a specific retain policy.
146 ///
147 /// # Parameters
148 /// - `retain`: If `true`, the result of the operation is kept even if the `Bind`
149 /// is not polled in a frame. If `false`, the result is cleared if not polled
150 /// for one frame, returning the `Bind` to an `Idle` state.
151 #[must_use]
152 pub const fn new(retain: bool) -> Self {
153 Self {
154 drawn_time_last: 0.0,
155 drawn_time_prev: 0.0,
156 data: None,
157 recv: None,
158 state: State::Idle,
159 last_start_time: 0.0,
160 last_complete_time: f64::MIN, // Set to a very low value to ensure `since_completed` is large initially.
161 retain,
162 times_executed: 0,
163 }
164 }
165
166 /// Internal helper to prepare the state and communication channel for a new async request.
167 #[allow(clippy::type_complexity)]
168 fn prepare_channel(
169 &mut self,
170 ) -> (
171 oneshot::Sender<Result<T, E>>,
172 oneshot::Receiver<Result<T, E>>,
173 ) {
174 self.poll(); // Ensure state is up-to-date before starting.
175
176 self.last_start_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
177 self.state = State::Pending;
178
179 oneshot::channel()
180 }
181
182 /// Internal async function that awaits the user's future and sends the result back.
183 async fn req_inner<F>(fut: F, tx: oneshot::Sender<Result<T, E>>)
184 where
185 F: Future<Output = Result<T, E>> + 'static,
186 T: MaybeSend,
187 {
188 let result = fut.await;
189 if matches!(tx.send(result), Ok(())) {
190 // If the send was successful, request a repaint to show the new data.
191 #[cfg(feature = "egui")]
192 if let Some(ctx) = CTX.get() {
193 ctx.request_repaint();
194 }
195 } else {
196 // This occurs if the `Bind` was dropped before the future completed.
197 warn!("Future result was dropped because the receiver was gone.");
198 }
199 }
200
201 /// Starts an asynchronous operation if the `Bind` is not already `Pending`.
202 ///
203 /// The provided future `f` is spawned onto the appropriate runtime (`tokio` for native,
204 /// `wasm-bindgen-futures` for WASM). The `Bind` state transitions to `Pending`.
205 ///
206 /// This method calls `poll()` internally.
207 pub fn request<Fut>(&mut self, f: Fut)
208 where
209 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
210 T: MaybeSend,
211 E: MaybeSend,
212 {
213 #[cfg(not(target_family = "wasm"))]
214 {
215 let (tx, rx) = self.prepare_channel();
216 ASYNC_RUNTIME.spawn(Self::req_inner(f, tx));
217 self.recv = Some(rx);
218 }
219
220 #[cfg(target_family = "wasm")]
221 {
222 let (tx, rx) = self.prepare_channel();
223 wasm_bindgen_futures::spawn_local(Self::req_inner(f, tx));
224 self.recv = Some(rx);
225 }
226
227 self.times_executed += 1;
228 }
229
230 /// Requests an operation to run periodically.
231 ///
232 /// If the `Bind` is not `Pending` and more than `secs` seconds have passed since the
233 /// last completion, a new request is started by calling `f`.
234 ///
235 /// # Returns
236 /// The time in seconds remaining until the next scheduled refresh. A negative value
237 /// indicates a refresh is overdue.
238 pub fn request_every_sec<Fut>(&mut self, f: impl FnOnce() -> Fut, secs: f64) -> f64
239 where
240 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
241 T: MaybeSend,
242 E: MaybeSend,
243 {
244 let since_completed = self.since_completed();
245
246 if self.get_state() != State::Pending && since_completed > secs {
247 self.request(f());
248 }
249
250 secs - since_completed
251 }
252
253 /// Clears any existing data and immediately starts a new async operation.
254 ///
255 /// If an operation was `Pending`, its result will be discarded. The background task is not
256 /// cancelled and will run to completion.
257 ///
258 /// This is a convenience method equivalent to calling `clear()` followed by `request()`.
259 pub fn refresh<Fut>(&mut self, f: Fut)
260 where
261 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
262 T: MaybeSend,
263 E: MaybeSend,
264 {
265 self.clear();
266 self.request(f);
267 }
268
269 /// Takes ownership of the result if the operation is `Finished`.
270 ///
271 /// If the state is `Finished`, this method returns `Some(result)`, consumes the data
272 /// internally, and resets the state to `Idle`. If the state is not `Finished`,
273 /// it returns `None`.
274 ///
275 /// This method calls `poll()` internally.
276 pub fn take(&mut self) -> Option<Result<T, E>> {
277 self.poll();
278
279 if matches!(self.state, State::Finished) {
280 assert!(
281 self.data.is_some(),
282 "State was Finished but data was None. This indicates a bug."
283 );
284 self.state = State::Idle;
285 self.data.take()
286 } else {
287 None
288 }
289 }
290
291 /// Manually sets the data and moves the state to `Finished`.
292 ///
293 /// This can be used to inject data into the `Bind` without running an async operation.
294 ///
295 /// # Panics
296 /// Panics if the current state is not `Idle`.
297 pub fn fill(&mut self, data: Result<T, E>) {
298 self.poll();
299
300 assert!(
301 matches!(self.state, State::Idle),
302 "Cannot fill a Bind that is not Idle."
303 );
304
305 self.state = State::Finished;
306 self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
307 self.data = Some(data);
308 }
309
310 /// Checks if the current state is `Idle`.
311 /// This method calls `poll()` internally.
312 pub fn is_idle(&mut self) -> bool {
313 self.poll();
314 matches!(self.state, State::Idle)
315 }
316
317 /// Checks if the current state is `Pending`.
318 /// This method calls `poll()` internally.
319 pub fn is_pending(&mut self) -> bool {
320 self.poll();
321 matches!(self.state, State::Pending)
322 }
323
324 /// Checks if the current state is `Finished`.
325 /// This method calls `poll()` internally.
326 pub fn is_finished(&mut self) -> bool {
327 self.poll();
328 matches!(self.state, State::Finished)
329 }
330
331 /// Returns `true` if the operation finished during the current `egui` frame.
332 /// This method calls `poll()` internally.
333 #[allow(clippy::float_cmp)]
334 pub fn just_completed(&mut self) -> bool {
335 self.poll();
336 self.last_complete_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
337 }
338
339 /// If the operation just completed this frame, invokes the provided closure with
340 /// a reference to the result.
341 pub fn on_finished(&mut self, f: impl FnOnce(&Result<T, E>)) {
342 if self.just_completed()
343 && let Some(ref d) = self.data
344 {
345 f(d);
346 }
347 }
348
349 /// Returns `true` if the operation started during the current `egui` frame.
350 /// This method calls `poll()` internally.
351 #[allow(clippy::float_cmp)]
352 pub fn just_started(&mut self) -> bool {
353 self.poll();
354 self.last_start_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
355 }
356
357 /// Gets the `egui` time when the operation started.
358 /// This method calls `poll()` internally.
359 pub fn get_start_time(&mut self) -> f64 {
360 self.poll();
361 self.last_start_time
362 }
363
364 /// Gets the `egui` time when the operation completed.
365 /// This method calls `poll()` internally.
366 pub fn get_complete_time(&mut self) -> f64 {
367 self.poll();
368 self.last_complete_time
369 }
370
371 /// Gets the duration between the start and completion of the operation.
372 /// This method calls `poll()` internally.
373 pub fn get_elapsed(&mut self) -> f64 {
374 self.poll();
375 self.last_complete_time - self.last_start_time
376 }
377
378 /// Gets the time elapsed since the operation started.
379 /// This method calls `poll()` internally.
380 pub fn since_started(&mut self) -> f64 {
381 self.poll();
382 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_start_time
383 }
384
385 /// Gets the time elapsed since the operation completed.
386 /// This method calls `poll()` internally.
387 pub fn since_completed(&mut self) -> f64 {
388 self.poll();
389 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_complete_time
390 }
391
392 /// Returns an immutable reference to the stored data, if any.
393 /// This method calls `poll()` internally.
394 pub fn read(&mut self) -> &Option<Result<T, E>> {
395 self.poll();
396 &self.data
397 }
398 /// Returns an immutable reference in the ref pattern to the stored data, if any.
399 /// This method calls `poll()` internally.
400 pub fn read_as_ref(&mut self) -> Option<Result<&T, &E>> {
401 self.poll();
402 self.data.as_ref().map(Result::as_ref)
403 }
404
405 /// Returns a mutable reference to the stored data, if any.
406 /// This method calls `poll()` internally.
407 pub fn read_mut(&mut self) -> &mut Option<Result<T, E>> {
408 self.poll();
409 &mut self.data
410 }
411 /// Returns a mutable reference in the ref pattern to the stored data, if any.
412 /// This method calls `poll()` internally.
413 pub fn read_as_mut(&mut self) -> Option<Result<&mut T, &mut E>> {
414 self.poll();
415 self.data.as_mut().map(Result::as_mut)
416 }
417
418 /// Returns the current `State` of the binding.
419 /// This method calls `poll()` internally.
420 pub fn get_state(&mut self) -> State {
421 self.poll();
422 self.state
423 }
424
425 /// Returns the ref filled state of the `Bind`, allowing for exhaustive pattern matching.
426 ///
427 /// This is often the most ergonomic way to display UI based on the `Bind`'s state.
428 /// This method calls `poll()` internally.
429 ///
430 /// # Example
431 /// ```ignore
432 /// match my_bind.state() {
433 /// StateWithData::Idle => { /* ... */ }
434 /// StateWithData::Pending => { ui.spinner(); }
435 /// StateWithData::Finished(data) => { ui.label(format!("Data: {data:?}")); }
436 /// StateWithData::Failed(err) => { ui.label(format!("Error: {err:?}")); }
437 /// }
438 /// ```
439 pub fn state(&mut self) -> StateWithData<'_, T, E> {
440 self.poll();
441 match self.state {
442 State::Idle => StateWithData::Idle,
443 State::Pending => StateWithData::Pending,
444 State::Finished => match self.data.as_ref() {
445 Some(Ok(data)) => StateWithData::Finished(data),
446 Some(Err(err)) => StateWithData::Failed(err),
447 None => {
448 // This case should be unreachable due to internal invariants.
449 // If state is Finished, data must be Some.
450 self.state = State::Idle;
451 StateWithData::Idle
452 }
453 },
454 }
455 }
456
457 /// Returns the ref filled state or starts a new request if idle.
458 ///
459 /// This method is an ergonomic way to drive a UI. If the `Bind` is `Idle` and has no
460 /// data, it immediately calls the provided closure `f` to start an async operation,
461 /// transitioning the state to `Pending`.
462 ///
463 /// In all cases, it returns the current `StateWithData` for immediate use in a `match`
464 /// statement, making it easy to display a loading indicator, the finished data, or an error.
465 ///
466 /// # Example
467 /// ```ignore
468 /// // In your UI update function:
469 /// match my_bind.state_or_request(fetch_data) {
470 /// StateWithData::Idle => { /* This branch is typically not reached on the first call */ }
471 /// StateWithData::Pending => { ui.spinner(); }
472 /// StateWithData::Finished(data) => { ui.label(format!("Data: {:?}", data)); }
473 /// StateWithData::Failed(err) => { ui.label(format!("Error: {:?}", err)); }
474 /// }
475 /// ```
476 pub fn state_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> StateWithData<'_, T, E>
477 where
478 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
479 T: MaybeSend,
480 E: MaybeSend,
481 {
482 self.poll();
483
484 if self.data.is_none() && matches!(self.state, State::Idle) {
485 self.request(f());
486 }
487 self.state()
488 }
489
490 /// Clears any stored data and resets the state to `Idle`.
491 ///
492 /// If an operation was `Pending`, its result will be discarded. The background task is not
493 /// cancelled and will run to completion.
494 ///
495 /// This method calls `poll()` internally.
496 pub fn clear(&mut self) {
497 self.poll();
498 self.state = State::Idle;
499 self.data = None;
500 }
501
502 /// Returns a reference to the data, or starts a new request if idle.
503 ///
504 /// If data is already available (`Finished`), it returns a reference to it.
505 /// If the state is `Idle` and no data is present, it calls `f` to start a new async
506 /// operation and returns `None`.
507 /// If `Pending`, it returns `None`.
508 ///
509 /// This method calls `poll()` internally.
510 pub fn read_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&Result<T, E>>
511 where
512 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
513 T: MaybeSend,
514 E: MaybeSend,
515 {
516 self.poll();
517
518 if self.data.is_none() && matches!(self.state, State::Idle) {
519 self.request(f());
520 }
521 self.data.as_ref()
522 }
523
524 /// Returns a mutable reference to the data, or starts a new request if idle.
525 ///
526 /// This is the mutable version of `read_or_request`.
527 ///
528 /// This method calls `poll()` internally.
529 pub fn read_mut_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&mut Result<T, E>>
530 where
531 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
532 T: MaybeSend,
533 E: MaybeSend,
534 {
535 self.poll();
536
537 if self.data.is_none() && matches!(self.state, State::Idle) {
538 self.request(f());
539 }
540 self.data.as_mut()
541 }
542
543 /// Drives the state machine. This should be called once per frame before accessing state.
544 ///
545 /// **Note**: Most other methods on `Bind` call this internally, so you usually don't
546 /// need to call it yourself.
547 ///
548 /// This method performs several key actions:
549 /// 1. Checks if a pending future has completed and, if so, updates the state to `Finished`.
550 /// 2. Updates internal frame timers used for `retain` logic and time tracking.
551 /// 3. If `retain` is `false`, it clears the data if the `Bind` was not polled in the previous frame.
552 ///
553 /// # Panics
554 /// - Panics if the state is `Pending` but the internal receiver is missing. This indicates a bug in `egui-async`.
555 /// - Panics if the `oneshot` channel's sender is dropped without sending a value, which would mean the
556 /// spawned task terminated unexpectedly.
557 pub fn poll(&mut self) {
558 let curr_frame = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
559
560 // Avoid re-polling within the same frame.
561 #[allow(clippy::float_cmp)]
562 if curr_frame == self.drawn_time_last {
563 return;
564 }
565
566 // Shift frame times for tracking visibility across frames.
567 self.drawn_time_prev = self.drawn_time_last;
568 self.drawn_time_last = curr_frame;
569
570 // If `retain` is false and the UI element associated with this `Bind` was not rendered
571 // in the previous frame, we clear its data to free resources and ensure a fresh load.
572 if !self.retain && !self.was_drawn_last_frame() {
573 // Manually clear state to avoid a recursive call to poll() from clear().
574 self.state = State::Idle;
575 self.data = None;
576 }
577
578 if matches!(self.state, State::Pending) {
579 match self
580 .recv
581 .as_mut()
582 .expect("BUG: State is Pending but receiver is missing.")
583 .try_recv()
584 {
585 Ok(result) => {
586 self.data = Some(result);
587 self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
588 self.state = State::Finished;
589 self.recv = None; // Drop the receiver as it's no longer needed.
590 }
591 Err(oneshot::error::TryRecvError::Empty) => {
592 // Future is still running, do nothing.
593 }
594 Err(oneshot::error::TryRecvError::Closed) => {
595 // This is a critical error: the task's sender was dropped without sending a value.
596 // This should only happen if the runtime shuts down unexpectedly.
597 panic!("Async task's sender was dropped without sending a result.");
598 }
599 }
600 }
601 }
602
603 /// Checks if this `Bind` has been polled during the current `egui` frame.
604 #[allow(clippy::float_cmp)]
605 pub fn was_drawn_this_frame(&self) -> bool {
606 self.drawn_time_last == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
607 }
608
609 /// Checks if this `Bind` was polled during the previous `egui` frame.
610 ///
611 /// This is used internally to implement the `retain` logic.
612 #[allow(clippy::float_cmp)]
613 pub fn was_drawn_last_frame(&self) -> bool {
614 self.drawn_time_prev == LAST_FRAME.load(std::sync::atomic::Ordering::Relaxed)
615 }
616
617 /// Returns the total number of times an async operation has been executed.
618 pub const fn count_executed(&self) -> usize {
619 self.times_executed
620 }
621}