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
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.
- Plugin Registration: You must register the
EguiAsyncPlugin
withegui
. The easiest way is to callctx.plugin_or_default::<egui_async::EguiAsyncPlugin>();
once per frame. This plugin updates a global frame timer used by allBind
instances. Bind::request()
: When you start an operation, it spawns aFuture
onto a runtime (tokio
on native,wasm-bindgen-futures
on web).- Communication: The spawned task is given a
tokio::sync::oneshot::Sender
. When the future completes, it sends theResult
back to theBind
instance, which holds theReceiver
. - Polling: On each frame,
Bind
checks its receiver to see if the result has arrived. If it has,Bind
transitions from thePending
state to theFinished
state. - 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-async
Then, 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
true
if the code was executed in this call,false
otherwise.