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}