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 `EguiAsyncPlugin`.
13pub static CURR_FRAME: AtomicF64 = AtomicF64::new(0.0);
14/// The `egui` time of the previous frame, updated by `EguiAsyncPlugin`.
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 `EguiAsyncPlugin`.
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    /// Returns whether finished data is retained across undrawn frames.
167    #[must_use]
168    pub const fn retain(&self) -> bool {
169        self.retain
170    }
171
172    /// Sets retain policy for finished data.
173    pub const fn set_retain(&mut self, retain: bool) {
174        self.retain = retain;
175    }
176
177    /// Internal helper to prepare the state and communication channel for a new async request.
178    #[allow(clippy::type_complexity)]
179    fn prepare_channel(
180        &mut self,
181    ) -> (
182        oneshot::Sender<Result<T, E>>,
183        oneshot::Receiver<Result<T, E>>,
184    ) {
185        self.poll(); // Ensure state is up-to-date before starting.
186
187        self.last_start_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
188        self.state = State::Pending;
189
190        oneshot::channel()
191    }
192
193    /// Internal async function that awaits the user's future and sends the result back.
194    async fn req_inner<F>(fut: F, tx: oneshot::Sender<Result<T, E>>)
195    where
196        F: Future<Output = Result<T, E>> + 'static,
197        T: MaybeSend,
198    {
199        let result = fut.await;
200        if matches!(tx.send(result), Ok(())) {
201            // If the send was successful, request a repaint to show the new data.
202            #[cfg(feature = "egui")]
203            if let Some(ctx) = CTX.get() {
204                ctx.request_repaint();
205            }
206        } else {
207            // This occurs if the `Bind` was dropped before the future completed.
208            warn!("Future result was dropped because the receiver was gone.");
209        }
210    }
211
212    /// Starts an asynchronous operation if the `Bind` is not already `Pending`.
213    ///
214    /// The provided future `f` is spawned onto the appropriate runtime (`tokio` for native,
215    /// `wasm-bindgen-futures` for WASM). The `Bind` state transitions to `Pending`.
216    ///
217    /// This method calls `poll()` internally.
218    pub fn request<Fut>(&mut self, f: Fut)
219    where
220        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
221        T: MaybeSend,
222        E: MaybeSend,
223    {
224        #[cfg(not(target_family = "wasm"))]
225        {
226            let (tx, rx) = self.prepare_channel();
227            tracing::trace!("spawning async request #{:?}", self.times_executed + 1);
228            ASYNC_RUNTIME.spawn(Self::req_inner(f, tx));
229            self.recv = Some(rx);
230        }
231
232        #[cfg(target_family = "wasm")]
233        {
234            let (tx, rx) = self.prepare_channel();
235            tracing::trace!("spawning async request #{:?}", self.times_executed + 1);
236            wasm_bindgen_futures::spawn_local(Self::req_inner(f, tx));
237            self.recv = Some(rx);
238        }
239
240        self.times_executed += 1;
241    }
242
243    /// Convenience: periodic request using `std::time::Duration`.
244    #[must_use]
245    pub fn request_every<Fut>(&mut self, f: impl FnOnce() -> Fut, every: std::time::Duration) -> f64
246    where
247        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
248        T: MaybeSend,
249        E: MaybeSend,
250    {
251        self.request_every_sec(f, every.as_secs_f64())
252    }
253
254    /// Requests an operation to run periodically.
255    ///
256    /// If the `Bind` is not `Pending` and more than `secs` seconds have passed since the
257    /// last completion, a new request is started by calling `f`.
258    ///
259    /// # Returns
260    /// The time in seconds remaining until the next scheduled refresh. A negative value
261    /// indicates a refresh is overdue.
262    pub fn request_every_sec<Fut>(&mut self, f: impl FnOnce() -> Fut, secs: f64) -> f64
263    where
264        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
265        T: MaybeSend,
266        E: MaybeSend,
267    {
268        let since_completed = self.since_completed_raw();
269
270        if self.get_state() != State::Pending && since_completed > secs {
271            self.request(f());
272        }
273
274        secs - since_completed
275    }
276
277    /// Clears any existing data and immediately starts a new async operation.
278    ///
279    /// If an operation was `Pending`, its result will be discarded. The background task is not
280    /// cancelled and will run to completion.
281    ///
282    /// This is a convenience method equivalent to calling `clear()` followed by `request()`.
283    pub fn refresh<Fut>(&mut self, f: Fut)
284    where
285        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
286        T: MaybeSend,
287        E: MaybeSend,
288    {
289        self.clear();
290        self.request(f);
291    }
292
293    /// Takes ownership of the result if the operation is `Finished`.
294    ///
295    /// If the state is `Finished`, this method returns `Some(result)`, consumes the data
296    /// internally, and resets the state to `Idle`. If the state is not `Finished`,
297    /// it returns `None`.
298    ///
299    /// This method calls `poll()` internally.
300    pub fn take(&mut self) -> Option<Result<T, E>> {
301        self.poll();
302
303        if matches!(self.state, State::Finished) {
304            assert!(
305                self.data.is_some(),
306                "State was Finished but data was None. This indicates a bug."
307            );
308            self.state = State::Idle;
309            self.data.take()
310        } else {
311            None
312        }
313    }
314
315    /// Manually sets the data and moves the state to `Finished`.
316    ///
317    /// This can be used to inject data into the `Bind` without running an async operation.
318    ///
319    /// # Panics
320    /// Panics if the current state is not `Idle`.
321    pub fn fill(&mut self, data: Result<T, E>) {
322        self.poll();
323
324        assert!(
325            matches!(self.state, State::Idle),
326            "Cannot fill a Bind that is not Idle."
327        );
328
329        self.state = State::Finished;
330        self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
331        self.data = Some(data);
332    }
333
334    /// Checks if the current state is `Idle`.
335    /// This method calls `poll()` internally.
336    pub fn is_idle(&mut self) -> bool {
337        self.poll();
338        matches!(self.state, State::Idle)
339    }
340
341    /// Checks if the current state is `Pending`.
342    /// This method calls `poll()` internally.
343    pub fn is_pending(&mut self) -> bool {
344        self.poll();
345        matches!(self.state, State::Pending)
346    }
347
348    /// Checks if the current state is `Finished`.
349    /// This method calls `poll()` internally.
350    pub fn is_finished(&mut self) -> bool {
351        self.poll();
352        matches!(self.state, State::Finished)
353    }
354
355    /// Returns `true` if finished with `Ok`.
356    #[must_use]
357    pub fn is_ok(&mut self) -> bool {
358        self.poll();
359        matches!(self.data, Some(Ok(_)))
360    }
361
362    /// Returns `true` if finished with `Err`.
363    #[must_use]
364    pub fn is_err(&mut self) -> bool {
365        self.poll();
366        matches!(self.data, Some(Err(_)))
367    }
368
369    /// Returns `true` if the operation finished during the current `egui` frame.
370    /// This method calls `poll()` internally.
371    #[allow(clippy::float_cmp)]
372    pub fn just_completed(&mut self) -> bool {
373        self.poll();
374        self.last_complete_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
375    }
376
377    /// If the operation just completed this frame, invokes the provided closure with
378    /// a reference to the result.
379    pub fn on_finished(&mut self, f: impl FnOnce(&Result<T, E>)) {
380        if self.just_completed()
381            && let Some(ref d) = self.data
382        {
383            f(d);
384        }
385    }
386
387    /// Returns `true` if the operation started during the current `egui` frame.
388    /// This method calls `poll()` internally.
389    #[allow(clippy::float_cmp)]
390    pub fn just_started(&mut self) -> bool {
391        self.poll();
392        self.last_start_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
393    }
394
395    /// Gets the `egui` time when the operation started.
396    /// This method calls `poll()` internally.
397    pub fn get_start_time(&mut self) -> f64 {
398        self.poll();
399        self.last_start_time
400    }
401
402    /// Gets the `egui` time when the operation completed.
403    /// This method calls `poll()` internally.
404    pub fn get_complete_time(&mut self) -> f64 {
405        self.poll();
406        self.last_complete_time
407    }
408
409    /// Gets the duration between the start and completion of the operation.
410    /// This method calls `poll()` internally.
411    pub fn get_elapsed(&mut self) -> f64 {
412        self.poll();
413        self.last_complete_time - self.last_start_time
414    }
415
416    /// Gets the time elapsed since the operation started.
417    /// This method calls `poll()` internally.
418    pub fn since_started(&mut self) -> f64 {
419        self.poll();
420        CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_start_time
421    }
422
423    /// Gets the time elapsed since the operation completed.
424    /// This method calls `poll()` internally.
425    pub fn since_completed(&mut self) -> f64 {
426        self.poll();
427        self.since_completed_raw()
428    }
429    fn since_completed_raw(&self) -> f64 {
430        CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_complete_time
431    }
432
433    /// Returns an immutable reference to the stored data, if any.
434    /// This method calls `poll()` internally.
435    pub fn read(&mut self) -> &Option<Result<T, E>> {
436        self.poll();
437        &self.data
438    }
439    /// Returns an immutable reference in the ref pattern to the stored data, if any.
440    /// This method calls `poll()` internally.
441    pub fn read_as_ref(&mut self) -> Option<Result<&T, &E>> {
442        self.poll();
443        self.data.as_ref().map(Result::as_ref)
444    }
445
446    /// Returns a mutable reference to the stored data, if any.
447    /// This method calls `poll()` internally.
448    pub fn read_mut(&mut self) -> &mut Option<Result<T, E>> {
449        self.poll();
450        &mut self.data
451    }
452    /// Returns a mutable reference in the ref pattern to the stored data, if any.
453    /// This method calls `poll()` internally.
454    pub fn read_as_mut(&mut self) -> Option<Result<&mut T, &mut E>> {
455        self.poll();
456        self.data.as_mut().map(Result::as_mut)
457    }
458
459    /// Returns the current `State` of the binding.
460    /// This method calls `poll()` internally.
461    pub fn get_state(&mut self) -> State {
462        self.poll();
463        self.state
464    }
465
466    /// Returns the ref filled state of the `Bind`, allowing for exhaustive pattern matching.
467    ///
468    /// This is often the most ergonomic way to display UI based on the `Bind`'s state.
469    /// This method calls `poll()` internally.
470    /// Invariant: `State::Finished` implies `data.is_some()`.
471    ///
472    /// # Example
473    /// ```ignore
474    /// match my_bind.state() {
475    ///     StateWithData::Idle => { /* ... */ }
476    ///     StateWithData::Pending => { ui.spinner(); }
477    ///     StateWithData::Finished(data) => { ui.label(format!("Data: {data:?}")); }
478    ///     StateWithData::Failed(err) => { ui.label(format!("Error: {err:?}")); }
479    /// }
480    /// ```
481    pub fn state(&mut self) -> StateWithData<'_, T, E> {
482        self.poll();
483        match self.state {
484            State::Idle => StateWithData::Idle,
485            State::Pending => StateWithData::Pending,
486            State::Finished => match self.data.as_ref() {
487                Some(Ok(data)) => StateWithData::Finished(data),
488                Some(Err(err)) => StateWithData::Failed(err),
489                None => {
490                    // This case should be unreachable due to internal invariants.
491                    // If state is Finished, data must be Some.
492                    self.state = State::Idle;
493                    StateWithData::Idle
494                }
495            },
496        }
497    }
498
499    /// Returns the ref filled state or starts a new request if idle.
500    ///
501    /// This method is an ergonomic way to drive a UI. If the `Bind` is `Idle` and has no
502    /// data, it immediately calls the provided closure `f` to start an async operation,
503    /// transitioning the state to `Pending`.
504    ///
505    /// In all cases, it returns the current `StateWithData` for immediate use in a `match`
506    /// statement, making it easy to display a loading indicator, the finished data, or an error.
507    ///
508    /// # Example
509    /// ```ignore
510    /// // In your UI update function:
511    /// match my_bind.state_or_request(fetch_data) {
512    ///     StateWithData::Idle => { /* This branch is typically not reached on the first call */ }
513    ///     StateWithData::Pending => { ui.spinner(); }
514    ///     StateWithData::Finished(data) => { ui.label(format!("Data: {:?}", data)); }
515    ///     StateWithData::Failed(err) => { ui.label(format!("Error: {:?}", err)); }
516    /// }
517    /// ```
518    pub fn state_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> StateWithData<'_, T, E>
519    where
520        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
521        T: MaybeSend,
522        E: MaybeSend,
523    {
524        self.poll();
525
526        if self.data.is_none() && matches!(self.state, State::Idle) {
527            self.request(f());
528        }
529        self.state()
530    }
531
532    /// Clears any stored data and resets the state to `Idle`.
533    ///
534    /// If an operation was `Pending`, its result will be discarded. The background task is not
535    /// cancelled and will run to completion.
536    ///
537    /// This method calls `poll()` internally.
538    pub fn clear(&mut self) {
539        self.poll();
540        self.state = State::Idle;
541        self.data = None;
542    }
543
544    /// Returns a reference to the data, or starts a new request if idle.
545    ///
546    /// If data is already available (`Finished`), it returns a reference to it.
547    /// If the state is `Idle` and no data is present, it calls `f` to start a new async
548    /// operation and returns `None`.
549    /// If `Pending`, it returns `None`.
550    ///
551    /// This method calls `poll()` internally.
552    pub fn read_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&Result<T, E>>
553    where
554        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
555        T: MaybeSend,
556        E: MaybeSend,
557    {
558        self.poll();
559
560        if self.data.is_none() && matches!(self.state, State::Idle) {
561            self.request(f());
562        }
563        self.data.as_ref()
564    }
565
566    /// Returns a mutable reference to the data, or starts a new request if idle.
567    ///
568    /// This is the mutable version of `read_or_request`.
569    ///
570    /// This method calls `poll()` internally.
571    pub fn read_mut_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&mut Result<T, E>>
572    where
573        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
574        T: MaybeSend,
575        E: MaybeSend,
576    {
577        self.poll();
578
579        if self.data.is_none() && matches!(self.state, State::Idle) {
580            self.request(f());
581        }
582        self.data.as_mut()
583    }
584
585    /// Drives the state machine. This should be called once per frame before accessing state.
586    ///
587    /// **Note**: Most other methods on `Bind` call this internally, so you usually don't
588    /// need to call it yourself.
589    ///
590    /// This method performs several key actions:
591    /// 1. Checks if a pending future has completed and, if so, updates the state to `Finished`.
592    /// 2. Updates internal frame timers used for `retain` logic and time tracking.
593    /// 3. If `retain` is `false`, it clears the data if the `Bind` was not polled in the previous frame.
594    ///
595    /// # Panics
596    /// - Panics if the state is `Pending` but the internal receiver is missing. This indicates a bug in `egui-async`.
597    /// - Panics if the `oneshot` channel's sender is dropped without sending a value, which would mean the
598    ///   spawned task terminated unexpectedly.
599    pub fn poll(&mut self) {
600        let curr_frame = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
601
602        // Avoid re-polling within the same frame.
603        #[allow(clippy::float_cmp)]
604        if curr_frame == self.drawn_time_last {
605            return;
606        }
607
608        // Shift frame times for tracking visibility across frames.
609        self.drawn_time_prev = self.drawn_time_last;
610        self.drawn_time_last = curr_frame;
611
612        // If `retain` is false and the UI element associated with this `Bind` was not rendered
613        // in the previous frame, we clear its data to free resources and ensure a fresh load.
614        if !self.retain && !self.was_drawn_last_frame() {
615            // Manually clear state to avoid a recursive call to poll() from clear().
616            self.state = State::Idle;
617            self.data = None;
618        }
619
620        if matches!(self.state, State::Pending) {
621            match self
622                .recv
623                .as_mut()
624                .expect("BUG: State is Pending but receiver is missing.")
625                .try_recv()
626            {
627                Ok(result) => {
628                    self.data = Some(result);
629                    self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
630                    self.state = State::Finished;
631                    self.recv = None; // Drop the receiver as it's no longer needed.
632                }
633                Err(oneshot::error::TryRecvError::Empty) => {
634                    // Future is still running, do nothing.
635                }
636                Err(oneshot::error::TryRecvError::Closed) => {
637                    // This is a critical error: the task's sender was dropped without sending a value.
638                    // This should only happen if the runtime shuts down unexpectedly.
639                    panic!("Async task's sender was dropped without sending a result.");
640                }
641            }
642        }
643    }
644
645    /// Checks if this `Bind` has been polled during the current `egui` frame.
646    #[allow(clippy::float_cmp)]
647    pub fn was_drawn_this_frame(&self) -> bool {
648        self.drawn_time_last == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
649    }
650
651    /// Checks if this `Bind` was polled during the previous `egui` frame.
652    ///
653    /// This is used internally to implement the `retain` logic.
654    #[allow(clippy::float_cmp)]
655    pub fn was_drawn_last_frame(&self) -> bool {
656        self.drawn_time_prev == LAST_FRAME.load(std::sync::atomic::Ordering::Relaxed)
657    }
658
659    /// Returns the total number of times an async operation has been executed.
660    pub const fn count_executed(&self) -> usize {
661        self.times_executed
662    }
663}