Rust GUI Tutorial: Building GUIs in Rust with egui and Struct-based State Management

Rust GUI

In this article Rust GUI Tutorial, I’ll walk you through creating a simple GUI application in Rust using the egui library. If you’re new to Rust, don’t worry—I’ll explain every step clearly. We’ll focus on building a basic UI, handling button clicks, and managing application state using Rust’s struct and impl block. By the end, you’ll understand how to efficiently organize your application’s state and logic, an important skill when developing real-world applications.

Why Use egui?

egui is a lightweight, immediate mode GUI Rust library, perfect for developing fast, real-time user interfaces. Unlike other declarative GUI libraries (like Iced), egui redraws the UI every frame, making it ideal for applications that need to react quickly to user inputs or changes.

What You’ll Learn:

  • Creating a basic GUI application using egui.
  • Changing text dynamically based on user interaction.
  • Managing application state efficiently using Rust’s struct.
  • Implementing the Default trait to initialize your struct with default values.

Let’s dive right in!


Step 1: Setting Up Your Rust Project

First, you need to create a new Rust project. If you haven’t installed Rust yet, follow the instructions at rust-lang.org.

Create a new project with:

cargo new egui-demo
cd egui-demo

Next, add egui and eframe to your Cargo.toml:

[dependencies]
egui = "0.23"
eframe = "0.23"

Step 2: Structuring Your Rust GUI Application with struct and impl

In Rust, state management is typically handled by defining a struct to represent the application’s state. All the variables that change or affect the UI (like text fields, counters, or settings) should be stored in this struct.

The Basic Structure

We’ll start by defining a struct called MyApp that holds the state of our application, including a counter and a label that changes when a button is clicked:

struct MyApp {
    counter: i32,
    label_text: String,
}

This struct will hold all the data that our UI will interact with. Now let’s write some methods for this struct using the impl block.

Implementing Logic with impl

We want to create some functionality for our app, such as incrementing a counter when a button is clicked and updating the label text to reflect the current counter value. Here’s how we define that logic:

impl MyApp {
    // Increment the counter and update the label
    fn increment_counter(&mut self) {
        self.counter += 1;
        self.update_label_text();
    }

    // Update the label to reflect the current counter value
    fn update_label_text(&mut self) {
        self.label_text = format!("Counter value: {}", self.counter);
    }
}
  • increment_counter: Adds 1 to the counter and updates the label text.
  • update_label_text: Updates the text displayed in the UI based on the counter’s value.

Step 3: Using the Default Trait for Initialization

In Rust, the Default trait allows you to define default values for your struct. This is useful when you want to easily create an instance of MyApp without manually specifying the values every time.

Here’s how to implement Default for MyApp:

impl Default for MyApp {
    fn default() -> Self {
        Self {
            counter: 0,  // Initialize counter to 0
            label_text: String::from("Counter value: 0"), // Initial label text
        }
    }
}

Now, you can create a default instance of MyApp using:

let app = MyApp::default();

This makes initializing your struct cleaner and ensures that default values are set properly.


Step 4: Building the UI in egui

Now that we’ve defined our state and logic, let’s build the user interface (UI) using egui. In egui, the UI is drawn every frame, and you can modify its state dynamically.

Here’s how we can create the main UI of our app:

use eframe::egui::{self, CentralPanel};
use eframe::{App, NativeOptions};

fn main() -> Result<(), eframe::Error> {
    let options = NativeOptions::default();
    eframe::run_native(
        "Counter App",
        options,
        Box::new(|_cc| Box::new(MyApp::default())),
    )
}

impl App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        CentralPanel::default().show(ctx, |ui| {
            ui.label(&self.label_text);

            if ui.button("Increment").clicked() {
                self.increment_counter();
            }
        });
    }
}

Here’s what this code does:

  1. MyApp::default(): Creates an instance of MyApp with default values (counter = 0, initial label text).
  2. ui.label(): Displays the current label text.
  3. ui.button("Increment"): Creates a button labeled “Increment”, and if clicked, it calls self.increment_counter() to update the counter and label text.

Step 5: Running the Application

Once you’ve written the code, run your application:

cargo run

You should see a simple window with a label showing “Counter value: 0” and a button labeled “Increment.” Every time you click the button, the counter will increase, and the label will update to reflect the new value.


Understanding the Importance of struct, impl, and Default

1. Using struct for State Management

In Rust, state is stored in structs. By keeping the state (like counter and label_text) inside MyApp, you ensure that it’s easy to update and access throughout the application’s lifecycle. This also prevents scattered state management and keeps the code organized.

2. Implementing Logic with impl

The impl block is where you define all the functionality for your struct. Separating logic like increment_counter into its own method makes the code modular, reusable, and easier to maintain. It’s also a common Rust pattern for organizing methods related to the struct.

3. Default for Cleaner Initialization

Using the Default trait allows you to define sensible default values for your app’s state. This makes creating new instances of your struct easy and ensures that all fields are initialized properly without manual setup.


Conclusion

In this tutorial, we covered the fundamentals of creating a GUI application in Rust using egui. We also learned how to manage state using Rust’s struct, organize logic with impl, and use the Default trait for convenient initialization.

By following these best practices, you’ll be able to build more complex Rust applications with a clean, maintainable architecture. Whether you’re creating tools, games, or desktop apps, the principles here will serve you well as your projects grow in complexity.

Feel free to modify and expand this example to add more features, such as text input, sliders, or multi-page interfaces. With egui, you have a lightweight and powerful toolset for building responsive and dynamic user interfaces.

References

  1. Rust Programming Language Official Documentation
  2. egui Documentation
  3. Rust impl and Methods – Rust By Example
  4. Understanding the Rust Default Trait
  5. Immediate Mode GUIs Explained
  6. Rust – Understanding Ownership and Borrowing for Optimal Performance

Stay tuned for more articles where we dive deeper into Rust, GUIs, and building real-world applications!

  1. Rust GUI
  2. egui Rust
  3. Rust Struct State Management
  4. Immediate Mode GUI Rust
  5. Rust Default Trait

Recommended Posts

No comment yet, add your voice below!


Add a Comment

Your email address will not be published. Required fields are marked *

1 × one =