egui-async 0.4.0

A simple library for running async tasks in egui and binding their results to your UI.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
//! Core state management for asynchronous operations.
//!
//! This module provides the `Bind` struct, which is the heart of `egui-async`. It acts as a
//! state machine to manage the lifecycle of a `Future`, from initiation to completion, and
//! holds the resulting data or error.
use std::{fmt::Debug, future::Future};

use atomic_float::AtomicF64;
use tokio::sync::oneshot;
use tracing::warn;

/// The `egui` time of the current frame, updated by `EguiAsyncPlugin`.
pub static CURR_FRAME: AtomicF64 = AtomicF64::new(0.0);
/// The `egui` time of the previous frame, updated by `EguiAsyncPlugin`.
pub static LAST_FRAME: AtomicF64 = AtomicF64::new(0.0);

/// A lazily initialized Tokio runtime for executing async tasks on non-WASM targets.
#[cfg(not(target_family = "wasm"))]
pub static ASYNC_RUNTIME: std::sync::LazyLock<tokio::runtime::Runtime> =
    std::sync::LazyLock::new(|| {
        tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime.")
    });

/// A global holder for the `egui::Context`, used to request repaints from background tasks.
///
/// This is initialized once by `EguiAsyncPlugin`.
#[cfg(feature = "egui")]
pub static CTX: std::sync::OnceLock<egui::Context> = std::sync::OnceLock::new();

/// Represents the execution state of an asynchronous operation managed by `Bind`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum State {
    /// No operation is running, and no data is available from a previous run.
    #[default]
    Idle,
    /// An operation is currently in-flight.
    Pending,
    /// An operation has completed, and its result (success or error) is available.
    Finished,
}

/// Represents the detailed state of a `Bind`, including available data.
pub enum StateWithData<'a, T, E> {
    /// No operation is running.
    Idle,
    /// An operation is currently in-flight.
    Pending,
    /// An operation has completed with a successful result.
    Finished(&'a T),
    /// An operation has completed with an error.
    Failed(&'a E),
}

impl<T: Debug, E: Debug> Debug for StateWithData<'_, T, E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            StateWithData::Idle => f.write_str("Idle"),
            StateWithData::Pending => f.write_str("Pending"),
            StateWithData::Finished(t) => f.debug_tuple("Finished").field(t).finish(),
            StateWithData::Failed(e) => f.debug_tuple("Failed").field(e).finish(),
        }
    }
}

bitflags::bitflags! {
    /// Configuration flags for `Bind` behavior.
    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
    pub struct ConfigFlags: u8 {
        /// If `true`, the `data` from a `Finished` state is preserved even if the `Bind` instance
        /// is not polled for one or more frames. If `false`, the data is cleared.
        const RETAIN = 0b0000_0001;
        /// Opt-in: Physically abort the background task on Native when
        /// the Bind is cleared or a new request is made.
        ///
        /// **Warning:** This terminates the task immediately. If the future has
        /// critical side effects (e.g., I/O, cleanup), they may not complete.
        ///
        /// Due to browser limitations, **this flag has no effect on WASM targets**.
        const ABORT  = 0b0000_0010;
    }
}

impl Default for ConfigFlags {
    fn default() -> Self {
        Self::empty()
    }
}

/// Internal container to keep the receiver and its control handle synchronized.
/// This should exist only when the state is `Pending`.
struct InFlight<T, E> {
    /// The receiving end of a one-shot channel used to get the result from the background task.
    recv: oneshot::Receiver<Result<T, E>>,

    /// The abort handle for the spawned task (native only).
    #[cfg(not(target_family = "wasm"))]
    handle: tokio::task::AbortHandle,
}

impl<T, E> Debug for InFlight<T, E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut out = f.debug_struct("InFlight");
        out.field("recv", &"oneshot::Receiver<...>");

        #[cfg(not(target_family = "wasm"))]
        out.field("handle", &self.handle);

        out.finish()
    }
}

impl<T, E> InFlight<T, E> {
    fn abort(&self) {
        #[cfg(not(target_family = "wasm"))]
        self.handle.abort();
    }

    fn poll_result(&mut self) -> Result<Result<T, E>, oneshot::error::TryRecvError> {
        self.recv.try_recv()
    }
}

/// A state manager for a single asynchronous operation, designed for use with `egui`.
///
/// `Bind` tracks the lifecycle of a `Future` and stores its `Result<T, E>`. It acts as a
/// bridge between the immediate-mode UI and the background async task, ensuring the UI
/// can react to changes in state (e.g., show a spinner while `Pending`, display the
/// result when `Finished`, or show an error).
pub struct Bind<T, E> {
    /// The `egui` time of the most recent frame where this `Bind` was polled.
    drawn_time_last: f64,
    /// The `egui` time of the second most recent frame where this `Bind` was polled.
    drawn_time_prev: f64,

    /// The result of the completed async operation. `None` if the task is not `Finished`.
    pub(crate) data: Option<Result<T, E>>,
    in_flight: Option<InFlight<T, E>>,

    /// The current execution state of the async operation.
    pub(crate) state: State,
    /// The `egui` time when the most recent operation was started.
    last_start_time: f64,
    /// The `egui` time when the most recent operation was completed.
    last_complete_time: f64,

    /// Configuration option flags
    pub config: ConfigFlags,

    /// A counter for how many times an async operation has been started.
    times_executed: usize,
}

impl<T, E> Debug for Bind<T, E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut out = f.debug_struct("Bind");
        let mut out = out
            .field("state", &self.state)
            .field("config", &self.config)
            .field("drawn_time_last", &self.drawn_time_last)
            .field("drawn_time_prev", &self.drawn_time_prev)
            .field("last_start_time", &self.last_start_time)
            .field("last_complete_time", &self.last_complete_time)
            .field("times_executed", &self.times_executed);

        // Avoid printing the full data/recv content for cleaner debug output.
        if self.data.is_some() {
            out = out.field("data", &"Some(...)");
        } else {
            out = out.field("data", &"None");
        }

        if let Some(in_flight) = &self.in_flight {
            out = out.field("in_flight", in_flight);
        } else {
            out = out.field("in_flight", &"None");
        }

        out.finish()
    }
}

impl<T: 'static, E: 'static> Default for Bind<T, E> {
    /// Creates a default `Bind` instance in an `Idle` state.
    ///
    /// The `retain` flag is set to `false`. This implementation does not require `T` or `E`
    /// to implement `Default`.
    fn default() -> Self {
        Self::new(false)
    }
}

/// A trait alias for `Send` on native targets.
///
/// On WASM, this trait has no bounds, allowing non-`Send` types to be used in `Bind`
/// since WASM is single-threaded.
#[cfg(not(target_family = "wasm"))]
pub trait MaybeSend: Send {}
#[cfg(not(target_family = "wasm"))]
impl<T: Send> MaybeSend for T {}

/// A trait alias with no bounds on WASM targets.
///
/// This allows `Bind` to work with `!Send` futures and data types in a single-threaded
/// web environment.
#[cfg(target_family = "wasm")]
pub trait MaybeSend {}
#[cfg(target_family = "wasm")]
impl<T> MaybeSend for T {}

impl<T: 'static, E: 'static> Bind<T, E> {
    /// Creates a new `Bind` instance with a specific retain policy.
    ///
    /// # Parameters
    /// - `retain`: If `true`, the result of the operation is kept even if the `Bind`
    ///   is not polled in a frame. If `false`, the result is cleared if not polled
    ///   for one frame, returning the `Bind` to an `Idle` state.
    #[must_use]
    pub const fn new(retain: bool) -> Self {
        Self {
            drawn_time_last: 0.0,
            drawn_time_prev: 0.0,

            data: None,
            in_flight: None,

            state: State::Idle,
            last_start_time: 0.0,
            last_complete_time: f64::MIN, // Set to a very low value to ensure `since_completed` is large initially.

            config: if retain {
                ConfigFlags::RETAIN
            } else {
                ConfigFlags::empty()
            },

            times_executed: 0,
        }
    }

    /// Returns whether finished data is retained across undrawn frames.
    #[must_use]
    pub const fn retain(&self) -> bool {
        self.config.contains(ConfigFlags::RETAIN)
    }

    /// Sets retain policy for finished data.
    pub fn set_retain(&mut self, retain: bool) {
        if retain {
            self.config.insert(ConfigFlags::RETAIN);
        } else {
            self.config.remove(ConfigFlags::RETAIN);
        }
    }

    /// Returns whether background tasks are physically aborted when cleared or replaced.
    ///
    /// This flag only affects non-WASM targets.
    #[must_use]
    pub const fn abort_on_clear(&self) -> bool {
        self.config.contains(ConfigFlags::ABORT)
    }

    /// Sets whether background tasks are physically aborted when cleared or replaced.
    ///
    /// **Note:** This has no effect on WASM targets due to browser execution models.
    pub fn set_abort(&mut self, abort: bool) {
        if abort {
            self.config.insert(ConfigFlags::ABORT);
        } else {
            self.config.remove(ConfigFlags::ABORT);
        }
    }

    /// Internal async function that awaits the user's future and sends the result back.
    async fn req_inner<F>(fut: F, tx: oneshot::Sender<Result<T, E>>)
    where
        F: Future<Output = Result<T, E>> + 'static,
        T: MaybeSend,
    {
        let result = fut.await;
        if matches!(tx.send(result), Ok(())) {
            // If the send was successful, request a repaint to show the new data.
            #[cfg(feature = "egui")]
            if let Some(ctx) = CTX.get() {
                ctx.request_repaint();
            }
        } else {
            // This occurs if the `Bind` was dropped before the future completed.
            warn!("Future result was dropped because the receiver was gone.");
        }
    }

    /// Starts an asynchronous operation if the `Bind` is not already `Pending`.
    ///
    /// The provided future `f` is spawned onto the appropriate runtime (`tokio` for native,
    /// `wasm-bindgen-futures` for WASM). The `Bind` state transitions to `Pending`.
    ///
    /// This method calls `poll()` internally.
    pub fn request<Fut>(&mut self, f: Fut)
    where
        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
        T: MaybeSend,
        E: MaybeSend,
    {
        // Drive state machine to catch results from tasks finishing this frame
        self.poll();

        // Handle existing task based on config; sets state to Idle if still Pending
        self.abort();

        self.last_start_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
        let (tx, rx) = oneshot::channel();

        // Spawn the async task
        tracing::trace!("spawning async request #{}", self.times_executed + 1);

        #[cfg(not(target_family = "wasm"))]
        let in_flight = InFlight {
            recv: rx,
            handle: ASYNC_RUNTIME.spawn(Self::req_inner(f, tx)).abort_handle(),
        };

        #[cfg(target_family = "wasm")]
        let in_flight = {
            wasm_bindgen_futures::spawn_local(Self::req_inner(f, tx));
            InFlight { recv: rx }
        };

        self.in_flight = Some(in_flight);
        self.state = State::Pending;
        self.times_executed += 1;
    }

    /// Convenience: periodic request using `std::time::Duration`.
    #[must_use]
    pub fn request_every<Fut>(&mut self, f: impl FnOnce() -> Fut, every: std::time::Duration) -> f64
    where
        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
        T: MaybeSend,
        E: MaybeSend,
    {
        self.request_every_sec(f, every.as_secs_f64())
    }

    /// Requests an operation to run periodically.
    ///
    /// If the `Bind` is not `Pending` and more than `secs` seconds have passed since the
    /// last completion, a new request is started by calling `f`.
    ///
    /// # Returns
    /// The time in seconds remaining until the next scheduled refresh. A negative value
    /// indicates a refresh is overdue.
    pub fn request_every_sec<Fut>(&mut self, f: impl FnOnce() -> Fut, secs: f64) -> f64
    where
        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
        T: MaybeSend,
        E: MaybeSend,
    {
        let state = self.get_state();
        let since_completed = self.since_completed_raw();

        if state != State::Pending && since_completed > secs {
            self.request(f());
        }

        secs - since_completed
    }

    /// Explicitly cancels the in-flight task and resets the state to `Idle`.
    ///
    /// If the [`ConfigFlags::ABORT`] flag is set, the background task is physically
    /// terminated (Native only). Otherwise, the result is simply ignored.
    pub fn abort(&mut self) {
        // Logical: Take the in-flight handle. This detaches the Bind from the task.
        if let Some(task) = self.in_flight.take() {
            // Physical: Only signal the runtime to kill the task if configured.
            if self.config.contains(ConfigFlags::ABORT) {
                task.abort();
            }
        }

        // Ensure state is synchronized with the removal of the in-flight task.
        if matches!(self.state, State::Pending) {
            self.state = State::Idle;
        }
    }

    /// Clears any existing data and immediately starts a new async operation.
    ///
    /// If an operation was `Pending`, its result will be discarded. The background task is not
    /// cancelled and will run to completion.
    ///
    /// This is a convenience method equivalent to calling `clear()` followed by `request()`.
    pub fn refresh<Fut>(&mut self, f: Fut)
    where
        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
        T: MaybeSend,
        E: MaybeSend,
    {
        self.clear();
        self.request(f);
    }

    /// Takes ownership of the result if the operation is `Finished`.
    ///
    /// If the state is `Finished`, this method returns `Some(result)`, consumes the data
    /// internally, and resets the state to `Idle`. If the state is not `Finished`,
    /// it returns `None`.
    ///
    /// This method calls `poll()` internally.
    pub fn take(&mut self) -> Option<Result<T, E>> {
        self.poll();

        if matches!(self.state, State::Finished) {
            assert!(
                self.data.is_some(),
                "State was Finished but data was None. This indicates a bug."
            );
            self.state = State::Idle;
            self.data.take()
        } else {
            None
        }
    }

    /// Manually sets the data and moves the state to `Finished`.
    ///
    /// This can be used to inject data into the `Bind` without running an async operation.
    ///
    /// If an operation is currently `Pending`, it will be aborted. If data is already
    /// present, it will be overwritten.
    pub fn fill(&mut self, data: Result<T, E>) {
        if self.just_completed() || self.just_started() {
            tracing::warn!(
                "Bind::fill called multiple times in the same frame. This may indicate a logic error in your update loop."
            );
        }

        // Ensure clean state: abort pending tasks and reset to Idle.
        self.clear();

        self.state = State::Finished;

        let now = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
        self.last_start_time = now;
        self.last_complete_time = now;

        self.data = Some(data);
    }

    /// Returns `Some(&T)` when finished successfully.
    #[must_use]
    pub fn ok_ref(&mut self) -> Option<&T> {
        self.poll();
        self.data.as_ref()?.as_ref().ok()
    }

    /// Returns `Some(&E)` when finished with error.
    #[must_use]
    pub fn err_ref(&mut self) -> Option<&E> {
        self.poll();
        self.data.as_ref()?.as_ref().err()
    }

    /// Takes and returns `T` only if finished successfully.
    pub fn take_ok(&mut self) -> Option<T> {
        self.poll();
        match self.data.take()? {
            Ok(t) => {
                self.state = State::Idle;
                Some(t)
            }
            Err(e) => {
                self.data = Some(Err(e));
                None
            }
        }
    }

    /// Checks if the current state is `Idle`.
    /// This method calls `poll()` internally.
    pub fn is_idle(&mut self) -> bool {
        self.poll();
        matches!(self.state, State::Idle)
    }

    /// Checks if the current state is `Pending`.
    /// This method calls `poll()` internally.
    pub fn is_pending(&mut self) -> bool {
        self.poll();
        matches!(self.state, State::Pending)
    }

    /// Checks if the current state is `Finished`.
    /// This method calls `poll()` internally.
    pub fn is_finished(&mut self) -> bool {
        self.poll();
        matches!(self.state, State::Finished)
    }

    /// Returns `true` if finished with `Ok`.
    #[must_use]
    pub fn is_ok(&mut self) -> bool {
        self.poll();
        matches!(self.data, Some(Ok(_)))
    }

    /// Returns `true` if finished with `Err`.
    #[must_use]
    pub fn is_err(&mut self) -> bool {
        self.poll();
        matches!(self.data, Some(Err(_)))
    }

    /// Returns `true` if the operation finished during the current `egui` frame.
    /// This method calls `poll()` internally.
    #[allow(clippy::float_cmp)]
    pub fn just_completed(&mut self) -> bool {
        self.poll();
        self.last_complete_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// If the operation just completed this frame, invokes the provided closure with
    /// a reference to the result.
    pub fn on_finished(&mut self, f: impl FnOnce(&Result<T, E>)) {
        if self.just_completed()
            && let Some(ref d) = self.data
        {
            f(d);
        }
    }

    /// Returns `true` if the operation started during the current `egui` frame.
    /// This method calls `poll()` internally.
    #[allow(clippy::float_cmp)]
    pub fn just_started(&mut self) -> bool {
        self.poll();
        self.last_start_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Gets the `egui` time when the operation started.
    /// This method calls `poll()` internally.
    pub fn get_start_time(&mut self) -> f64 {
        self.poll();
        self.last_start_time
    }

    /// Gets the `egui` time when the operation completed.
    /// This method calls `poll()` internally.
    pub fn get_complete_time(&mut self) -> f64 {
        self.poll();
        self.last_complete_time
    }

    /// Gets the duration between the start and completion of the operation.
    /// This method calls `poll()` internally.
    pub fn get_elapsed(&mut self) -> f64 {
        self.poll();
        self.last_complete_time - self.last_start_time
    }

    /// Gets the time elapsed since the operation started.
    /// This method calls `poll()` internally.
    pub fn since_started(&mut self) -> f64 {
        self.poll();
        CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_start_time
    }

    /// Gets the time elapsed since the operation completed.
    /// This method calls `poll()` internally.
    pub fn since_completed(&mut self) -> f64 {
        self.poll();
        self.since_completed_raw()
    }
    fn since_completed_raw(&self) -> f64 {
        CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_complete_time
    }

    /// Returns an immutable reference to the stored data, if any.
    /// This method calls `poll()` internally.
    pub fn read(&mut self) -> &Option<Result<T, E>> {
        self.poll();
        &self.data
    }
    /// Returns an immutable reference in the ref pattern to the stored data, if any.
    /// This method calls `poll()` internally.
    pub fn read_as_ref(&mut self) -> Option<Result<&T, &E>> {
        self.poll();
        self.data.as_ref().map(Result::as_ref)
    }

    /// Returns a mutable reference to the stored data, if any.
    /// This method calls `poll()` internally.
    pub fn read_mut(&mut self) -> &mut Option<Result<T, E>> {
        self.poll();
        &mut self.data
    }
    /// Returns a mutable reference in the ref pattern to the stored data, if any.
    /// This method calls `poll()` internally.
    pub fn read_as_mut(&mut self) -> Option<Result<&mut T, &mut E>> {
        self.poll();
        self.data.as_mut().map(Result::as_mut)
    }

    /// Returns the current `State` of the binding.
    /// This method calls `poll()` internally.
    pub fn get_state(&mut self) -> State {
        self.poll();
        self.state
    }

    /// Returns the ref filled state of the `Bind`, allowing for exhaustive pattern matching.
    ///
    /// This is often the most ergonomic way to display UI based on the `Bind`'s state.
    /// This method calls `poll()` internally.
    /// Invariant: `State::Finished` implies `data.is_some()`.
    ///
    /// # Example
    /// ```ignore
    /// match my_bind.state() {
    ///     StateWithData::Idle => { /* ... */ }
    ///     StateWithData::Pending => { ui.spinner(); }
    ///     StateWithData::Finished(data) => { ui.label(format!("Data: {data:?}")); }
    ///     StateWithData::Failed(err) => { ui.label(format!("Error: {err:?}")); }
    /// }
    /// ```
    pub fn state(&mut self) -> StateWithData<'_, T, E> {
        self.poll();
        match self.state {
            State::Idle => StateWithData::Idle,
            State::Pending => StateWithData::Pending,
            State::Finished => match self.data.as_ref() {
                Some(Ok(data)) => StateWithData::Finished(data),
                Some(Err(err)) => StateWithData::Failed(err),
                None => {
                    // This case should be unreachable due to internal invariants.
                    // If state is Finished, data must be Some.
                    self.state = State::Idle;
                    StateWithData::Idle
                }
            },
        }
    }

    /// Returns the ref filled state or starts a new request if idle.
    ///
    /// This method is an ergonomic way to drive a UI. If the `Bind` is `Idle` and has no
    /// data, it immediately calls the provided closure `f` to start an async operation,
    /// transitioning the state to `Pending`.
    ///
    /// In all cases, it returns the current `StateWithData` for immediate use in a `match`
    /// statement, making it easy to display a loading indicator, the finished data, or an error.
    ///
    /// # Example
    /// ```ignore
    /// // In your UI update function:
    /// match my_bind.state_or_request(fetch_data) {
    ///     StateWithData::Idle => { /* This branch is typically not reached on the first call */ }
    ///     StateWithData::Pending => { ui.spinner(); }
    ///     StateWithData::Finished(data) => { ui.label(format!("Data: {:?}", data)); }
    ///     StateWithData::Failed(err) => { ui.label(format!("Error: {:?}", err)); }
    /// }
    /// ```
    pub fn state_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> StateWithData<'_, T, E>
    where
        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
        T: MaybeSend,
        E: MaybeSend,
    {
        self.poll();

        if self.data.is_none() && matches!(self.state, State::Idle) {
            self.request(f());
        }
        self.state()
    }

    /// Clears any stored data and resets the state to `Idle`.
    ///
    /// If an operation was `Pending`, its result will be discarded. The background task is not
    /// cancelled and will run to completion.
    ///
    /// This method calls `poll()` internally.
    pub fn clear(&mut self) {
        self.poll();
        self.abort();
        self.state = State::Idle;
        self.data = None;
    }

    /// Returns a reference to the data, or starts a new request if idle.
    ///
    /// If data is already available (`Finished`), it returns a reference to it.
    /// If the state is `Idle` and no data is present, it calls `f` to start a new async
    /// operation and returns `None`.
    /// If `Pending`, it returns `None`.
    ///
    /// This method calls `poll()` internally.
    pub fn read_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&Result<T, E>>
    where
        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
        T: MaybeSend,
        E: MaybeSend,
    {
        self.poll();

        if self.data.is_none() && matches!(self.state, State::Idle) {
            self.request(f());
        }
        self.data.as_ref()
    }

    /// Returns a mutable reference to the data, or starts a new request if idle.
    ///
    /// This is the mutable version of `read_or_request`.
    ///
    /// This method calls `poll()` internally.
    pub fn read_mut_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&mut Result<T, E>>
    where
        Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
        T: MaybeSend,
        E: MaybeSend,
    {
        self.poll();

        if self.data.is_none() && matches!(self.state, State::Idle) {
            self.request(f());
        }
        self.data.as_mut()
    }

    /// Drives the state machine. This should be called once per frame before accessing state.
    ///
    /// **Note**: Most other methods on `Bind` call this internally, so you usually don't
    /// need to call it yourself.
    ///
    /// This method performs several key actions:
    /// 1. Checks if a pending future has completed and, if so, updates the state to `Finished`.
    /// 2. Updates internal frame timers used for `retain` logic and time tracking.
    /// 3. If `retain` is `false`, it clears the data if the `Bind` was not polled in the previous frame.
    ///
    /// # Panics
    /// - Panics if the state is `Pending` but the internal receiver is missing. This indicates a bug in `egui-async`.
    /// - Panics if the `oneshot` channel's sender is dropped without sending a value, which would mean the
    ///   spawned task terminated unexpectedly.
    pub fn poll(&mut self) {
        let curr_frame = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);

        // Avoid re-polling within the same frame.
        #[allow(clippy::float_cmp)]
        if curr_frame == self.drawn_time_last {
            return;
        }

        // Shift frame times for tracking visibility across frames.
        self.drawn_time_prev = self.drawn_time_last;
        self.drawn_time_last = curr_frame;

        // If `retain` is false and the UI element associated with this `Bind` was not rendered
        // in the previous frame, we clear its data to free resources and ensure a fresh load.
        if !self.retain() && !self.was_drawn_last_frame() {
            // Manually clear state to avoid a recursive call to poll() from clear().
            self.abort();
            self.state = State::Idle;
            self.data = None;
        }

        if matches!(self.state, State::Pending) {
            let task = self
                .in_flight
                .as_mut()
                .expect("BUG: Pending but no in_flight.");
            match task.poll_result() {
                Ok(result) => {
                    self.data = Some(result);
                    self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
                    self.state = State::Finished;
                    self.in_flight = None; // Drop the in_flight receiver as it's no longer needed.
                }
                Err(oneshot::error::TryRecvError::Empty) => {
                    // Future is still running, do nothing.
                }
                Err(oneshot::error::TryRecvError::Closed) => {
                    // Treat as cancellation: clear the pending state without crashing the app.
                    tracing::warn!(
                        "Async task cancelled: sender dropped without sending a result."
                    );
                    self.state = State::Idle;
                    self.in_flight = None;
                }
            }
        }
    }

    /// Checks if this `Bind` has been polled during the current `egui` frame.
    #[allow(clippy::float_cmp)]
    pub fn was_drawn_this_frame(&self) -> bool {
        self.drawn_time_last == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Checks if this `Bind` was polled during the previous `egui` frame.
    ///
    /// This is used internally to implement the `retain` logic.
    #[allow(clippy::float_cmp)]
    pub fn was_drawn_last_frame(&self) -> bool {
        self.drawn_time_prev == LAST_FRAME.load(std::sync::atomic::Ordering::Relaxed)
    }

    /// Returns the total number of times an async operation has been executed.
    pub const fn count_executed(&self) -> usize {
        self.times_executed
    }
}