Build a Note Viewer with Side Panel in egui | Learn egui in Neovim Ep 9
Video: Build a Note Viewer with Side Panel in egui | Learn egui in Neovim Ep 9 by Taught by Celeste AI - AI Coding Coach
egui::SidePanel::left("id")— a docked sidebar that lives at the top level of the window, alongsideCentralPanel.
In Episode 7 we built a two-column dashboard with horizontal + vertical. That technique works inside a content area. Today we use the higher-level alternative: SidePanel, which docks to a window edge and is the standard egui pattern for permanent navigation panels.
What we are building
A note viewer. A sidebar on the left lists titles; the central panel shows the body of whichever note is selected. Click a title in the sidebar — the body updates. Same pattern you would see in Notion, Bear, or a thousand other apps.
The state
pub struct MyApp {
notes: Vec<Note>,
selected: usize,
}
struct Note {
title: String,
body: String,
}
A vector of Notes and an index into the vector. Whatever index selected holds, that note is the one currently displayed.
SidePanel + CentralPanel
egui::SidePanel::left("notes_panel")
.min_width(150.0)
.show(ctx, |ui| {
// sidebar widgets
});
egui::CentralPanel::default().show(ctx, |ui| {
// main content
});
Two top-level panels. The sidebar docks to the left edge; the central panel claims everything that is left over. Note both .show(ctx, ...) — they take the context, not a parent ui. They are top-level layout regions.
SidePanel::left("notes_panel") — the string is a unique ID. egui uses it to remember the panel's resized width across frames. Two side panels with the same ID would interfere.
.min_width(150.0) — the panel cannot be resized narrower than 150 pixels. Without min_width, the user could collapse the sidebar to nothing by dragging its edge. There is also .max_width, .default_width, .resizable(false) for a fixed-width panel.
Selectable labels
for i in 0..self.notes.len() {
let selected = i == self.selected;
if ui.selectable_label(selected, &self.notes[i].title).clicked() {
self.selected = i;
}
}
ui.selectable_label(selected: bool, text) is the right widget for a list of clickable items where one is highlighted. It looks like a normal label, but it gets a highlighted background when selected is true and turns into a button-like region when hovered.
The first argument controls the highlight; the second is the label text. The widget returns a Response, and .clicked() fires when the user clicks the row.
We pass i == self.selected so exactly one row in the list is highlighted at any time. Clicking another row updates self.selected = i, and the next frame highlights that row instead.
For a more elaborate list (with icons, multi-line entries, drag-and-drop) you would build custom widgets. For a navigation list, selectable_label is exactly enough.
Reading the selected note
egui::CentralPanel::default().show(ctx, |ui| {
let note = &self.notes[self.selected];
ui.heading(¬e.title);
ui.separator();
ui.label(¬e.body);
});
Borrow the selected note from the vector by index. Render its title as a heading and its body as a label. egui's label automatically wraps long text to the panel width, so multi-line bodies display naturally.
The shared state pattern: the sidebar writes self.selected, the central panel reads self.selected. Both run in the same update. No event subscription, no message passing.
SidePanel vs nested layouts
We have now seen two ways to build a sidebar:
- Nested
horizontal+vertical(Episode 7) — works inside any panel. SidePanel(today) — docks at the top level of the window.
When to pick which:
- App-level navigation (a permanent sidebar that should always be there) —
SidePanel. - A two-column layout inside a tab or a card —
horizontal+vertical. - Resizable sidebar —
SidePaneldoes this for free (drag the edge); the nested approach requires manual handling.
For most desktop apps, the right combination is one or two top-level SidePanels plus a CentralPanel. Internal layouts (forms, lists, settings panes) use nested horizontal/vertical/Grid.
Other panel types
egui has a small family of top-level panels:
SidePanel::left(id)— docked to the left edge.SidePanel::right(id)— docked to the right edge.TopBottomPanel::top(id)— docked to the top edge (menu bar).TopBottomPanel::bottom(id)— docked to the bottom edge (status bar).CentralPanel::default()— fills whatever is left.
You can have multiple of each. A typical IDE-style app might have a left side panel for the file tree, a right side panel for an inspector, a top panel for the menu, a bottom panel for the status bar, and a central panel for the editor. We will build something like that in Episode 11.
Running it
cargo run. The window opens with a sidebar on the left listing four note titles and a central area showing the welcome note. Click "Rust Tips" in the sidebar — the central area updates. Drag the right edge of the sidebar to resize.
The body text wraps to the central panel's width. Resize the window and the wrapping adjusts.
Common mistakes
Forgetting the unique ID. SidePanel::left("foo") and SidePanel::left("foo") clash. Use distinct IDs.
Adding SidePanel inside another panel.show. Top-level panels go directly under ctx, not inside another ui. Calling SidePanel::left().show(ctx, ...) from inside a CentralPanel::default().show(ctx, ...) is a logic error.
Order matters. Top-level panels are docked in the order they are added. If you add CentralPanel before SidePanel, the central will claim the whole window first and the sidebar will not have room. Always declare side and top-bottom panels before the central panel.
Storing usize indices that go out of date. If you remove an item from self.notes and self.selected was the index of a later item, the index is now stale. Either clamp to notes.len() - 1 after every mutation, or use a more stable identifier.
What's next
Next episode: scroll area. A list of log entries with a scroll viewport that auto-scrolls to the bottom as new entries arrive. egui::ScrollArea::vertical().stick_to_bottom(true) — the standard log-viewer pattern.
Recap
egui::SidePanel::left(id) for top-level docked sidebars. min_width to keep them sane. selectable_label(is_selected, text) for a list of clickable rows where one is highlighted. The pattern for a sidebar-plus-content app is one SidePanel plus one CentralPanel, with shared state in the app struct.
Next episode: scroll area. See you in the next one.