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