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///
19/// This is the default runtime. It is only created once a task is spawned without another
20/// runtime chosen through [`set_global_runtime`] or [`Bind::set_runtime`].
21#[cfg(not(target_family = "wasm"))]
22pub static ASYNC_RUNTIME: std::sync::LazyLock<tokio::runtime::Runtime> =
23 std::sync::LazyLock::new(|| {
24 tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime.")
25 });
26
27/// The runtime set by [`set_global_runtime`], if any.
28#[cfg(not(target_family = "wasm"))]
29static GLOBAL_RUNTIME: std::sync::OnceLock<tokio::runtime::Handle> = std::sync::OnceLock::new();
30
31/// Sets the Tokio runtime that every `Bind` spawns its tasks onto, in place of the built-in
32/// [`ASYNC_RUNTIME`]. A `Bind` with its own runtime ([`Bind::set_runtime`]) still uses that.
33///
34/// Call this once at startup, before the first request. Tasks already spawned stay on the
35/// runtime they were spawned on, and the built-in runtime is never created if no task needs it.
36///
37/// The runtime must stay alive and keep running tasks for as long as `Bind`s use it. A
38/// multi-thread runtime runs tasks on its worker threads, but a current-thread runtime only runs
39/// them while a thread is blocked on it. Requests spawned onto a runtime that has shut down are
40/// cancelled, and the `Bind` returns to `Idle`.
41///
42/// Native only: on WASM, tasks always run on the browser's event loop.
43///
44/// # Errors
45/// Returns the handle if a global runtime was already set.
46///
47/// # Example
48/// ```
49/// let runtime = tokio::runtime::Runtime::new().expect("failed to create runtime");
50/// egui_async::set_global_runtime(runtime.handle().clone()).expect("global runtime already set");
51///
52/// // Every `Bind` now spawns its tasks onto `runtime`, which must outlive them.
53/// ```
54#[cfg(not(target_family = "wasm"))]
55pub fn set_global_runtime(handle: tokio::runtime::Handle) -> Result<(), tokio::runtime::Handle> {
56 GLOBAL_RUNTIME.set(handle)
57}
58
59/// Returns the runtime that a `Bind` without its own runtime spawns tasks onto.
60#[cfg(not(target_family = "wasm"))]
61fn global_runtime() -> &'static tokio::runtime::Handle {
62 GLOBAL_RUNTIME
63 .get()
64 .unwrap_or_else(|| ASYNC_RUNTIME.handle())
65}
66
67/// A global holder for the `egui::Context`, used to request repaints from background tasks.
68///
69/// This is initialized once by `EguiAsyncPlugin`.
70///
71/// Not available on threaded WASM (`wasm32` with the `atomics` target feature), where
72/// `egui::Context` is neither `Send` nor `Sync`. There, the context is kept in a thread-local
73/// on the thread that runs the plugin instead.
74#[cfg(all(
75 feature = "egui",
76 not(all(target_arch = "wasm32", target_feature = "atomics"))
77))]
78pub static CTX: std::sync::OnceLock<egui::Context> = std::sync::OnceLock::new();
79
80#[cfg(all(feature = "egui", target_arch = "wasm32", target_feature = "atomics"))]
81thread_local! {
82 /// Thread-local holder for the `egui::Context` on threaded WASM.
83 ///
84 /// Only the thread that runs `EguiAsyncPlugin` (the main thread) has a context. Tasks are
85 /// spawned with `wasm_bindgen_futures::spawn_local`, which polls them on the thread that
86 /// spawned them, so results requested from that thread are delivered where the context lives.
87 static CTX: std::cell::OnceCell<egui::Context> = const { std::cell::OnceCell::new() };
88}
89
90/// Stores the `egui::Context` used to request repaints, if it is not already set.
91#[cfg(feature = "egui")]
92pub(crate) fn init_ctx(ctx: &egui::Context) {
93 #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))]
94 CTX.get_or_init(|| ctx.clone());
95
96 #[cfg(all(target_arch = "wasm32", target_feature = "atomics"))]
97 let _ = CTX.try_with(|cell| {
98 cell.get_or_init(|| ctx.clone());
99 });
100}
101
102/// Requests a repaint of the stored `egui::Context`, if one has been set.
103///
104/// On threaded WASM, only the thread that runs `EguiAsyncPlugin` can request a repaint; on any
105/// other thread this does nothing.
106fn request_repaint() {
107 #[cfg(all(
108 feature = "egui",
109 not(all(target_arch = "wasm32", target_feature = "atomics"))
110 ))]
111 if let Some(ctx) = CTX.get() {
112 ctx.request_repaint();
113 }
114
115 #[cfg(all(feature = "egui", target_arch = "wasm32", target_feature = "atomics"))]
116 let _ = CTX.try_with(|cell| {
117 if let Some(ctx) = cell.get() {
118 ctx.request_repaint();
119 }
120 });
121}
122
123/// Represents the execution state of an asynchronous operation managed by `Bind`.
124#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
125pub enum State {
126 /// No operation is running, and no data is available from a previous run.
127 #[default]
128 Idle,
129 /// An operation is currently in-flight.
130 Pending,
131 /// An operation has completed, and its result (success or error) is available.
132 Finished,
133}
134
135/// Represents the detailed state of a `Bind`, including available data.
136pub enum StateWithData<'a, T, E> {
137 /// No operation is running.
138 Idle,
139 /// An operation is currently in-flight.
140 Pending,
141 /// An operation has completed with a successful result.
142 Finished(&'a T),
143 /// An operation has completed with an error.
144 Failed(&'a E),
145}
146
147impl<T: Debug, E: Debug> Debug for StateWithData<'_, T, E> {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 match self {
150 StateWithData::Idle => f.write_str("Idle"),
151 StateWithData::Pending => f.write_str("Pending"),
152 StateWithData::Finished(t) => f.debug_tuple("Finished").field(t).finish(),
153 StateWithData::Failed(e) => f.debug_tuple("Failed").field(e).finish(),
154 }
155 }
156}
157
158bitflags::bitflags! {
159 /// Configuration flags for `Bind` behavior.
160 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
161 pub struct ConfigFlags: u8 {
162 /// If `true`, the `data` from a `Finished` state is preserved even if the `Bind` instance
163 /// is not polled for one or more frames. If `false`, the data is cleared.
164 const RETAIN = 0b0000_0001;
165 /// Opt-in: Physically abort the background task when the Bind is aborted,
166 /// cleared, or a new request is made.
167 ///
168 /// On native targets, the Tokio task is aborted. On WASM, the spawned local
169 /// task is dropped the next time the executor polls it, without polling the
170 /// future again.
171 ///
172 /// **Warning:** This terminates the task at its current `.await` point. If the
173 /// future has critical side effects (e.g., I/O, cleanup), they may not complete.
174 const ABORT = 0b0000_0010;
175 }
176}
177
178impl Default for ConfigFlags {
179 fn default() -> Self {
180 Self::empty()
181 }
182}
183
184/// Internal container to keep the receiver and its control handle synchronized.
185/// This should exist only when the state is `Pending`.
186struct InFlight<T, E> {
187 /// The receiving end of a one-shot channel used to get the result from the background task.
188 recv: oneshot::Receiver<Result<T, E>>,
189
190 /// The abort handle for the spawned task (native only).
191 #[cfg(not(target_family = "wasm"))]
192 handle: tokio::task::AbortHandle,
193
194 /// Signals the spawned task to stop (WASM only). Dropping it unsent lets the task run on.
195 #[cfg(target_family = "wasm")]
196 abort_tx: oneshot::Sender<()>,
197}
198
199impl<T, E> Debug for InFlight<T, E> {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 let mut out = f.debug_struct("InFlight");
202 out.field("recv", &"oneshot::Receiver<...>");
203
204 #[cfg(not(target_family = "wasm"))]
205 out.field("handle", &self.handle);
206
207 #[cfg(target_family = "wasm")]
208 out.field("abort_tx", &"oneshot::Sender<()>");
209
210 out.finish()
211 }
212}
213
214impl<T, E> InFlight<T, E> {
215 fn abort(self) {
216 #[cfg(not(target_family = "wasm"))]
217 self.handle.abort();
218
219 // An error means the task already finished, so there is nothing to stop.
220 #[cfg(target_family = "wasm")]
221 let _ = self.abort_tx.send(());
222 }
223
224 fn poll_result(&mut self) -> Result<Result<T, E>, oneshot::error::TryRecvError> {
225 self.recv.try_recv()
226 }
227}
228
229/// A state manager for a single asynchronous operation, designed for use with `egui`.
230///
231/// `Bind` tracks the lifecycle of a `Future` and stores its `Result<T, E>`. It acts as a
232/// bridge between the immediate-mode UI and the background async task, ensuring the UI
233/// can react to changes in state (e.g., show a spinner while `Pending`, display the
234/// result when `Finished`, or show an error).
235pub struct Bind<T, E> {
236 /// The `egui` time of the most recent frame where this `Bind` was polled.
237 drawn_time_last: f64,
238 /// The `egui` time of the second most recent frame where this `Bind` was polled.
239 drawn_time_prev: f64,
240
241 /// The result of the completed async operation. `None` if the task is not `Finished`.
242 pub(crate) data: Option<Result<T, E>>,
243 in_flight: Option<InFlight<T, E>>,
244
245 /// The current execution state of the async operation.
246 pub(crate) state: State,
247 /// The `egui` time when the most recent operation was started.
248 last_start_time: f64,
249 /// The `egui` time when the most recent operation was completed.
250 last_complete_time: f64,
251
252 /// Configuration option flags
253 pub config: ConfigFlags,
254
255 /// The runtime this `Bind` spawns tasks onto, overriding the global runtime (native only).
256 #[cfg(not(target_family = "wasm"))]
257 runtime: Option<tokio::runtime::Handle>,
258
259 /// A counter for how many times an async operation has been started.
260 times_executed: usize,
261}
262
263impl<T, E> Debug for Bind<T, E> {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 let mut out = f.debug_struct("Bind");
266 let mut out = out
267 .field("state", &self.state)
268 .field("config", &self.config)
269 .field("drawn_time_last", &self.drawn_time_last)
270 .field("drawn_time_prev", &self.drawn_time_prev)
271 .field("last_start_time", &self.last_start_time)
272 .field("last_complete_time", &self.last_complete_time)
273 .field("times_executed", &self.times_executed);
274
275 #[cfg(not(target_family = "wasm"))]
276 {
277 out = out.field("runtime", &self.runtime);
278 }
279
280 // Avoid printing the full data/recv content for cleaner debug output.
281 if self.data.is_some() {
282 out = out.field("data", &"Some(...)");
283 } else {
284 out = out.field("data", &"None");
285 }
286
287 if let Some(in_flight) = &self.in_flight {
288 out = out.field("in_flight", in_flight);
289 } else {
290 out = out.field("in_flight", &"None");
291 }
292
293 out.finish()
294 }
295}
296
297impl<T: 'static, E: 'static> Default for Bind<T, E> {
298 /// Creates a default `Bind` instance in an `Idle` state.
299 ///
300 /// The `retain` flag is set to `false`. This implementation does not require `T` or `E`
301 /// to implement `Default`.
302 fn default() -> Self {
303 Self::new(false)
304 }
305}
306
307/// Runs `task` until it completes or a stop signal arrives on `abort_rx`.
308///
309/// The receiver is polled before the task: `Ok` drops the task without polling it again, while
310/// `Err` (the sender was dropped unsent) stops listening and lets the task run to completion.
311#[cfg(target_family = "wasm")]
312async fn abortable(task: impl Future<Output = ()>, abort_rx: oneshot::Receiver<()>) {
313 use std::task::Poll;
314
315 let mut task = std::pin::pin!(task);
316 let mut abort_rx = Some(abort_rx);
317
318 std::future::poll_fn(|cx| {
319 if let Some(rx) = abort_rx.as_mut() {
320 match std::pin::Pin::new(rx).poll(cx) {
321 Poll::Ready(Ok(())) => return Poll::Ready(()),
322 Poll::Ready(Err(_)) => abort_rx = None,
323 Poll::Pending => {}
324 }
325 }
326 task.as_mut().poll(cx)
327 })
328 .await;
329}
330
331/// A trait alias for `Send` on native targets.
332///
333/// On WASM, this trait has no bounds, allowing non-`Send` types to be used in `Bind`
334/// since WASM is single-threaded.
335#[cfg(not(target_family = "wasm"))]
336pub trait MaybeSend: Send {}
337#[cfg(not(target_family = "wasm"))]
338impl<T: Send> MaybeSend for T {}
339
340/// A trait alias with no bounds on WASM targets.
341///
342/// This allows `Bind` to work with `!Send` futures and data types in a single-threaded
343/// web environment.
344#[cfg(target_family = "wasm")]
345pub trait MaybeSend {}
346#[cfg(target_family = "wasm")]
347impl<T> MaybeSend for T {}
348
349impl<T: 'static, E: 'static> Bind<T, E> {
350 /// Creates a new `Bind` instance with a specific retain policy.
351 ///
352 /// # Parameters
353 /// - `retain`: If `true`, the result of the operation is kept even if the `Bind`
354 /// is not polled in a frame. If `false`, the result is cleared if not polled
355 /// for one frame, returning the `Bind` to an `Idle` state.
356 #[must_use]
357 pub const fn new(retain: bool) -> Self {
358 Self {
359 drawn_time_last: 0.0,
360 drawn_time_prev: 0.0,
361
362 data: None,
363 in_flight: None,
364
365 state: State::Idle,
366 last_start_time: 0.0,
367 last_complete_time: f64::MIN, // Set to a very low value to ensure `since_completed` is large initially.
368
369 config: if retain {
370 ConfigFlags::RETAIN
371 } else {
372 ConfigFlags::empty()
373 },
374
375 #[cfg(not(target_family = "wasm"))]
376 runtime: None,
377
378 times_executed: 0,
379 }
380 }
381
382 /// Sets the Tokio runtime this `Bind` spawns its tasks onto, for use when building a `Bind`.
383 ///
384 /// See [`Bind::set_runtime`].
385 ///
386 /// # Example
387 /// ```
388 /// # use egui_async::Bind;
389 /// let runtime = tokio::runtime::Runtime::new().expect("failed to create runtime");
390 /// let bind: Bind<String, String> = Bind::new(false).with_runtime(runtime.handle().clone());
391 /// ```
392 #[cfg(not(target_family = "wasm"))]
393 #[must_use]
394 pub fn with_runtime(mut self, handle: tokio::runtime::Handle) -> Self {
395 self.runtime = Some(handle);
396 self
397 }
398
399 /// Returns the Tokio runtime set for this `Bind`, if any.
400 ///
401 /// `None` means it uses the global runtime: the one set with [`set_global_runtime`], or the
402 /// built-in [`ASYNC_RUNTIME`].
403 #[cfg(not(target_family = "wasm"))]
404 #[must_use]
405 pub const fn runtime(&self) -> Option<&tokio::runtime::Handle> {
406 self.runtime.as_ref()
407 }
408
409 /// Sets the Tokio runtime this `Bind` spawns its tasks onto, overriding the global runtime.
410 /// `None` returns it to the global runtime.
411 ///
412 /// This takes effect from the next request; a task already in flight stays on the runtime it
413 /// was spawned on. The same requirements as for [`set_global_runtime`] apply.
414 ///
415 /// Native only: on WASM, tasks always run on the browser's event loop.
416 #[cfg(not(target_family = "wasm"))]
417 pub fn set_runtime(&mut self, handle: Option<tokio::runtime::Handle>) {
418 self.runtime = handle;
419 }
420
421 /// Returns whether finished data is retained across undrawn frames.
422 #[must_use]
423 pub const fn retain(&self) -> bool {
424 self.config.contains(ConfigFlags::RETAIN)
425 }
426
427 /// Sets retain policy for finished data.
428 pub fn set_retain(&mut self, retain: bool) {
429 if retain {
430 self.config.insert(ConfigFlags::RETAIN);
431 } else {
432 self.config.remove(ConfigFlags::RETAIN);
433 }
434 }
435
436 /// Returns whether background tasks are physically aborted when cleared or replaced.
437 #[must_use]
438 pub const fn abort_on_clear(&self) -> bool {
439 self.config.contains(ConfigFlags::ABORT)
440 }
441
442 /// Sets whether background tasks are physically aborted when cleared or replaced.
443 ///
444 /// See [`ConfigFlags::ABORT`] for how this behaves on native and WASM targets.
445 pub fn set_abort(&mut self, abort: bool) {
446 if abort {
447 self.config.insert(ConfigFlags::ABORT);
448 } else {
449 self.config.remove(ConfigFlags::ABORT);
450 }
451 }
452
453 /// Internal async function that awaits the user's future and sends the result back.
454 async fn req_inner<F>(fut: F, tx: oneshot::Sender<Result<T, E>>)
455 where
456 F: Future<Output = Result<T, E>> + 'static,
457 T: MaybeSend,
458 {
459 let result = fut.await;
460 if matches!(tx.send(result), Ok(())) {
461 // If the send was successful, request a repaint to show the new data.
462 request_repaint();
463 } else {
464 // This occurs if the `Bind` was dropped before the future completed.
465 warn!("Future result was dropped because the receiver was gone.");
466 }
467 }
468
469 /// Starts an asynchronous operation if the `Bind` is not already `Pending`.
470 ///
471 /// The provided future `f` is spawned onto the appropriate runtime (`tokio` for native,
472 /// `wasm-bindgen-futures` for WASM). The `Bind` state transitions to `Pending`.
473 ///
474 /// This method calls `poll()` internally.
475 pub fn request<Fut>(&mut self, f: Fut)
476 where
477 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
478 T: MaybeSend,
479 E: MaybeSend,
480 {
481 // Drive state machine to catch results from tasks finishing this frame
482 self.poll();
483
484 // Handle existing task based on config; sets state to Idle if still Pending
485 self.abort();
486
487 self.last_start_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
488 let (tx, rx) = oneshot::channel();
489
490 // Spawn the async task
491 tracing::trace!("spawning async request #{}", self.times_executed + 1);
492
493 #[cfg(not(target_family = "wasm"))]
494 let in_flight = {
495 let runtime = self.runtime.as_ref().unwrap_or_else(|| global_runtime());
496 InFlight {
497 recv: rx,
498 handle: runtime.spawn(Self::req_inner(f, tx)).abort_handle(),
499 }
500 };
501
502 #[cfg(target_family = "wasm")]
503 let in_flight = {
504 let (abort_tx, abort_rx) = oneshot::channel();
505 wasm_bindgen_futures::spawn_local(abortable(Self::req_inner(f, tx), abort_rx));
506 InFlight { recv: rx, abort_tx }
507 };
508
509 self.in_flight = Some(in_flight);
510 self.state = State::Pending;
511 self.times_executed += 1;
512 }
513
514 /// Convenience: periodic request using `std::time::Duration`.
515 #[must_use]
516 pub fn request_every<Fut>(&mut self, f: impl FnOnce() -> Fut, every: std::time::Duration) -> f64
517 where
518 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
519 T: MaybeSend,
520 E: MaybeSend,
521 {
522 self.request_every_sec(f, every.as_secs_f64())
523 }
524
525 /// Requests an operation to run periodically.
526 ///
527 /// If the `Bind` is not `Pending` and more than `secs` seconds have passed since the
528 /// last completion, a new request is started by calling `f`.
529 ///
530 /// # Returns
531 /// The time in seconds remaining until the next scheduled refresh. A negative value
532 /// indicates a refresh is overdue.
533 pub fn request_every_sec<Fut>(&mut self, f: impl FnOnce() -> Fut, secs: f64) -> f64
534 where
535 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
536 T: MaybeSend,
537 E: MaybeSend,
538 {
539 let state = self.get_state();
540 let since_completed = self.since_completed_raw();
541
542 if state != State::Pending && since_completed > secs {
543 self.request(f());
544 }
545
546 secs - since_completed
547 }
548
549 /// Explicitly cancels the in-flight task and resets the state to `Idle`.
550 ///
551 /// If the [`ConfigFlags::ABORT`] flag is set, the background task is physically
552 /// terminated: aborted on native, or dropped at its next poll on WASM. Otherwise, the
553 /// task runs to completion and its result is simply ignored.
554 pub fn abort(&mut self) {
555 // Logical: Take the in-flight handle. This detaches the Bind from the task.
556 if let Some(task) = self.in_flight.take() {
557 // Physical: Only signal the runtime to kill the task if configured.
558 if self.config.contains(ConfigFlags::ABORT) {
559 task.abort();
560 }
561 }
562
563 // Ensure state is synchronized with the removal of the in-flight task.
564 if matches!(self.state, State::Pending) {
565 self.state = State::Idle;
566 }
567 }
568
569 /// Clears any existing data and immediately starts a new async operation.
570 ///
571 /// If an operation was `Pending`, its result will be discarded. The background task runs to
572 /// completion unless the [`ConfigFlags::ABORT`] flag is set.
573 ///
574 /// This is a convenience method equivalent to calling `clear()` followed by `request()`.
575 pub fn refresh<Fut>(&mut self, f: Fut)
576 where
577 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
578 T: MaybeSend,
579 E: MaybeSend,
580 {
581 self.clear();
582 self.request(f);
583 }
584
585 /// Takes ownership of the result if the operation is `Finished`.
586 ///
587 /// If the state is `Finished`, this method returns `Some(result)`, consumes the data
588 /// internally, and resets the state to `Idle`. If the state is not `Finished`,
589 /// it returns `None`.
590 ///
591 /// This method calls `poll()` internally.
592 pub fn take(&mut self) -> Option<Result<T, E>> {
593 self.poll();
594
595 if matches!(self.state, State::Finished) {
596 assert!(
597 self.data.is_some(),
598 "State was Finished but data was None. This indicates a bug."
599 );
600 self.state = State::Idle;
601 self.data.take()
602 } else {
603 None
604 }
605 }
606
607 /// Manually sets the data and moves the state to `Finished`.
608 ///
609 /// This can be used to inject data into the `Bind` without running an async operation.
610 ///
611 /// If an operation is currently `Pending`, it will be aborted. If data is already
612 /// present, it will be overwritten.
613 pub fn fill(&mut self, data: Result<T, E>) {
614 if self.just_completed() || self.just_started() {
615 tracing::warn!(
616 "Bind::fill called multiple times in the same frame. This may indicate a logic error in your update loop."
617 );
618 }
619
620 // Ensure clean state: abort pending tasks and reset to Idle.
621 self.clear();
622
623 self.state = State::Finished;
624
625 let now = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
626 self.last_start_time = now;
627 self.last_complete_time = now;
628
629 self.data = Some(data);
630 }
631
632 /// Returns `Some(&T)` when finished successfully.
633 #[must_use]
634 pub fn ok_ref(&mut self) -> Option<&T> {
635 self.poll();
636 self.data.as_ref()?.as_ref().ok()
637 }
638
639 /// Returns `Some(&E)` when finished with error.
640 #[must_use]
641 pub fn err_ref(&mut self) -> Option<&E> {
642 self.poll();
643 self.data.as_ref()?.as_ref().err()
644 }
645
646 /// Takes and returns `T` only if finished successfully.
647 pub fn take_ok(&mut self) -> Option<T> {
648 self.poll();
649 match self.data.take()? {
650 Ok(t) => {
651 self.state = State::Idle;
652 Some(t)
653 }
654 Err(e) => {
655 self.data = Some(Err(e));
656 None
657 }
658 }
659 }
660
661 /// Checks if the current state is `Idle`.
662 /// This method calls `poll()` internally.
663 pub fn is_idle(&mut self) -> bool {
664 self.poll();
665 matches!(self.state, State::Idle)
666 }
667
668 /// Checks if the current state is `Pending`.
669 /// This method calls `poll()` internally.
670 pub fn is_pending(&mut self) -> bool {
671 self.poll();
672 matches!(self.state, State::Pending)
673 }
674
675 /// Checks if the current state is `Finished`.
676 /// This method calls `poll()` internally.
677 pub fn is_finished(&mut self) -> bool {
678 self.poll();
679 matches!(self.state, State::Finished)
680 }
681
682 /// Returns `true` if finished with `Ok`.
683 #[must_use]
684 pub fn is_ok(&mut self) -> bool {
685 self.poll();
686 matches!(self.data, Some(Ok(_)))
687 }
688
689 /// Returns `true` if finished with `Err`.
690 #[must_use]
691 pub fn is_err(&mut self) -> bool {
692 self.poll();
693 matches!(self.data, Some(Err(_)))
694 }
695
696 /// Returns `true` if the operation finished during the current `egui` frame.
697 /// This method calls `poll()` internally.
698 #[allow(clippy::float_cmp)]
699 pub fn just_completed(&mut self) -> bool {
700 self.poll();
701 self.last_complete_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
702 }
703
704 /// If the operation just completed this frame, invokes the provided closure with
705 /// a reference to the result.
706 pub fn on_finished(&mut self, f: impl FnOnce(&Result<T, E>)) {
707 if self.just_completed()
708 && let Some(ref d) = self.data
709 {
710 f(d);
711 }
712 }
713
714 /// Returns `true` if the operation started during the current `egui` frame.
715 /// This method calls `poll()` internally.
716 #[allow(clippy::float_cmp)]
717 pub fn just_started(&mut self) -> bool {
718 self.poll();
719 self.last_start_time == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
720 }
721
722 /// Gets the `egui` time when the operation started.
723 /// This method calls `poll()` internally.
724 pub fn get_start_time(&mut self) -> f64 {
725 self.poll();
726 self.last_start_time
727 }
728
729 /// Gets the `egui` time when the operation completed.
730 /// This method calls `poll()` internally.
731 pub fn get_complete_time(&mut self) -> f64 {
732 self.poll();
733 self.last_complete_time
734 }
735
736 /// Gets the duration between the start and completion of the operation.
737 /// This method calls `poll()` internally.
738 pub fn get_elapsed(&mut self) -> f64 {
739 self.poll();
740 self.last_complete_time - self.last_start_time
741 }
742
743 /// Gets the time elapsed since the operation started.
744 /// This method calls `poll()` internally.
745 pub fn since_started(&mut self) -> f64 {
746 self.poll();
747 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_start_time
748 }
749
750 /// Gets the time elapsed since the operation completed.
751 /// This method calls `poll()` internally.
752 pub fn since_completed(&mut self) -> f64 {
753 self.poll();
754 self.since_completed_raw()
755 }
756 fn since_completed_raw(&self) -> f64 {
757 CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed) - self.last_complete_time
758 }
759
760 /// Returns an immutable reference to the stored data, if any.
761 /// This method calls `poll()` internally.
762 pub fn read(&mut self) -> &Option<Result<T, E>> {
763 self.poll();
764 &self.data
765 }
766 /// Returns an immutable reference in the ref pattern to the stored data, if any.
767 /// This method calls `poll()` internally.
768 pub fn read_as_ref(&mut self) -> Option<Result<&T, &E>> {
769 self.poll();
770 self.data.as_ref().map(Result::as_ref)
771 }
772
773 /// Returns a mutable reference to the stored data, if any.
774 /// This method calls `poll()` internally.
775 pub fn read_mut(&mut self) -> &mut Option<Result<T, E>> {
776 self.poll();
777 &mut self.data
778 }
779 /// Returns a mutable reference in the ref pattern to the stored data, if any.
780 /// This method calls `poll()` internally.
781 pub fn read_as_mut(&mut self) -> Option<Result<&mut T, &mut E>> {
782 self.poll();
783 self.data.as_mut().map(Result::as_mut)
784 }
785
786 /// Returns the current `State` of the binding.
787 /// This method calls `poll()` internally.
788 pub fn get_state(&mut self) -> State {
789 self.poll();
790 self.state
791 }
792
793 /// Returns the ref filled state of the `Bind`, allowing for exhaustive pattern matching.
794 ///
795 /// This is often the most ergonomic way to display UI based on the `Bind`'s state.
796 /// This method calls `poll()` internally.
797 /// Invariant: `State::Finished` implies `data.is_some()`.
798 ///
799 /// # Example
800 /// ```ignore
801 /// match my_bind.state() {
802 /// StateWithData::Idle => { /* ... */ }
803 /// StateWithData::Pending => { ui.spinner(); }
804 /// StateWithData::Finished(data) => { ui.label(format!("Data: {data:?}")); }
805 /// StateWithData::Failed(err) => { ui.label(format!("Error: {err:?}")); }
806 /// }
807 /// ```
808 pub fn state(&mut self) -> StateWithData<'_, T, E> {
809 self.poll();
810 match self.state {
811 State::Idle => StateWithData::Idle,
812 State::Pending => StateWithData::Pending,
813 State::Finished => match self.data.as_ref() {
814 Some(Ok(data)) => StateWithData::Finished(data),
815 Some(Err(err)) => StateWithData::Failed(err),
816 None => {
817 // This case should be unreachable due to internal invariants.
818 // If state is Finished, data must be Some.
819 self.state = State::Idle;
820 StateWithData::Idle
821 }
822 },
823 }
824 }
825
826 /// Returns the ref filled state or starts a new request if idle.
827 ///
828 /// This method is an ergonomic way to drive a UI. If the `Bind` is `Idle` and has no
829 /// data, it immediately calls the provided closure `f` to start an async operation,
830 /// transitioning the state to `Pending`.
831 ///
832 /// In all cases, it returns the current `StateWithData` for immediate use in a `match`
833 /// statement, making it easy to display a loading indicator, the finished data, or an error.
834 ///
835 /// # Example
836 /// ```ignore
837 /// // In your UI update function:
838 /// match my_bind.state_or_request(fetch_data) {
839 /// StateWithData::Idle => { /* This branch is typically not reached on the first call */ }
840 /// StateWithData::Pending => { ui.spinner(); }
841 /// StateWithData::Finished(data) => { ui.label(format!("Data: {:?}", data)); }
842 /// StateWithData::Failed(err) => { ui.label(format!("Error: {:?}", err)); }
843 /// }
844 /// ```
845 pub fn state_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> StateWithData<'_, T, E>
846 where
847 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
848 T: MaybeSend,
849 E: MaybeSend,
850 {
851 self.poll();
852
853 if self.data.is_none() && matches!(self.state, State::Idle) {
854 self.request(f());
855 }
856 self.state()
857 }
858
859 /// Clears any stored data and resets the state to `Idle`.
860 ///
861 /// If an operation was `Pending`, its result will be discarded. The background task runs to
862 /// completion unless the [`ConfigFlags::ABORT`] flag is set.
863 ///
864 /// This method calls `poll()` internally.
865 pub fn clear(&mut self) {
866 self.poll();
867 self.abort();
868 self.state = State::Idle;
869 self.data = None;
870 }
871
872 /// Returns a reference to the data, or starts a new request if idle.
873 ///
874 /// If data is already available (`Finished`), it returns a reference to it.
875 /// If the state is `Idle` and no data is present, it calls `f` to start a new async
876 /// operation and returns `None`.
877 /// If `Pending`, it returns `None`.
878 ///
879 /// This method calls `poll()` internally.
880 pub fn read_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&Result<T, E>>
881 where
882 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
883 T: MaybeSend,
884 E: MaybeSend,
885 {
886 self.poll();
887
888 if self.data.is_none() && matches!(self.state, State::Idle) {
889 self.request(f());
890 }
891 self.data.as_ref()
892 }
893
894 /// Returns a mutable reference to the data, or starts a new request if idle.
895 ///
896 /// This is the mutable version of `read_or_request`.
897 ///
898 /// This method calls `poll()` internally.
899 pub fn read_mut_or_request<Fut>(&mut self, f: impl FnOnce() -> Fut) -> Option<&mut Result<T, E>>
900 where
901 Fut: Future<Output = Result<T, E>> + MaybeSend + 'static,
902 T: MaybeSend,
903 E: MaybeSend,
904 {
905 self.poll();
906
907 if self.data.is_none() && matches!(self.state, State::Idle) {
908 self.request(f());
909 }
910 self.data.as_mut()
911 }
912
913 /// Drives the state machine. This should be called once per frame before accessing state.
914 ///
915 /// **Note**: Most other methods on `Bind` call this internally, so you usually don't
916 /// need to call it yourself.
917 ///
918 /// This method performs several key actions:
919 /// 1. Checks if a pending future has completed and, if so, updates the state to `Finished`.
920 /// 2. Updates internal frame timers used for `retain` logic and time tracking.
921 /// 3. If `retain` is `false`, it clears the data if the `Bind` was not polled in the previous frame.
922 ///
923 /// # Panics
924 /// - Panics if the state is `Pending` but the internal receiver is missing. This indicates a bug in `egui-async`.
925 /// - Panics if the `oneshot` channel's sender is dropped without sending a value, which would mean the
926 /// spawned task terminated unexpectedly.
927 pub fn poll(&mut self) {
928 let curr_frame = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
929
930 // Avoid re-polling within the same frame.
931 #[allow(clippy::float_cmp)]
932 if curr_frame == self.drawn_time_last {
933 return;
934 }
935
936 // Shift frame times for tracking visibility across frames.
937 self.drawn_time_prev = self.drawn_time_last;
938 self.drawn_time_last = curr_frame;
939
940 // If `retain` is false and the UI element associated with this `Bind` was not rendered
941 // in the previous frame, we clear its data to free resources and ensure a fresh load.
942 if !self.retain() && !self.was_drawn_last_frame() {
943 // Manually clear state to avoid a recursive call to poll() from clear().
944 self.abort();
945 self.state = State::Idle;
946 self.data = None;
947 }
948
949 if matches!(self.state, State::Pending) {
950 let task = self
951 .in_flight
952 .as_mut()
953 .expect("BUG: Pending but no in_flight.");
954 match task.poll_result() {
955 Ok(result) => {
956 self.data = Some(result);
957 self.last_complete_time = CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed);
958 self.state = State::Finished;
959 self.in_flight = None; // Drop the in_flight receiver as it's no longer needed.
960 }
961 Err(oneshot::error::TryRecvError::Empty) => {
962 // Future is still running, do nothing.
963 }
964 Err(oneshot::error::TryRecvError::Closed) => {
965 // Treat as cancellation: clear the pending state without crashing the app.
966 tracing::warn!(
967 "Async task cancelled: sender dropped without sending a result."
968 );
969 self.state = State::Idle;
970 self.in_flight = None;
971 }
972 }
973 }
974 }
975
976 /// Checks if this `Bind` has been polled during the current `egui` frame.
977 #[allow(clippy::float_cmp)]
978 pub fn was_drawn_this_frame(&self) -> bool {
979 self.drawn_time_last == CURR_FRAME.load(std::sync::atomic::Ordering::Relaxed)
980 }
981
982 /// Checks if this `Bind` was polled during the previous `egui` frame.
983 ///
984 /// This is used internally to implement the `retain` logic.
985 #[allow(clippy::float_cmp)]
986 pub fn was_drawn_last_frame(&self) -> bool {
987 self.drawn_time_prev == LAST_FRAME.load(std::sync::atomic::Ordering::Relaxed)
988 }
989
990 /// Returns the total number of times an async operation has been executed.
991 pub const fn count_executed(&self) -> usize {
992 self.times_executed
993 }
994}