Fork me on GitHub

egui.info

Unofficial notes about the excellent egui crate

egui-215-todo-list

This is an example of a multi-window todo list.

Source

Screenshots

Screenshot

Description

Before creating our App struct, we create a struct to hold the information about each todo item.

#[derive(Clone)]
struct TodoItem {
    name: String,
    body: String,
}

impl Default for TodoItem {
    fn default() -> Self {
        Self {
            name: "New Todo Item".to_owned(),
            body: String::new(),
        }
    }
}

Then we create our ExampleApp struct which contains our todo items as a map of an id to an item, the next valid id, and the currently edited id and item.

struct ExampleApp {
    // The todo items, with a unique u32 identifier
    items: HashMap<u32, TodoItem>,
    // A counter to keep track of the next valid ID -- it is incremented every time it's used
    next_id: u32,

    // the id and item currently edited, if any
    currently_edited: Option<(u32, TodoItem)>,
}

We open the editor window by setting ExampleApp::currently_edited to any value except None. In this case we set it to Some((id, item)) where id and item are clones of an id or item from our ExampleApp::items field.

// For every item, show its name as a clickable label.
for (id, item) in items {
    // Add some spacing to let it breathe
    ui.add_space(5.0);

    // Add a clickable label using egui::Label::sense()
    if ui
        .add(egui::Label::new(&item.name).sense(egui::Sense::click()))
        .clicked()
    {
        // Set this item to be the currently edited one
        self.currently_edited = Some((id, item));
    };

    // Add some spacing to let it breathe
    ui.add_space(5.0);
}

Then we have to keep calling egui::Context::show_viewport_immediate. Calling it only once will make the window appear only once, while calling it every frame will keep it from closing.

We give egui::Context::show_viewport_immediate a function that receives a &egui::Context and can render UI using it as normal. In that function we can also mutate our self variable (our instance of ExampleApp)

// If we're currently editing an item, we have to keep calling ctx.show_viewport_immediate
// Remove the currently edited id and item, replace later if necessary
if let Some((id, mut item)) = self.currently_edited.take() {
    let viewport_id = egui::ViewportId::from_hash_of(format!("edit {id}"));
    let viewport_builder = egui::ViewportBuilder::default()
        .with_inner_size((300.0, 300.0))
        .with_title(format!("edit {}", item.name));

    // This function is like eframe::App::update, except it can access ExampleApp as well
    let viewport_cb = |ctx: &egui::Context, _| {
        egui::CentralPanel::default().show(ctx, |ui| {
            ui.label("Name:");
            ui.text_edit_singleline(&mut item.name);
            ui.label("Body:");
            ui.text_edit_multiline(&mut item.body);

            if ui.button("Save").clicked() {
                // Insert our changed item at the id
                self.items.insert(id, item);

                // Set the currently edited item to nothing
                self.currently_edited = None;
            } else if ui.button("Cancel").clicked()
                || ctx.input(|i| i.viewport().close_requested())
            {
                // Set the currently edited item to nothing
                self.currently_edited = None;
            } else if ui.button("Remove").clicked() {
                // Remove the currently edited item
                self.items.remove(&id);

                // Set the currently edited item to nothing
                self.currently_edited = None;
            } else {
                // Otherwise set the currently edited item to this item again so the window won't close
                self.currently_edited = Some((id, item));
            }
        });
    };

    ctx.show_viewport_immediate(viewport_id, viewport_builder, viewport_cb);
}