0%

Study Notes of egui Part II: UI Components

October 20, 2025

Egui

Rust

1. Project Repository

2. Standard UI Elements

2.1. Global Separated Layout
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
    top_menu(ctx);
    self.folder_col.view(ctx);
    self.scripts_col.view(ctx);
}
2.2. Top Menu

pub fn top_menu(ctx: &egui::Context) {
    egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
        // The top panel is often a good place for a menu bar:

        egui::MenuBar::new().ui(ui, |ui| {
            // NOTE: no File->Quit on web pages!
            let is_web = cfg!(target_arch = "wasm32");
            if !is_web {
                ui.menu_button("File", |ui| {
                    if ui.button("Quit").clicked() {
                        ctx.send_viewport_cmd(egui::ViewportCommand::Close);
                    }
                });
                ui.add_space(16.0);
            }

            // egui::widgets::g lobal_theme_preference_buttons(ui);
        });
    });
}
2.3. Left sizable column:
2.3.1. Left Panel

pub fn view(&self, ctx: &egui::Context) {
    egui::SidePanel::left("Folders Panel")
        .resizable(true)
        .default_width(300.0)
        .width_range(200.0..=600.0)
        .show(ctx, |ui| {
            ui.label("Scripts Folders");

            ui.separator();

            Self::add_folder_button(ui);

            ui.add_space(10.0);

            Self::folders(ui);
        });
}
2.3.2. Loop a list of Struct

fn folders(ui: &mut Ui) {
    egui::ScrollArea::vertical().show(ui, |ui| {
        crate::with_folder_state(|state| {
            let folders_vec = (*state.folder_list.read().unwrap()).clone();
            let selected_id = *state.selected_folder_id.read().unwrap();
            let rename_folder = state.folder_to_rename.read().unwrap().as_ref().cloned();
            let rename_text = state.rename_text.read().unwrap().as_ref().cloned();

            if folders_vec.is_empty() {
                ui.label("No folders yet...");
            } else {
                for folder in &*folders_vec {
                    let is_renaming = rename_folder
                        .as_ref()
                        .map(|f| f.id == folder.id)
                        .unwrap_or(false);
                    let display_name = if is_renaming {
                        rename_text.as_ref().unwrap_or(&folder.name)
                    } else {
                        &folder.name
                    };
                    let mut folder_item = FolderItem::new(folder, selected_id, display_name);
                    folder_item.view(ui);
                }
            }
        });
    });
}

The highlighted is the egui-way modularize part of the code into a component. We explain more detail of folder item in section 2.4. Component with Local State.

2.4. Component with Local State

We break down the component into several sections:

2.4.1. flex-1 clickable button with indicator as "selected label"

struct FolderItem<'a> {
    folder: &'a crate::prisma::scripts_folder::Data,
    selected_id: Option<i32>,
    display_name: &'a str,
}

impl<'a> FolderItem<'a> {
    fn view(&mut self, ui: &mut egui::Ui) {
        ui.horizontal(|ui| {
            let is_selected = self.selected_id == Some(self.folder.id);

            // Calculate space for label (available width minus estimated menu space)
            let available_width = ui.available_width();
            let dots_menu_width = 40.0; // Estimate for menu button
            let label_width = (available_width - dots_menu_width).max(0.0);

            // Make label expand to fill calculated space
            ui.add_sized(
                [label_width, ui.available_height() + 5.0],
                |ui: &mut egui::Ui| {
                    let response = ui.selectable_label(is_selected, self.display_name);
                    if response.clicked() {
                        dispatch_folder_command(FolderCommand::SelectFolder {
                            folder_id: self.folder.id,
                        });
                    }
                    response
                },
            );

            self.dots_menu(ui, self.folder);
        });
    }
}
2.4.2. Dots menu

1struct FolderItem<'a> {
2    folder: &'a crate::prisma::scripts_folder::Data,
3    selected_id: Option<i32>,
4    display_name: &'a str,
5}
6
7impl<'a> FolderItem<'a> {
8    fn dots_menu(&mut self, ui: &mut egui::Ui, folder: &crate::prisma::scripts_folder::Data) {
9        let (delete_folder, rename_folder) = crate::with_folder_state(|state| {
10            let delete_folder = state.folder_to_delete.read().unwrap().as_ref().cloned();
11            let rename_folder = state.folder_to_rename.read().unwrap().as_ref().cloned();
12            (delete_folder, rename_folder)
13        });
14
15        ui.menu_button("...", |ui| {
16            if ui
17                .add_sized([120.0, 20.0], |ui: &mut egui::Ui| {
18                    ui.button("Rename Folder")
19                })
20                .clicked()
21            {
22                let folder_ = Arc::new(folder.clone());
23                crate::with_folder_state(|state| {
24                    *state.folder_to_rename.write().unwrap() = Some(folder_.clone());
25                    *state.rename_text.write().unwrap() = Some(folder_.name.clone());
26                });
27            }
28            if ui
29                .add_sized([120.0, 20.0], |ui: &mut egui::Ui| {
30                    ui.button("Delete Folder")
31                })
32                .clicked()
33            {
34                let folder_ = Arc::new(folder.clone());
35                crate::with_folder_state(|state| {
36                    *state.folder_to_delete.write().unwrap() = Some(folder_);
37                });
38            }
39        });
2.4.3. Confirm Delete Dialog

40        // Show delete confirmation if this folder is selected for deletion
41        if let Some(folder_) = delete_folder
42            && folder_.id == folder.id
43        {
44            egui::Window::new("Confirm Delete")
45                .collapsible(false)
46                .resizable(false)
47                .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
48                .show(ui.ctx(), |ui| {
49                    ui.label(format!(
50                        "Are you sure you want to delete this folder: {}?",
51                        folder.name
52                    ));
53                    ui.add_space(20.0);
54                    ui.horizontal(|ui| {
55                        if ui.button("Cancel").clicked() {
56                            crate::with_folder_state(|state| {
57                                *state.folder_to_delete.write().unwrap() = None;
58                            });
59                        }
60                        if ui.button("Delete").clicked() {
61                            dispatch_folder_command(FolderCommand::DeleteFolder {
62                                folder_id: folder.id,
63                            });
64                            crate::with_folder_state(|state| {
65                                *state.folder_to_delete.write().unwrap() = None;
66                            });
67                        }
68                    });
69                });
70        }
2.4.4. Rename Folder Dialog

71        if let Some(folder_) = rename_folder
72            && folder_.id == folder.id
73        {
74            egui::Window::new("Rename Folder")
75                .collapsible(false)
76                .resizable(false)
77                .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
78                .show(ui.ctx(), |ui| {
79                    ui.label("Input new folder name:");
80                    ui.add_space(10.0);
81                    let mut text = crate::with_folder_state(|state| {
82                        state
83                            .rename_text
84                            .read()
85                            .unwrap()
86                            .as_ref()
87                            .cloned()
88                            .unwrap_or_default()
89                    });
90                    ui.text_edit_singleline(&mut text);
91                    crate::with_folder_state(|state| {
92                        *state.rename_text.write().unwrap() = Some(text.clone());
93                    });
94                    ui.add_space(20.0);
95                    ui.horizontal(|ui| {
96                        if ui.button("Cancel").clicked() {
97                            crate::with_folder_state(|state| {
98                                *state.folder_to_rename.write().unwrap() = None;
99                                *state.rename_text.write().unwrap() = None;
100                            });
101                        }
102                        if ui.button("Rename").clicked() {
103                            dispatch_folder_command(FolderCommand::RenameFolder {
104                                folder_id: folder_.id,
105                                new_name: text,
106                            });
107                            crate::with_folder_state(|state| {
108                                *state.folder_to_rename.write().unwrap() = None;
109                                *state.rename_text.write().unwrap() = None;
110                            });
111                        }
112                    });
113                });
114        }
115    }
116}
2.5. Right column (Automatically Resizable)

pub fn scripts_col(ctx: &egui::Context) {
    egui::CentralPanel::default().show(ctx, |ui| {
        ui.add_space(-6.0); // Reduce top padding
        ui.label("Scripts");
        ui.separator();

        // Example 1: Using Frame with uniform margin
        egui::Frame::new()
            .inner_margin(16.0) // Same margin on all sides
            .show(ui, |ui| {
                ui.label("This is inside a Frame with 16px margin on all sides");
            });

        ui.add_space(10.0);
        ...
    });
}
2.6. Div like Element

This acts like div with display flex:

egui::Frame::new()
    .fill(egui::Color32::from_rgb(240, 240, 240)) // Light gray background, like a div
    .stroke(egui::Stroke::new(
        1.0,
        egui::Color32::from_rgb(200, 200, 200),
    )) // Subtle border
    .corner_radius(4.0) // Rounded corners
    .inner_margin(8.0) // Padding inside the frame
    .show(ui, |ui| {
        ui.horizontal_wrapped(|ui| {
            ui.spacing_mut().item_spacing.x = 0.0;
            ui.label("This demo showcases how to use ");
            ui.code("Ui::response");
            ui.label(" to create interactive container widgets that may contain other widgets.");
        });
    });
2.7. Theme-aware Div

Frame::canvas(ui.style())
    .fill(visuals.bg_fill.gamma_multiply(0.3))
    .stroke(visuals.bg_stroke)
    .inner_margin(ui.spacing().menu_margin)
    .show(ui, |ui| {
        ui.set_width(ui.available_width());

        ui.add_space(32.0);
        ui.vertical_centered(|ui| {
            Label::new(
                RichText::new(format!("{}", self.count))
                    .color(text_color)
                    .size(32.0),
            )
            .selectable(false)
            .ui(ui);
        });
        ui.add_space(32.0);

        ui.horizontal(|ui| {
            if ui.button("Reset").clicked() {
                self.count = 0;
            }
            if ui.button("+ 100").clicked() {
                self.count += 100;
            }
        });
    });
2.8. Group

ui.group(|ui| {
    ui.label("This is inside a group() - has background and padding");
});
2.9. Frame with Border Radius

egui::Frame::new()
    .fill(ui.visuals().window_fill())
    .stroke(ui.visuals().window_stroke())
    .corner_radius(4.0)
    .inner_margin(12.0)
    .show(ui, |ui| {
        ui.label("Frame with background, border, rounded corners, and 12px margin");
    });
ui.add_space(10.0);
2.10. Frame with Margin

egui::Frame::new()
    .inner_margin(16.0) // Same margin on all sides
    .show(ui, |ui| {
        ui.label("This is inside a Frame with 16px margin on all sides");
    });

ui.add_space(10.0);
2.11. Space-between Layout

frame.show(ui, |ui| {
    ui.horizontal(|ui| {
        ui.label(format!("Name: {}", script.name));
        if ui.button("Rename").clicked() {
            self.renaming_script_id = Some(script.id);
            self.renaming_name = script.name.clone();
        }
        ui.with_layout(
            egui::Layout::right_to_left(egui::Align::Center),
            |ui| {
                if ui.button("Execute").clicked() {
                    // Execute the script command
                    crate::run_terminal_command(script.command.clone());
                }
                if ui.button("Edit").clicked() {
                    self.editing_script_id = Some(script.id);
                    self.editing_command = script.command.clone();
                }
                if ui.button("Copy").clicked() {
                    ui.ctx().copy_text(script.command.clone());
                }
            },
        );
    });
  }
)
2.12. Text Editor

fn edit_script_window(&mut self, ui: &mut Ui, script_id: i32) {
    egui::Window::new("Edit Script")
        .collapsible(false)
        .resizable(true)
        .default_height(400.0)
        .default_width(600.0)
        .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
        .show(ui.ctx(), |ui| {
            ui.add(
                egui::TextEdit::multiline(&mut self.editing_command)
                    .font(egui::TextStyle::Monospace)
                    .code_editor()
                    .desired_rows(20)
                    .desired_width(580.0),
            );
            ui.add_space(20.0);
            ui.horizontal(|ui| {
                if ui.button("Cancel").clicked() {
                    self.editing_script_id = None;
                }
                if ui.button("Save").clicked() {
                    dispatch_folder_command(FolderCommand::UpdateScript {
                        script_id,
                        new_command: self.editing_command.clone(),
                    });
                    self.editing_script_id = None;
                }
            });
        });
}