egui-async 0.7.1

A simple library for running async tasks in egui and binding their results to your UI.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
# ๐Ÿ”ฎ egui-async

[![Crates.io](https://img.shields.io/crates/v/egui-async)](https://crates.io/crates/egui-async)
[![Docs.rs](https://docs.rs/egui-async/badge.svg)](https://docs.rs/egui-async)
[![License](https://img.shields.io/crates/l/egui-async)](https://snyk.io/articles/apache-license/#apache-license-vs-mit)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/xangelix?color=ea4aaa&style=flat)](https://github.com/sponsors/xangelix)

A simple, batteries-included library for running async tasks across frames in [`egui`](https://egui.rs/) and binding their results to your UI.

Supports both **native** (Tokio) and **wasm32** (Web) targets out of the box.

## ๐Ÿ“– Overview

Immediate-mode GUI libraries like `egui` are fantastic, but they pose a significant challenge: **how do you run long-running async tasks (like HTTP requests or file I/O) without freezing the UI thread?**

`egui-async` solves this by providing a `Bind<T, E>` struct. This struct acts as a state machine that bridges the gap between your immediate-mode render loop and your background async runtime.

It handles the lifecycle of the Future, manages the state transitions (`Idle` โ†’ `Pending` โ†’ `Finished`), and provides ergonomic UI widgets to visualize that state.

## โœจ Features

- ๐Ÿ”„ **Smart State Management**: Automatically tracks `Idle`, `Pending`, and `Finished` states. No more manual `Option<Result<...>>` juggling.
- ๐ŸŒ **Universal Support**: Seamlessly switches between `tokio` (native) and `wasm-bindgen-futures` (web). Write your code once, run everywhere.
- โšก **Lazy Loading**: `read_or_request` allows you to ergonomically trigger async fetches just by trying to read the data in your UI code.
- โฑ๏ธ **Periodic Updates**: Built-in support for polling data at specific intervals (e.g., every 10 seconds).
- ๐Ÿ›‘ **Task Abortion**: Supports physically aborting background tasks on native and WASM targets when the UI state changes.
- ๐Ÿงฉ **Widgets**: Drop-in UI components like `AsyncButton`, `AsyncSearch`, and `AsyncView` that handle spinners, debouncing, and layout snapping automatically.
- ๐Ÿ› ๏ธ **Batteries Included**: Includes the async runtime for you, along with helper extension traits for popups and retry logic.

## ๐Ÿ“ฆ Installation

```bash
cargo add egui-async
```

### ๐Ÿงฉ Compatibility

`egui` APIs change frequently. Ensure you are using a compatible version of `egui-async` for your project.

| `egui-async` | `egui` |
| ------------ | ------ |
| `>=0.6.0`    | `0.36` |
| `>=0.5.0`    | `0.35` |
| `>=0.4.0`    | `0.34` |
| `>=0.2.0`    | `0.33` |
| `<=0.1.1`    | `0.32` |

## ๐Ÿš€ Quick Start

Using `egui-async` requires two steps: registering the plugin and using a `Bind`.

### 1. Register the Plugin

You **must** register the `EguiAsyncPlugin`. With `egui` 0.34, the best place to do this is in the `logic` step of your app, which runs before `ui` and manages background repaints.

```rust
impl eframe::App for MyApp {
    fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        // ๐Ÿ‘‡ Crucial: Call this once per frame!
        ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
    }

    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        // We use `show_inside` because `ui` already provides a root UI context.
        egui::CentralPanel::default().show_inside(ui, |ui| {
            // Your UI code here...
        });
    }
}
```

### 2. Bind Data

Use `Bind<T, E>` to manage your data.

```rust
use egui_async::Bind;

struct MyApp {
    // Holds a Result<String, String>
    my_ip: Bind<String, String>,
}

// Inside your update loop:
if let Some(res) = self.my_ip.read_or_request(|| async {
    // This async block runs in the background!
    reqwest::get("https://icanhazip.com/")
        .await
        .map_err(|e| e.to_string())?
        .text()
        .await
        .map_err(|e| e.to_string()) // Our Bind<, E> error type is String
}) {
    match res {
        Ok(ip)  => ui.label(format!("IP: {ip}")),
        Err(err) => ui.colored_label(egui::Color32::RED, err),
    }
} else {
    // While the future is running, this block (None) block executes:
    ui.spinner();
}
```

## ๐Ÿ’ก Common Usage Patterns & Examples

`egui-async` is designed to fit several different UI patterns depending on how much control you need.

### 1. Lazy Loading

Use `read_or_request` to defer network or I/O operations until the UI explicitly attempts to render the data. If the data is absent, the request is triggered and the method returns `None`, allowing you to render a loading state.

```rust
// If data is missing, start fetching it.
// If fetching, show a spinner.
// If finished, show the result.
if let Some(result) = self.data.read_or_request(fetch_data) {
    ui.label(format!("Data: {:?}", result));
} else {
    ui.spinner();
}
```

### 2. Explicit State Control

For more complex UI flows, such as authentication screens, use `state_or_request` to exhaustively match against the underlying state machine (`Idle`, `Pending`, `Finished`, or `Failed`). This provides full control over the layout during transitions.

```rust
use egui_async::StateWithData;

match self.login.state() {
    StateWithData::Idle => {
        // State: Idle -> Show Input Form
        ui.label("Please enter your credentials:");
        ui.add_space(10.0);

        // Use a Grid to align the labels and text boxes nicely
        egui::Grid::new("login_form")
            .num_columns(2)
            .spacing([10.0, 10.0])
            .show(ui, |ui| {
                ui.label("Username:");
                ui.text_edit_singleline(&mut self.username);
                ui.end_row();

                ui.label("Password:");
                ui.add(
                    egui::TextEdit::singleline(&mut self.password).password(true),
                );
                ui.end_row();
            });

        ui.add_space(20.0);

        // TRIGGER: User clicks button -> Transitions to Pending
        if ui.button("Log In").clicked() {
            let fut = perform_login(self.username.clone(), self.password.clone());
            self.login.request(fut);
        }
    }

    StateWithData::Pending => {
        // State: Pending -> Show Loading Indicator
        // We disable inputs or just hide them. Here we show a spinner.
        ui.spinner();
        ui.label("Authenticating...");

        ui.add_space(10.0);

        // Option: Allow cancelling the request
        if ui.button("Cancel").clicked() {
            // This physically aborts the background task if configured
            self.login.clear();
        }
    }

    StateWithData::Finished(success_msg) => {
        // State: Finished (Ok) -> Show Success Screen
        ui.label(
            egui::RichText::new("Login Successful!")
                .color(egui::Color32::GREEN)
                .size(20.0),
        );
        ui.label(success_msg);

        ui.add_space(20.0);

        // TRIGGER: Reset to Idle to allow logging in again
        if ui.button("Log Out").clicked() {
            self.username.clear();
            self.password.clear();
            self.login.clear();
        }
    }

    StateWithData::Failed(err_msg) => {
        // State: Failed (Err) -> Show Error and Retry
        ui.label(
            egui::RichText::new("Login Failed")
                .color(egui::Color32::RED)
                .strong(),
        );
        ui.label(err_msg);

        ui.add_space(20.0);

        // TRIGGER: Reset to Idle to try again (keeps previous username/pass typed in)
        if ui.button("Try Again").clicked() {
            self.login.clear();
        }
    }
}
```

### 3. Periodic Polling

Use `request_every_sec` to automatically re-trigger asynchronous tasks at fixed intervals. To prevent request stacking, the interval timer is calculated strictly from the completion of the previous request.

```rust
// Automatically re-run the future if 5.0 seconds have passed since the last finish.
self.server_status.request_every_sec(check_server_health, 5.0);

// Display the current (cached) data
if let Some(Ok(status)) = self.server_status.read() {
    ui.label(format!("Server Status: {status}"));
}

```

### 4. UI Extension Traits

`egui-async` provides the `UiExt` trait to inject asynchronous behaviors directly into `egui::Ui`. This includes standardized components like debounced refresh buttons and error popups.

```rust
use egui_async::UiExt; // Import the trait

// Renders a button that spins while Pending.
// Clicking it forces a refresh.
// It also auto-refreshes every 60 seconds.
//
// We use egui's underlying monotonic clock for timing,
// so we aren't making IO time calls every frame! ๐Ÿ˜‰
ui.refresh_button(&mut self.data, fetch_data, 60.0);

// If the bind failed, show a popup window with the error and a "Retry" button.
self.data.read_or_error(fetch_data, ui);

```

### 5. Other Built-in Widgets

For other standard use cases, utilize the included drop-in widgets (`AsyncButton`, `AsyncSearch`, `AsyncView`). These components automatically handle loading states, input debouncing, and layout transitions.

```rust
use egui_async::egui::{AsyncButton, AsyncView, StateLayout};

// A button that prevents double-clicks and shows an inline spinner
AsyncButton::new(&mut self.submit_bind, "Submit")
    .pending_text("Processing...")
    .show(ui, perform_submission);

// A declarative container that completely manages the Idle/Pending/Ok/Err lifecycle
AsyncView::new(&mut self.data_bind)
    .state_layout(StateLayout::CenterHorizontal)
    .show(ui, fetch_data, |ui, data| {
        ui.label(format!("Success: {data}"));
    });

```

### ๐Ÿ› ๏ธ Building Custom Widgets

Building your own async-aware widgets is straightforward. Pass a `&mut Bind<T, E>` to your widget, and use its state methods (`is_pending()`, `read()`, etc.) to drive your rendering logic, calling `bind.request()` or `bind.refresh()` when the user interacts.

```rust
pub fn my_custom_widget(
    ui: &mut egui::Ui,
    bind: &mut Bind<String, ()>,
    fetch: impl FnOnce() -> impl Future<Output = Result<String, ()>> + 'static
) {
    if bind.is_pending() {
        ui.spinner();
    } else if ui.button("Fetch Data").clicked() {
        bind.request(fetch());
    }
}

```

### ๐Ÿง‘โ€๐Ÿ’ป See it in action:

You can find complete, runnable examples for all these patterns in the [`examples/`](https://github.com/xangelix/egui-async/tree/main/examples) directory of the repository:

- [`simple.rs`]examples/simple.rs โ€“ A minimal HTTP fetch example.
- [`login.rs`]examples/login.rs โ€“ The full "State Machine" pattern with forms and validation.
- [`periodic.rs`]examples/periodic.rs โ€“ A dashboard widget that auto-refreshes.
- [`advanced.rs`]examples/advanced.rs - An online, IP locator tool, with maps.
- [`widgets.rs`]examples/widgets.rs - A showcase of all included async widgets (`AsyncButton`, `AsyncSearch`, `AsyncView`).

Look at the code before you run it and try to predict what it does and what it will look like!

## โš™๏ธ Configuration

### Retain Policy

By default (`Bind::default()`), `egui-async` assumes immediate-mode behavior: if you stop calling `poll()` (or `read_*`) on a Bind for a frame, it assumes the UI element is no longer visible and drops the data to save memory and reset the state.

If you want to keep data and state even when the UI component is hidden (e.g., inside a collapsed header or a closed tab), set `retain` to true:

```rust
let mut bind = Bind::new(true); // Retain = true
```

### Task Abort

You can configure `Bind` to physically abort the background task when the request is aborted, cleared, or overwritten. This is useful for cancelling heavy computations or large downloads.

```rust
let mut bind = Bind::new(true);
bind.set_abort(true); // Enable physical cancellation
```

With the flag set, the in-flight task is aborted whenever the `Bind` lets go of it:

- `abort()`, `clear()`, `refresh()`, or `fill()` is called.
- A new `request()` replaces it.
- With `retain = false`, the `Bind` is drawn again after missing a frame (e.g., the user switched tabs), which resets it.

How the task is stopped depends on the target:

- On **native** targets, the Tokio task is aborted.
- On **WASM** targets, each request carries a small stop signal. The spawned task checks it before running your `Future` each time the browser polls it. Sending the signal also wakes the task, so it is dropped promptly, on the browser's next microtask turn. Your `Future` is never polled again after `abort()`.

Without `set_abort(true)`, or if the `Bind` itself is dropped, the `Future` runs to completion and its result is ignored by the `Bind`.

> **โš ๏ธ Note:** Aborting stops the `Future` at its current `.await` point, so code after that point (e.g., saving a response or cleanup) will not run. Code in `Drop` implementations still runs.
>
> Aborting drops the Rust `Future`, but it does not necessarily cancel work the browser or runtime has already started. A `fetch` that was already sent may still reach the server, depending on the HTTP library. Tasks your `Future` spawned itself are not aborted either.

### Custom Tokio Runtime

On native targets, `egui-async` spawns tasks onto its own built-in Tokio runtime by default, so you never need to pass a runtime around. If your app already has a runtime, you can use it instead, at two levels:

**Globally**, once at startup. All `Bind`s then use your runtime without any other code changes, and the built-in runtime is never created:

```rust
let runtime = tokio::runtime::Runtime::new()?;
egui_async::set_global_runtime(runtime.handle().clone()).expect("global runtime already set");
// Or, inside `#[tokio::main]`: set_global_runtime(tokio::runtime::Handle::current())
```

**Per `Bind`**, e.g., if you have several runtimes. This takes precedence over the global runtime, and every method and widget that drives the `Bind` uses it:

```rust
let mut bind = Bind::new(false).with_runtime(io_runtime.handle().clone());
bind.set_runtime(None); // Back to the global runtime
```

A few things to keep in mind:

- Set the global runtime **before the first request**. It can only be set once; a second call returns `Err`. Tasks already spawned stay on the runtime they started on.
- Your runtime must **stay alive and keep running tasks** while `Bind`s use it. A multi-thread runtime does this on its own worker threads. A current-thread runtime (including `#[tokio::main(flavor = "current_thread")]`) only runs tasks while a thread is blocked on it, which the UI thread usually isn't.
- If the runtime has shut down, new requests on it are cancelled and the `Bind` returns to `Idle`.
- These APIs are **native only**. On WASM, tasks always run on the browser's event loop, so wrap calls in `#[cfg(not(target_family = "wasm"))]` in cross-platform code.

### ๐Ÿ”ง Under the Hood

How does `egui-async` bridge the gap between Immediate Mode GUI (60fps loop) and Asynchronous Runtimes?

1. **The Plugin**: The `EguiAsyncPlugin` acts as the heartbeat. It synchronizes a global atomic clock with `egui`'s input time. This allows `Bind` instances to measure durations (like "time since finished") without expensive syscalls or mutex locking on every frame.
2. **The Channel**: When you call `request()`, we spawn a task on the runtime (Tokio or Wasm). We give that task a `oneshot::Sender`. The `Bind` struct holds the `oneshot::Receiver`.
3. **Non-Blocking Polling**: On every frame where the UI element is drawn, `Bind::poll()` checks the receiver.

- If the channel is empty, it returns `Pending` (and `egui` continues drawing).
- If the channel has data, it moves the state to `Finished`.
- **Crucially**, if the data arrives between frames, `Bind` _automatically requests a repaint_ from the `Context`, ensuring your UI updates immediately without user interaction.

#### Spawning

On **Native** targets, tasks are spawned onto a `tokio` runtime: the built-in one, or your own (see [Custom Tokio Runtime](#custom-tokio-runtime)).

On **WASM** targets, tasks are spawned onto the browser's event loop using `wasm_bindgen_futures::spawn_local`. This detection happens automatically at compile time.

### โš ๏ธ Common Issues

**1. Forgetting the Plugin**
If your UI is stuck in `Pending` forever or your periodic requests aren't triggering, check your `App` implementation. `egui-async` requires its plugin to be registered every frame so it can track time and trigger background repaints.

With `egui` 0.34+, the required place to do this is inside the `logic` function:

```rust
impl eframe::App for MyApp {
    fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        // Without this, egui-async has no concept of time!
        ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
    }

    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
        // ... your UI code ...
    }
}
```

**2. Dropping the Bind**
The `Bind` struct owns the receiving end of the async channel. If you create a `Bind` inside a function scope (local variable) instead of your App struct, it will be dropped at the end of the function, cancelling the specific UI binding.

- **Bad:** `let mut my_bind = Bind::new(false);` inside `update()`.
- **Good:** `self.my_bind` inside `struct MyApp`.

**3. The "Disappearing Data" Mystery**
By default, `Bind` uses `retain = false`. This means if `read()` or `poll()` is **not** called during a specific frame (e.g., the user switched to a different tab in your app), `egui-async` assumes the data is no longer needed and clears it to free memory.

- **Fix:** If you want data to persist while hidden, use `Bind::new(true)`.

#### ๐ŸŒ WASM Configuration

Some crates have features that must be enabled, or must be disabled, under WASM or WASM running in a browser runtime. If you're seeing nonsensical errors in your browser console, consider removing dependencies until things work, then slowly adding them back to see what breaks the runtime.

A very common example of this issue is the `getrandom` crate. Be sure to read relevant documentation for how to handle the browser runtime. [See the `getrandom` wasm32 documentation](https://docs.rs/getrandom/0.3.4/getrandom/#webassembly-support).

#### ๐Ÿงต Threaded WASM

`egui-async` supports threaded WebAssembly builds (`wasm32` with the `atomics` target feature). There, `egui::Context` is neither `Send` nor `Sync`, so `egui-async` keeps it in a thread-local on the thread that runs `EguiAsyncPlugin` (your main thread), and `bind::CTX` is not available.

This works because `wasm_bindgen_futures::spawn_local` polls each task on the thread that spawned it, so results of requests made from the main thread are delivered where the context lives. In threaded builds, **only that thread can request a repaint**: a `Bind` driven from another thread (e.g., a Web Worker) still receives its result, but the UI will not repaint until something else triggers a frame.

These limits apply on every target, threaded or not:

- Results that arrive before the plugin's first frame do not request a repaint. The next frame still picks them up.
- Only the first `egui::Context` the plugin sees receives repaints, so run one `egui` app per page.

## ๐Ÿค Contributing

Contributions are more than welcome! If you find a bug or have a feature request, please open an issue. If you want to contribute code, please submit a pull request.

There are many opportunities to create async-native structs on the UiExt trait inside of `src/egui.rs` that would make for great first-contributions!

### Special thank you to our contributors:

- @sectore

## โš–๏ธ License

This project is licensed under either of

- Apache License, Version 2.0, ([LICENSE-APACHE]https://spdx.org/licenses/Apache-2.0)
- MIT license ([LICENSE-MIT]https://spdx.org/licenses/MIT)

at your option.

## Note

This is **not** an official `egui` product. Please refer to [https://github.com/emilk/egui](https://github.com/emilk/egui) for official crates and recommendations.