Rust egui Horizontal Layout — Build an Action Toolbar (Ep 6)
Video: Rust egui Horizontal Layout — Build an Action Toolbar (Ep 6) by Taught by Celeste AI - AI Coding Coach
ui.horizontal(|ui| { ... })— three buttons in a row, left to right. The first proper layout block.
We have used ui.horizontal already (Episode 4 for label-plus-input, Episode 3 for two buttons next to each other). Today we look at it on its own. A real-world pattern: a toolbar of action buttons, each performing a different action, with a status line below.
What we are building
An action toolbar with three buttons — Add, Remove, Reset — and a status line that updates on each click. Plus a counter that the buttons mutate.
The script
use eframe::egui;
pub struct MyApp {
message: String,
count: i32,
}
impl Default for MyApp {
fn default() -> Self {
Self {
message: String::from("Ready"),
count: 0,
}
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Action Toolbar");
ui.separator();
ui.horizontal(|ui| {
if ui.button("Add").clicked() {
self.count += 1;
self.message = format!("Added! Count: {}", self.count);
}
if ui.button("Remove").clicked() {
self.count -= 1;
self.message = format!("Removed! Count: {}", self.count);
}
if ui.button("Reset").clicked() {
self.count = 0;
self.message = String::from("Reset!");
}
});
ui.separator();
ui.horizontal(|ui| {
ui.label("Status:");
ui.label(&self.message);
});
ui.horizontal(|ui| {
ui.label("Count:");
ui.label(format!("{}", self.count));
});
});
}
}
Three ui.horizontal blocks: one for the toolbar, two for the status display.
How ui.horizontal works
ui.horizontal(|ui| {
// widgets here flow left to right
});
ui.horizontal takes a closure that receives a fresh Ui. Widgets added inside that Ui are placed left to right instead of top to bottom. When the closure ends, the next widget on the outer ui returns to vertical flow.
This is the basic egui layout primitive. There is no Flexbox-style sophistication, no constraint solver — just a directional layout that knows how to stack things.
Three buttons in a row
ui.horizontal(|ui| {
if ui.button("Add").clicked() {
self.count += 1;
self.message = format!("Added! Count: {}", self.count);
}
if ui.button("Remove").clicked() {
self.count -= 1;
self.message = format!("Removed! Count: {}", self.count);
}
if ui.button("Reset").clicked() {
self.count = 0;
self.message = String::from("Reset!");
}
});
Each button is added inside the horizontal layout. Each if statement runs its body if the user clicked that specific button this frame. Two state mutations happen on every click: count (the actual value) and message (the status string).
message is just an audit log — it tells you what happened most recently. In a real app you might write to a log file or update a status bar widget.
Two-label rows
ui.horizontal(|ui| {
ui.label("Status:");
ui.label(&self.message);
});
Label-and-value pairs are common: "Status: Ready", "Count: 42". Each pair lives in its own horizontal block. The first label is static; the second is dynamic.
&self.message works because &String implements Into<RichText>. You do not need format! if you already have a String.
Spacing between widgets
By default egui adds a small gap between horizontally laid-out widgets. You can control this with ui.spacing_mut().item_spacing.x = 12.0; before adding widgets, or globally via ctx.style_mut.
Other layout helpers:
ui.add_space(20.0)— adds horizontal space (in ahorizontalblock) or vertical space (in averticalblock).ui.separator()insidehorizontaladds a thin vertical divider.ui.allocate_space(Vec2::new(8.0, 0.0))— likeadd_spacebut explicit about both axes.
Vertical inside horizontal
You can nest layouts. A horizontal block containing two vertical blocks creates a two-column layout — exactly what Episode 7 builds. We are saving that for the next episode.
Running it
cargo run. A row of three buttons appears at the top. Click Add — the count increments, the status updates. Click Remove — the count decrements. Click Reset — back to zero, status reads "Reset!".
The two label rows below stay aligned because each is in its own horizontal. They do not auto-align with the toolbar buttons — egui does not have grid-like global alignment. For aligned forms, use the Grid widget (Episode 8).
Common mistakes
Mixing layout directions in one closure. Once you are inside a horizontal, every widget flows horizontally. To break to a new row, end the horizontal block and start a new one (or use ui.end_row() inside a Grid).
Forgetting the closure receives a different ui. Easy to typo ui for the outer one when you mean the inner one. They are both named ui, but the inner one is the layout context.
Trying to set widget widths to fill the row. horizontal sizes widgets to their content. To stretch, use ui.add(widget.fill(true)) or a more advanced layout. We will revisit when we cover layouts in Episode 7.
Putting too much inside one horizontal. Toolbars with twelve buttons get cramped. At that point, group buttons with separators inside the horizontal, or wrap to multiple rows with horizontal_wrapped.
What's next
Next episode: vertical and nested layouts. A two-column dashboard with a navigation panel on the left and details on the right. We will use ui.horizontal and ui.vertical in combination — the basic primitive of a multi-pane layout.
Recap
ui.horizontal(|ui| { ... }) flows widgets left to right. Each label-value pair, each toolbar of buttons, each row in a status panel typically gets its own horizontal block. Combine with ui.vertical to nest. egui has no grid solver; the layout is directional and explicit.
Next episode: nested layouts. See you in the next one.