▸ Agent Skills
1 min read

egui Quick Reference and Pattern Catalog

A concise recipe catalog for immediate-mode GUI development in pure Rust using egui.


1. Top-Level Containers

Immediate-mode layouts run once per frame. Panels must be declared before the central panel.

use egui::{CentralPanel, SidePanel, TopBottomPanel, Window};

fn render_layout(ctx: &egui::Context) {
    // Top bar for menus or headers:
    TopBottomPanel::top("top_panel").show(ctx, |ui| {
        ui.horizontal(|ui| {
            ui.heading("Application Title");
            ui.separator();
            if ui.button("Settings").clicked() {
                // handle settings toggle
            }
        });
    });

    // Left navigation or tools sidebar:
    SidePanel::left("left_sidebar")
        .resizable(true)
        .default_width(200.0)
        .width_range(150.0..=400.0)
        .show(ctx, |ui| {
            ui.label("Sidebar Controls");
        });

    // Central panel fills remaining space:
    CentralPanel::default().show(ctx, |ui| {
        ui.label("Main canvas or viewport content");
    });

    // Floating, draggable window:
    Window::new("Inspector")
        .default_open(true)
        .resizable(true)
        .vscroll(true)
        .show(ctx, |ui| {
            ui.label("Window contents");
        });
}

2. Layouts and Alignment

// Horizontal row of widgets:
ui.horizontal(|ui| {
    ui.label("Status:");
    ui.colored_label(egui::Color32::GREEN, "Online");
});

// Vertical column:
ui.vertical(|ui| {
    ui.label("Item 1");
    ui.label("Item 2");
});

// Multi-column layout:
ui.columns(2, |columns| {
    columns[0].label("Left Column");
    columns[1].label("Right Column");
});

// Tabular grid:
egui::Grid::new("properties_grid")
    .num_columns(2)
    .spacing([20.0, 8.0])
    .striped(true)
    .show(ui, |ui| {
        ui.label("Resolution");
        ui.label("1920x1080");
        ui.end_row();

        ui.label("Framerate");
        ui.label("60 FPS");
        ui.end_row();
    });

// Scrollable container:
egui::ScrollArea::vertical()
    .max_height(300.0)
    .auto_shrink([false, false])
    .show(ui, |ui| {
        for i in 0..100 {
            ui.label(format!("Row {i}"));
        }
    });

// Collapsible section:
ui.collapsing("Advanced Settings", |ui| {
    ui.label("Hidden options here");
});

3. Common Widgets

// Text & Labels:
ui.heading("Heading Text");
ui.label("Normal text");
ui.colored_label(egui::Color32::RED, "Warning");

// Buttons & Actions:
if ui.button("Click Me").clicked() {
    // Action performed
}

// Input Fields:
ui.text_edit_singleline(&mut string_var);
ui.text_edit_multiline(&mut text_content);

// Sliders and Drag Values:
ui.add(egui::Slider::new(&mut float_val, 0.0..=1.0).text("Volume"));
ui.add(egui::DragValue::new(&mut int_val).speed(1));

// Checkbox and Radio:
ui.checkbox(&mut is_active, "Enable Feature");
ui.radio_value(&mut selected_mode, Mode::Fast, "Fast");
ui.radio_value(&mut selected_mode, Mode::Accurate, "Accurate");

// Combo Box / Dropdown:
egui::ComboBox::from_label("Quality")
    .selected_text(format!("{:?}", current_quality))
    .show_ui(ui, |ui| {
        ui.selectable_value(&mut current_quality, Quality::Low, "Low");
        ui.selectable_value(&mut current_quality, Quality::High, "High");
    });

4. Custom Widget & Painter Canvas

use egui::{Color32, Pos2, Rect, Sense, Stroke, Vec2};

fn draw_custom_canvas(ui: &mut egui::Ui) -> egui::Response {
    let desired_size = Vec2::new(200.0, 100.0);
    // Allocate space on screen and receive a Response for mouse interaction:
    let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click_and_drag());

    if ui.is_rect_visible(rect) {
        let painter = ui.painter_at(rect);

        // Draw background rectangle:
        painter.rect_filled(rect, 4.0, Color32::from_gray(30));

        // Draw outline:
        painter.rect_stroke(rect, 4.0, Stroke::new(1.0, Color32::from_gray(80)));

        // Draw interactive circle at pointer position when dragging:
        if let Some(mouse_pos) = response.interact_pointer_pos() {
            painter.circle_filled(mouse_pos, 8.0, Color32::GOLD);
        } else {
            painter.circle_filled(rect.center(), 6.0, Color32::LIGHT_BLUE);
        }
    }

    response
}

5. State Retention Across Frames

Immediate-mode GUIs do not keep widget structs alive across frames. Store your application state in your own structs, or use egui::Id to store transient UI state in egui’s memory cache.

// Generate stable unique IDs:
let my_id = ui.make_persistent_id("my_custom_state");

// Read or write transient state in egui memory:
let mut is_expanded: bool = ui.data_mut(|d| d.get_temp(my_id).unwrap_or(false));
if ui.button("Toggle Internal").clicked() {
    is_expanded = !is_expanded;
    ui.data_mut(|d| d.insert_temp(my_id, is_expanded));
}

6. Standalone Application (eframe)

Minimal template for building desktop tools without a game engine:

use eframe::egui;

#[derive(Default)]
struct DemoApp {
    counter: i32,
}

impl eframe::App for DemoApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.heading("Standalone eframe Tool");
            if ui.button("Increment").clicked() {
                self.counter += 1;
            }
            ui.label(format!("Count: {}", self.counter));
        });
    }
}

fn main() -> eframe::Result {
    let options = eframe::NativeOptions::default();
    eframe::run_native(
        "Demo Tool",
        options,
        Box::new(|_cc| Ok(Box::new(DemoApp::default()))),
    )
}

Last updated Oct 08, 2026