Skip to main content

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///
19/// This is the default runtime. It is only created once a task is spawned without another
20/// runtime chosen through [`set_global_runtime`] or [`Bind::set_runtime`].
21#[cfg(not(target_family = "wasm"))]
22pub static ASYNC_RUNTIME: std::sync::LazyLock<tokio::runtime::Runtime> =
23    std::sync::LazyLock::new(|| {
24        tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime.")
25    });
26
27/// The runtime set by [`set_global_runtime`], if any.
28#[cfg(not(target_family = "wasm"))]
29static GLOBAL_RUNTIME: std::sync::OnceLock<tokio::runtime::Handle> = std::sync::OnceLock::new();
30
31/// Sets the Tokio runtime that every `Bind` spawns its tasks onto, in place of the built-in
32/// [`ASYNC_RUNTIME`]. A `Bind` with its own runtime ([`Bind::set_runtime`]) still uses that.
33///
34/// Call this once at startup, before the first request. Tasks already spawned stay on the
35/// runtime they were spawned on, and the built-in runtime is never created if no task needs it.
36///
37/// The runtime must stay alive and keep running tasks for as long as `Bind`s use it. A
38/// multi-thread runtime runs tasks on its worker threads, but a current-thread runtime only runs
39/// them while a thread is blocked on it. Requests spawned onto a runtime that has shut down are
40/// cancelled, and the `Bind` returns to `Idle`.
41///
42/// Native only: on WASM, tasks always run on the browser's event loop.
43///
44/// # Errors
45/// Returns the handle if a global runtime was already set.
46///
47/// # Example
48/// ```
49/// let runtime = tokio::runtime::Runtime::new().expect("failed to create runtime");
50/// egui_async::set_global_runtime(runtime.handle().clone()).expect("global runtime already set");
51///
52/// // Every `Bind` now spawns its tasks onto `runtime`, which must outlive them.
53/// ```
54#[cfg(not(target_family = "wasm"))]
55pub fn set_global_runtime(handle: tokio::runtime::Handle) -> Result<(), tokio::runtime::Handle> {
56    GLOBAL_RUNTIME.set(handle)
57}
58
59/// Returns the runtime that a `Bind` without its own runtime spawns tasks onto.
60#[cfg(not(target_family = "wasm"))]
61fn global_runtime() -> &'static tokio::runtime::Handle {
62    GLOBAL_RUNTIME
63        .get()
64        .unwrap_or_else(|| ASYNC_RUNTIME.handle())
65}
66
67/// A global holder for the `egui::Context`, used to request repaints from background tasks.
68///
69/// This is initialized once by `EguiAsyncPlugin`.
70///
71/// Not available on threaded WASM (`wasm32` with the `atomics` target feature), where
72/// `egui::Context` is neither `Send` nor `Sync`. There, the context is kept in a thread-local
73/// on the thread that runs the plugin instead.
74#[cfg(all(
75    feature = "egui",
76    not(all(target_arch = "wasm32", target_feature = "atomics"))
77))]
78pub static CTX: std::sync::OnceLock<egui::Context> = std::sync::OnceLock::new();
79
80#[cfg(all(feature = "egui", target_arch = "wasm32", target_feature = "atomics"))]
81thread_local! {
82    /// Thread-local holder for the `egui::Context` on threaded WASM.
83    ///
84    /// Only the thread that runs `EguiAsyncPlugin` (the main thread) has a context. Tasks are
85    /// spawned with `wasm_bindgen_futures::spawn_local`, which polls them on the thread that
86    /// spawned them, so results requested from that thread are delivered where the context lives.
87    static CTX: std::cell::OnceCell<egui::Context> = const { std::cell::OnceCell::new() };
88}
89
90/// Stores the `egui::Context` used to request repaints, if it is not already set.
91#[cfg(feature = "egui")]
92pub(crate) fn init_ctx(ctx: &egui::Context) {
93    #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))]
94    CTX.get_or_init(|| ctx.clone());
95
96    #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
97    let _ = CTX.try_with(|cell| {
98        cell.get_or_init(|| ctx.clone());
99    });
100}
101
102/// Requests a repaint of the stored `egui::Context`, if one has been set.
103///
104/// On threaded WASM, only the thread that runs `EguiAsyncPlugin` can request a repaint; on any
105/// other thread this does nothing.
106fn request_repaint() {
107    #[cfg(all(
108        feature = "egui",
109        not(all(target_arch = "wasm32", target_feature = "atomics"))
110    ))]
111    if let Some(ctx) = CTX.get() {
112        ctx.request_repaint();
113    }
114
115    #[cfg(all(feature = "egui", target_arch = "wasm32", target_feature = "atomics"))]
116    let _ = CTX.try_with(|cell| {
117        if let Some(ctx) = cell.get() {
118            ctx.request_repaint();
119        }
120    });
121}
122
123/// Represents the execution state of an asynchronous operation managed by `Bind`.
124#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
125pub enum State {
126    /// No operation is running, and no data is available from a previous run.
127    #[default]
128    Idle,
129    /// An operation is currently in-flight.
130    Pending,
131    /// An operation has completed, and its result (success or error) is available.
132    Finished,
133}
134
135/// Represents the detailed state of a `Bind`, including available data.
136pub enum StateWithData<'a, T, E> {
137    /// No operation is running.
138    Idle,
139    /// An operation is currently in-flight.
140    Pending,
141    /// An operation has completed with a successful result.
142    Finished(&'a T),
143    /// An operation has completed with an error.
144    Failed(&'a E),
145}
146
147impl<T: Debug, E: Debug> Debug for StateWithData<'_, T, E> {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            StateWithData::Idle => f.write_str("Idle"),
151            StateWithData::Pending => f.write_str("Pending"),
152            StateWithData::Finished(t) => f.debug_tuple("Finished").field(t).finish(),
153            StateWithData::Failed(e) => f.debug_tuple("Failed").field(e).finish(),
154        }
155    }
156}
157
158bitflags::bitflags! {
159    /// Configuration flags for `Bind` behavior.
160    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
161    pub struct ConfigFlags: u8 {
162        /// If `true`, the `data` from a `Finished` state is preserved even if the `Bind` instance
163        /// is not polled for one or more frames. If `false`, the data is cleared.
164        const RETAIN = 0b0000_0001;
165        /// Opt-in: Physically abort the background task when the Bind is aborted,
166        /// cleared, or a new request is made.
167        ///
168        /// On native targets, the Tokio task is aborted. On WASM, the spawned local
169        /// task is dropped the next time the executor polls it, without polling the
170        /// future again.
171        ///
172        /// **Warning:** This terminates the task at its current `.await` point. If the
173        /// future has critical side effects (e.g., I/O, cleanup), they may not complete.
174        const ABORT  = 0b0000_0010;
175    }
176}
177
178impl Default for ConfigFlags {
179    fn default() -> Self {
180        Self::empty()
181    }
182}
183
184/// Internal container to keep the receiver and its control handle synchronized.
185/// This should exist only when the state is `Pending`.
186struct InFlight<T, E> {
187    /// The receiving end of a one-shot channel used to get the result from the background task.
188    recv: oneshot::Receiver<Result<T, E>>,
189
190    /// The abort handle for the spawned task (native only).
191    #[cfg(not(target_family = "wasm"))]
192    handle: tokio::task::AbortHandle,
193
194    /// Signals the spawned task to stop (WASM only). Dropping it unsent lets the task run on.
195    #[cfg(target_family = "wasm")]
196    abort_tx: oneshot::Sender<()>,
197}
198
199impl<T, E> Debug for InFlight<T, E> {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        let mut out = f.debug_struct("InFlight");
202        out.field("recv", &"oneshot::Receiver<...>");
203
204        #[cfg(not(target_family = "wasm"))]
205        out.field("handle", &self.handle);
206
207        #[cfg(target_family = "wasm")]
208        out.field("abort_tx", &"oneshot::Sender<()>");
209
210        out.finish()
211    }
212}
213
214impl<T, E> InFlight<T, E> {
215    fn abort(self) {
216        #[cfg(not(target_family = "wasm"))]
217        self.handle.abort();
218
219        // An error means the task already finished, so there is nothing to stop.
220        #[cfg(target_family = "wasm")]
221        let _ = self.abort_tx.send(());
222    }
223
224    fn poll_result(&mut self) -> Result<Result<T, E>, oneshot::error::TryRecvError> {
225        self.recv.try_recv()
226    }
227}
228
229/// A state manager for a single asynchronous operation, designed for use with `egui`.
230///
231/// `Bind` tracks the lifecycle of a `Future` and stores its `Result<T, E>`. It acts as a
232/// bridge between the immediate-mode UI and the background async task, ensuring the UI
233/// can react to changes in state (e.g., show a spinner while `Pending`, display the
234/// result when `Finished`, or show an error).
235pub struct Bind<T, E> {
236    /// The `egui` time of the most recent frame where this `Bind` was polled.
237    drawn_time_last: f64,
238    /// The `egui` time of the second most recent frame where this `Bind` was polled.
239    drawn_time_prev: f64,
240
241    /// The result of the completed async operation. `None` if the task is not `Finished`.
242    pub(crate) data: Option<Result<T, E>>,
243    in_flight: Option<InFlight<T, E>>,
244
245    /// The current execution state of the async operation.
246    pub(crate) state: State,
247    /// The `egui` time when the most recent operation was started.
248    last_start_time: f64,
249    /// The `egui` time when the most recent operation was completed.
250    last_complete_time: f64,
251
252    /// Configuration option flags
253    pub config: ConfigFlags,
254
255    /// The runtime this `Bind` spawns tasks onto, overriding the global runtime (native only).
256    #[cfg(not(target_family = "wasm"))]
257    runtime: Option<tokio::runtime::Handle>,
258
259    /// A counter for how many times an async operation has been started.
260    times_executed: usize,
261}
262
263impl<T, E> Debug for Bind<T, E> {
264    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265        let mut out = f.debug_struct("Bind");
266        let mut out = out
267            .field("state", &self.state)
268            .field("config", &self.config)
269            .field("drawn_time_last", &self.drawn_time_last)
270            .field("drawn_time_prev", &self.drawn_time_prev)
271            .field("last_start_time", &self.last_start_time)
272            .field("last_complete_time", &self.last_complete_time)
273            .field("times_executed", &self.times_executed);
274
275        #[cfg(not(target_family = "wasm"))]
276        {
277            out = out.field("runtime", &self.runtime);
278        }
279
280        // Avoid printing the full data/recv content for cleaner debug output.
281        if self.data.is_some() {
282            out = out.field("data", &"Some(...)");
283        } else {
284            out = out.field("data", &"None");
285        }
286
287        if let Some(in_flight) = &self.in_flight {
288            out = out.field("in_flight", in_flight);
289        } else {
290            out = out.field("in_flight", &"None");
291        }
292
293        out.finish()
294    }
295}
296
297impl<T: 'static, E: 'static> Default for Bind<T, E> {
298    /// Creates a default `Bind` instance in an `Idle` state.
299    ///
300    /// The `retain` flag is set to `false`. This implementation does not require `T` or `E`
301    /// to implement `Default`.
302    fn default() -> Self {
303        Self::new(false)
304    }
305}
306
307/// Runs `task` until it completes or a stop signal arrives on `abort_rx`.
308///
309/// The receiver is polled before the task: `Ok` drops the task without polling it again, while
310/// `Err` (the sender was dropped unsent) stops listening and lets the task run to completion.
311#[cfg(target_family = "wasm")]
312async fn abortable(task: impl Future<Output = ()>, abort_rx: oneshot::Receiver<()>) {
313    use std::task::Poll;
314
315    let mut task = std::pin::pin!(task);
316    let mut abort_rx = Some(abort_rx);
317
318    std::future::poll_fn(|cx| {
319        if let Some(rx) = abort_rx.as_mut() {
320            match std::pin::Pin::new(rx).poll(cx) {
321                Poll::Ready(Ok(())) => return Poll::Ready(()),
322                Poll::Ready(Err(_)) => abort_rx = None,
323                Poll::Pending => {}
324            }
325        }
326        task.as_mut().poll(cx)
327    })
328    .await;
329}
330
331/// A trait alias for `Send` on native targets.
332///
333/// On WASM, this trait has no bounds, allowing non-`Send` types to be used in `Bind`
334/// since WASM is single-threaded.
335#[cfg(not(target_family = "wasm"))]
336pub trait MaybeSend: Send {}
337#[cfg(not(target_family = "wasm"))]
338impl<T: Send> MaybeSend for T {}
339
340/// A trait alias with no bounds on WASM targets.
341///
342/// This allows `Bind` to work with `!Send` futures and data types in a single-threaded
343/// web environment.
344#[cfg(target_family = "wasm")]
345pub trait MaybeSend {}
346#[cfg(target_family = "wasm")]
347impl<T> MaybeSend for T {}
348
349impl<T: 'static, E: 'static> Bind<T, E> {
350    /// Creates a new `Bind` instance with a specific retain policy.
351    ///
352    /// # Parameters
353    /// - `retain`: If `true`, the result of the operation is kept even if the `Bind`
354    ///   is not polled in a frame. If `false`, the result is cleared if not polled
355    ///   for one frame, returning the `Bind` to an `Idle` state.
356    #[must_use]
357    pub const fn new(retain: bool) -> Self {
358        Self {
359            drawn_time_last: 0.0,
360            drawn_time_prev: 0.0,
361
362            data: None,
363            in_flight: None,
364
365            state: State::Idle,
366            last_start_time: 0.0,
367            last_complete_time: f64::MIN, // Set to a very low value to ensure `since_completed` is large initially.
368
369            config: if retain {
370                ConfigFlags::RETAIN
371            } else {
372                ConfigFlags::empty()
373            },
374
375            #[cfg(not(target_family = "wasm"))]
376            runtime: None,
377
378            times_executed: 0,
379        }
380    }
381
382    /// Sets the Tokio runtime this `Bind` spawns its tasks onto, for use when building a `Bind`.
383    ///
384    /// See [`Bind::set_runtime`].
385    ///
386    /// # Example
387    /// ```
388    /// # use egui_async::Bind;
389    /// let runtime = tokio::runtime::Runtime::new().expect("failed to create runtime");
390    /// let bind: Bind<String, String> = Bind::new(false).with_runtime(runtime.handle().clone());
391    /// ```
392    #[cfg(not(target_family = "wasm"))]
393    #[must_use]
394    pub fn with_runtime(mut self, handle: tokio::runtime::Handle) -> Self {
395        self.runtime = Some(handle);
396        self
397    }
398
399    /// Returns the Tokio runtime set for this `Bind`, if any.
400    ///
401    /// `None` means it uses the global runtime: the one set with [`set_global_runtime`], or the
402    /// built-in [`ASYNC_RUNTIME`].
403    #[cfg(not(target_family = "wasm"))]
404    #[must_use]
405    pub const fn runtime(&self) -> Option<&tokio::runtime::Handle> {
406        self.runtime.as_ref()
407    }
408
409    /// Sets the Tokio runtime this `Bind` spawns its tasks onto, overriding the global runtime.
410    /// `None` returns it to the global runtime.
411    ///
412    /// This takes effect from the next request; a task already in flight stays on the runtime it
413    /// was spawned on. The same requirements as for [`set_global_runtime`] apply.
414    ///
415    /// Native only: on WASM, tasks always run on the browser's event loop.
416    #[cfg(not(target_family = "wasm"))]
417    pub fn set_runtime(&mut self, handle: Option<tokio::runtime::Handle>) {
418        self.runtime = handle;
419    }
420
421    /// Returns whether finished data is retained across undrawn frames.
422    #[must_use]
423    pub const fn retain(&self) -> bool {
424        self.config.contains(ConfigFlags::RETAIN)
425    }
426
427    /// Sets retain policy for finished data.
428    pub fn set_retain(&mut self, retain: bool) {
429        if retain {
430            self.config.insert(ConfigFlags::RETAIN);
431        } else {
432            self.config.remove(ConfigFlags::RETAIN);
433        }
434    }
435
436    /// Returns whether background tasks are physically aborted when cleared or replaced.
437    #[must_use]
438    pub const fn abort_on_clear(&self) -> bool {
439        self.config.contains(ConfigFlags::ABORT)
440    }
441
442    /// Sets whether background tasks are physically aborted when cleared or replaced.
443    ///
444    /// See [`ConfigFlags::ABORT`] for how this behaves on native and WASM targets.
445    pub fn set_abort(&mut self, abort: bool) {
446        if abort {
447            self.config.insert(ConfigFlags::ABORT);
448        } else {
449            self.config.remove(ConfigFlags::ABORT);
450        }
451    }
452
453    /// Internal async function that awaits the user's future and sends the result back.
454    async fn req_inner<F>(fut: F, tx: oneshot::Sender<Result<T, E>>)
455    where
456        F: Future<Output = Result<T, E>> + 'static,
457        T: MaybeSend,
458    {
459        let result = fut.await;
460        if matches!(tx.send(result), Ok(())) {
461            // If the send was successful, request a repaint to show the new data.
462            request_repaint();
463        } else {
464            // This occurs if the `Bind` was dropped before the future completed.
465            warn!("Future result was dropped because the receiver was gone.");
466        }
467    }
468
469    /// Starts an asynchronous operation if the `Bind` is not already `Pending`.
470    ///
471    /// The provided future `f` is spawned onto the appropriate runtime (`tokio` for native,
472    /// `wasm-bindgen-futures` for WASM). The `Bind` state transitions to `Pending`.
473    ///
474    /// This method calls `poll()` internally.
475    pub fn request<Fut>(&mut self, f: Fut)
476    where
477        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
478        T: MaybeSend,
479        E: MaybeSend,
480    {
481        // Drive state machine to catch results from tasks finishing this frame
482        self.poll();
483
484        // Handle existing task based on config; sets state to Idle if still Pending
485        self.abort();
486
487        self.last_start_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
488        let (tx, rx) = oneshot::channel();
489
490        // Spawn the async task
491        tracing::trace!("spawning async request #{}", self.times_executed + 1);
492
493        #[cfg(not(target_family = "wasm"))]
494        let in_flight = {
495            let runtime = self.runtime.as_ref().unwrap_or_else(|| global_runtime());
496            InFlight {
497                recv: rx,
498                handle: runtime.spawn(Self::req_inner(f, tx)).abort_handle(),
499            }
500        };
501
502        #[cfg(target_family = "wasm")]
503        let in_flight = {
504            let (abort_tx, abort_rx) = oneshot::channel();
505            wasm_bindgen_futures::spawn_local(abortable(Self::req_inner(f, tx), abort_rx));
506            InFlight { recv: rx, abort_tx }
507        };
508
509        self.in_flight = Some(in_flight);
510        self.state = State::Pending;
511        self.times_executed += 1;
512    }
513
514    /// Convenience: periodic request using `std::time::Duration`.
515    #[must_use]
516    pub fn request_every<Fut>(&mut self, f: impl FnOnce() -> Fut, every: std::time::Duration) -> f64
517    where
518        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
519        T: MaybeSend,
520        E: MaybeSend,
521    {
522        self.request_every_sec(f, every.as_secs_f64())
523    }
524
525    /// Requests an operation to run periodically.
526    ///
527    /// If the `Bind` is not `Pending` and more than `secs` seconds have passed since the
528    /// last completion, a new request is started by calling `f`.
529    ///
530    /// # Returns
531    /// The time in seconds remaining until the next scheduled refresh. A negative value
532    /// indicates a refresh is overdue.
533    pub fn request_every_sec<Fut>(&mut self, f: impl FnOnce() -> Fut, secs: f64) -> f64
534    where
535        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
536        T: MaybeSend,
537        E: MaybeSend,
538    {
539        let state = self.get_state();
540        let since_completed = self.since_completed_raw();
541
542        if state != State::Pending && since_completed > secs {
543            self.request(f());
544        }
545
546        secs - since_completed
547    }
548
549    /// Explicitly cancels the in-flight task and resets the state to `Idle`.
550    ///
551    /// If the [`ConfigFlags::ABORT`] flag is set, the background task is physically
552    /// terminated: aborted on native, or dropped at its next poll on WASM. Otherwise, the
553    /// task runs to completion and its result is simply ignored.
554    pub fn abort(&mut self) {
555        // Logical: Take the in-flight handle. This detaches the Bind from the task.
556        if let Some(task) = self.in_flight.take() {
557            // Physical: Only signal the runtime to kill the task if configured.
558            if self.config.contains(ConfigFlags::ABORT) {
559                task.abort();
560            }
561        }
562
563        // Ensure state is synchronized with the removal of the in-flight task.
564        if matches!(self.state, State::Pending) {
565            self.state = State::Idle;
566        }
567    }
568
569    /// Clears any existing data and immediately starts a new async operation.
570    ///
571    /// If an operation was `Pending`, its result will be discarded. The background task runs to
572    /// completion unless the [`ConfigFlags::ABORT`] flag is set.
573    ///
574    /// This is a convenience method equivalent to calling `clear()` followed by `request()`.
575    pub fn refresh<Fut>(&mut self, f: Fut)
576    where
577        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
578        T: MaybeSend,
579        E: MaybeSend,
580    {
581        self.clear();
582        self.request(f);
583    }
584
585    /// Takes ownership of the result if the operation is `Finished`.
586    ///
587    /// If the state is `Finished`, this method returns `Some(result)`, consumes the data
588    /// internally, and resets the state to `Idle`. If the state is not `Finished`,
589    /// it returns `None`.
590    ///
591    /// This method calls `poll()` internally.
592    pub fn take(&mut self) -> Option<Result<T, E>> {
593        self.poll();
594
595        if matches!(self.state, State::Finished) {
596            assert!(
597                self.data.is_some(),
598                "State was Finished but data was None. This indicates a bug."
599            );
600            self.state = State::Idle;
601            self.data.take()
602        } else {
603            None
604        }
605    }
606
607    /// Manually sets the data and moves the state to `Finished`.
608    ///
609    /// This can be used to inject data into the `Bind` without running an async operation.
610    ///
611    /// If an operation is currently `Pending`, it will be aborted. If data is already
612    /// present, it will be overwritten.
613    pub fn fill(&mut self, data: Result<T, E>) {
614        if self.just_completed() || self.just_started() {
615            tracing::warn!(
616                "Bind::fill called multiple times in the same frame. This may indicate a logic error in your update loop."
617            );
618        }
619
620        // Ensure clean state: abort pending tasks and reset to Idle.
621        self.clear();
622
623        self.state = State::Finished;
624
625        let now = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
626        self.last_start_time = now;
627        self.last_complete_time = now;
628
629        self.data = Some(data);
630    }
631
632    /// Returns `Some(&T)` when finished successfully.
633    #[must_use]
634    pub fn ok_ref(&mut self) -> Option<&T> {
635        self.poll();
636        self.data.as_ref()?.as_ref().ok()
637    }
638
639    /// Returns `Some(&E)` when finished with error.
640    #[must_use]
641    pub fn err_ref(&mut self) -> Option<&E> {
642        self.poll();
643        self.data.as_ref()?.as_ref().err()
644    }
645
646    /// Takes and returns `T` only if finished successfully.
647    pub fn take_ok(&mut self) -> Option<T> {
648        self.poll();
649        match self.data.take()? {
650            Ok(t) => {
651                self.state = State::Idle;
652                Some(t)
653            }
654            Err(e) => {
655                self.data = Some(Err(e));
656                None
657            }
658        }
659    }
660
661    /// Checks if the current state is `Idle`.
662    /// This method calls `poll()` internally.
663    pub fn is_idle(&mut self) -> bool {
664        self.poll();
665        matches!(self.state, State::Idle)
666    }
667
668    /// Checks if the current state is `Pending`.
669    /// This method calls `poll()` internally.
670    pub fn is_pending(&mut self) -> bool {
671        self.poll();
672        matches!(self.state, State::Pending)
673    }
674
675    /// Checks if the current state is `Finished`.
676    /// This method calls `poll()` internally.
677    pub fn is_finished(&mut self) -> bool {
678        self.poll();
679        matches!(self.state, State::Finished)
680    }
681
682    /// Returns `true` if finished with `Ok`.
683    #[must_use]
684    pub fn is_ok(&mut self) -> bool {
685        self.poll();
686        matches!(self.data, Some(Ok(_)))
687    }
688
689    /// Returns `true` if finished with `Err`.
690    #[must_use]
691    pub fn is_err(&mut self) -> bool {
692        self.poll();
693        matches!(self.data, Some(Err(_)))
694    }
695
696    /// Returns `true` if the operation finished during the current `egui` frame.
697    /// This method calls `poll()` internally.
698    #[allow(clippy::float_cmp)]
699    pub fn just_completed(&mut self) -> bool {
700        self.poll();
701        self.last_complete_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
702    }
703
704    /// If the operation just completed this frame, invokes the provided closure with
705    /// a reference to the result.
706    pub fn on_finished(&mut self, f: impl FnOnce(&Result<T, E>)) {
707        if self.just_completed()
708            && let Some(ref d) = self.data
709        {
710            f(d);
711        }
712    }
713
714    /// Returns `true` if the operation started during the current `egui` frame.
715    /// This method calls `poll()` internally.
716    #[allow(clippy::float_cmp)]
717    pub fn just_started(&mut self) -> bool {
718        self.poll();
719        self.last_start_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
720    }
721
722    /// Gets the `egui` time when the operation started.
723    /// This method calls `poll()` internally.
724    pub fn get_start_time(&mut self) -> f64 {
725        self.poll();
726        self.last_start_time
727    }
728
729    /// Gets the `egui` time when the operation completed.
730    /// This method calls `poll()` internally.
731    pub fn get_complete_time(&mut self) -> f64 {
732        self.poll();
733        self.last_complete_time
734    }
735
736    /// Gets the duration between the start and completion of the operation.
737    /// This method calls `poll()` internally.
738    pub fn get_elapsed(&mut self) -> f64 {
739        self.poll();
740        self.last_complete_time - self.last_start_time
741    }
742
743    /// Gets the time elapsed since the operation started.
744    /// This method calls `poll()` internally.
745    pub fn since_started(&mut self) -> f64 {
746        self.poll();
747        CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_start_time
748    }
749
750    /// Gets the time elapsed since the operation completed.
751    /// This method calls `poll()` internally.
752    pub fn since_completed(&mut self) -> f64 {
753        self.poll();
754        self.since_completed_raw()
755    }
756    fn since_completed_raw(&self) -> f64 {
757        CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_complete_time
758    }
759
760    /// Returns an immutable reference to the stored data, if any.
761    /// This method calls `poll()` internally.
762    pub fn read(&mut self) -> &Option<Result<T, E>> {
763        self.poll();
764        &self.data
765    }
766    /// Returns an immutable reference in the ref pattern to the stored data, if any.
767    /// This method calls `poll()` internally.
768    pub fn read_as_ref(&mut self) -> Option<Result<&T, &E>> {
769        self.poll();
770        self.data.as_ref().map(Result::as_ref)
771    }
772
773    /// Returns a mutable reference to the stored data, if any.
774    /// This method calls `poll()` internally.
775    pub fn read_mut(&mut self) -> &mut Option<Result<T, E>> {
776        self.poll();
777        &mut self.data
778    }
779    /// Returns a mutable reference in the ref pattern to the stored data, if any.
780    /// This method calls `poll()` internally.
781    pub fn read_as_mut(&mut self) -> Option<Result<&mut T, &mut E>> {
782        self.poll();
783        self.data.as_mut().map(Result::as_mut)
784    }
785
786    /// Returns the current `State` of the binding.
787    /// This method calls `poll()` internally.
788    pub fn get_state(&mut self) -> State {
789        self.poll();
790        self.state
791    }
792
793    /// Returns the ref filled state of the `Bind`, allowing for exhaustive pattern matching.
794    ///
795    /// This is often the most ergonomic way to display UI based on the `Bind`'s state.
796    /// This method calls `poll()` internally.
797    /// Invariant: `State::Finished` implies `data.is_some()`.
798    ///
799    /// # Example
800    /// ```ignore
801    /// match my_bind.state() {
802    ///     StateWithData::Idle => { /* ... */ }
803    ///     StateWithData::Pending => { ui.spinner(); }
804    ///     StateWithData::Finished(data) => { ui.label(format!("Data: {data:?}")); }
805    ///     StateWithData::Failed(err) => { ui.label(format!("Error: {err:?}")); }
806    /// }
807    /// ```
808    pub fn state(&mut self) -> StateWithData<'_, T, E> {
809        self.poll();
810        match self.state {
811            State::Idle => StateWithData::Idle,
812            State::Pending => StateWithData::Pending,
813            State::Finished => match self.data.as_ref() {
814                Some(Ok(data)) => StateWithData::Finished(data),
815                Some(Err(err)) => StateWithData::Failed(err),
816                None => {
817                    // This case should be unreachable due to internal invariants.
818                    // If state is Finished, data must be Some.
819                    self.state = State::Idle;
820                    StateWithData::Idle
821                }
822            },
823        }
824    }
825
826    /// Returns the ref filled state or starts a new request if idle.
827    ///
828    /// This method is an ergonomic way to drive a UI. If the `Bind` is `Idle` and has no
829    /// data, it immediately calls the provided closure `f` to start an async operation,
830    /// transitioning the state to `Pending`.
831    ///
832    /// In all cases, it returns the current `StateWithData` for immediate use in a `match`
833    /// statement, making it easy to display a loading indicator, the finished data, or an error.
834    ///
835    /// # Example
836    /// ```ignore
837    /// // In your UI update function:
838    /// match my_bind.state_or_request(fetch_data) {
839    ///     StateWithData::Idle => { /* This branch is typically not reached on the first call */ }
840    ///     StateWithData::Pending => { ui.spinner(); }
841    ///     StateWithData::Finished(data) => { ui.label(format!("Data: {:?}", data)); }
842    ///     StateWithData::Failed(err) => { ui.label(format!("Error: {:?}", err)); }
843    /// }
844    /// ```
845    pub fn state_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> StateWithData<'_, T, E>
846    where
847        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
848        T: MaybeSend,
849        E: MaybeSend,
850    {
851        self.poll();
852
853        if self.data.is_none() && matches!(self.state, State::Idle) {
854            self.request(f());
855        }
856        self.state()
857    }
858
859    /// Clears any stored data and resets the state to `Idle`.
860    ///
861    /// If an operation was `Pending`, its result will be discarded. The background task runs to
862    /// completion unless the [`ConfigFlags::ABORT`] flag is set.
863    ///
864    /// This method calls `poll()` internally.
865    pub fn clear(&mut self) {
866        self.poll();
867        self.abort();
868        self.state = State::Idle;
869        self.data = None;
870    }
871
872    /// Returns a reference to the data, or starts a new request if idle.
873    ///
874    /// If data is already available (`Finished`), it returns a reference to it.
875    /// If the state is `Idle` and no data is present, it calls `f` to start a new async
876    /// operation and returns `None`.
877    /// If `Pending`, it returns `None`.
878    ///
879    /// This method calls `poll()` internally.
880    pub fn read_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&Result<T, E>>
881    where
882        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
883        T: MaybeSend,
884        E: MaybeSend,
885    {
886        self.poll();
887
888        if self.data.is_none() && matches!(self.state, State::Idle) {
889            self.request(f());
890        }
891        self.data.as_ref()
892    }
893
894    /// Returns a mutable reference to the data, or starts a new request if idle.
895    ///
896    /// This is the mutable version of `read_or_request`.
897    ///
898    /// This method calls `poll()` internally.
899    pub fn read_mut_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&mut Result<T, E>>
900    where
901        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
902        T: MaybeSend,
903        E: MaybeSend,
904    {
905        self.poll();
906
907        if self.data.is_none() && matches!(self.state, State::Idle) {
908            self.request(f());
909        }
910        self.data.as_mut()
911    }
912
913    /// Drives the state machine. This should be called once per frame before accessing state.
914    ///
915    /// **Note**: Most other methods on `Bind` call this internally, so you usually don't
916    /// need to call it yourself.
917    ///
918    /// This method performs several key actions:
919    /// 1. Checks if a pending future has completed and, if so, updates the state to `Finished`.
920    /// 2. Updates internal frame timers used for `retain` logic and time tracking.
921    /// 3. If `retain` is `false`, it clears the data if the `Bind` was not polled in the previous frame.
922    ///
923    /// # Panics
924    /// - Panics if the state is `Pending` but the internal receiver is missing. This indicates a bug in `egui-async`.
925    /// - Panics if the `oneshot` channel's sender is dropped without sending a value, which would mean the
926    ///   spawned task terminated unexpectedly.
927    pub fn poll(&mut self) {
928        let curr_frame = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
929
930        // Avoid re-polling within the same frame.
931        #[allow(clippy::float_cmp)]
932        if curr_frame == self.drawn_time_last {
933            return;
934        }
935
936        // Shift frame times for tracking visibility across frames.
937        self.drawn_time_prev = self.drawn_time_last;
938        self.drawn_time_last = curr_frame;
939
940        // If `retain` is false and the UI element associated with this `Bind` was not rendered
941        // in the previous frame, we clear its data to free resources and ensure a fresh load.
942        if !self.retain() && !self.was_drawn_last_frame() {
943            // Manually clear state to avoid a recursive call to poll() from clear().
944            self.abort();
945            self.state = State::Idle;
946            self.data = None;
947        }
948
949        if matches!(self.state, State::Pending) {
950            let task = self
951                .in_flight
952                .as_mut()
953                .expect("BUG: Pending but no in_flight.");
954            match task.poll_result() {
955                Ok(result) => {
956                    self.data = Some(result);
957                    self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
958                    self.state = State::Finished;
959                    self.in_flight = None; // Drop the in_flight receiver as it's no longer needed.
960                }
961                Err(oneshot::error::TryRecvError::Empty) => {
962                    // Future is still running, do nothing.
963                }
964                Err(oneshot::error::TryRecvError::Closed) => {
965                    // Treat as cancellation: clear the pending state without crashing the app.
966                    tracing::warn!(
967                        "Async task cancelled: sender dropped without sending a result."
968                    );
969                    self.state = State::Idle;
970                    self.in_flight = None;
971                }
972            }
973        }
974    }
975
976    /// Checks if this `Bind` has been polled during the current `egui` frame.
977    #[allow(clippy::float_cmp)]
978    pub fn was_drawn_this_frame(&self) -> bool {
979        self.drawn_time_last == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
980    }
981
982    /// Checks if this `Bind` was polled during the previous `egui` frame.
983    ///
984    /// This is used internally to implement the `retain` logic.
985    #[allow(clippy::float_cmp)]
986    pub fn was_drawn_last_frame(&self) -> bool {
987        self.drawn_time_prev == LAST_FRAME.load(std::sync::atomic::Ordering::Relaxed)
988    }
989
990    /// Returns the total number of times an async operation has been executed.
991    pub const fn count_executed(&self) -> usize {
992        self.times_executed
993    }
994}