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 since_completed = self.since_completed_raw();
280
281 if self.get_state() != State::Pending && since_completed > secs {
282 self.request(f());
283 }
284
285 secs - since_completed
286 }
287
288 /// Clears any existing data and immediately starts a new async operation.
289 ///
290 /// If an operation was `Pending`, its result will be discarded. The background task is not
291 /// cancelled and will run to completion.
292 ///
293 /// This is a convenience method equivalent to calling `clear()` followed by `request()`.
294 pub fn refresh<Fut>(&mut self, f: Fut)
295 where
296 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
297 T: MaybeSend,
298 E: MaybeSend,
299 {
300 self.clear();
301 self.request(f);
302 }
303
304 /// Takes ownership of the result if the operation is `Finished`.
305 ///
306 /// If the state is `Finished`, this method returns `Some(result)`, consumes the data
307 /// internally, and resets the state to `Idle`. If the state is not `Finished`,
308 /// it returns `None`.
309 ///
310 /// This method calls `poll()` internally.
311 pub fn take(&mut self) -> Option<Result<T, E>> {
312 self.poll();
313
314 if matches!(self.state, State::Finished) {
315 assert!(
316 self.data.is_some(),
317 "State was Finished but data was None. This indicates a bug."
318 );
319 self.state = State::Idle;
320 self.data.take()
321 } else {
322 None
323 }
324 }
325
326 /// Manually sets the data and moves the state to `Finished`.
327 ///
328 /// This can be used to inject data into the `Bind` without running an async operation.
329 ///
330 /// # Panics
331 /// Panics if the current state is not `Idle`.
332 pub fn fill(&mut self, data: Result<T, E>) {
333 self.poll();
334
335 assert!(
336 matches!(self.state, State::Idle),
337 "Cannot fill a Bind that is not Idle."
338 );
339
340 self.state = State::Finished;
341
342 let now = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
343 self.last_start_time = now;
344 self.last_complete_time = now;
345
346 self.data = Some(data);
347 }
348
349 /// Returns `Some(&T)` when finished successfully.
350 #[must_use]
351 pub fn ok_ref(&mut self) -> Option<&T> {
352 self.poll();
353 self.data.as_ref()?.as_ref().ok()
354 }
355
356 /// Returns `Some(&E)` when finished with error.
357 #[must_use]
358 pub fn err_ref(&mut self) -> Option<&E> {
359 self.poll();
360 self.data.as_ref()?.as_ref().err()
361 }
362
363 /// Takes and returns `T` only if finished successfully.
364 pub fn take_ok(&mut self) -> Option<T> {
365 self.poll();
366 match self.data.take()? {
367 Ok(t) => {
368 self.state = State::Idle;
369 Some(t)
370 }
371 Err(e) => {
372 self.data = Some(Err(e));
373 None
374 }
375 }
376 }
377
378 /// Checks if the current state is `Idle`.
379 /// This method calls `poll()` internally.
380 pub fn is_idle(&mut self) -> bool {
381 self.poll();
382 matches!(self.state, State::Idle)
383 }
384
385 /// Checks if the current state is `Pending`.
386 /// This method calls `poll()` internally.
387 pub fn is_pending(&mut self) -> bool {
388 self.poll();
389 matches!(self.state, State::Pending)
390 }
391
392 /// Checks if the current state is `Finished`.
393 /// This method calls `poll()` internally.
394 pub fn is_finished(&mut self) -> bool {
395 self.poll();
396 matches!(self.state, State::Finished)
397 }
398
399 /// Returns `true` if finished with `Ok`.
400 #[must_use]
401 pub fn is_ok(&mut self) -> bool {
402 self.poll();
403 matches!(self.data, Some(Ok(_)))
404 }
405
406 /// Returns `true` if finished with `Err`.
407 #[must_use]
408 pub fn is_err(&mut self) -> bool {
409 self.poll();
410 matches!(self.data, Some(Err(_)))
411 }
412
413 /// Returns `true` if the operation finished during the current `egui` frame.
414 /// This method calls `poll()` internally.
415 #[allow(clippy::float_cmp)]
416 pub fn just_completed(&mut self) -> bool {
417 self.poll();
418 self.last_complete_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
419 }
420
421 /// If the operation just completed this frame, invokes the provided closure with
422 /// a reference to the result.
423 pub fn on_finished(&mut self, f: impl FnOnce(&Result<T, E>)) {
424 if self.just_completed()
425 && let Some(ref d) = self.data
426 {
427 f(d);
428 }
429 }
430
431 /// Returns `true` if the operation started during the current `egui` frame.
432 /// This method calls `poll()` internally.
433 #[allow(clippy::float_cmp)]
434 pub fn just_started(&mut self) -> bool {
435 self.poll();
436 self.last_start_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
437 }
438
439 /// Gets the `egui` time when the operation started.
440 /// This method calls `poll()` internally.
441 pub fn get_start_time(&mut self) -> f64 {
442 self.poll();
443 self.last_start_time
444 }
445
446 /// Gets the `egui` time when the operation completed.
447 /// This method calls `poll()` internally.
448 pub fn get_complete_time(&mut self) -> f64 {
449 self.poll();
450 self.last_complete_time
451 }
452
453 /// Gets the duration between the start and completion of the operation.
454 /// This method calls `poll()` internally.
455 pub fn get_elapsed(&mut self) -> f64 {
456 self.poll();
457 self.last_complete_time - self.last_start_time
458 }
459
460 /// Gets the time elapsed since the operation started.
461 /// This method calls `poll()` internally.
462 pub fn since_started(&mut self) -> f64 {
463 self.poll();
464 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_start_time
465 }
466
467 /// Gets the time elapsed since the operation completed.
468 /// This method calls `poll()` internally.
469 pub fn since_completed(&mut self) -> f64 {
470 self.poll();
471 self.since_completed_raw()
472 }
473 fn since_completed_raw(&self) -> f64 {
474 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_complete_time
475 }
476
477 /// Returns an immutable reference to the stored data, if any.
478 /// This method calls `poll()` internally.
479 pub fn read(&mut self) -> &Option<Result<T, E>> {
480 self.poll();
481 &self.data
482 }
483 /// Returns an immutable reference in the ref pattern to the stored data, if any.
484 /// This method calls `poll()` internally.
485 pub fn read_as_ref(&mut self) -> Option<Result<&T, &E>> {
486 self.poll();
487 self.data.as_ref().map(Result::as_ref)
488 }
489
490 /// Returns a mutable reference to the stored data, if any.
491 /// This method calls `poll()` internally.
492 pub fn read_mut(&mut self) -> &mut Option<Result<T, E>> {
493 self.poll();
494 &mut self.data
495 }
496 /// Returns a mutable reference in the ref pattern to the stored data, if any.
497 /// This method calls `poll()` internally.
498 pub fn read_as_mut(&mut self) -> Option<Result<&mut T, &mut E>> {
499 self.poll();
500 self.data.as_mut().map(Result::as_mut)
501 }
502
503 /// Returns the current `State` of the binding.
504 /// This method calls `poll()` internally.
505 pub fn get_state(&mut self) -> State {
506 self.poll();
507 self.state
508 }
509
510 /// Returns the ref filled state of the `Bind`, allowing for exhaustive pattern matching.
511 ///
512 /// This is often the most ergonomic way to display UI based on the `Bind`'s state.
513 /// This method calls `poll()` internally.
514 /// Invariant: `State::Finished` implies `data.is_some()`.
515 ///
516 /// # Example
517 /// ```ignore
518 /// match my_bind.state() {
519 /// StateWithData::Idle => { /* ... */ }
520 /// StateWithData::Pending => { ui.spinner(); }
521 /// StateWithData::Finished(data) => { ui.label(format!("Data: {data:?}")); }
522 /// StateWithData::Failed(err) => { ui.label(format!("Error: {err:?}")); }
523 /// }
524 /// ```
525 pub fn state(&mut self) -> StateWithData<'_, T, E> {
526 self.poll();
527 match self.state {
528 State::Idle => StateWithData::Idle,
529 State::Pending => StateWithData::Pending,
530 State::Finished => match self.data.as_ref() {
531 Some(Ok(data)) => StateWithData::Finished(data),
532 Some(Err(err)) => StateWithData::Failed(err),
533 None => {
534 // This case should be unreachable due to internal invariants.
535 // If state is Finished, data must be Some.
536 self.state = State::Idle;
537 StateWithData::Idle
538 }
539 },
540 }
541 }
542
543 /// Returns the ref filled state or starts a new request if idle.
544 ///
545 /// This method is an ergonomic way to drive a UI. If the `Bind` is `Idle` and has no
546 /// data, it immediately calls the provided closure `f` to start an async operation,
547 /// transitioning the state to `Pending`.
548 ///
549 /// In all cases, it returns the current `StateWithData` for immediate use in a `match`
550 /// statement, making it easy to display a loading indicator, the finished data, or an error.
551 ///
552 /// # Example
553 /// ```ignore
554 /// // In your UI update function:
555 /// match my_bind.state_or_request(fetch_data) {
556 /// StateWithData::Idle => { /* This branch is typically not reached on the first call */ }
557 /// StateWithData::Pending => { ui.spinner(); }
558 /// StateWithData::Finished(data) => { ui.label(format!("Data: {:?}", data)); }
559 /// StateWithData::Failed(err) => { ui.label(format!("Error: {:?}", err)); }
560 /// }
561 /// ```
562 pub fn state_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> StateWithData<'_, T, E>
563 where
564 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
565 T: MaybeSend,
566 E: MaybeSend,
567 {
568 self.poll();
569
570 if self.data.is_none() && matches!(self.state, State::Idle) {
571 self.request(f());
572 }
573 self.state()
574 }
575
576 /// Clears any stored data and resets the state to `Idle`.
577 ///
578 /// If an operation was `Pending`, its result will be discarded. The background task is not
579 /// cancelled and will run to completion.
580 ///
581 /// This method calls `poll()` internally.
582 pub fn clear(&mut self) {
583 self.poll();
584 self.state = State::Idle;
585 self.data = None;
586 self.recv = None;
587 }
588
589 /// Returns a reference to the data, or starts a new request if idle.
590 ///
591 /// If data is already available (`Finished`), it returns a reference to it.
592 /// If the state is `Idle` and no data is present, it calls `f` to start a new async
593 /// operation and returns `None`.
594 /// If `Pending`, it returns `None`.
595 ///
596 /// This method calls `poll()` internally.
597 pub fn read_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&Result<T, E>>
598 where
599 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
600 T: MaybeSend,
601 E: MaybeSend,
602 {
603 self.poll();
604
605 if self.data.is_none() && matches!(self.state, State::Idle) {
606 self.request(f());
607 }
608 self.data.as_ref()
609 }
610
611 /// Returns a mutable reference to the data, or starts a new request if idle.
612 ///
613 /// This is the mutable version of `read_or_request`.
614 ///
615 /// This method calls `poll()` internally.
616 pub fn read_mut_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&mut Result<T, E>>
617 where
618 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
619 T: MaybeSend,
620 E: MaybeSend,
621 {
622 self.poll();
623
624 if self.data.is_none() && matches!(self.state, State::Idle) {
625 self.request(f());
626 }
627 self.data.as_mut()
628 }
629
630 /// Drives the state machine. This should be called once per frame before accessing state.
631 ///
632 /// **Note**: Most other methods on `Bind` call this internally, so you usually don't
633 /// need to call it yourself.
634 ///
635 /// This method performs several key actions:
636 /// 1. Checks if a pending future has completed and, if so, updates the state to `Finished`.
637 /// 2. Updates internal frame timers used for `retain` logic and time tracking.
638 /// 3. If `retain` is `false`, it clears the data if the `Bind` was not polled in the previous frame.
639 ///
640 /// # Panics
641 /// - Panics if the state is `Pending` but the internal receiver is missing. This indicates a bug in `egui-async`.
642 /// - Panics if the `oneshot` channel's sender is dropped without sending a value, which would mean the
643 /// spawned task terminated unexpectedly.
644 pub fn poll(&mut self) {
645 let curr_frame = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
646
647 // Avoid re-polling within the same frame.
648 #[allow(clippy::float_cmp)]
649 if curr_frame == self.drawn_time_last {
650 return;
651 }
652
653 // Shift frame times for tracking visibility across frames.
654 self.drawn_time_prev = self.drawn_time_last;
655 self.drawn_time_last = curr_frame;
656
657 // If `retain` is false and the UI element associated with this `Bind` was not rendered
658 // in the previous frame, we clear its data to free resources and ensure a fresh load.
659 if !self.retain && !self.was_drawn_last_frame() {
660 // Manually clear state to avoid a recursive call to poll() from clear().
661 self.state = State::Idle;
662 self.data = None;
663 self.recv = None;
664 }
665
666 if matches!(self.state, State::Pending) {
667 match self
668 .recv
669 .as_mut()
670 .expect("BUG: State is Pending but receiver is missing.")
671 .try_recv()
672 {
673 Ok(result) => {
674 self.data = Some(result);
675 self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
676 self.state = State::Finished;
677 self.recv = None; // Drop the receiver as it's no longer needed.
678 }
679 Err(oneshot::error::TryRecvError::Empty) => {
680 // Future is still running, do nothing.
681 }
682 Err(oneshot::error::TryRecvError::Closed) => {
683 // Treat as cancellation: clear the pending state without crashing the app.
684 tracing::warn!(
685 "Async task cancelled: sender dropped without sending a result."
686 );
687 self.state = State::Idle;
688 self.recv = None;
689 }
690 }
691 }
692 }
693
694 /// Checks if this `Bind` has been polled during the current `egui` frame.
695 #[allow(clippy::float_cmp)]
696 pub fn was_drawn_this_frame(&self) -> bool {
697 self.drawn_time_last == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
698 }
699
700 /// Checks if this `Bind` was polled during the previous `egui` frame.
701 ///
702 /// This is used internally to implement the `retain` logic.
703 #[allow(clippy::float_cmp)]
704 pub fn was_drawn_last_frame(&self) -> bool {
705 self.drawn_time_prev == LAST_FRAME.load(std::sync::atomic::Ordering::Relaxed)
706 }
707
708 /// Returns the total number of times an async operation has been executed.
709 pub const fn count_executed(&self) -> usize {
710 self.times_executed
711 }
712}