Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

9.2 Foundational Concepts

To begin developing GUI applications with egui, understanding its fundamental structure and the recommended setup process is important. The eframe crate serves as the primary and officially recommended abstraction layer, simplifying the complexities of cross-platform windowing and rendering for egui applications.

Setting Up a First egui Project with eframe

Section titled “Setting Up a First egui Project with eframe”

The most straightforward and officially recommended method for initiating an egui application is by utilising eframe. This framework provides support for both native desktop and web deployments, abstracting away much of the underlying platform-specific complexities.

The process begins with standard Rust project creation:

  1. Create a new Rust project: Open your terminal or command prompt and execute the command: cargo new my_egui. This command generates a new directory named my_egui containing a basic Rust project structure, including a Cargo.toml file and a src/main.rs file as is usual.
Terminal window
PS C:\temp> cargo new my_egui
Creating binary (application) `my_egui` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
  1. Add eframe as a dependency: The easiest way to add egui is to use cargo to update your cargo.toml file as follows:
Terminal window
PS C:\temp> cargo add eframe

Which should result in a change similar to the following:

[package]
name = "my_egui"
version = "0.1.0"
edition = "2024"
[dependencies]
eframe = "0.32.3"

The following is a minimal, but sophisticated, “Hello World” application structure (src/main.rs): This establishes a basic window and an interactive element, demonstrating the core egui application loop. The #[cfg_attr(not(debug_assertions), windows_subsystem = "windows")] attribute is a common practice to suppress the console window on Windows when building release versions, preventing a command prompt from appearing alongside the GUI.

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// Hide console window on Windows in release
use eframe::{egui, App, Frame};
use egui::{CentralPanel, Context, Label, Ui};
/// Defines the application's mutable state.
/// This struct holds all data that needs to persist and change throughout the application's lifetime.
struct MyApp {
label_text: String,
button_clicks: u32,
}
/// Implements the Default trait to provide an initial state for MyApp.
/// This is a convenient way to set up the application's starting values.
impl Default for MyApp {
fn default() -> Self {
Self {
label_text: "Hello, egui!".to_owned(),
button_clicks: 0,
}
}
}
/// Implements the eframe::App trait, which defines the core logic of the egui application.
/// The update method is the heart of an immediate mode GUI.
impl App for MyApp {
/// The update method is called once per frame by eframe to redraw the UI and handle input.
/// ctx: Provides access to the egui context, used for adding widgets and managing UI state.
/// _frame: Provides access to the native window frame, for operations like closing the window.
fn update(&mut self, ctx: &Context, _frame: &mut Frame) {
// CentralPanel occupies the main, central area of the window.
// It's a convenient way to place UI elements that fill the available space.
CentralPanel::default().show(ctx, |ui| {
ui.heading("My First egui Application"); // Display a prominent heading
ui.label(&self.label_text); // Display the current text from our application state
// Create a button. The .clicked() method returns true if the button was pressed.
// In immediate mode, this check happens every frame.
if ui.button("Click Me!").clicked() {
self.button_clicks += 1; // Increment the counter
// Update the label text based on the new counter value.
// This change will be reflected in the next frame's redraw.
self.label_text = format!("Button clicked {} times!", self.button_clicks);
}
});
}
}
/// The main function is the entry point of the Rust program.
/// It sets up and runs the eframe application.
fn main() -> eframe::Result<()> {
// Configure native window options, such as initial size and title.
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
// Set initial window size
..Default::default() // Use default values for other options
};
// Run the native eframe application.
// "My egui" is the window title.
// options configures the native window.
// The closure Box::new(|_cc| Ok(Box::new(MyApp::default()))) creates and returns
// an instance of our MyApp struct, which defines the application's UI and logic.
eframe::run_native(
"My egui", // Window title
options,
Box::new(|_cc| Ok(Box::new(MyApp::default()))),
)
}

Figure 2. An egui first example.

In this example, the MyApp struct serves as the container for the application’s mutable state, holding values like label_text and button_clicks. The impl App for MyApp block defines the update method, which is invoked by eframe on every frame to handle UI rendering and user input. Within update, CentralPanel::default().show(ctx, |ui| {... }) establishes a central area for UI elements. These elements are then added using the ui object. ui.heading and ui.label are used to display static and dynamic text, respectively, while ui.button("Click Me!").clicked() demonstrates a fundamental interaction: when the button is pressed, the button_clicks counter is incremented, and the label_text is updated accordingly. This change is immediately reflected in the subsequent frame’s redraw.

Figure 3. The egui documentation at https://docs.rs/egui/latest/egui/

The rust documentation describes all of the functionality available with egui. This documentation will be available during the examination. For example, to learn more about the ui.button use the menu on the left → Crate Items → Structs → Ui → button (https://docs.rs/egui/latest/egui/struct.Ui.html#method.button)

The use of eframe as an abstraction layer is an important aspect of achieving cross-platform compatibility. Instead of egui directly interacting with low-level operating system windowing APIs (such as Win32 on Windows, X11 on Linux, or Cocoa on macOS) or graphics APIs (like DirectX, Vulkan, or Metal), eframe handles this intricate complexity. It achieves this by leveraging platform-specific libraries, such as winit for window management and glow (for OpenGL rendering) or wgpu (for WebGPU rendering) for graphics. This design means that egui itself remains largely agnostic to the specific platform details, as eframe provides a unified, higher-level interface. This separation of concerns is a fundamental principle in modern software engineering, illustrating how complex cross-platform compatibility is achieved. The UI framework (egui) focuses solely on rendering widgets and processing abstract input, while a dedicated backend framework (eframe) translates these abstract operations into platform-specific system calls. This significantly simplifies the developer’s task, allowing them to write egui-specific code without requiring deep knowledge of the underlying operating system or graphics APIs.

Concept Match

Match the eframe/egui Application Structure

Quiz
Select 0/1

What is the primary role of the `eframe` crate in a Rust GUI application?

Quiz
Select 0/1

In the immediate mode paradigm used by egui, how frequently is the `update` method typically called?

In Rust, a closure is an anonymous function you can create in-place. As you have seen C++ lambdas previously, you are already 90% of the way there. They serve the exact same purpose: creating a short, on-the-fly function that can “capture” variables from its surrounding environment. The syntax is very similar.

  • C++ Lambda: [capture_list](parameters) { body }
  • Rust Closure: |parameters| { body }

The most significant difference isn’t the syntax, but the capturing mechanism. In C++, you are explicit about *how *you capture variables:

  • [&] captures by reference.
  • [=] captures by value (a copy).
  • [this] captures the this pointer by value.
  • [&a, b] captures a by reference and b by value.

In Rust, capturing is implicit and inferred by the compiler. Rust analyzes what you do with the captured variables inside the closure and automatically chooses the most efficient and safe way to borrow or move them. Based on how it captures, the compiler assigns one of three traits to the closure:

  1. Fn (The “Immutable Borrow”): The closure only needs to read captured variables.
  2. FnMut (The “Mutable Borrow”): The closure needs to mutate (change) captured variables. This is the most common trait in egui.
  3. FnOnce (The “Ownership Move”): The closure takes ownership of a captured variable (i.e., consumes it). As the name implies, it can only be called once.

So, you don’t tell the Rust compiler how to capture (like [&] or [=]). You just use the variables, and the compiler infers whether it needs &T, &mut T, or T, assigning the Fn, FnMut, or FnOnce trait accordingly.

Rust closures are commonly used in egui for capturing variables and simplifying the overall UI layout process. They are also required later when we look at spawning threads in Chapter 11.

In egui, you don’t typically use closures for a simple button click (that’s usually just an if statement). Instead, you use closures to define layout and pass in the app’s state. The most common pattern is passing a closure to a layout function like egui::CentralPanel::show or ui.horizontal. This closure captures your application’s state (self) so it can read and modify it.

Imagine you have a simple egui app with some state:

// This is the main state for our application
struct MyApp {
name: String,
counter: i32,
}
// This is just a snippet from the main 'update' method
// where the UI is drawn every frame.
fn update(&mut self, ctx: &egui::Context) {
// egui::CentralPanel::show takes two arguments:
// 1. The context (ctx)
// 2. A closure!
egui::CentralPanel::default().show(ctx, |ui| {
// This is the body of our closure.
// 'ui' is the parameter (its type is &mut egui::Ui)
// --- 1. Capturing `&mut self` implicitly ---
// We need to modify self.counter, so the closure
// captures `self` as a mutable borrow (&mut self).
// This makes the closure `FnMut`.
ui.heading("A Simple egui App");
ui.label(format!("Counter: {}", self.counter));
if ui.button("Increment").clicked() {
self.counter += 1; // Needs &mut self
}
// --- 2. A nested closure ---
// ui.horizontal also takes a closure!
ui.horizontal(|ui_inner| {
// This closure ALSO captures `&mut self`
ui_inner.label("Your name: ");
ui_inner.text_edit_singleline(&mut self.name); // Needs &mut self
});
});
}

One point of confusion is that ui_inner (or ui in the outer closure) is not a special keyword. It is a parameter name that I chose for the closure. It could have been called derek. So:

  1. We call ui.horizontal(...).
  2. We pass it a closure: |ui_inner| { ... }.
  3. The horizontal function executes.
  4. Inside horizontal, it creates a new &mut egui::Ui (let’s call it horizontal_ui). This new Ui object is configured to make widgets line up side-by-side.
  5. The horizontal function then calls our closure and passes its horizontal_ui as the first and only argument.
  6. Our closure receives that argument and names it ui_inner.
  7. We can then call methods on ui_inner (like ui_inner.label(...)) to add widgets to that special horizontal layout.

So, ui_inner is just the name we gave to the &mut egui::Ui that ui.horizontal gave back to us.

Code Cloze
Rust

Using Closures for Layout in egui

Quiz
Select 0/1

How does Rust handle variable capturing for closures compared to C++ lambdas?

Quiz
Select 0/1

If a closure in egui modifies a variable belonging to the application state (e.g., `self.counter += 1`), which trait does the Rust compiler assign to it?

Before diving into custom graphics with the Painter API, it’s essential to master the standard widgets that form the backbone of most user interfaces. egui provides a rich set of built-in components like labels, buttons, text inputs, sliders, and checkboxes. This example demonstrates how to wire up these widgets to a shared application state to create a simple, interactive counter.

Key Concepts Introduced in this Example:

  • Widget Interaction: Using buttons to modify application state.
  • Displaying State: Using labels to display data from the application state.
  • Two-Way Data Binding: Using a slider that both reads from and writes to the application state.
  • Layouts: Using ui.horizontal to arrange widgets.

Figure 5 The egui Counter App

The Code \

This is Cargo.toml:

[package]
name = "egui"
version = "0.1.0"
edition = "2024"
[dependencies]
eframe = "0.32.3"

And main.rs:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// This attribute hides the console window on Windows when running in *release mode*.
// It keeps the app looking like a normal desktop GUI app rather than a console program.
use eframe::egui;
// Import the main GUI library (egui) through the eframe framework.
// eframe provides an easy way to build native desktop apps using egui for rendering.
// Define the application structure
struct CounterApp {
count: i32, // This variable stores the current counter value.
}
// Provide a default value for CounterApp.
impl Default for CounterApp {
fn default() -> Self {
Self { count: 0 } // Start the counter at 0 when the app launches.
}
}
// Implement the eframe::App trait, which defines how the app behaves each frame.
impl eframe::App for CounterApp {
// The update() function is called on every frame to redraw the user interface.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// The CentralPanel is a main area that automatically expands to fill the window.
egui::CentralPanel::default().show(ctx, |ui| {
// Add a title at the top
ui.heading("EEN1097 Simple Counter App");
ui.separator(); // Draws a horizontal line for visual separation.
// Set up a layout so that all widgets below are centred vertically.
ui.with_layout(egui::Layout::top_down(egui::Align::Center), |ui| {
ui.add_space(20.0); // Add vertical spacing before the counter.
// Display the current counter value using a large font size.
ui.label(egui::RichText::new(self.count.to_string()).size(40.0));
ui.add_space(10.0); // Add some space before the buttons.
// Create a horizontal row for the two buttons.
ui.horizontal(|ui| {
// "Decrease" button
if ui.button("Decrease").clicked() {
self.count -= 1; // Reduce count by 1 when clicked.
}
// "Increase" button
if ui.button("Increase").clicked() {
self.count += 1; // Increase count by 1 when clicked.
}
});
ui.add_space(10.0); // Add spacing before the slider.
// Add a slider to adjust the count within a fixed range (0 to 100).
// The .text("Count") label appears beside the slider.
ui.add(egui::Slider::new(&mut self.count, 0..=100).text("Count"));
});
});
}
}
// Entry point of the application
fn main() -> eframe::Result<()> {
// Define options for the native window, such as size and style.
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([300.0, 200.0]), // Set initial window size.
..Default::default()
};
// Launch the native app.
// Arguments:
// - "Counter App": the window title.
// - `options`: window configuration defined above.
// - A closure that creates and returns an instance of `CounterApp`.
eframe::run_native(
"Counter App",
options,
Box::new(|_cc| Ok(Box::new(CounterApp::default()))),
)
}

Explanation \

  1. Application State: The CounterApp struct holds a single integer called count. This field is the “single source of truth” for our application. All widgets will read from or write to this value.
  2. Displaying Data: We use ui.label() to display the current count. The format! macro is used to convert the i32 into a String that the label can show. egui::RichText is used to easily style the text with a larger font size.
  3. Responding to Button Clicks: The pattern “if ui.button("...").clicked()“ is the core of event handling in egui. The .clicked() method returns true for the single frame in which the button is clicked, allowing us to run code in response — in this case, modifying self.count. The ui.horizontal block ensures the buttons are placed side-by-side.
  4. Two-Way Data Binding with a Slider: The egui::Slider is a powerful example of egui’s immediate mode data handling. By passing a mutable reference (&mut self.count), we allow the slider to both read the current value of count to set its position and write a new value back to count when the user interacts with it. This creates a seamless two-way binding with a single line of code.
Concept Match

Match the Counter App Concepts

Code Cloze
Rust

Interactive Widgets in egui

Quiz
Select 0/1

How does the `egui::Slider` implement two-way data binding in the Counter App example?