Rust, Egui and Commands

Rust, Egui and Commands

For a side project, the security card game, I decided last year to write a simple CLI in rust to create cards. Soon after the CLI was finished, it became clear we could need a simple GUI to play the game and test out mechanics. Having already a code base in Rust, I decided to give Rust a chance and look for a simple GUI library. My choice was egui, an easy to use immediate mode GUI. With not much development time, we were able to try out the game by sharing a screen in a video call. However, there were (are) basically not tests. While the game rule engine now has test, the UI has none. With a bit of time on my hands, I started to think about the reasons. This article will talk about one reason, and how this can be fixed.

You will find the whole code for this article in this repository. There are also code snippets and links throughout the article.

Disclaimer

I am not a professional rust developer and also not an expert in egui. If you find some wrong or strange ideas in this article, or simply disagree, please get in touch with me. And if you find this helpful, I appreciate if you share this idea. Generally, I am happy to receive feedback.

Also, while this pattern is nothing fancy, I did never see it written down for rust and egui.

EGUI

Let us start with some basics about egui. Egui is an immediate mode UI, which means we specify all the UI elements in the update loop together with their values. A retained mode GUI would need us to store a reference to an UI element and then call a method on this UI element to update its value. You can read about the pros and cons in the egui readme. For us it is important to remember the following facts:

  • we have a single thread which draws the UI every frame — we should not block this
  • we have one update function in which we define the GUI and its state
  • the update function is called and run for every frame inside this single thread

Basics and State

GUI

We can start using the eframe template which gives us a small app on which we can build on. As this is not a tutorial, I will not go over the changes I made step by step. In short, I have changed a label, added a Check button and a place to display the result. I have also added a button to create a new window inside the main window. The implementation is not very robust and certainly not an optimal one, but it will help to understand the point I am going to make. Can you spot the issue with the window IDs in the code (Hint)? Also, the text input is way too big.

The main function inside main.rs calls

    eframe::run_native(
        "eframe template",
        native_options,
        Box::new(|_| Ok(Box::new(egui_command_pattern::CommandPatternApp::new()))),
    )

and creates a new CommandPatternApp struct. This struct looks as follows

pub struct CommandPatternApp {
    // Example stuff:
    label: String,  
    correct_answer: Option<bool>,
    value: f32,
    windows: Vec<WindowContent>,
}

together with the update function signature

impl eframe::App for CommandPatternApp {
    /// Called each time the UI needs repainting, which may be many times per second.
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        Self::create_top_menu(ctx);

        self.create_central_panel(ctx);
    }
}

it becomes clear, that this CommandPatternApp struct holds the state of the application. This also means, every time a user interacts with a control, e.g. enters text, we need to update the state which then will be used in the next iteration to draw the updated GUI.

Reacting to user interaction

Let us have a look at the input field with the Check button.

ui.horizontal(|ui| {
  ui.label("What is 5 + 5?");
  ui.text_edit_singleline(&mut self.label);
  if ui.button("Check").clicked() {
    self.correct_answer = Some(10 == self.label.parse::<i32>().unwrap_or(0));
  };
});

In a horizontal layout (row) we add a label, then a single line text edit field with a mutable borrow to the label field of our state struct. Every change in the text edit will be written into this field and be available in the next iteration of the update loop. Then we add a button with a click handling. When the button is clicked we update the correct_answer field of our state struct. This field is later used to print a label underneath this row.

This is really, straightforward. Right? However, this is also a bit of a problem. While one can debate if and how we should test the binding of self.label to the text edit field, we should certainly be able to write tests for our validation logic determining if our answer is correct or not.

One simple solution would be to move this line in to its own function and then write a test for it. However, then you will have multiple places in your code which are called when a user interacts. Also, there is nothing convening the intent of the user interaction.

Commands

I do not like to have similar, code at multiple places without a need. Also, when a project grows, the same logic even may be implemented multiple times at multiple places without a necessity. I also do not like to have to know where to look for user interaction handling code. It could be all over the place. And it also may not convey a semantic meaning. Can we make this more self-documenting and clearer?

How about using something like this

pub(crate) enum Command {
    VerifyAnswer(String),
    IncrementByButton,
    CreateNewWindow(WindowContent),
    CloseWindow(usize),
    NoOP,
}

pub(crate) trait CommandHandler {
    fn handle_command(&mut self, command: Command);
}

impl CommandHandler for CommandPatternApp {
    fn handle_command(&mut self, command: Command) {
        match command {
            Command::VerifyAnswer(answer) => {
                dbg!("Handle verify answer command");
                self.correct_answer = Some(10 == answer.parse::<i32>().unwrap_or(0));
            }
        _ => { /* all the other commands */ } // do not do this in production code, there you rely on exhaustive pattern matching
    }
}

Now we can rewrite our button click handling like this:

if ui.button("Check").clicked() {
  let value = self.label.clone();
  self.handle_command(Command::VerifyAnswer(value));
 };

We now convey the intent of the interaction as well as centralize the logic for state modification at one place. Also we document all the commands a user can perform in the Command enum. A new developer (or myself in 4 weeks) can now look at the Command enum and see immediately which interaction should be possible - your IDE may help you find where these commands are created and therefore used. Also, there is a single function we can write tests for and the compiler helps us check if we have unhandled commands.

That is the basic idea. Most likely, nothing new and rather simple.

Callback functions

This self.handle_command call can get a bit problematic if we have windows which are not part of the CommandPatternApp struct and therefore have no self reference. As long as the new window is created in a instance method like in here, we simply can call:

fn add_close_button(&mut self, content: &WindowContent, ui: &mut Ui) {
  if ui.button("Close").clicked() {
    self.handle_command(Command::CloseWindow(content.id));
  }
}

and be happy.

But what if the window is created by a static method like this:

pub(crate) fn create_window<F>(cmd_callback: &mut F, ctx: &Context, content: &WindowContent)
where 
  F: FnMut(Command)

and we only want to supply a callback?

Then we need to modify our draw_windows function and supply a mutable borrow to a closure.

fn draw_windows(&mut self, ctx: &Context) {
    let windows_to_draw = self.windows.clone(); // this must be done before the mutable borrow of self happens in the closure
    let mut callback = |cmd: Command| self.handle_command(cmd);
    windows_to_draw.iter().for_each(|content| {
        create_window(
            &mut callback,
            ctx,
            content,
        );
    });
}

This callback closure captures a mutable borrow to self and is itself borrowed mutably by each window. Be aware, we still only have one mutable borrow of self in the scope of the draw_windows function. Also, each mutable borrow of the callback is happening inside an own scope (iteration) of the loop. As we are in a single threaded environment, we do not think about mutexes. By the way, do not change the order of cloning the windows vector and creating the callback. The borrow checker will not like it.

While we now have only one place where we handle the commands, we can take advantage of egui being an immediate mode UI and also knowing we only have one thread.

One call to rule them all

How about putting the command handling at a single place in the lifecycle of a frame? How about putting it directly at the beginning of the update loop. And how about also putting not the complete window content into the app state but only the values we need?

This commit changes the CreateWindowCommand to only getting the window id and moves the content creation into the loop inside the draw_windows function. It also fixes the issue with the window IDs. To be frank, moving the content creation inside the loop is not strictly needed for this example.

But how about putting the command handling call into one place? Well, knowing we only have one single thread, we can modify our state to hold a

pub struct CommandPatternApp {
    // other stuff...
    pub(crate) cmd_to_run: Option<Command>
}

cmd_to_run field which is read and handled as first instruction of the update loop

if let Some(cmd) = &self.cmd_to_run {
  self.handle_command(cmd.clone())
}

Now we have one place where all the state changes happen (the handle_command function) and a single place where we can check if a user interaction triggered a command. At least for me, this looks much more understandable and testable than the first implementation where the state mutation was done in place and all over the code.

Conclusion

We have seen three possible ways to handle state changes on user interaction inside egui. The first one, performing the state change directly where the element (e.g., button) is defined works well but is not easily testable nor conveys intent. The second possibility introduced Commands, creates them when a user interacts with an element and then calls a function to handle the commands. This centralizes the state change, decouples it from the element definition, conveys intent and makes the intended state change easily testable. We also have seen how to supply this hanlde_command function as a callback to function outside the CommandPatternApp implementation. Last but not least, we centralized the place where the handle_command function is called to a single place in the application.

Both, the second and the third pattern convey intent and make the state change followed by a user interaction easily testable. Personally, I favor the third method over the second as we then only have a single place changing the state. However, this may result in issues when a user happens to trigger two commands during one update iteration (e.g. low frame rate or long-running commands). We could then work with a vector of commands instead of a single field or start to force a redrawing when a user interacts with an element. However, I feel these are issues not of the shown patterns but more of using an immediate UI.

Code

You can find all the code in this repository. The branches contain the following:

Tests are only included int the one_call_to_rule_them_all branch.

Window Id Hint

Create two new windows and close the one with ID 0. Then create another one and count the windows. If running in debug mode, try to move the new window.