Rust egui Text Input — Build a Greeting Form (Ep 4)
Video: Rust egui Text Input — Build a Greeting Form (Ep 4) by Taught by Celeste AI - AI Coding Coach
ui.text_edit_singleline(&mut self.name)— one line that gives you a text box bound to a String field.
In Episode 3 we read button clicks. Today we read text. The widget for that is text_edit_singleline, and the API has the same shape as everything else in egui — pass a mutable reference to the state, get a Response back, and the next frame reflects whatever the user typed.
What we are building
A form with a text input. Type your name, see "Hello, [name]!" appear below — but only when the field is non-empty.
The script
src/app.rs:
use eframe::egui;
pub struct MyApp {
name: String,
}
impl Default for MyApp {
fn default() -> Self {
Self { name: String::new() }
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Greeting Form");
ui.separator();
ui.horizontal(|ui| {
ui.label("Your name:");
ui.text_edit_singleline(&mut self.name);
});
ui.separator();
if !self.name.is_empty() {
ui.label(format!("Hello, {}!", self.name));
}
});
}
}
The state is one String. The widget binds directly to it.
ui.text_edit_singleline
ui.text_edit_singleline(&mut self.name);
That is the entire interaction. Pass a mutable reference to a String; egui renders a single-line text box that reads the current value, displays it, and writes back any keypress the user makes. There is no "on change" callback. The field is the source of truth, and the field gets updated in place.
This is the same trick as ui.checkbox(&mut self.dark_mode, "...") — the widget owns the binding to the state. egui calls these bound widgets, and once you see the pattern, you start writing forms in five lines.
For multi-line input there is ui.text_edit_multiline(&mut self.body). For passwords (with masked characters), build a text edit explicitly: ui.add(egui::TextEdit::singleline(&mut self.pw).password(true)).
Conditional rendering
if !self.name.is_empty() {
ui.label(format!("Hello, {}!", self.name));
}
The greeting label only appears when there is something to greet. Because the UI is rebuilt every frame, this if is consulted every frame, and the label appears or vanishes seamlessly as the user types and erases.
This is the immediate-mode pay-off in concrete form. To show or hide a widget, you wrap it in an if. There is no widget.set_visible(false), no animation system to coordinate with the layout. Just normal Rust control flow.
Layout: label + input on one row
ui.horizontal(|ui| {
ui.label("Your name:");
ui.text_edit_singleline(&mut self.name);
});
Without horizontal, the label and the text box would stack vertically — label on one line, input on the next. With horizontal, they sit side by side, label first, input second. This is the standard form layout pattern. If you want labels and inputs aligned in columns across multiple rows, use a Grid (Episode 8).
Using the typed value
Once self.name contains the user's input, you can use it anywhere — display it, save it, send it over the network, validate it. Because mutation is &mut self.name, the value is live the moment the user releases a key.
If you want to react only when the user finishes editing (e.g. presses Enter), check Response.lost_focus() and ui.input(|i| i.key_pressed(egui::Key::Enter)):
let response = ui.text_edit_singleline(&mut self.name);
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
// submit
}
For a beginner form, the always-live binding is fine. For login forms or commit-style inputs, the on-Enter pattern is clearer.
Running it
cargo run. The window has a heading, a label-and-input row, a separator, and nothing else. Type your name. As you type, "Hello, [whatever you typed]!" appears below. Erase your input — it disappears.
The latency from keypress to screen update is one frame, typically 16ms. It feels instantaneous.
Validation
For a real form, you want validation. Where does that go? Inside the update method, alongside the binding:
ui.text_edit_singleline(&mut self.email);
if !self.email.contains('@') {
ui.colored_label(egui::Color32::RED, "Email looks invalid");
}
The check runs every frame. The error message appears or disappears as the user types. No reactive framework needed — the immediate-mode loop is the reactive system.
Common mistakes
Trying to bind to a non-String. text_edit_singleline requires &mut String. For numbers, use egui::DragValue (Episode 20) which converts the text to and from numeric types automatically.
Forgetting to make the field public. If the app struct lives in another module, the field must be pub (or use accessor methods). Default access in Rust is private to the module.
Calling text_edit_singleline without a mutable reference. The borrow checker will complain. The widget needs &mut.
Putting expensive validation inline. A regex check that runs every frame on a long string can hurt frame rate. Validate on Enter (lost_focus + Key::Enter) or cache the result.
What's next
Next episode: checkboxes and radio buttons. A theme preferences app with three checkboxes (dark mode, sidebar, notifications) and three radio buttons (small/medium/large). Same pattern: pass a mutable reference, the widget mutates the state, the next frame reflects the choice.
Recap
ui.text_edit_singleline(&mut self.field) for a text box bound to a String. ui.text_edit_multiline for a textarea. ui.horizontal to put a label next to its input. if !self.name.is_empty() to show or hide widgets dynamically. Bound widgets are how egui handles all input — text, checkboxes, sliders, drag-values share the pattern.
Next episode: checkboxes and radios. See you in the next one.