Lesson 0002: The Immediate-Mode Loop & Mutable State (&mut)
Building the PCB Layer Visibility Inspector inside your eframe::App
If you have spent years building UIs in HTML/React, Qt, or Flutter, your muscle memory is wired for retained-mode widgets: you construct a checkbox object once, wire up an onChange callback, and spend the rest of the afternoon keeping its internal state synchronised with your data model.See Emil Ernerfeldt's official Understanding Immediate Mode section in the egui documentation for why eliminating widget callbacks prevents out-of-sync UI bugs.
Turns out, egui throws all of that out the window.
In Lesson 0001, we created our pcb-reviewer Cargo crate and booted a native window with eframe::run_native. Now, we are going to fill in the heart of that file—PcbReviewerApp::ui—to build our Layer Visibility Inspector: toggling Top Copper (F.Cu), Bottom Copper (B.Cu), Silkscreen (F.SilkS), and the unrouted Ratsnest on and off.
In egui, there are no persistent checkbox or button objects. Instead, your plain Rust struct holds the single source of truth, and on every frame, your eframe::App::ui method runs straight from top to bottom.
flowchart TD
Start["Frame Start: eframe calls ui(&mut self, ui, frame)"] --> Side["1. egui::Panel::left('layers').show(ui, |ui| { ... })"]
Side --> Check["ui.checkbox(&mut self.show_top_copper, 'Top Cu')"]
Check -->|"Reads bool to draw check<br/>Flips bool in-place if clicked"| Mutated["self.show_top_copper updated immediately"]
Mutated --> Central["2. egui::CentralPanel::default().show(ui, |ui| { ... })"]
Central -->|"Reads updated self fields"| Paint["Paints visible PCB layers for this frame"]
Paint --> End["Frame End: &mut self borrow released until next repaint"]
Figure 2.1: Inside a single call to ui(&mut self, ui, frame), Panel::left mutates self in-place via &mut before CentralPanel reads self to paint the board.
1. Your Rust struct Is the Only State
Recall the PcbReviewerApp struct we initialised in Lesson 0001. Four bool fields track which PCB layers are currently visible:
pub struct PcbReviewerApp {
pub show_top_copper: bool,
pub show_bottom_copper: bool,
pub show_silkscreen: bool,
pub show_ratsnest: bool,
}
2. Why egui Widgets Take &mut References
When eframe redraws your window, it calls:In eframe::App::ui, &mut self is shorthand for self: &mut PcbReviewerApp—an exclusive mutable borrow of your struct for the duration of a single frame (Rust Book §4.2).
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame)
So, inside ui, we place a left egui::Panel for our layer controls, followed by an egui::CentralPanel for our 2D PCB canvas. Look closely at what we pass into ui.checkbox:
egui::Panel::left("layer_panel").show(ui, |ui| {
ui.heading("PCB Layers");
ui.checkbox(&mut self.show_top_copper, "Top Copper (F.Cu)");
ui.checkbox(&mut self.show_bottom_copper, "Bottom Copper (B.Cu)");
ui.checkbox(&mut self.show_silkscreen, "Silkscreen (F.SilkS)");
ui.checkbox(&mut self.show_ratsnest, "Ratsnest (Unrouted)");
});
Two Rust mechanics make those six lines work without a single callback:Always declare Panel::left, Panel::right, Panel::top, and Panel::bottom before CentralPanel. According to the CentralPanel docs, it claims all remaining space in the root Ui after outer panels have been laid out.
- The Closure (
|ui| { ... }):.show(ui, |ui| { ... })takes an inline anonymous function (Rust Book §13.1) receiving a temporary&mut egui::Uiregion. When the closure ends, the inneruiis dropped—only yourselfstruct persists to the next frame. - The Mutable Borrow (
&mut self.show_top_copper): Passing&mutgrantsui.checkboxtemporary read-write access to that singleboolfield. In one function call,ui.checkboxreads the boolean to draw the tick mark, checks whether the mouse clicked that rectangle this frame, and if so, flips the boolean in place before returning. No listeners, no state-sync plumbing.
3. Making Button Actions Testable & Mutant-Proof: Block by Block
Now, what about preset buttons like "Isolate Top Copper" or "Show All Layers"? When you click a button, ui.button("...").clicked() returns true for that single frame.
It is tempting to inline the boolean assignments right inside the if ui.button(...).clicked() block. But, if you extract that state transition into a method on PcbReviewerApp taking &mut self, your ui() loop stays clean and you can test and mutate every state transition with cargo test and cargo mutants (Testing & Mutation Cheat Sheet).The reason our unit test setup matters here is interesting: if a test starts from a state where show_top_copper is already true and the other layers are already false, and then calls app.isolate_top_copper(), cargo mutants can replace the body of isolate_top_copper with an empty no-op () and the test will still pass! Starting from the opposite state forces the method to actually flip every field.
Let's walk through the new code added to src/main.rs in this lesson block by block, then inspect the complete runnable file in context.
Block 1: Pure Layer Preset Methods (impl PcbReviewerApp)
impl PcbReviewerApp {
pub fn reset_layers(&mut self) {
self.show_top_copper = true;
self.show_bottom_copper = true;
self.show_silkscreen = true;
self.show_ratsnest = false;
}
pub fn isolate_top_copper(&mut self) {
self.show_top_copper = true;
self.show_bottom_copper = false;
self.show_silkscreen = false;
self.show_ratsnest = false;
}
pub fn show_all_layers(&mut self) {
self.show_top_copper = true;
self.show_bottom_copper = true;
self.show_silkscreen = true;
self.show_ratsnest = true;
}
}
pub fn isolate_top_copper(&mut self): Takes an exclusive mutable borrow (&mut self) ofPcbReviewerAppso it can enableself.show_top_copperwhile turning off the three remaining layer flags in place.pub fn show_all_layers(&mut self): Mutably borrowsselfand sets all fourboolfields—including the unroutedself.show_ratsnestoverlay—totrue.- Why Keep State Transitions Out of
ui(): Because these methods only touch plain Rust booleans onselfand don't require anegui::Uihandle or GPU window, we can call them directly in#[test]functions and verify every field assignment withcargo-mutants.
Block 2: The Left Layer Inspector & Central Viewport (impl eframe::App)
impl eframe::App for PcbReviewerApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::Panel::left("layer_panel").show(ui, |ui| {
ui.heading("PCB Layers");
ui.checkbox(&mut self.show_top_copper, "Top Copper (F.Cu)");
ui.checkbox(&mut self.show_bottom_copper, "Bottom Copper (B.Cu)");
ui.checkbox(&mut self.show_silkscreen, "Silkscreen (F.SilkS)");
ui.checkbox(&mut self.show_ratsnest, "Ratsnest (Unrouted)");
ui.separator();
if ui.button("Isolate Top Copper").clicked() {
self.isolate_top_copper();
}
if ui.button("Show All Layers").clicked() {
self.show_all_layers();
}
});
egui::CentralPanel::default().show(ui, |ui| {
ui.heading("PCB Viewport Ready");
if self.show_top_copper {
ui.label("Rendering Top Copper (F.Cu)...");
}
if self.show_bottom_copper {
ui.label("Rendering Bottom Copper (B.Cu)...");
}
if self.show_silkscreen {
ui.label("Rendering Silkscreen (F.SilkS)...");
}
if self.show_ratsnest {
ui.label("Rendering Ratsnest (Unrouted)...");
}
});
}
}
egui::Panel::left("layer_panel").show(ui, |ui| { ... }): Carves out a resizable left sidebar identified by the unique ID string"layer_panel"beforeCentralPanelclaims the rest of the window.ui.checkbox(&mut self.show_..., "..."): Each checkbox temporarily borrows one boolean field mutably (&mut), draws its current state, and flips theboolimmediately if clicked during this frame.ui.separator()&ui.button("...").clicked():ui.separator()draws a horizontal dividing line.ui.button(...)returns anegui::Responsevalue; calling.clicked()returnstrueon the frame the user clicks the button, triggeringself.isolate_top_copper()orself.show_all_layers().- Immediate Same-Frame Consistency in
CentralPanel: BecausePanel::leftruns beforeCentralPanel::default().show(...), any change made by a checkbox or preset button in the left panel is already stored inselfwhenCentralPanelchecksif self.show_top_coppera few microseconds later in the exact same frame.
Block 3: Unit Tests for Layer Preset Transitions (#[cfg(test)] mod tests)
#[test]
fn isolate_top_copper_forces_only_top_copper_visible() {
let mut app = PcbReviewerApp {
show_top_copper: false,
show_bottom_copper: true,
show_silkscreen: true,
show_ratsnest: true,
};
app.isolate_top_copper();
assert!(app.show_top_copper);
assert!(!app.show_bottom_copper);
assert!(!app.show_silkscreen);
assert!(!app.show_ratsnest);
}
#[test]
fn show_all_layers_enables_every_layer() {
let mut app = PcbReviewerApp {
show_top_copper: false,
show_bottom_copper: false,
show_silkscreen: false,
show_ratsnest: false,
};
app.show_all_layers();
assert!(app.show_top_copper);
assert!(app.show_bottom_copper);
assert!(app.show_silkscreen);
assert!(app.show_ratsnest);
}
- Starting from the Opposite State (
let mut app = PcbReviewerApp { ... }): Notice thatisolate_top_copper_forces_only_top_copper_visiblestarts withshow_top_copper: falseand the other three layerstrue, whileshow_all_layers_enables_every_layerstarts with all four fieldsfalse. That forces each method to actively flip every single boolean it touches. - Exhaustive Field Assertions: Checking all four fields with
assert!(...)andassert!(!...)guarantees that ifcargo-mutantsreplaces either method body with(), the test suite immediately catches the fault.
Complete src/main.rs Listing
Here is the complete, runnable apps/0002-the-immediate-mode-loop/src/main.rs file with all blocks assembled in context:
use eframe::egui;
fn main() -> eframe::Result {
let mut options = eframe::NativeOptions::default();
options.viewport = egui::ViewportBuilder::default()
.with_inner_size([1024.0, 720.0]);
eframe::run_native(
"Schematic & PCB Design Reviewer",
options,
Box::new(|_cc| Ok(Box::new(PcbReviewerApp::default()))),
)
}
pub struct PcbReviewerApp {
pub show_top_copper: bool,
pub show_bottom_copper: bool,
pub show_silkscreen: bool,
pub show_ratsnest: bool,
}
impl Default for PcbReviewerApp {
fn default() -> Self {
let mut app = Self {
show_top_copper: false,
show_bottom_copper: false,
show_silkscreen: false,
show_ratsnest: false,
};
app.reset_layers();
app
}
}
impl PcbReviewerApp {
pub fn reset_layers(&mut self) {
self.show_top_copper = true;
self.show_bottom_copper = true;
self.show_silkscreen = true;
self.show_ratsnest = false;
}
pub fn isolate_top_copper(&mut self) {
self.show_top_copper = true;
self.show_bottom_copper = false;
self.show_silkscreen = false;
self.show_ratsnest = false;
}
pub fn show_all_layers(&mut self) {
self.show_top_copper = true;
self.show_bottom_copper = true;
self.show_silkscreen = true;
self.show_ratsnest = true;
}
}
impl eframe::App for PcbReviewerApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::Panel::left("layer_panel").show(ui, |ui| {
ui.heading("PCB Layers");
ui.checkbox(&mut self.show_top_copper, "Top Copper (F.Cu)");
ui.checkbox(&mut self.show_bottom_copper, "Bottom Copper (B.Cu)");
ui.checkbox(&mut self.show_silkscreen, "Silkscreen (F.SilkS)");
ui.checkbox(&mut self.show_ratsnest, "Ratsnest (Unrouted)");
ui.separator();
if ui.button("Isolate Top Copper").clicked() {
self.isolate_top_copper();
}
if ui.button("Show All Layers").clicked() {
self.show_all_layers();
}
});
egui::CentralPanel::default().show(ui, |ui| {
ui.heading("PCB Viewport Ready");
if self.show_top_copper {
ui.label("Rendering Top Copper (F.Cu)...");
}
if self.show_bottom_copper {
ui.label("Rendering Bottom Copper (B.Cu)...");
}
if self.show_silkscreen {
ui.label("Rendering Silkscreen (F.SilkS)...");
}
if self.show_ratsnest {
ui.label("Rendering Ratsnest (Unrouted)...");
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_enables_physical_layers_and_hides_ratsnest() {
let app = PcbReviewerApp::default();
assert!(app.show_top_copper);
assert!(app.show_bottom_copper);
assert!(app.show_silkscreen);
assert!(!app.show_ratsnest);
}
#[test]
fn isolate_top_copper_forces_only_top_copper_visible() {
let mut app = PcbReviewerApp {
show_top_copper: false,
show_bottom_copper: true,
show_silkscreen: true,
show_ratsnest: true,
};
app.isolate_top_copper();
assert!(app.show_top_copper);
assert!(!app.show_bottom_copper);
assert!(!app.show_silkscreen);
assert!(!app.show_ratsnest);
}
#[test]
fn show_all_layers_enables_every_layer() {
let mut app = PcbReviewerApp {
show_top_copper: false,
show_bottom_copper: false,
show_silkscreen: false,
show_ratsnest: false,
};
app.show_all_layers();
assert!(app.show_top_copper);
assert!(app.show_bottom_copper);
assert!(app.show_silkscreen);
assert!(app.show_ratsnest);
}
}
Toggle the checkboxes or click the preset buttons inside the simulated egui window below. Watch how each interaction immediately mutates self via &mut and repaints the 2D PCB viewport in the very same frame.
1. Persistent Rust State (self)
2. What Happened This Frame
Complete the three missing expressions in PcbReviewerApp::ui below so that: (1) the checkbox mutably borrows self.show_ratsnest, (2) the "Isolate Top Copper" button checks whether it was clicked this frame, and (3) the CentralPanel closure receives the ui handle.
impl eframe::App for PcbReviewerApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::Panel::left("layers").show(ui, |ui| {
ui.heading("Layers");
ui.checkbox(, "Ratsnest");
if ui.button("Isolate Top Copper"). {
self.show_top_copper = true;
self.show_bottom_copper = false;
self.show_silkscreen = false;
self.show_ratsnest = false;
}
});
egui::CentralPanel::default().show(ui, {
if self.show_top_copper {
ui.label("Rendering Top Copper (F.Cu)...");
}
});
}
}
Add the two unit tests below to your #[cfg(test)] mod tests block in src/main.rs. Notice why we start with show_top_copper: false and the other layers true before calling app.isolate_top_copper()—so when cargo mutants replaces isolate_top_copper or show_all_layers with a no-op (), the test assertions immediately catch the mutant!
#[test]
fn isolate_top_copper_forces_only_top_copper_visible() {
let mut app = PcbReviewerApp {
show_top_copper: false,
show_bottom_copper: true,
show_silkscreen: true,
show_ratsnest: true,
};
app.isolate_top_copper();
assert!(app.show_top_copper);
assert!(!app.show_bottom_copper);
assert!(!app.show_silkscreen);
assert!(!app.show_ratsnest);
}
#[test]
fn show_all_layers_enables_every_layer() {
let mut app = PcbReviewerApp {
show_top_copper: false,
show_bottom_copper: false,
show_silkscreen: false,
show_ratsnest: false,
};
app.show_all_layers();
assert!(app.show_top_copper);
assert!(app.show_bottom_copper);
assert!(app.show_silkscreen);
assert!(app.show_ratsnest);
}
Answer from memory before peeking back at the prose or the Immediate-Mode Cheat Sheet. Effortful retrieval builds long-term storage strength.
Recommended Primary Source
egui Crate Root Documentation — "Understanding Immediate Mode" & "Quick Start" by Emil Ernerfeldt.
Pay special attention to how ui.label, ui.button, and mutable state fit together, and why egui has no widget lifecycle callbacks.
&mut self inside closures, or when eframe decides to run ui() vs. sleep? Ask me any follow-up question in chat, or click straight through to Lesson 0003: Custom 2D Canvas & egui::Painter!