egui TopBottomPanel Tutorial - Recipe Viewer App | Learn Rust GUI Ep 11
Video: egui TopBottomPanel Tutorial - Recipe Viewer App | Learn Rust GUI Ep 11 by Taught by Celeste AI - AI Coding Coach
TopBottomPanel::top("menu")andTopBottomPanel::bottom("status")— the menu bar and status bar that complete a desktop layout.
After Episode 9 (SidePanel) and Episode 10 (ScrollArea), the missing piece for a "real desktop app look" is the top menu bar and the bottom status bar. egui's TopBottomPanel handles both. Combine it with a left sidebar and a central content area, and you have the four-panel layout almost every IDE, mail client, or chat app uses.
What we are building
A recipe viewer with three panels:
- Top panel: a menu-bar-like header showing the app title and selectable recipe titles.
- Bottom panel: a status bar showing the selected recipe's category, time, and servings.
- Central panel: the recipe details — ingredients and steps.
Click a title in the top panel; the central and bottom panels update.
The script (the new bits)
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("Recipe Viewer");
ui.separator();
for i in 0..self.recipes.len() {
if ui.selectable_label(
i == self.selected,
&self.recipes[i].title,
).clicked() {
self.selected = i;
}
}
});
});
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
let recipe = &self.recipes[self.selected];
ui.horizontal(|ui| {
ui.label(format!("Category: {}", recipe.category));
ui.separator();
ui.label(format!("Time: {}", recipe.time));
ui.separator();
ui.label(format!("Servings: {}", recipe.servings));
});
});
egui::CentralPanel::default().show(ctx, |ui| {
let recipe = &self.recipes[self.selected];
ui.heading(&recipe.title);
ui.separator();
ui.heading("Ingredients");
for item in &recipe.ingredients {
ui.label(format!(" - {}", item));
}
// ... steps below
});
Three panels declared with .show(ctx, ...). Each gets a unique ID. The central panel claims whatever space is left.
TopBottomPanel::top
egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { ... });
Docks to the top edge of the window, full width. Height defaults to the content's natural size. Use it for menu bars, toolbars, app titles.
Inside the closure, lay out widgets just like any panel — usually one or more ui.horizontal blocks since menu bars are typically rows.
For a real menu bar with hierarchical menus, look at egui::menu::bar and egui::menu::menu_button:
egui::menu::bar(ui, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Open").clicked() { /* ... */ }
if ui.button("Save").clicked() { /* ... */ }
});
ui.menu_button("Edit", |ui| {
if ui.button("Cut").clicked() { /* ... */ }
});
});
That gives you traditional drop-down menus. Today's tutorial uses simpler selectable labels because the recipe list is short and we want one-click selection.
TopBottomPanel::bottom
egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| { ... });
Same idea, docked to the bottom. Use it for status bars: cursor position, selection count, sync state, the little "Loaded in 47ms" footer at the bottom of an editor.
In our recipe viewer the bottom panel shows three pieces of metadata about the selected recipe. They sit in a horizontal layout with separators between them — the standard "bullet-separated facts" status bar look.
Panel order matters
TopBottomPanel::top("menu_bar").show(ctx, ...);
TopBottomPanel::bottom("status_bar").show(ctx, ...);
CentralPanel::default().show(ctx, ...);
egui processes top-level panels in the order you declare them. Each one claims its space; the next one sees the remaining region.
So the convention is: TopBottomPanel first, then SidePanel, then CentralPanel last. The central panel always closes the layout.
If you put the central panel first, it would claim the whole window and the side/top panels would have no room.
The shared-state coordination
let recipe = &self.recipes[self.selected];
This line appears three times — once in each panel — to access the currently selected recipe. The top panel writes self.selected; the central and bottom panels read it.
That is the entire coordination between panels. There is no observer or pub-sub; the immediate-mode loop re-runs every frame and each panel reads the same self.
If you ever feel tempted to add an event bus to coordinate egui panels, stop. The shared mutable struct is the coordination.
Index borrow gotcha
let selected = i == self.selected;
if ui.selectable_label(selected, &self.notes[i].title).clicked() {
self.selected = i;
}
We compute selected before taking the immutable borrow of &self.notes[i], because we need to compare to self.selected (which would conflict with the borrow if computed inside the same expression). This pattern — compute booleans before borrowing — is standard egui-with-Rust hygiene.
Running it
cargo run. The top of the window has the app title and three recipe names as selectable labels. Click "Greek Salad" — the central panel switches to the Greek Salad recipe, and the bottom panel's category, time, and servings update too. The whole UI is consistent because every panel reads from the same self.selected.
Common mistakes
Forgetting to declare CentralPanel last. Other panels claim space first; the central panel takes the leftover. Reverse the order and the layout breaks.
Two TopBottomPanels with the same ID. The second silently fights the first for layout. Always use unique IDs.
Trying to nest TopBottomPanel inside another panel. Like SidePanel, it is a top-level construct. Pass ctx to .show, never another ui.
Putting too much in the bottom panel. Status bars work because they are dense and quick to read. A bottom panel with five rows of widgets is no longer a status bar; it is a second content area, and you should reconsider the layout.
What's next
Next episode: color picker. A palette app that lets the user pick a color, saves it to a list, and lets them remove entries. We will use ui.color_edit_button_rgb and the egui::Color32 / egui::Rgba types.
Recap
TopBottomPanel::top(id) for headers and menu bars. TopBottomPanel::bottom(id) for status bars. Combine with SidePanel and CentralPanel for a full IDE-style layout. Declare CentralPanel last. Coordinate panels through shared state in the app struct.
Next episode: color picker. See you in the next one.