Skip to content

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

9.3 Widgets and Interaction

This content is a draft and will not be included in production builds.

egui provides a rich set of built-in widgets and flexible layout options, enabling developers to construct interactive user interfaces with relative ease. Its immediate mode nature fundamentally shapes how these widgets are used and how user interactions are handled.

The egui::Ui object is central to adding and arranging widgets within an egui application. It acts as the primary interface for declaring UI elements during each frame’s update cycle.

egui offers a comprehensive collection of common widgets:

  • Labels: Display static or dynamic text using ui.label("Some text") or ui.label(&my_string_variable).
  • Buttons: Create interactive buttons with ui.button("Click Me!").
  • Text Inputs: Allow users to enter and modify text with ui.text_edit_singleline(&mut my_string) for single-line input.
  • Sliders: Provide a way to select a value within a defined range using ui.add(egui::Slider::new(&mut my_f32, 0.0..=100.0)).
  • Drag Values: Similar to text inputs but allow value changes by dragging the mouse, often used for numerical adjustments: ui.add(egui::DragValue::new(&mut my_f32)).
  • Checkboxes: Enable boolean selections with ui.checkbox(&mut my_boolean, "Enable Feature").

For text formatting, egui provides helper methods on the Ui struct that apply default styles, similar to HTML tags: label() for plain text, heading() for larger text, strong() for bold, code() for inline code snippets, and hyperlink() for clickable URLs.

Layout in egui is managed through various panel types and horizontal/vertical containers. CentralPanel, SidePanel, TopBottomPanel, and Window are used to define large regions of the application window. For arranging widgets within these regions, ui.horizontal(|ui| {... }) and ui.vertical(|ui| {... }) are used to group elements side-by-side or stacked vertically, respectively. egui also supports responsive layouts based on breakpoints, allowing UI adaptation to different screen sizes.

Here is an expanded example of a simple counter application, demonstrating core widgets and layout:

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use eframe::{egui, App, Frame};
use egui::{CentralPanel, Context, Ui, RichText, Color32};
// Import RichText and Color32 for styling
/// Defines the application's mutable state.
struct MyApp {
counter: i32,
input_text: String,
is_checked: bool,
slider_value: f32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
counter: 0,
input_text: "Edit me!".to_owned(),
is_checked: false,
slider_value: 50.0,
}
}
}
impl App for MyApp {
fn update(&mut self, ctx: &Context, _frame: &mut Frame) {
CentralPanel::default().show(ctx, |ui| {
ui.heading("egui Widgets and Layout Demo");
ui.add_space(10.0); // Add some vertical spacing
// Horizontal layout for counter buttons and label
ui.horizontal(|ui| {
ui.label("Counter:");
if ui.button(RichText::new("").color(Color32::RED)).clicked() {
self.counter -= 1;
}
ui.label(RichText::new(self.counter.to_string()).strong());
// Display counter value
if ui.button(RichText::new("+").color(Color32::GREEN)).clicked() {
self.counter += 1;
}
});
ui.add_space(20.0);
// Text input and display
ui.horizontal(|ui| {
ui.label("Your Message:");
ui.text_edit_singleline(&mut self.input_text);
});
ui.label(format!("Echo: {}", self.input_text));
ui.add_space(20.0);
// Checkbox
ui.checkbox(&mut self.is_checked, "Enable Feature");
if self.is_checked {
ui.label("Feature is enabled!");
} else {
ui.label("Feature is disabled.");
}
ui.add_space(20.0);
// Slider
ui.add(egui::Slider::new(&mut self.slider_value, 0.0..=100.0)
.text("Adjust Value"));
ui.label(format!("Slider value: {:.1}", self.slider_value));
ui.add_space(20.0);
// Hyperlink example
ui.hyperlink("https://www.egui.rs/#demo").on_hover_text("Visit egui demo");
});
}
}
fn main() -> eframe::Result<()> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([400.0, 500.0]),
..Default::default()
};
eframe::run_native(
"My egui", // Window title
options,
Box::new(|_cc| Ok(Box::new(MyApp::default()))),
)
}

On execution, this gives the output as illustrated in Figure 3.

Figure 3. The egui demo with labels, buttons, text input, sliders, drag values and checkboxes.

Concept Match

Match the Widget to its Purpose

Code Cloze
Rust

Implementing Interactive Widgets

Quiz
Select 0/1

How does egui allocate space for widgets within a region like a CentralPanel by default?

The concept of event-driven programming is fundamental to GUI applications, where the application responds to user actions or system events. In egui, as an immediate mode GUI, event handling is approached differently from traditional retained mode frameworks.

In egui, all egui Widgets are essentially function calls that return a Response struct. Instead of registering explicit callbacks or listeners that are invoked when an event occurs, developers directly query the Response object returned by a widget function during each frame’s update cycle. This Response struct provides methods such as .clicked(), .hovered(), and .dragged(), which return a boolean indicating whether the corresponding interaction occurred in the current frame.

So, to handle a button click, you can simply call ui.button("Label").clicked() within the update method. If this method returns true, the button was clicked, and the associated action can be performed immediately. This direct query mechanism means there are no separate event listener registrations or complex callback chains to manage, simplifying the code for many common interactions.

Consider the button click handling already present in the earlier “Hello World” example. The if ui.button("Click Me!").clicked() {... } construct directly demonstrates this immediate mode event handling. The condition clicked() is evaluated every frame, and if true, the counter is incremented. Similarly, for a slider, egui reads the current value and updates it if the mouse is dragging the slider, without explicitly storing the slider’s value internally; the application is responsible for managing the underlying state.

This approach to event handling in immediate mode, relying on direct querying of widget responses, offers a streamlined and often more intuitive way to manage user interactions compared to the traditional callback-based systems of retained mode GUIs. It reduces the boilerplate code associated with event listener registration and the complexities of managing long chains of callbacks. By checking for interactions directly in the same frame that widgets are drawn, the code for simple interactions becomes more concise and easier to understand. This can lead to a more direct and readable flow for UI logic, as the interaction logic is co-located with the UI declaration, which can be particularly advantageous for students learning GUI concepts.

Concept Match

Match the Response Method

Quiz
Select 0/1

What exactly does a function like `ui.button('Label')` return in egui?

Quiz
Select 0/1

Why is there no need for 'event listeners' or 'callbacks' in a basic egui interaction loop?

Architectural Discussion: The “Unresponsive GUI” Problem

Section titled “Architectural Discussion: The “Unresponsive GUI” Problem”

As you build more complex edge applications, you will inevitably encounter long-running tasks: waiting for an I2C sensor to reply, establishing a Wi-Fi connection, or running an AI inference model. If you execute this “heavy lifting” directly inside the update() function, the entire GUI will freeze until the task completes, dropping the frame rate to zero.

The standard architectural solution in Rust is to use Background Threads and Channels (std::sync::mpsc).

  1. A background thread is spawned to talk to the hardware (I2C, SPI, Network).
  2. It sends updates (like new sensor readings) to the GUI thread via a channel transmitter (Sender).
  3. The update() loop simply reads from the channel receiver (Receiver) using the non-blocking try_recv() method.
  4. If new data is available, the GUI state is updated; if not, the GUI continues rendering smoothly at 60 FPS.

This pattern decouples data generation from immediate-mode UI rendering. We will dive much deeper into threads and channels in Chapter 10 (Concurrency), but keep this pattern in mind as the correct way to handle hardware interactions in your GUI apps.

Keeping the GUI Live: ctx.request_repaint()

Section titled “Keeping the GUI Live: ctx.request_repaint()”

By default, egui repaints the window only when the user interacts with it (mouse movement, keypress, etc.). This is efficient for static UIs, but it means a background thread that delivers new sensor data will not trigger a visual update automatically.

ctx.request_repaint() schedules an additional repaint immediately. For applications that poll at a fixed rate, ctx.request_repaint_after(Duration) is preferred because it avoids redrawing the window faster than the incoming data rate:

use std::time::Duration;
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Drain all pending messages without blocking
while let Ok(value) = self.receiver.try_recv() {
self.data.push(value);
}
// Schedule the next check in 100 ms (~10 Hz polling)
ctx.request_repaint_after(Duration::from_millis(100));
// ... widget declarations follow as normal ...
}

The combination of try_recv() (non-blocking drain) and request_repaint_after() (rate-limited repaint) is the standard pattern for all live-data egui applications.

Code Cloze
Rust

The Responsive GUI Pattern

Quiz
Select 0/1

What is the consequence of calling a blocking function like `receiver.recv()` or `thread::sleep()` directly inside the `update()` method?

Quiz
Select 0/1

Why is `ctx.request_repaint_after(Duration)` generally preferred over `ctx.request_repaint()` for live sensor data?