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