Rust GUIs With Egui

I will document some design patterns I’ve used while writing complicated GUIs in Rust with egui. This is not an introduction to Rust or Egui.

Multiple Panels

Multiple Panels State Gif

This example shows how to make a UI that can switch between multiple panels/states/UIs/whatever you want to call them. This is accomplished by having a struct, StateUi, which holds any state and can be instructed to switch states with the Action enum. Note that this does not preserve State when switching between the UIs.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
fn main() -> eframe::Result<()> {
    eframe::run_native(
		"Howdy, do!",
		eframe::NativeOptions::default(),
		Box::new(|_cc| Box::new(StateUi::new()))
	)
}

struct StateUi {
    state: Box<dyn State>
}

impl StateUi {
    pub fn new() -> Self {
        Self {
            state: Box::new(MainUi::new()),
        }
    }
}

impl eframe::App for StateUi {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default()
            .show(ctx, |ui| {
                let resp = self.state.show(ui);

                match resp {
                    Response::Switch(state) => self.state = state,
                    Response::None => ()
                };
            });
    }
}

trait State {
    fn show(&mut self, ui: &mut egui::Ui) -> Response;
}

enum Response {
    Switch(Box<dyn State>),
    None,
}

struct MainUi {
    counter: i32
}

impl MainUi {
    pub fn new() -> Self {
        Self { counter: 0 }
    }
}

impl State for MainUi {
    fn show(&mut self, ui: &mut egui::Ui) -> Response {
        ui.label("I'm the main UI!");
        ui.add(egui::Slider::new(&mut self.counter, 0..=69));

        if ui.button("Other UI").clicked() {
            let other = Box::new(OtherUi::new());
            return Response::Switch(other);
        }

        Response::None
    }
}

struct OtherUi {
    text: String
}

impl OtherUi {
    pub fn new() -> Self {
        Self { text: String::new() }
    }
}

impl State for OtherUi {
    fn show(&mut self, ui: &mut egui::Ui) -> Response {
        ui.label("I'm the other UI!");
        ui.text_edit_singleline(&mut self.text);

        if ui.button("Main UI").clicked() {
            let main = Box::new(MainUi::new());
            return Response::Switch(main);
        }

        Response::None
    }
}

Any number of additional UIs can be added, as long as they implement the State trait

Multiple Panels Preserving State

Panels Preserving State Gif

This example is similar to the one above, but each UI is preserved when switching. The major difference is that the panels are held in a vector but only one is shown at a time, determined by state_idx.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
fn main() -> eframe::Result<()> {
    eframe::run_native(
        "Howdy do!",
        eframe::NativeOptions::default(),
        Box::new(|_cc| Box::new(StateUi::new())),
    )
}

struct StateUi {
    state_idx: usize,
    states: Vec<Box<dyn State>>,
}

impl StateUi {
    pub fn new() -> Self {
        Self {
            state_idx: 0,
            states: vec![Box::new(MainUi::new()), Box::new(OtherUi::new())],
        }
    }
}

impl eframe::App for StateUi {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            let resp = self.states[self.state_idx].show(ui);

            match resp {
                Response::Switch(state_idx) => self.state_idx = state_idx,
                Response::None => (),
            };
        });
    }
}

trait State {
    fn show(&mut self, ui: &mut egui::Ui) -> Response;
}

enum Response {
    Switch(usize),
    None,
}

struct MainUi {
    counter: i32,
}

impl MainUi {
    pub fn new() -> Self {
        Self { counter: 0 }
    }
}

impl State for MainUi {
    fn show(&mut self, ui: &mut egui::Ui) -> Response {
        ui.label("I'm the main UI!");
        ui.add(egui::Slider::new(&mut self.counter, 0..=69));

        if ui.button("Other UI").clicked() {
            return Response::Switch(1);
        }

        Response::None
    }
}

struct OtherUi {
    text: String,
}

impl OtherUi {
    pub fn new() -> Self {
        Self {
            text: String::new(),
        }
    }
}

impl State for OtherUi {
    fn show(&mut self, ui: &mut egui::Ui) -> Response {
        ui.label("I'm the other UI!");
        ui.text_edit_singleline(&mut self.text);

        if ui.button("Main UI").clicked() {
            return Response::Switch(0);
        }

        Response::None
    }
}

Running Complex Operations

Complex Operations GUI gif

This example shows how a button can invoke a complex operation that may take a while without blocking the main thread. Note how we call request_repaint_after if the task is not yet done. Without this, the UI would only update on mouse movement and the user would wait and wait an wait.

While this method feels ugly, it keeps the complexity spirit demon at bay by keeping everything within the JoinHandle, rather than having separate start_task(), is_finished(), and get_result() functions. However, I’ll admit that having an Option<JoinHandle<Result<Vec<Users>, FetchError>>> in your UI doesn’t look nice.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
use std::{thread::{JoinHandle, self}, time::Duration};

fn main() -> eframe::Result<()> {
    eframe::run_native(
        "Howdy do!",
        eframe::NativeOptions::default(),
        Box::new(|_cc| Box::new(App::new())),
    )
}

struct App {
    magic_number: usize,
    magic_number_rx: Option<JoinHandle<usize>>,
}

impl App {
    pub fn new() -> Self {
        Self {
            magic_number: 0,
            magic_number_rx: None
        }
    }
}

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            let mut enabled = true;

            if let Some(rx) = &self.magic_number_rx {
                enabled = false;

                // Refreshes to check if task is done
                ctx.request_repaint_after(Duration::from_millis(100));
				// Set cursor icon to a spinner to indicate the machine is thinking
                ctx.output_mut(|o| o.cursor_icon = egui::CursorIcon::Progress);

                if rx.is_finished() {
                    let num = self.magic_number_rx.take().unwrap().join().unwrap();
                    self.magic_number = num;
                }
            }

            ui.horizontal(|ui| {
                let button = egui::Button::new("Get magic number");
                if ui.add_enabled(enabled, button).clicked() {
                    self.magic_number_rx = Some(get_magic_number());
                }

                if !enabled {
                    ui.spinner();
                }
            });

            ui.label(format!("The magic number is: {}", self.magic_number));
        });
    }
}

fn get_magic_number() -> JoinHandle<usize> {
    thread::spawn(move || {
        println!("Big thinking...");
        thread::sleep(Duration::from_secs(1));
        42
    })
}

This relies on the glorious take() method which I encourage you to learn about

Previous:
Media Mover
Next:
HORUS
Rust