Rust egui Tutorial #2 — Text Widgets: Labels, Headings & More
Video: Rust egui Tutorial #2 — Text Widgets: Labels, Headings & More by Taught by Celeste AI - AI Coding Coach
Three widgets —
label,heading,monospace— and one separator. The text primitives behind every egui app.
In Episode 1 we got a window on screen. Today we fill it with text. egui has a small, focused set of text widgets and one visual separator. Once you know these, you can lay out documentation pages, headers, chat messages, status bars, anything that is mostly words.
What we are building
A demo window with the three text widget types side by side. A heading on top, normal labels below, monospace labels for code-like text, and ui.separator() to draw thin lines between sections.
The script
use eframe::egui;
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([800.0, 600.0]),
..Default::default()
};
eframe::run_native(
"Labels and Headings",
options,
Box::new(|_cc| Ok(Box::new(MyApp::default()))),
)
}
struct MyApp {
name: String,
}
impl Default for MyApp {
fn default() -> Self {
Self { name: "World".to_string() }
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Labels and Headings");
ui.separator();
ui.label("This is a normal label.");
ui.label("Labels display read-only text.");
ui.separator();
ui.heading("About This App");
ui.label("Headings are larger and bolder than labels.");
ui.label(format!("Hello, {}!", self.name));
ui.separator();
ui.monospace("fn main() -> eframe::Result");
ui.monospace(" // monospace text looks like code");
});
}
}
The boilerplate is identical to Episode 1. The interesting work is inside update.
ui.label
The default text widget. Takes a &str or anything that implements Into<RichText>. Renders in the proportional body font at the default size and colour.
Two calls in this script:
ui.label("This is a normal label.");
ui.label("Labels display read-only text.");
Each call places one label, and labels stack vertically by default — the next widget appears below. If you want labels side by side, wrap them in ui.horizontal(|ui| { ... }) (we will see this in Episode 3).
label is the right choice for any read-only prose. Buttons, links, input fields are different widgets.
ui.heading
Identical to label, but renders larger and bolder using the heading font. Use it for section titles.
ui.heading("Labels and Headings");
You can have as many headings as you want; egui does not impose semantic levels (h1, h2, h3) the way HTML does. Visually there is one heading style. For multi-level hierarchies, use RichText::new("...").size(24.0).strong() and tune the size yourself.
ui.monospace
Renders in a fixed-width font. Used for code, file paths, command-line examples, anything where character alignment matters.
ui.monospace("fn main() -> eframe::Result");
ui.monospace(" // monospace text looks like code");
Both lines render in the same character-width font, which means the indent on the second line lines up vertically with the start of the first line. A regular label with two leading spaces would compress those spaces and you would lose the indent.
For multi-line code blocks, use ui.code(text) (a slightly different style with a subtle background) or build up a longer string and pass it to monospace.
ui.separator
A thin horizontal line. Visual divider between sections.
ui.separator();
That is the entire API. No arguments. egui draws a 1-pixel line in the current panel's accent colour, with appropriate spacing above and below. Use it whenever you want visual breathing room between groups of widgets.
For vertical separators (in horizontal layouts), use ui.add(egui::Separator::default().vertical()).
Format strings as labels
ui.label(format!("Hello, {}!", self.name));
format! returns a String, which ui.label accepts. This is how you display state. Anywhere you have a value in self, you can format it into a label.
A subtle thing about immediate mode: that format! runs every frame. If self.name is "World", you are allocating a new "Hello, World!" string sixty times per second. For most UIs this is fine — strings are cheap. For very high-frequency updates with very large strings, you might cache. But do not optimise prematurely; egui is fast enough that simple format! usage is rarely the bottleneck.
Running it
cargo run from the project directory. The window appears with three sections separated by horizontal lines:
- A heading "Labels and Headings" at the top, then two normal labels.
- Another heading "About This App", then two more labels (one of which renders "Hello, World!").
- Two monospace lines that look like code.
Resize the window — the text wraps automatically. egui handles word-wrap and re-layout per frame, so the UI stays usable at any window size.
When to use each
label: most text. Body copy, descriptions, status messages.heading: section titles. Use sparingly; an app full of headings is just an app full of noise.monospace/code: file paths, code snippets, command-line output, anything where alignment matters.separator: visual grouping. Two or three per panel is plenty; more makes the UI feel busy.
Common mistakes
Trying to inline text styling with markdown syntax. egui does not parse markdown. **bold** renders literally as asterisks. For styled text, use RichText::new("bold text").strong() and pass that to label.
Calling label for everything including buttons. Labels are read-only; they do not respond to clicks (well, technically they emit hover and click events, but they are not styled like buttons). Use ui.button(...) for clickable text.
Stacking too many separators. A separator below every label produces a striped look that is visually exhausting. Group several labels together and separate the groups, not the individual lines.
What's next
Next episode: buttons and state. A counter app with +, -, and Reset buttons. Same single-struct, immediate-mode pattern — and you will see how interactivity slots in. Click a button, mutate the struct, the next frame reflects the change.
Recap
Three text widgets: label for normal text, heading for section titles, monospace for fixed-width text. One separator widget for visual grouping. format! to inject state. The whole API surface for static text in egui fits on one slide.
Next episode: buttons and state. See you in the next one.