Skip to content

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

🦀Tutorial: Rust GUIs

Rust GUI Tutorial: Building an Edge Device Dashboard with egui

Section titled “Rust GUI Tutorial: Building an Edge Device Dashboard with egui”

Welcome! This tutorial will guide you through building a graphical user interface (GUI) in Rust using egui and its “native” runner, eframe. egui is an immediate-mode GUI library that is easy to use, highly portable, and very efficient, making it a perfect fit for building dashboards and tools for edge devices. We’ll build a Edge Device Monitor step-by-step.

This tutorial assumes a basic understanding of Rust (structs, impl blocks, Vec, enums) and that cargo is set up. We carry the Edge Programming theme forward in this tutorial by creating a simple dashboard for monitoring a fictional edge device.

Good luck!


First create a new project using the VSCode IDE and then use the **cargo **tool to create a new project, for example, in the c:\ directory.

Use the terminal in this directory to create a project for the tutorial — for example:

Terminal window
cargo new een1097_tutorial4
cd een1097_tutorial4
cargo add eframe

Open the folder in VSCode and you should see the following:

(Note: You may have a newer version of eframe than in this image (0.33.0), which may introduce some differences between my video solution and the code you need to write for the latest version. Version 0.x.x means that it will be in a constant state of change.)

How do you create the most basic “Hello, World!” application with egui? This involves setting up the main application struct, implementing the eframe::App trait, and running the application. Begin by replace the contents of your src/main.rs with this starting code example:

use eframe::egui;
// 1. Define a struct for our application state
struct EdgeApp;
// 2. Implement the eframe::App trait for our struct
impl eframe::App for EdgeApp {
// 3. The update method is called on every frame
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// 4. Show a "CentralPanel" which fills the entire window
egui::CentralPanel::default().show(ctx, |ui| {
// 5. Add a simple label to the UI
ui.heading("Hello EEN1097!");
});
}
}
fn main() -> Result<(), eframe::Error> {
// 6. Configure the native window options
// Configure native window options
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size(egui::vec2(400.0, 200.0))
.with_resizable(false),
..Default::default()
};
// 7. Run the native application
eframe::run_native(
"Hello egui", // Window title
options,
Box::new(|_cc| Ok(Box::new(EdgeApp))), // Create and box our app state
)
}

When run, the window should appear as in Figure Q1a. The code works as follows:

  1. EdgeApp is our application’s state. For now, it’s empty.
  2. We implement the eframe::App trait, which requires an update method.
  3. The update method is where all GUI logic goes. It’s called for every frame (e.g., 60 times per second).
  4. egui::CentralPanel::default().show(ctx, |ui| ...) is the standard way to create a main panel that fills the window.
  5. ui.heading(...) is a widget. We add it to the ui (User Interface) provided by the panel’s closure.
  6. eframe::run_native starts the application and opens a window.

Figure Q1a: The initial window state for Question 1.

**Tasks: **Use the documentation for egui provided at: Docs.rs to adapt the provided code as follows:

  1. Change the panel text to “EEN1097 Edge Dashboard Panel”
  2. Change the window title to be “EEN1097 Edge Dashboard”
  3. The Heading text in this example is added by using the ui.heading() function, which is described here. Use the documentation to add a ui.label() (scroll down in the left-hand column of the documentation) underneath the heading so that the interface now looks like Figure Q1b below:

Figure Q1b: The final window state for Question 1.


GUIs are all about state. In this question we examine how you store application state (like a counter) and add a Button to modify it?

Modify EdgeApp to hold a counter and add a Button to increment it. Perform the following steps:

  1. Add an integer field to the EdgeApp struct to hold the number of ping counts called ping_count
  2. Add a “default” constructor that sets the initial value to 0. Use an implementation of the Default trait for your struct (run_native uses this to create the initial app state.)
impl Default for EdgeApp {
fn default() -> Self {
Self { ping_count: 0 }
}
}
  1. Use the documentation to add a ui.button that when clicked increases the ping count by 1.
  2. Use a label to display the current ping count, as illustrated in Figure Q2.

Note: to invoke the Default trait we modify the final line of the run_native call as follows:

eframe::run_native(
"EEN1097 Edge Dashboard", // Window title
options,
Box::new(|_cc| Ok(Box::new(EdgeApp::default()))),
)

Figure Q2: The final state for Question 2. (Each press on “Ping Device” increases the count)


The next question examines how we can get text input from a user. Complete the following steps to develop an interface as illustrated in Figure Q3:

  1. Adapt the EdgeApp struct to add a device_name field as a String. Modify the Default implementation so that the default value is set to “Default Name”.
  2. Modify the EdgeApp to add a TextEdit widget to let the user set a name for the edge device, which is then displayed within a formatted label.

Figure Q3: The final state for Question 3.


Question 4. Basic Layout with ui.horizontal

Section titled “Question 4. Basic Layout with ui.horizontal”

In this question you investigate how to arrange widgets side-by-side. Use ui.horizontal to place a label and its corresponding widget on the same line, creating a simple form.

  1. Add a ui.separator to add some vertical space as per Figure Q4.
  2. Use the ui.horizontal layout to place the ping device button beside the message using the following example code:
ui.horizontal(|ui| {
ui.label("Same");
ui.label("row");
});

Figure Q4: The final state for Question 4. Note the horizontal line underneath the subtitle, and note that the “Ping Device” is now to the left of the message.


In this section we examine how you create more complex layouts, like a sidebar for navigation and a main area for content.

Figure Q5a. The different Panels from the https://www.egui.rs/#demo example.

  1. Create a side panel egui::SidePanel on the left that has the heading “Settings” in which we will place a “Device Name” label and the edit field.
  2. Place the Device Monitor in a egui::CentralPanel.
  3. Tidy up the spacing so that it looks like Figure Q5b. Use ui.set_width() which takes a normalised float — e.g., 0.3 would be 30% of the available space. Please review the expected outcome in Figure Q5b.

Figure Q5b. The final outcome of Question 5.


In this question we examine how you can build GUIs that react to more than just clicks. Most widget methods return a Response object. Use this to show a Tooltip when the user hovers over the “Ping Device” button.

  1. Change the Ping Button so that it is an instance of ui.button. For example, let ping_button = ui.button()...
  2. Use the on_hover_text() function to display the output in Figure Q6.

Figure Q6. The final outcome of Question 6 with the hover text appearing when you float over the button with the mouse.


A common task is setting numerical parameters. How do you use a Slider to control a value in your state? Add a slider to control a “Refresh Rate” for your device.

  1. Add a refresh_rate field to the EdgeApp struct as a f32 and ensure that it is set to 1.0 by default, as illustrated in Figure Q7.
  2. Add an egui::Slider to your interface with the range 0.1..=10.0 using the documentation at: https://docs.rs/egui/latest/egui/widgets/struct.Slider.html and use the .text() element to add a description to the slider.
  3. Add a label that displays the Refresh Rate as defined by the slider value, as illustrated in Figure Q7.

Figure Q7. The final outcome of Question 7 with the slider


This question examines how you let a user choose from a fixed set of options. This is a perfect use case for a Rust enum and an egui::ComboBox which is described in the documentation here.

  1. Add a DeviceMode enum and add three states “Idle”, “Active”, and “Error”. Please add the following line in front of the enum to allow for comparison #[derive(Debug, PartialEq)]
  2. Add a mode field to your EdgeApp struct that is a DeviceMode that defaults to Idle.
  3. Add a “Device Mode” setting to the GUI using the egui::ComboBox as described in the linked website above.

The final outcome should function as illustrated in Figure Q8.

Figure Q8. The final outcome of Question 8 with the egui::ComboBox


Terminal window
Update rustc?
Please note that for the next section I needed to update to a rustc version newer than 1.88.0. My current version is 1.87.0 Here are the steps I took:
PS C:\EEN1097\egui_test> rustc --version
rustc 1.87.0 (17067e9ac 2025-05-09)
PS C:\EEN1097\egui_test> cargo --version
cargo 1.87.0 (99624be96 2025-05-06)
PS C:\EEN1097\egui_test> rustup update stable
PS C:\EEN1097\egui_test> rustc --version
rustc 1.91.0 (f8297e351 2025-10-28)
PS C:\EEN1097\egui_test> cargo --version
cargo 1.91.0 (ea2d97820 2025-10-10)

We now want to add a graph to the dashboard by adding a simple, static line plot. The egui_plot module can do this. Let’s plot some sample “historical data.” as illustrated in Figure Q9

Figure Q9. The final outcome of Question 9 with the egui_plot

  1. Modify the Cargo.toml file to add the plot feature by using cargo as follows:
Terminal window
cargo add egui_plot
  1. Add the following line to the top of your main.rs file.
Terminal window
use egui_plot::{Line, Plot, PlotPoints};
  1. Add a separator() and then some sample data as follows:
let historical_data: PlotPoints = vec![
[0.0, 1.0], [1.0, 3.0], [2.0, 2.0], [3.0, 4.0], [4.0, 3.5],
].into();
  1. Build a line from the data plot points as follows:
let line = Line::new("Temp degC", historical_data)
.color(Color32::from_rgb(100, 200, 100));
  1. Then render the plot widget:
Plot::new("device_plot")
.height(200.0)
.show(ui, |plot_ui| { plot_ui.line(line); });

Question 10. Custom Graphics with egui::Painter

Section titled “Question 10. Custom Graphics with egui::Painter”

As seen in Question 9, egui_plot is great for plotting linear data, but what if you want to draw custom graphics, like a live custom dashboard indicator? This requires using the Painter API to draw shapes directly. In this question you will generate a custom indicator that reacts to the Refresh Rate slider. The final version should look like Figure Q10.

Figure Q10. The final outcome of Question 10 using egui::Painter

  1. Remove the new code from Question 9 to display the data using egui_plot
  2. The interface is becoming very ‘busy’ so we will write a new function and call this from the update() function.
  3. You are provided with the following code which should not be placed inside any other implementation blocks:
impl EdgeApp {
/// Draws a custom horizontal bar (0-10 Hz) below the current interface.
fn draw_refresh_rate_bar(&self, ui: &mut egui::Ui) {
use egui::{Color32, Pos2, Rect, Vec2, CornerRadius};
ui.add_space(8.0);
ui.heading("Refresh Rate Indicator");
ui.label("Displays 0.0-10.0 Hz");
// Reserve an area to paint into.
let desired_size = Vec2::new(ui.available_width(), 60.0);
let (response, painter) = ui.allocate_painter(desired_size,
egui::Sense::hover());
// The rectangle for our bar background.
let rect = response.rect;
let rounding = CornerRadius::same(8);
// Background
painter.rect_filled(rect, rounding, Color32::from_gray(32));
// Compute fill proportion (0.0-1.0)
let proportion = (self.refresh_rate / 10.0).clamp(0.0, 1.0);
// Inner padding
let pad = 6.0;
let inner = Rect::from_min_max(
Pos2::new(rect.min.x + pad, rect.min.y + pad),
Pos2::new(rect.max.x - pad, rect.max.y - pad),
);
// Filled width
let fill_w = inner.width() * proportion;
let fill_rect = Rect::from_min_max(
inner.min,
Pos2::new(inner.min.x + fill_w, inner.max.y),
);
// Fill and outline
painter.rect_filled(fill_rect, CornerRadius::same(6),
Color32::from_rgb(80, 180, 120));
// Overlay the current value as text
let value_text = format!("Missing Code");
painter.text(
inner.center(),
egui::Align2::CENTER_CENTER,
value_text,
egui::FontId::proportional(14.0),
Color32::WHITE,
);
}
}
  1. Fix the “Missing Code” formatted string to function as per Figure Q10.
  2. Add a separator and call this code from your update function using self.draw_refresh_rate_bar(ui);

The video solutions for these questions are available in the next section.