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 /// # Panics
431 /// Panics if the current state is not `Idle`.
432 pub fn fill(&mut self, data: Result<T, E>) {
433 self.poll();
434
435 assert!(
436 matches!(self.state, State::Idle),
437 "Cannot fill a Bind that is not Idle."
438 );
439
440 self.state = State::Finished;
441
442 let now = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
443 self.last_start_time = now;
444 self.last_complete_time = now;
445
446 self.data = Some(data);
447 }
448
449 /// Returns `Some(&T)` when finished successfully.
450 #[must_use]
451 pub fn ok_ref(&mut self) -> Option<&T> {
452 self.poll();
453 self.data.as_ref()?.as_ref().ok()
454 }
455
456 /// Returns `Some(&E)` when finished with error.
457 #[must_use]
458 pub fn err_ref(&mut self) -> Option<&E> {
459 self.poll();
460 self.data.as_ref()?.as_ref().err()
461 }
462
463 /// Takes and returns `T` only if finished successfully.
464 pub fn take_ok(&mut self) -> Option<T> {
465 self.poll();
466 match self.data.take()? {
467 Ok(t) => {
468 self.state = State::Idle;
469 Some(t)
470 }
471 Err(e) => {
472 self.data = Some(Err(e));
473 None
474 }
475 }
476 }
477
478 /// Checks if the current state is `Idle`.
479 /// This method calls `poll()` internally.
480 pub fn is_idle(&mut self) -> bool {
481 self.poll();
482 matches!(self.state, State::Idle)
483 }
484
485 /// Checks if the current state is `Pending`.
486 /// This method calls `poll()` internally.
487 pub fn is_pending(&mut self) -> bool {
488 self.poll();
489 matches!(self.state, State::Pending)
490 }
491
492 /// Checks if the current state is `Finished`.
493 /// This method calls `poll()` internally.
494 pub fn is_finished(&mut self) -> bool {
495 self.poll();
496 matches!(self.state, State::Finished)
497 }
498
499 /// Returns `true` if finished with `Ok`.
500 #[must_use]
501 pub fn is_ok(&mut self) -> bool {
502 self.poll();
503 matches!(self.data, Some(Ok(_)))
504 }
505
506 /// Returns `true` if finished with `Err`.
507 #[must_use]
508 pub fn is_err(&mut self) -> bool {
509 self.poll();
510 matches!(self.data, Some(Err(_)))
511 }
512
513 /// Returns `true` if the operation finished during the current `egui` frame.
514 /// This method calls `poll()` internally.
515 #[allow(clippy::float_cmp)]
516 pub fn just_completed(&mut self) -> bool {
517 self.poll();
518 self.last_complete_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
519 }
520
521 /// If the operation just completed this frame, invokes the provided closure with
522 /// a reference to the result.
523 pub fn on_finished(&mut self, f: impl FnOnce(&Result<T, E>)) {
524 if self.just_completed()
525 && let Some(ref d) = self.data
526 {
527 f(d);
528 }
529 }
530
531 /// Returns `true` if the operation started during the current `egui` frame.
532 /// This method calls `poll()` internally.
533 #[allow(clippy::float_cmp)]
534 pub fn just_started(&mut self) -> bool {
535 self.poll();
536 self.last_start_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
537 }
538
539 /// Gets the `egui` time when the operation started.
540 /// This method calls `poll()` internally.
541 pub fn get_start_time(&mut self) -> f64 {
542 self.poll();
543 self.last_start_time
544 }
545
546 /// Gets the `egui` time when the operation completed.
547 /// This method calls `poll()` internally.
548 pub fn get_complete_time(&mut self) -> f64 {
549 self.poll();
550 self.last_complete_time
551 }
552
553 /// Gets the duration between the start and completion of the operation.
554 /// This method calls `poll()` internally.
555 pub fn get_elapsed(&mut self) -> f64 {
556 self.poll();
557 self.last_complete_time - self.last_start_time
558 }
559
560 /// Gets the time elapsed since the operation started.
561 /// This method calls `poll()` internally.
562 pub fn since_started(&mut self) -> f64 {
563 self.poll();
564 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_start_time
565 }
566
567 /// Gets the time elapsed since the operation completed.
568 /// This method calls `poll()` internally.
569 pub fn since_completed(&mut self) -> f64 {
570 self.poll();
571 self.since_completed_raw()
572 }
573 fn since_completed_raw(&self) -> f64 {
574 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_complete_time
575 }
576
577 /// Returns an immutable reference to the stored data, if any.
578 /// This method calls `poll()` internally.
579 pub fn read(&mut self) -> &Option<Result<T, E>> {
580 self.poll();
581 &self.data
582 }
583 /// Returns an immutable reference in the ref pattern to the stored data, if any.
584 /// This method calls `poll()` internally.
585 pub fn read_as_ref(&mut self) -> Option<Result<&T, &E>> {
586 self.poll();
587 self.data.as_ref().map(Result::as_ref)
588 }
589
590 /// Returns a mutable reference to the stored data, if any.
591 /// This method calls `poll()` internally.
592 pub fn read_mut(&mut self) -> &mut Option<Result<T, E>> {
593 self.poll();
594 &mut self.data
595 }
596 /// Returns a mutable reference in the ref pattern to the stored data, if any.
597 /// This method calls `poll()` internally.
598 pub fn read_as_mut(&mut self) -> Option<Result<&mut T, &mut E>> {
599 self.poll();
600 self.data.as_mut().map(Result::as_mut)
601 }
602
603 /// Returns the current `State` of the binding.
604 /// This method calls `poll()` internally.
605 pub fn get_state(&mut self) -> State {
606 self.poll();
607 self.state
608 }
609
610 /// Returns the ref filled state of the `Bind`, allowing for exhaustive pattern matching.
611 ///
612 /// This is often the most ergonomic way to display UI based on the `Bind`'s state.
613 /// This method calls `poll()` internally.
614 /// Invariant: `State::Finished` implies `data.is_some()`.
615 ///
616 /// # Example
617 /// ```ignore
618 /// match my_bind.state() {
619 /// StateWithData::Idle => { /* ... */ }
620 /// StateWithData::Pending => { ui.spinner(); }
621 /// StateWithData::Finished(data) => { ui.label(format!("Data: {data:?}")); }
622 /// StateWithData::Failed(err) => { ui.label(format!("Error: {err:?}")); }
623 /// }
624 /// ```
625 pub fn state(&mut self) -> StateWithData<'_, T, E> {
626 self.poll();
627 match self.state {
628 State::Idle => StateWithData::Idle,
629 State::Pending => StateWithData::Pending,
630 State::Finished => match self.data.as_ref() {
631 Some(Ok(data)) => StateWithData::Finished(data),
632 Some(Err(err)) => StateWithData::Failed(err),
633 None => {
634 // This case should be unreachable due to internal invariants.
635 // If state is Finished, data must be Some.
636 self.state = State::Idle;
637 StateWithData::Idle
638 }
639 },
640 }
641 }
642
643 /// Returns the ref filled state or starts a new request if idle.
644 ///
645 /// This method is an ergonomic way to drive a UI. If the `Bind` is `Idle` and has no
646 /// data, it immediately calls the provided closure `f` to start an async operation,
647 /// transitioning the state to `Pending`.
648 ///
649 /// In all cases, it returns the current `StateWithData` for immediate use in a `match`
650 /// statement, making it easy to display a loading indicator, the finished data, or an error.
651 ///
652 /// # Example
653 /// ```ignore
654 /// // In your UI update function:
655 /// match my_bind.state_or_request(fetch_data) {
656 /// StateWithData::Idle => { /* This branch is typically not reached on the first call */ }
657 /// StateWithData::Pending => { ui.spinner(); }
658 /// StateWithData::Finished(data) => { ui.label(format!("Data: {:?}", data)); }
659 /// StateWithData::Failed(err) => { ui.label(format!("Error: {:?}", err)); }
660 /// }
661 /// ```
662 pub fn state_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> StateWithData<'_, T, E>
663 where
664 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
665 T: MaybeSend,
666 E: MaybeSend,
667 {
668 self.poll();
669
670 if self.data.is_none() && matches!(self.state, State::Idle) {
671 self.request(f());
672 }
673 self.state()
674 }
675
676 /// Clears any stored data and resets the state to `Idle`.
677 ///
678 /// If an operation was `Pending`, its result will be discarded. The background task is not
679 /// cancelled and will run to completion.
680 ///
681 /// This method calls `poll()` internally.
682 pub fn clear(&mut self) {
683 self.poll();
684 self.abort();
685 self.state = State::Idle;
686 self.data = None;
687 }
688
689 /// Returns a reference to the data, or starts a new request if idle.
690 ///
691 /// If data is already available (`Finished`), it returns a reference to it.
692 /// If the state is `Idle` and no data is present, it calls `f` to start a new async
693 /// operation and returns `None`.
694 /// If `Pending`, it returns `None`.
695 ///
696 /// This method calls `poll()` internally.
697 pub fn read_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&Result<T, E>>
698 where
699 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
700 T: MaybeSend,
701 E: MaybeSend,
702 {
703 self.poll();
704
705 if self.data.is_none() && matches!(self.state, State::Idle) {
706 self.request(f());
707 }
708 self.data.as_ref()
709 }
710
711 /// Returns a mutable reference to the data, or starts a new request if idle.
712 ///
713 /// This is the mutable version of `read_or_request`.
714 ///
715 /// This method calls `poll()` internally.
716 pub fn read_mut_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&mut Result<T, E>>
717 where
718 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
719 T: MaybeSend,
720 E: MaybeSend,
721 {
722 self.poll();
723
724 if self.data.is_none() && matches!(self.state, State::Idle) {
725 self.request(f());
726 }
727 self.data.as_mut()
728 }
729
730 /// Drives the state machine. This should be called once per frame before accessing state.
731 ///
732 /// **Note**: Most other methods on `Bind` call this internally, so you usually don't
733 /// need to call it yourself.
734 ///
735 /// This method performs several key actions:
736 /// 1. Checks if a pending future has completed and, if so, updates the state to `Finished`.
737 /// 2. Updates internal frame timers used for `retain` logic and time tracking.
738 /// 3. If `retain` is `false`, it clears the data if the `Bind` was not polled in the previous frame.
739 ///
740 /// # Panics
741 /// - Panics if the state is `Pending` but the internal receiver is missing. This indicates a bug in `egui-async`.
742 /// - Panics if the `oneshot` channel's sender is dropped without sending a value, which would mean the
743 /// spawned task terminated unexpectedly.
744 pub fn poll(&mut self) {
745 let curr_frame = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
746
747 // Avoid re-polling within the same frame.
748 #[allow(clippy::float_cmp)]
749 if curr_frame == self.drawn_time_last {
750 return;
751 }
752
753 // Shift frame times for tracking visibility across frames.
754 self.drawn_time_prev = self.drawn_time_last;
755 self.drawn_time_last = curr_frame;
756
757 // If `retain` is false and the UI element associated with this `Bind` was not rendered
758 // in the previous frame, we clear its data to free resources and ensure a fresh load.
759 if !self.retain() && !self.was_drawn_last_frame() {
760 // Manually clear state to avoid a recursive call to poll() from clear().
761 self.abort();
762 self.state = State::Idle;
763 self.data = None;
764 }
765
766 if matches!(self.state, State::Pending) {
767 let task = self
768 .in_flight
769 .as_mut()
770 .expect("BUG: Pending but no in_flight.");
771 match task.poll_result() {
772 Ok(result) => {
773 self.data = Some(result);
774 self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
775 self.state = State::Finished;
776 self.in_flight = None; // Drop the in_flight receiver as it's no longer needed.
777 }
778 Err(oneshot::error::TryRecvError::Empty) => {
779 // Future is still running, do nothing.
780 }
781 Err(oneshot::error::TryRecvError::Closed) => {
782 // Treat as cancellation: clear the pending state without crashing the app.
783 tracing::warn!(
784 "Async task cancelled: sender dropped without sending a result."
785 );
786 self.state = State::Idle;
787 self.in_flight = None;
788 }
789 }
790 }
791 }
792
793 /// Checks if this `Bind` has been polled during the current `egui` frame.
794 #[allow(clippy::float_cmp)]
795 pub fn was_drawn_this_frame(&self) -> bool {
796 self.drawn_time_last == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
797 }
798
799 /// Checks if this `Bind` was polled during the previous `egui` frame.
800 ///
801 /// This is used internally to implement the `retain` logic.
802 #[allow(clippy::float_cmp)]
803 pub fn was_drawn_last_frame(&self) -> bool {
804 self.drawn_time_prev == LAST_FRAME.load(std::sync::atomic::Ordering::Relaxed)
805 }
806
807 /// Returns the total number of times an async operation has been executed.
808 pub const fn count_executed(&self) -> usize {
809 self.times_executed
810 }
811}