Modul 24 — GUI mit egui
Deep Dive: egui/eframe für Augenblicks-GUIs — warum Immediate Mode in Systemsprachen passt, State-Management mit Rusts Ownership, Layout & Style, Custom-Painting und wann Retained Mode die bessere Wahl ist.
Immediate Mode vs. Retained Mode: Warum egui anders denkt
Lektion 121 von 5
GUI-Paradigmen kurz gefasst
Die meisten Toolkits speichern den Zustand der gesamten UI (Hierarchie von
Widgets, Text, Farben) und fragen dich nur: "Was soll sich ändern?" — das
ist Retained Mode. egui tut das Gegenteil: Jeden Frame baust du die ganze
UI neu auf — Immediate Mode. Bei jedem ui.label("Hallo") passiert:
Verwaltung von Platz, Maus-Interaktionen, Rendering — sofort. Kein Widget-Baum,
keine Eltern-Kind Produktionen, nur Code, der dem aktuellen Zustand entspricht.
Warum das in Rust perfekt passt:
- Effizienz: Du zahlst nur für das, was du in diesem Frame zeigst.
- State: Rusts Ownership-System zwingt dich, den Zustand explizit zu
verwalten (
&mut self). Retained Mode Toolkits verstecken ihren Zustand inRc<RefCell<…>>— mit egui bleibt alles auf der Hand. - Einfachheit: Kein Widget-Lifecycle, kein Event-System, keine Callbacks. Nur Code, den der Compiler statisch prüft.
// Retained Mode findet man in den meisten GUI-Frameworks:
// 1. Baum definieren: let button = Button::new("Klick"); window.add(button);
// 2. Baum leben lassen: window.show();
// — das Toolkit speichert den Baum zwischen Frames und fragt nur nach Änderungen.
// egui / Immediate Mode:
// Jeden Frame wird der gesamte Zustand der UI durch Code neu erzeugt.
Wann Immediate, wann Retained?
| Kriterium | Immediate Mode (egui) | Retained Mode (iced/Slint/Druid) |
|---|---|---|
| Einfaches UI, schnelle Iteration | ✅ Ideal | ⚠️ Overhead |
| Komplexe Hierarchien | ⚠️ Performance-Check | ✅ Natürlich |
| State-Management | ✅ Rusts &mut & Ownership | ⚠️ Rc/RefCell typisch |
| Animationen / dynamische UIs | ✅ Jeder Frame frisch | ✅ Gut |
| Compile-time Safety | ✅ Volle Typprüfung | ⚠️ Lifecycle-Komplexität |
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Der Update-Zyklus & State: eframe und das App-Trait
Lektion 122 von 5
Hallo egui: Die Minimal-App
use eframe::egui;
struct MeineApp {
name: String,
alter: u32,
}
impl Default for MeineApp {
fn default() -> Self {
Self {
name: "Rostfreier Nutzer".to_owned(),
alter: 28,
}
}
}
impl eframe::App for MeineApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
// Der ganze UI-Code lebt hier, wird jeden Frame neu ausgeführt
ui.heading("Hallo egui!");
ui.horizontal(|ui| {
ui.label("Name:");
ui.text_edit_singleline(&mut self.name);
});
ui.add(egui::Slider::new(&mut self.alter, 0..=120).text("Alter"));
if ui.button("Grüße").clicked() {
println!("Hallo {}!", self.name);
}
});
}
}
fn main() {
let options = eframe::NativeOptions::default();
eframe::run_native(
"Meine egui-App",
options,
Box::new(|_cc| Box::new(MeineApp::default())),
);
}
eframe::run_nativestartet die Event-Schleife — ein Frame pro Event oder periodische Repaints.- Dein nicht-UI-Zustand (hier
name,alter) lebt in deinemApp-Typ. updatewird mit&mut selfgerufen — Rust garantiert: nur eine Instanz bearbeitet den Zustand zur Zeit → keine Datenrennen ohneArc<Mutex>.- Jedes
ui.…()kehrt sofort mit einemResponseObjekt zurück, das zählt, ob der Nutzer interagiert hat (.clicked(),.changed(),.hovered()).
State-Lebenszyklus in einer Immediate-Mode UI
- App-Instanziierung:
MeineApp::default()— initialer Zustand - pro Frame:
ctx.begin_frame()(implizit durchCentralPanel::default().show(...))- Dein
update()wird aufgerufen → Εinmaliger Zustand eines Frames: Allokationen werdenRDrop am Frame-Ende wieder freigegeben ctx.end_frame()→ Rendering-Befehle werden erfüllt
Daher: Keine Notwendigkeit, Widgets in Structs zu speichern. Die UI ist Code,
die Daten sind dein App.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Layout, Panels und Style: Egui im Visuellen Raum
Lektion 123 von 5
Die Layout-Regeln
eguiΧ Layout-Strategie ist " ausrichtend und wrapping ". Jedes Widget reserves so viel Platz wie nötig, kein komplexes CSS-Grid.
Panels: Die gewohnten Bereiche
egui::CentralPanel::default()
.show(ctx, |ui| { /* Hauptinhalt */ });
egui::TopBottomPanel::top("top_bar").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| { /* … */ });
});
});
egui::SidePanel::left("side").show(ctx, |ui| {
ui.label("Sidebar");
});
Wichtig: Die Reihenfolge im Code ist die Reihenfolge der Überlappung
SidePanel → TopBottomPanel → CentralPanel.
Horizontal & Vertical
ui.horizontal(|ui| {
ui.label("Vorname:");
ui.text_edit_singleline(&mut first_name);
});
ui.vertical(|ui| {
ui.label(" disaster");
ui Drugs::new("…");
});
Separators, Spacing & Alignment
ui.separator(); // Horizontale Linie
ui.spacing_mut().slider_width = 200.0; // Global für diesen UI-Kontext
ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
ui.label("Rechts ausgelegt");
});
Style anpassen
egui trennt Logik (App Zustand) von Aussehen totals.
// Global für die ganze App (einmalig in update):
ctx.set_visuals(egui::Visuals::dark());
// oder
ctx.set_visuals(egui::Visuals::light());
// Per-Widget:
ui.style_mut().text_styles.insert(
egui::TextStyle::Heading,
egui::FontId::new(24.0, egui::FontFamily::Proportional),
);
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Ownership & Borrowing: Rests Ownership-System trifft auf UI
Lektion 124 von 5
State, Mutability und die UI
Jedes Widget, das benutzerseitig geänderte Werte ausgibt, nimmt eine &mut T Referenz:
ui.text_edit_singleline(&mut self.name);
ui.add(egui::Slider::new(&mut self.alter, 0..=120));
if ui.checkbox(&mut self.active, "Aktiv").changed() {
// self.active wurde geaendert
}
Das bedeutet: Ein Wert kann nicht gleichzeitig in zwei Widgets bearbeitet werden. Das ist kein Bug, sondern ein Feature: Es verhindert Datenrennen und Unbestimmtheit.
Der Push-Id-Trick: Eindeutige Widget-Identitäten
egui erkennt Widgets durch ihren Id. Standardmäßig wird die Id aus dem Widget-Typ und der Hierarchie im UI-Baum abgeleitet.
Falls du z. B. eine Liste vonItems dynamisch darstellst und jedes Item sein eigenes Textfeld haben soll, musst du eindeutige Ids erzwingen:
for (id, item) in self.items.iter_mut().enumerate() {
ui.push_id(id, |ui| {
ui.text_edit_singleline(&mut item.name);
});
}
Ohne push_id hätten alle Textfelder die gleiche Id und würden eine gemeinsame
Eingabe teilen.
Retained State ohne expliziten Zustand: ui.data
Manchmal braucht nur die UI einen Zustand (z. B. ein Collapsing-Hheader ist
auf- oder zugeklappt). Dafu"r gibt es egui::Memory:
// Speichere UI-internen Zustand an einer bestimmten Id:
ui.data_mut(|d| d.insert_temp(id, true));
// Lese ihn im naechsten Frame wieder aus:
let was_open = ui.data(|d| *d.get_temp::<bool>(id).unwrap_or(false));
Das ist Frame-lokaler Zustand — wenn das Widget in einem Frame nicht gezeichnet wird, wird der Zustand gelöscht.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren:
Painting, Repaints & Performance: Wenn es um Pixel geht
Lektion 125 von 5
Custom Painting:Shapes und Farben direkt auf die Leinwand
egui bietet direkten Zugriff auf das Painter-Objekt für eigene Zeichenoperationen:
use egui::{Color32, Pos2, Stroke};
ui.painter().circle_filled(
Pos2::new(100.0, 100.0),
30.0,
Color32::from_rgb(100, 200, 100),
);
ui.painter().line_segment(
[Pos2::new(50.0, 50.0), Pos2::new(150.0, 150.0)],
Stroke::new(3.0, Color32::RED),
);
- All Koordinaten sind in logischen Punkten, nicht Pixel.
- Farben sind
Color32— 32-bit RGBA. Strokedefiniert Linien: Breite + Farbe + optional gestrichelte Linien.
Wann wird neu gezeichnet?
egui folgt dem "changegd"-Prinzip:
- Änderungen in deinem Zustand (z. B.
self.name = …) → nächsten Frame wird neu labourt. - Nutzer-Interaktionen (Maus, Tastatur) → sofortiger Repaint.
- Explizite Anfragen:
ctx.request_repaint()erzwingt einen neuen Frame, auch wenn sich nichts geändert hat (z. B. für Animationen).
Immediate Mode kostet CPU — aber nur für das, was sichtbar ist. Wiederholte
request_repaint() ohne Arbeit → 60 fps Loop, CPU bleibt niedrig (keine
GPU-Sync-Risiken).
Backends: Wo läuft egui?
- eframe (Standard): OpenGL (glow) oder Vulkan (wgpu) auf Desktop.
- Web:
eframe+wasm-bindgen→ runs in the Browser via WebGL/WebGPU. - Embedded über
egui-winit+ eigenem Renderer.
Für maximale Performance auf Desktop:
# Cargo.toml
[dependencies]
eframe = { version = "0.27", features = ["wgpu"] }
egui vs. die Konkurrenz
| Toolkit | Paradigma | Reife | Typische Use-Cases | Ganz in Rust? |
|---|---|---|---|---|
| egui | Immediate Mode | ✅ | Tools, Debug-UIs, einfache GUIs mit State | ✅ |
| iced | Retained/Elm | ✅ | Desktop-Apps, komplexe UIs | ✅ (wgpu) |
| Slint | Retained/Yocto | ✅ | Eingebettete Systeme, QML-ähnlich | ❌ (C++/Rust) |
| Druid | Retained | ⚠️ | Hochperformante Desktop-Apps | ✅ |
| Dioxus | Retained/VirtualDOM | ⚠️ | React-ähnliche Apps | ✅ |
egui glänzt, wenn du schnell, _iterativ und “systemnah“ eine UI brauchst — Developer Tools, Debug-Overlays, Prototypen. Für klassische Desktop-Anwendungen mit komplexer Navigation und State-Hierarchien sind Retained-Mode-Toolkits oft besser.
✅ Wichtige Erkenntnisse
Haken setzen, um deinen Lernfortschritt zu markieren: