Rust egui Checkboxes & Radios — Build Theme Preferences (Ep 5)
Video: Rust egui Checkboxes & Radios — Build Theme Preferences (Ep 5) by Taught by Celeste AI - AI Coding Coach
Three checkboxes for booleans, three radio buttons for an enum. One pattern, two widget types.
Today we add the two simplest selection widgets in egui. Checkboxes for independent on/off choices. Radio buttons for picking exactly one option from a set. Both follow the bound widget pattern from Episode 4 — pass a mutable reference, the widget mutates the field, the next frame reflects the change.
What we are building
A theme preferences panel. Three checkboxes for independent settings (dark mode, show sidebar, notifications). A three-option radio group for font size (Small / Medium / Large). A live "Current settings" summary below.
The state
#[derive(PartialEq)]
enum FontSize {
Small,
Medium,
Large,
}
pub struct MyApp {
dark_mode: bool,
show_sidebar: bool,
notifications: bool,
font_size: FontSize,
}
Three bool fields for the checkboxes. One enum field for the radio group. The enum derives PartialEq — that is required because radio buttons compare candidate values to the current value.
The widgets
ui.checkbox(&mut self.dark_mode, "Dark mode");
ui.checkbox(&mut self.show_sidebar, "Show sidebar");
ui.checkbox(&mut self.notifications, "Enable notifications");
ui.radio_value(&mut self.font_size, FontSize::Small, "Small");
ui.radio_value(&mut self.font_size, FontSize::Medium, "Medium");
ui.radio_value(&mut self.font_size, FontSize::Large, "Large");
Two patterns. The structure is the same; the semantics differ.
ui.checkbox
ui.checkbox(&mut self.dark_mode, "Dark mode");
Two arguments: a mutable bool and a label. The widget renders a checkbox with the label to its right. Click toggles the bool. Each checkbox is independent — toggling dark_mode does not affect show_sidebar.
Use checkboxes for choices where multiple options can be active simultaneously: feature flags, filter selections, multi-select lists.
ui.radio_value
ui.radio_value(&mut self.font_size, FontSize::Small, "Small");
ui.radio_value(&mut self.font_size, FontSize::Medium, "Medium");
ui.radio_value(&mut self.font_size, FontSize::Large, "Large");
Three arguments: a mutable reference to the enum, the value this button represents, and a label. The widget renders a radio circle. It is filled if *self.font_size == FontSize::Small. Clicking it sets self.font_size = FontSize::Small.
The "exactly one selected" semantics emerge from the pattern: each radio button writes its own value to the same field. Whichever was clicked last wins. There is no group object, no IDs, no parent container — just three calls that share a binding.
This is why the enum needs PartialEq. egui calls *current == candidate to decide whether to render the radio as filled or empty.
Why an enum, not three bools?
You could model font size as is_small: bool, is_medium: bool, is_large: bool and use checkboxes. You should not. Three bools allow invalid states — all three true, none true. An enum makes invalid states unrepresentable: font_size: FontSize always holds exactly one of the three variants.
This is a general Rust principle and a general egui practice: model your domain so that bad states cannot exist. Then the UI is just a visualisation.
Reading state into a label
ui.label(format!(" Font size: {}", match self.font_size {
FontSize::Small => "Small",
FontSize::Medium => "Medium",
FontSize::Large => "Large",
}));
A match inside format!. This is how you display enum values — there is no Display derived by default. If you implement Display for FontSize, you could write format!("{}", self.font_size) directly.
Style tweaks via ctx.style_mut
ctx.style_mut(|style| {
style.text_styles.iter_mut().for_each(|(_, font_id)| {
font_id.size = 18.0;
});
});
This block, at the top of update, bumps every text style's font size to 18px. Great for demos and for screen recordings — the default 14px can be hard to read on a YouTube thumbnail.
ctx.style_mut is the entry point for theming. The Style struct exposes colors, fonts, spacing, animation timings, and dozens more knobs. We will explore theming properly in Episode 26.
Running it
cargo run. Three checkboxes appear at the top. Toggle them — the live summary at the bottom updates. Pick a font-size radio — only one is filled at a time, and the summary shows which.
Try clicking the same radio twice — it stays filled. Radios do not toggle off; they only switch among each other. To allow "no selection," wrap the field in Option<FontSize> and add a "None" radio.
When to use which
- Checkbox — independent boolean choices. A list of features to enable. Toggle states. Anything where each option is its own decision.
- Radio buttons — exclusive choice from a small set (2–5 options). Font size, theme, sort order.
- Combo box (Episode 13) — exclusive choice from a longer set (5+ options). When radios would take too much vertical space.
- Slider — continuous numeric value. Volume, brightness, opacity.
A common rule: if you would say "and" between options ("dark mode and notifications"), use checkboxes. If you would say "or" ("Small or Medium or Large"), use radios.
Common mistakes
Forgetting #[derive(PartialEq)] on the enum. radio_value will not compile.
Three radio buttons with three different fields. Each radio binds to its own field, and you end up able to "select" all three. Bind all radios in a group to the same field, with different value arguments.
Using radio_value without a discriminant value. The signature is (state, candidate, label) — three arguments, not two. Easy to forget the candidate when you are typing fast.
Modelling exclusive choice with three bools. Always use an enum. The compiler enforces the invariant.
What's next
Next episode: horizontal layouts. Buttons in a row instead of a column. The ui.horizontal block we used briefly in Episodes 3 and 4 gets a closer look — and we build a small action toolbar.
Recap
ui.checkbox(&mut bool, label) for independent on/off. ui.radio_value(&mut enum, value, label) for exclusive choice — three calls with the same field. Use enums for exclusive choices so invalid states cannot exist. ctx.style_mut for theming and font sizes.
Next episode: horizontal layouts. See you in the next one.