Building Native GUIs in Rust with eGUI



Rust is often associated with systems programming, CLI tools, and high-performance backends. However, the Rust GUI ecosystem is vibrant and growing. Among the many options (Iced, Tauri, Slint, Dio), eGUI stands out for its simplicity, ease of use, and "immediate mode" philosophy.
In this tutorial, we will build a native desktop application from scratch using eGUI.
Why eGUI?
eGUI (Easy GUI) is an immediate mode GUI library written in Rust.
- Immediate Mode: Unlike "retained mode" GUIs (like the DOM or Qt), where you create widget objects and update their state, immediate mode GUIs rebuild the entire interface every frame. This sounds expensive, but for simple to moderately complex apps, it's incredibly fast and reduces state management headaches.
- Cross-Platform: Runs on Windows, Mac, Linux, and even compiles to WASM for the web.
- Rust-Native: It's written in pure Rust, offering safety and performance.
Getting Started
Prerequisites
Make sure you have Rust installed.
rustc --version
Step 1: Create a Project
cargo new my_egui_app
cd my_egui_app
Add the eframe dependency. eframe is the official framework library that wraps egui for easy desktop and web integration.
[dependencies]
eframe = "0.24.0" # Check for the latest version
serde = { version = "1", features = ["derive"] } # Optional: for saving state
serde_json = "1"
Step 2: The "Hello World" of GUI
In src/main.rs, we need to define our application state and implement the eframe::App trait.
use eframe::egui;
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
initial_window_size: Some(egui::vec2(320.0, 240.0)),
..Default::default()
};
eframe::run_native(
"My First eGUI App",
options,
Box::new(|_cc| Box::new(MyApp::default())),
)
}
struct MyApp {
name: String,
age: u32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
name: "Arthur".to_owned(),
age: 42,
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("My Application");
ui.horizontal(|ui| {
ui.label("Your name: ");
ui.text_edit_singleline(&mut self.name);
});
ui.add(egui::Slider::new(&mut self.age, 0..=120).text("age"));
if ui.button("Click each year").clicked() {
self.age += 1;
}
ui.label(format!("Hello '{}', age {}", self.name, self.age));
});
}
}
Understanding the Code
MyAppStruct: This holds your application state. In Rust, state management is explicit.updatefunction: This is the heart of eGUI. It runs every frame (60 times a second ideally).uiHelper: Theuiobject is used to add widgets.ui.label,ui.button, etc., are added in the order they appear in code.
Step 3: Layouts and Widgets
Real applications need more than just a vertical list of controls. eGUI provides powerful layout tools.
Columns and Grids
ui.columns(2, |columns| {
columns[0].label("First Column");
columns[1].button("Second Column");
});
ui.separator();
egui::Grid::new("my_grid").show(ui, |ui| {
ui.label("Name");
ui.text_edit_singleline(&mut self.name);
ui.end_row();
ui.label("Age");
ui.add(egui::DragValue::new(&mut self.age));
ui.end_row();
});
Panels
A standard desktop app usually has a menu bar, a side panel, and a central area.
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Top Menu Bar
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
_frame.close();
}
});
});
});
// Side Panel
egui::SidePanel::left("side_panel").show(ctx, |ui| {
ui.heading("Side Panel");
ui.label("Useful for navigation or settings.");
});
// Central Panel (Remaining space)
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Main Content");
ui.label("The rest of the window content goes here.");
});
}
}
Advanced Example: A Todo List
Let's build something functional: A Todo List app that persists data.
1. Define State with Serialization
Update Cargo.toml to include serde and serde_json if you haven't.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct TodoApp {
todos: Vec<String>,
new_todo: String,
}
impl Default for TodoApp {
fn default() -> Self {
Self {
todos: Vec::new(),
new_todo: String::new(),
}
}
}
2. Implement Persistence
eGUI has built-in support for saving state to disk using eframe::Storage.
impl TodoApp {
fn new(cc: &eframe::CreationContext<'_>) -> Self {
if let Some(storage) = cc.storage {
return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
}
Default::default()
}
}
impl eframe::App for TodoApp {
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
// ... update ...
}
3. The UI Logic
impl eframe::App for TodoApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Rust Todo List");
ui.horizontal(|ui| {
ui.label("New Item:");
ui.text_edit_singleline(&mut self.new_todo);
if ui.button("Add").clicked() {
if !self.new_todo.is_empty() {
self.todos.push(self.new_todo.clone());
self.new_todo.clear();
}
}
});
ui.separator();
ui.heading("Your Tasks:");
// We need to collect indices to remove to avoid mutable borrow errors
let mut to_remove = Vec::new();
egui::ScrollArea::vertical().show(ui, |ui| {
for (index, todo) in self.todos.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(format!("• {}", todo));
if ui.button("❌").clicked() {
to_remove.push(index);
}
});
}
});
// Remove deleted items
for index in to_remove.iter().rev() {
self.todos.remove(*index);
}
});
}
}
Styling
eGUI comes with a default dark/light theme, but you can customize it fully.
let mut visuals = egui::Visuals::dark();
visuals.widgets.noninteractive.bg_fill = egui::Color32::from_rgb(10, 10, 20); // Dark blue background
ctx.set_visuals(visuals);
Conclusion
eGUI is a fantastic library for getting tools up and running quickly. It lacks the complex styling engines of CSS-based frameworks, but makes up for it in raw performance and development speed.
If you are building internal tools, debug interfaces, or scientific visualizations, eGUI is arguably the best choice in the Rust ecosystem today.