Expand description
§egui-async
A simple, batteries-included, library for running async tasks across frames in egui and binding their results to your UI.
Supports both native and wasm32 targets.
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
Futureand tracks its state. - WASM Support: Works seamlessly on both native and
wasm32targets. - Ergonomic Helpers: Methods like
read_or_request_or_errorsimplify UI logic into a single line. - Convenient Widgets: Includes a
refresh_buttonand helpers for error popups. - Minimal Dependencies: Built on
tokioand (for wasm)wasm-bindgen-futures.
§How it Works
egui-async works by bridging egui’s immediate-mode rendering loop with a background async runtime.
- Plugin Registration: You must register the
EguiAsyncPluginwithegui. The easiest way is to callctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();once per frame. This plugin updates a global frame timer used by allBindinstances. Bind::request(): When you start an operation, it spawns aFutureonto a runtime (tokioon native,wasm-bindgen-futureson web).- Communication: The spawned task is given a
tokio::sync::oneshot::Sender. When the future completes, it sends theResultback to theBindinstance, which holds theReceiver. - Polling: On each frame,
Bindchecks its receiver to see if the result has arrived. If it has,Bindtransitions from thePendingstate to theFinishedstate. - 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:
cargo add egui-asyncThen, use the Bind struct in your application:
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.
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.
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.
// 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)
- MIT license (LICENSE-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 for official crates and recommendations.
Re-exports§
pub use bind::Bind;pub use bind::State;pub use bind::StateWithData;pub use egui::EguiAsyncPlugin;pub use egui::UiExt;
Modules§
Macros§
- run_
once - A macro to run initialization code only once, even in the presence of multiple threads.
Returns
trueif the code was executed in this call,falseotherwise.