Trying egui: building a Cistercian Clock with Rust GUI
Tags: rust, gamedev, gui
Published: 2024-01-22 11:39:57

---
title: "Trying egui: building a Cistercian Clock with Rust GUI"
published: "true"
description: "Trying egui 🖥️ building a Cistercian clock in 🦀 Rust with the egui immediate mode library inspired by Dear ImGui."
tags: "rust, gamedev, gui"
canonical_url: "https://rodneylab.com/trying-egui/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hixkonfe6k5ef3wne5fj.png"

---
## 🖥️ What is egui?

In this post on trying egui, we
- take a look at what egui is;
- see why and how you might want to use it; and
- see an example of it in action, building a Cistercian clock.

The **Cistercian clock** uses an archaic form for representing numerals, which predates the modern Arabic numerals (1, 2, 3&hellip;).  I just built the app for fun, rather than any practical application.

egui is an **immediate mode** Graphical User Interface (**GUI**) library.  It is quite quick to get going on, and you might consider building a GUI app with it if you are just getting going with Rust.

Immediate mode describes the way egui handles updates.  The logic for interactions is kept inside the UI code.  This is in contrast to **retained mode**,  where a separate model of the UI state is maintained, and you use callback functions to update the user interface.  In egui UI code, closures used to display widgets also include code to update the app state, when the user interacts with the app.

### egui for Gaming

egui is often a practical choice for game backends; for tweaking game parameters on-screen, while running the game, without having to break into the code.  For example, you could tweak the size of a rendered game object or some difficulty parameter.

### Immediate Mode Advantages
- quick to build apps at the expense of increased CPU usage, compared to retained mode;
- easier implementation of buttons without callbacks; and
- no need to check the app is in sync with a virtual state model.

egui is inspired by **<a aria-label="Dear I m GUI" href="https://github.com/ocornut/imgui">Dear ImGui</a>**, from the C++ world.  If you are new game development in Rust, and are looking for a Dear ImGui alternative to pair with **Bevy** or **Macroquad**, egui be a help.

In this post, we use the egui template to get started quickly. With that running, we add the Cistercian clock code.  We don&rsquo;t take an in-depth look at the Rust code here, so you should be able to follow along, and get the app running even if you are new to Rust.

## 🦀 Setting up your System for Rust

Skip this if you already have Rust set up on your system.


1. Install rustup, the program for downloading and updating your local version of the Rust compiler and Rust tooling:
```shell
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
```

You will also need to install a C compiler (GCC, or Clang for example).  On macOS, run `xcode-select --install` for a minimal version of Xcode, if you do not already have the full version set up.  For full instructions, see the <a href="https://www.rust-lang.org/tools/install">official Install Rust guide</a>.

2. Check you are up and running:
```shell
rustc --version
```

At the time of writing, the current rustup version is `1.26` and you may have a newer version.  This is different to the Rust version.  We will see there is a file in the template which pins the Rust version for your egui project.

## ⚙️ Spinning up the egui Template

To help you get going quickly, there is a starter template.  If you have the <a aria-label="Git Hub C L I tool installed on your system" href="https://github.com/cli/cli#installation">GitHub CLI tool installed on your system</a>, the easiest way to use the template is to create a new repo from the Terminal:

```shell
gh repo create cistercian-clock \\
  --template="emilk/eframe_template" --public
git clone \\
  https://github.com/YOUR-GITHUB-PROFILE/cistercian-clock.git
```

Here, `--public` creates a public repo on your GitHub account (you can use `--private`, instead, if you prefer).

Alternatively, if you are not using `gh`, log into GitHub and then, <a aria-label="open the e gui template repo" href="https://github.com/emilk/eframe_template">open the egui template repo</a>.  If you are logged in, you should see a **Use this template** button towards the top and right of the page.  Click the button to create a new repo on your account, using the template, then from your machine, clone your new instance of the repo.

## Spinning up the Template

To run the code, jump into the new repo folder, in the Terminal, and use the command:

```shell
cargo run
```

This will be slower the first time, as cargo has to download packages, and then compile the code.  Rust compiles incrementally, so later compilations will be quicker, only recompiling anything that has changed.

Once compilation is complete, the app should pop up, and look something like this:

![Trying egui: Screenshot shows starter app running, with a light background, and the Light button highlighted, beside a Dark button, at the top of the window.  Below, the skeleton content has a title eframe template with and some sample widgets.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fr1ugmb1oaf2ccxhgccv.png)

## ⌚️ Customizing the Template

Following the template instructions, we should update some fields in `Cargo.toml` and some source files.

### Cargo.toml

`Cargo.toml` manages dependency versions, workspaces, and package metadata in Rust projects. Update it with your own details:

```toml
[package]
name = "cistercian_clock"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2021"
rust-version = "1.72"


[dependencies]
chrono = "0.4.31"
egui = "0.24.1"
eframe = { version = "0.24.1", default-features = false, features = [
    "accesskit",     # Make egui comptaible with screen readers. NOTE: adds a lot of dependencies.
    "default_fonts", # Embed the default egui fonts.
    "glow",          # Use the glow rendering backend. Alternative: "wgpu".
    "persistence",   # Enable restoring app state when restarting the app.
] }
image = { version = "0.24.7", default-features = false }
log = "0.4"

# You only need serde if you want app persistence:
serde = { version = "1", features = ["derive"] }
```

I also added a couple of crates (Rust packages):
- <a href="https://docs.rs/image/latest/chrono/">`chrono`</a> — for time utility functions
- <a href="https://docs.rs/image/latest/image/">`image`</a> — to decode our custom app logo file

You might notice there are two styles used for adding dependencies.  The more verbose one is helpful for managing output package size.  As an optimization, some crates come with extra **features**, which are disabled by default.  Others have features enabled by default, which you may not need in your current project.  You have to check docs for the crates you use, to see what will work best for your current project.  All Rust crates will have docs, and the relevant `docs.rs` pages are linked, just above, for `chrono` and `image`.

### `main.rs`

`src/main.rs` is the entry point for our app; the code the operating system will first run on executing it.  We change `TemplateApp` to `CistercianClockApp` in lines `18` and `35`:


```rust
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release

// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
    env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).

    let native_options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([400.0, 300.0])
            .with_min_inner_size([300.0, 220.0]),
        ..Default::default()
    };
    eframe::run_native(
        "eframe template",
        native_options,
        Box::new(|cc| Box::new(cistercian_clock::CistercianClockApp::new(cc))),
    )
}
```

### `app.rs`

`src/app.rs` contains the main app logic.  Change `TemplateApp` to `CistercianClockApp` here in lines `4`, `12`, `22` and `38`:

```rust
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] // if we add new fields, give them default values when deserializing old state
pub struct CistercianClockApp {
    // Example stuff:
    label: String,

    #[serde(skip)] // This how you opt-out of serialization of a field
    value: f32,
}
```

There is one final change in `src/lib.rs`, again replacing `TemplateApp` with `CitercianClockApp`:

```rust
#![warn(clippy::all, rust_2018_idioms)]

mod app;
pub use app::CistercianClockApp;
```

There are a couple <a href="https://github.com/emilk/eframe_template#getting-started">more customizations listed in the temple</a>, for WASM apps that we will skip over here.  Save `Cargo.toml`, `src/app.rs`, `src/lib.rs`, and `src/main.rs` and now run the command:

```shell
cargo run
```

The app should work just as before.  The title, and so on, in the app window  will still read &ldquo;eframe template&rdquo;; we will change those later.  Next, let&rsquo;s continue the customizations, adding our own app logo.

## 🖼️ Adding a Custom App Logo or Icon in egui

You will need a 256&times;256 pixel PNG logo for this part.  If you already have an SVG logo, see the post on <a href="https://rodneylab.com/open-source-favicon-generation/">Open Source Favicon Generation & Optimization</a> to convert it to a PNG.

Replace `assets/icon-256.png` with your PNG icon, then update `src/main.rs` to use it:

```rust
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release

use egui::IconData;

// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
    env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).

    let icon_image = image::open("assets/icon-256.png").unwrap();
    let width = icon_image.width();
    let height = icon_image.height();
    let icon_rgba8 = icon_image.into_rgba8().to_vec();
    let icon_data = IconData {
        rgba: icon_rgba8,
        width,
        height,
    };

    let native_options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([400.0, 300.0])
            .with_min_inner_size([300.0, 220.0])
            .with_icon(icon_data),
        ..Default::default()
    };
    eframe::run_native(
        "eframe template",
        native_options,
        Box::new(|cc| Box::new(cistercian_clock::CistercianClockApp::new(cc))),
    )
}
```

In line `4`, you see an example of including a dependency in Rust.  In line `11` we use the `image` package (installed earlier) to convert the PNG into the RGBA8 format expected by egui.  We could have imported the `image` similarly to `use egui::IconData`, as `use image::open`, then just used `open(...)` in line `11`, though I preferred sticking with `image::open` (in line `11`) to make it clearer where `open` comes from, limiting ambiguity, since `open` is a common function name.

Try rerunning your app, and you should see your own logo now!


Before moving on, let&rsquo;s update the app title (displayed on the window) and the window size, in `main.rs`:

```rust
    let native_options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([750.0, 600.0])
            .with_min_inner_size([750.0, 600.0])
            .with_icon(icon_data),
        ..Default::default()
    };
    eframe::run_native(
        "Cistercian Clock",
        native_options,
        Box::new(|cc| Box::new(cistercian_clock::CistercianClockApp::new(cc))),
    )
```

## 👀 cargo watch

You can make your workflow slightly more efficient by automatically re-compiling and running your egui app each time you save a source file.  <a href="https://docs.rs/crate/cargo-watch/latest">`cargo watch` is handy here</a>.  You can install it using Cargo:


```shell
cargo install cargo-watch
```

The simplest command is:

```shell
cargo watch
```

That will just run your app after each source file change.  I tend to run:

```shell
cargo watch -x check -x clippy -x run
```

Which will check your code compiles, lint it and then run it.  You can also throw `-x test` in there if working in a Test Driven Development workflow.

`clippy` is the Rust linter.  Some people find it annoying, though I learn quite a bit from it.  If clippy does not work, you might need to install first (`rustup component add clippy`).

## ⌚️ Trying egui: Cistercian Clock Code

We won&rsquo;t go through the code line-by-line, instead, paste it in, then we will look at a few of the more interesting parts.

Update `src/app.rs` by copying the <a href="https://raw.githubusercontent.com/rodneylab/cistercian-clock/main/src/app.rs">complete file from the finished egui Cistercian Clock project repo</a>.

That should all work now, and when the app restarts, you will see the time using Arabic numerals and a Cistercian numbers cheat sheet, in a scrollable window.  The light and dark themes have been updated, and the theme switch is now a single toggle button, replacing separating light and dark buttons.

![Trying egui: Screenshot shows Cistercian Clock app running, Int he main window, you see the 24-hour clock time as 14:46:04, below that same time represented compactly in colourful lines in Cistercian notation.  Further down a section titled Cistercian Numbers offers a cheat sheet for converting between the two number systems.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/82t1za2vtaz3jpuexli8.png)

## 🧐 Trying egui: Interesting Lines in the Code

### Trying egui: Overriding Themes

To override the default colour theme, I created an entire new theme.  This override (lines `14`-`84` in `src/app.rs`) specifies the colours to use for text, warning, hyperlinks etc, using RGB values.


```rust
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
    if ui.visuals().dark_mode {
        ctx.set_visuals(dark_mode_override());
    } else {
        ctx.set_visuals(light_mode_override());
    };

  // The top panel is often a good place for a menu bar:

  egui::menu::bar(ui, |ui| {
      // NOTE: no File->Quit on web pages!
      let is_web = cfg!(target_arch = "wasm32");
      if !is_web {
          ui.menu_button("File", |ui| {
              if ui.button("Quit").clicked() {
                  ctx.send_viewport_cmd(egui::ViewportCommand::Close);
              }
          });
          ui.add_space(16.0);
      }

      egui::widgets::global_dark_light_mode_switch(ui);
  });
});
```
  Clicking the toggle button switches the active visuals (code in line `531`).  egui will act on the changes in the next loop.  `dark_mode_override()` just returns the struct with the dark mode visuals,  I could have pasted the struct in place in line `513` (and similarly for the light mode).

### Trying egui: Requesting a Repaint

Comment out line `541` , then run the app.  The clock now only updates when you interact with the window; moving the mouse pointer, or clicking.  This is fine for many apps, but not ideal for a clock.

We need the clock time to update on its own.  `request_repaint_after` helps here.  I set it to update every second, and you can go smaller if you require higher precision.

<a href="https://doc.rust-lang.org/std/time/struct.Duration.html">`core::time::Duration`</a> is a Rust core library struct.  `new` is its initializer, which lets you specify a duration in seconds (first parameter) and nanoseconds (second parameter).  The **Rust Core Library** is smaller than the Rust Standard Library, and includes functionality which can run on more systems (some embedded systems or microchips have core support, but no std support).

Add line `541` back in so the clock updates normally again.

```rust
egui::CentralPanel::default().show(ctx, |ui| {
    let colours = if ui.visuals().dark_mode {
        DARK_CISTERCIAN_NUMERAL_COLOURS
    } else {
        LIGHT_CISTERCIAN_NUMERAL_COLOURS
    };
    ui.ctx().request_repaint_after(Duration::new(1, 0));
    // The central panel the region left after adding TopPanel's and SidePanel's
    ui.heading("Cistercian Time");
    ui.add_space(30.0);
});
```

### Trying egui: Adding a Vertical Scroll

We can also add a scrollable area, in egui, with only a few lines of code:

```rust
ScrollArea::vertical()
    .auto_shrink(false)
    .scroll_bar_visibility(ScrollBarVisibility::default())
    .show(ui, |ui| {
        ui.heading("Cistercian Numbers");
        ui.add_space(30.0);
        paint_number_row(ui, &colours, 0, 10);
        ui.add_space(30.0);
        for tens in 1..10 {
            paint_number_row(ui, &colours, 10 * tens, (tens + 1) * 10);
            ui.add_space(15.0);
        }
    // ...TRUNCATED
    });
```

Here, we wrap the scrollable content in a `ScrollArea` show method closure.

Reach out if anything is not clear, so I can update the article for everyone.  Scan the code for more egui features.  We are only scratching the surface of what egui can do here.  See the Wrapping Up section, further down, for pointers on where to go to learn more.

## 💿 Building and Installing the App

So far, we have been running the app in debug mode.  To build a native app, run:

```shell
cargo build --release
```

This will optimize the app for speed and binary size, so compilation will be a touch slower than previously.  The release mode app will be at `./target/release/cistercian_clock`.  You can run it from there, though it will be more convenient to install it on your system:

```shell
cargo install --path .
```

That should place the app in `~/.cargo/bin` or somewhere equivalent on your system (`--path` in the command above is the path to the app we want to install, so run the command from the project directory).  If that folder is included in your path, you will be able to run the installed app just using its name: `cistercian_clock`.


### Updating egui (for later)

This post was written with egui `0.24`, which uses Rust `1.72`.  Later versions might use a newer Rust version.  When you update the `egui` and `eframe` in `Cargo.toml`, also update the `toolchain.channel` field in the `rust-toolchain` file to the Rust version your new `egui` release pairs with.

## 🤨 What are immediate mode alternatives?

At the start, we mentioned that egui uses immediate mode.  It makes creating user interfaces quicker, without the need for callbacks to add interactivity.  egui is used in games, with integrations for popular gaming engines.  Some developers prefer it to creating Electron based apps, Gossip, the portable Nostr client is an example here.

One drawback of immediate mode is that it can make it harder to position content precisely.  For example, centring a button horizontally.  For precise layouts, staying in the Rust ecosystem, you might want to try Tauri — it uses web technology, allowing you to apply your existing CSS knowledge for pixel perfect layouts.  egui also currently falls short in terms of accessibility.  <a aria-label="Access Kit" href="https://github.com/AccessKit/accesskit">AccessKit</a> has improved matters here, though screen reader users may still prefer web content to your egui app, albeit with AccessKit enabled.

## 🙌🏽 Trying egui: Wrapping Up

In this trying egui post, we saw some key egui features and why you should consider it for your Rust GUI.  More specifically, we saw:

- what **immediate mode** is;
- how to **create an egui app**; and
- how to **add a custom logo or icon to an egui app**.

The <a ariaLabel="Open the demo code on the Rodney Lab Git Hub site" href="https://github.com/rodneylab/cistercian-clock">source code for the complete project is in a Rodney Lab GitHub repo</a>.  For far <a aria-label="more e gui features, see the e gui demo app" href="https://egui.rs/">more egui features, see the egui demo app</a>.  It includes links to the source code for the various widgets.  If you need more specifics on the APIs, then also <a aria-label="see the egui docs" href="https://docs.rs/egui/latest/egui/">see the egui docs</a>.

Let me know what you plan to build with egui.  Also, if you have already used Dear ImGui, let me know how egui compares.

## 🙏🏽 Trying egui: Feedback

If you have found this post useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on X, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on X (previously Twitter) and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a href="https://rodneylab.com/tags/deno/">Deno</a> as well as <a href="https://rodneylab.com/tags/seo/" aria-label="See posts on search engine optimisation">Search Engine Optimization</a> among other topics. Also, <a aria-label="Subscribe to the   Rodney Lab newsletter" href="https://newsletter.rodneylab.com/issue/latest-issue">subscribe to the newsletter to keep up-to-date</a> with our latest projects.