Embedded Rust GUI Progress report
As part of my ongoing research with embedded Rust on ESP32 devices, (starting with the T-Deck but eventually branching out to other devices), I started creating a simple reusable UI toolkit to automate the boring code that I end up writing over and over for every example. The code is on github but I haven't made a crate for it yet.
Goals
The goal for this embedded GUI lib is to be able to quickly create basic and fast UIs on any device that supports Rust’s embedded_graphics API. Since most of these devices put the display on the SPI bus, they tend to be fill-rate limited. This means it’s very important that the GUI architecture lend itself to avoiding overdraw. The lib does this by tracking dirty rectangles and using a clip rect to only draw what has actually changed on screen. It also means the screen is only redrawn when something actually changes.
View Graphs in Rust
GUIs are traditionally done using a tree structure, however this is something that is especially difficult to model in Rust due to the borrow checker. Most GUIs end up becoming a spider web of references, which Rust won’t let you do. To address this challenge I’ve come up with a few stratgies.
View
struct that defines all views. It contains the basic state that all views need like a name, bounds, if it’s visible, etc. To create a Button or Label, we instead of using subclassing we set optional fucntions on the struct, sort of like methods, which leads us to:
draw_button
fuction and set view.draw = Some(draw_button)
to use it. The same pattern is used for state, handling input, and doing view layout. If a view doesn’t need that functionality then it sets that attribute to None
.
Progress So Far
The Scene and View work pretty well. There are built in components for button, label, text input, toggle button, toggle group, panel, and a popup menu. All components are generic. They draw using the DrawingContext
and Theme
abstractions which are generic over Color
and Font
. This means a View
can be created that will work with any drawing surface and any theme with any color and font type. The views draw using semantic colors and fonts provided by the Theme at runtime. I'm not completely happy with the type signatures. There's so many generics that you have to copy everywhere, but it seems to work.
To use this library with a real device you need to provide implementations of DrawingContext
and build your own app loop which generates input events and redraws the scene. Look at the esp-test example to see how to do it.
To create your own views just assemble one at runtime. Build a View struct with your own values for state, input, layout, and draw. To have your code be notified when a view receives input, make the input function return an Action enum with some indication of what it did. Ex: a list view could return Action::Command(item)
indicating which item was selected. Here's an example of a toggle button:
pub struct SelectedState {
pub selected: bool,
}
pub fn make_toggle_button<C, F>(name: &str, title: &str) -> View<C, F> {
View {
name: name.into(),
title: title.into(),
bounds: Bounds::new(0, 0, 80, 30),
visible: true,
state: Some(Box::new(SelectedState::new())),
draw: Some(draw_toggle_button),
layout: None,
input: Some(input_toggle_button),
}
}
fn draw_toggle_button<C, F>(
e: &mut DrawEvent<C, F>,
) {
e.ctx.fill_rect(&e.view.bounds, button_fill);
e.ctx.stroke_rect(&e.view.bounds, &e.theme.fg);
// rest of drawing code
}
fn input_toggle_button<C, F>(event: &mut GuiEvent<C, F>) -> Option<Action> {
// get the view state
if let Some(state) = event.scene.get_view_state::<SelectedState>(event.target) {
// change it
state.selected = !state.selected;
}
// mark it dirty
event.scene.mark_dirty_view(event.target);
Some(Action::Command("toggled"))
}
Next Steps
I built all of this so that I could make a tiny web browser for the LilyGo T-Deck. I have this code running in a different repo which makes use of this GUI repo. The repaint rates are reasonable but not great. So next up:
So.. I’ve still got more work to do. If you have questions or want to contribute please file a ticket or open a discussion question.
Posted September 16th, 2025
Tagged: rust embedded embeddedrust