# egui-async
[](https://crates.io/crates/egui-async)
[](https://docs.rs/egui-async)
[](https://snyk.io/articles/apache-license/#apache-license-vs-mit)
A simple, batteries-included, library for running async tasks across frames in [`egui`](https://crates.io/crates/egui) and binding their results to your UI.
Supports both native and wasm32 targets.
```rust
if let Some(res) = self.data_bind.read_or_request(|| async {
reqwest::get("https://icanhazip.com/")
.await
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())
}) {
match res {
Ok(ip) => {
ui.label(format!("Your public IP is: {ip}"));
}
Err(err) => {
ui.colored_label(
egui::Color32::RED,
format!("Could not fetch IP.\nError: {err}"),
);
}
}
} else {
ui.label("Getting public IP...");
ui.spinner();
}
```
## What is this?
Immediate-mode GUI libraries like `egui` are fantastic, but they pose a challenge: how do you run a long-running or async task (like a network request), between frames, without blocking the UI thread?
`egui-async` provides a simple `Bind<T, E>` struct that wraps an async task, manages its state (`Idle`, `Pending`, `Finished`), and provides ergonomic helpers to render the UI based on that state.
It works with both `tokio` on native and `wasm-bindgen-futures` on the web, right out of the box.
## Features
- **Simple State Management**: Wraps any `Future` and tracks its state.
- **WASM Support**: Works seamlessly on both native and `wasm32` targets.
- **Ergonomic Helpers**: Methods like `read_or_request_or_error` simplify UI logic into a single line.
- **Convenient Widgets**: Includes a `refresh_button` and helpers for error popups.
- **Minimal Dependencies**: Built on `tokio` and (for wasm) `wasm-bindgen-futures`.
## How it Works
`egui-async` works by bridging `egui`'s immediate-mode rendering loop with a background async runtime.
1. **Plugin Registration**: You must register the `EguiAsyncPlugin` with `egui`. The easiest way is to call `ctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();` once per frame. This plugin updates a global frame timer used by all `Bind` instances.
2. **`Bind::request()`**: When you start an operation, it spawns a `Future` onto a runtime (`tokio` on native, `wasm-bindgen-futures` on web).
3. **Communication**: The spawned task is given a `tokio::sync::oneshot::Sender`. When the future completes, it sends the `Result` back to the `Bind` instance, which holds the `Receiver`.
4. **Polling**: On each frame, `Bind` checks its receiver to see if the result has arrived. If it has, `Bind` transitions from the `Pending` state to the `Finished` state.
5. **UI Update**: Your UI code can then check the `Bind`'s state and display the data, an error, or a loading indicator.
## Quickstart
Here is a minimal example using `eframe` that shows how to fetch data from an async function.
First, add `egui-async` to your dependencies:
```sh
cargo add egui-async
```
Then, use the `Bind` struct in your application:
```rust
use eframe::egui;
use egui_async::{Bind, EguiAsyncPlugin};
struct MyApp {
/// The Bind struct holds the state of our async operation.
data_bind: Bind<String, String>,
}
impl Default for MyApp {
fn default() -> Self {
Self {
// We initialize the Bind and tell it to not retain data
// if it's not visible for a frame.
// If set to true, this will retain data even as the
// element goes undrawn.
data_bind: Bind::new(false), // Same as Bind::default()
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// This registers the plugin that drives the async event loop.
// It's idempotent and cheap to call on every frame.
ctx.plugin_or_default::<EguiAsyncPlugin>(); // <-- REQUIRED
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Async Data Demo");
ui.add_space(10.0);
// Request if `data_bind` is None and idle
// Otherwise, just read it
if let Some(res) = self.data_bind.read_or_request(|| async {
reqwest::get("https://icanhazip.com/")
.await
.map_err(|e| e.to_string())?
.text()
.await
.map_err(|e| e.to_string())
}) {
match res {
Ok(ip) => {
ui.label(format!("Your public IP is: {ip}"));
}
Err(err) => {
ui.colored_label(
egui::Color32::RED,
format!("Could not fetch IP.\nError: {err}"),
);
}
}
} else {
ui.label("Getting public IP...");
ui.spinner();
}
});
}
}
// Boilerplate
fn main() {
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"egui-async example",
native_options,
Box::new(|_cc| Ok(Box::new(MyApp::default()))),
)
.unwrap();
}
```
## Common API Patterns
`egui-async` offers several helper methods on `Bind` to handle common UI scenarios. Here are the most frequently used patterns.
### The Full State Machine: `state_or_request`
This is the most powerful and explicit pattern. Use it when you want to render a different UI for every possible state: `Pending`, `Finished` with data, `Failed` with an error, or `Idle`. It's perfect for detailed components that need to show loading spinners, error messages, and the final data.
```rust
use egui_async::StateWithData;
match self.data_bind.state_or_request(my_async_fn) {
StateWithData::Idle => { /* This is usually skipped */ }
StateWithData::Pending => { ui.spinner(); }
StateWithData::Finished(data) => { ui.label(format!("Success: {data}")); }
StateWithData::Failed(err) => { ui.colored_label(egui::Color32::RED, err.to_string()); }
}
```
-----
### Simple Data Display: `read_or_request`
Use this pattern when you primarily care about the successful result and want a simple loading state. It returns an `Option<&Result<T, E>>`. If the value is `Some`, you can handle the `Ok` and `Err` cases. If it's `None`, the request is `Pending`, so you can show a spinner.
```rust
if let Some(result) = self.data_bind.read_or_request(my_async_fn) {
match result {
Ok(data) => { ui.label(format!("Your IP is: {data}")); }
Err(err) => { ui.colored_label(egui::Color32::RED, err.to_string()); }
}
} else {
ui.spinner();
ui.label("Loading...");
}
```
-----
### Periodic Refresh: `request_every_sec`
Use this for data that should be updated automatically on a timer, like a dashboard widget. You provide an interval in seconds, and `egui-async` will trigger a new request when the interval has passed since the last successful completion.
```rust
// In your update loop:
let refresh_interval_secs = 20.0;
self.live_data.request_every_sec(fetch_live_data, refresh_interval_secs);
// You can still read the data to display it
if let Some(Ok(data)) = self.live_data.read() {
ui.label(format!("Live data: {data}"));
}
```
## 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.
## Contribution
Contributions are welcome! Please feel free to submit a pull request or open an issue.
## Todo
In the future I may consider a registry architecture rather than polling on each request, which would allow mature threading-- however this poses unique difficulties of its own. Feel free to take a shot at it in a PR.
A builder API is a likely "want" for 1.0.
## Notes
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.