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

egui Widgets and Basic Interaction
Section titled “egui Widgets and Basic Interaction”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.
Core Widgets and Layouts
Section titled “Core Widgets and Layouts”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")orui.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.

🧩 Knowledge Check
Section titled “🧩 Knowledge Check”Match the Widget to its Purpose
Implementing Interactive Widgets
How does egui allocate space for widgets within a region like a CentralPanel by default?
Event Handling in Immediate Mode
Section titled “Event Handling in Immediate Mode”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.
🧩 Knowledge Check
Section titled “🧩 Knowledge Check”Match the Response Method
What exactly does a function like `ui.button('Label')` return in egui?
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).
- A background thread is spawned to talk to the hardware (I2C, SPI, Network).
- It sends updates (like new sensor readings) to the GUI thread via a channel transmitter (
Sender). - The
update()loop simply reads from the channel receiver (Receiver) using the non-blockingtry_recv()method. - 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.
🧩 Knowledge Check
Section titled “🧩 Knowledge Check”The Responsive GUI Pattern
What is the consequence of calling a blocking function like `receiver.recv()` or `thread::sleep()` directly inside the `update()` method?
Why is `ctx.request_repaint_after(Duration)` generally preferred over `ctx.request_repaint()` for live sensor data?
© 2026 Derek Molloy, Dublin City University. All rights reserved.