Back to Blog

egui Dropdown Menus in Rust — ComboBox + FontId + RichText | Ep 13

Sandy LaneSandy Lane

Video: egui Dropdown Menus in Rust — ComboBox + FontId + RichText | Ep 13 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

egui::ComboBox for picking from a list. RichText::new(...).font(...) for styling text. Together: a live font-preview UI.

Today combines three new pieces. ComboBox for dropdown menus when radio buttons would be too many. FontId for choosing a font family and size. RichText for applying styling to a single label. Put them together and you get a font preview app: pick a style, pick a size, see your preview text re-render in that style.

What we are building

A two-pane font previewer. The left panel has two dropdowns — font style (Proportional or Monospace) and font size (Small / Medium / Large / Extra Large) — plus an editable preview text area. The central panel renders the preview text in the chosen font.

The state

#[derive(PartialEq, Clone, Copy)]
pub enum FontStyle { Proportional, Monospace }

#[derive(PartialEq, Clone, Copy)]
pub enum FontSize { Small, Medium, Large, ExtraLarge }

pub struct MyApp {
  font_style: FontStyle,
  font_size: FontSize,
  preview_text: String,
}

Two enums for the constrained choices, plus a String for the preview text. The enums need PartialEq (for ComboBox comparison) and Clone, Copy (so the small enum values can be passed around without explicit cloning).

Each enum has helper methods:

impl FontStyle {
  fn label(&self) -> &str { /* "Proportional" or "Monospace" */ }
  fn family(&self) -> egui::FontFamily { /* the egui type */ }
}

impl FontSize {
  fn label(&self) -> &str { /* user-facing string */ }
  fn points(&self) -> f32 { /* 14.0 / 20.0 / 28.0 / 40.0 */ }
}

Mapping enum variants to egui types. This pattern — enum-with-display-and-egui-mapping methods — keeps the enum the source of truth and the conversions in one place.

ComboBox

egui::ComboBox::from_id_salt("font_style")
  .selected_text(self.font_style.label())
  .show_ui(ui, |ui| {
    ui.selectable_value(&mut self.font_style, FontStyle::Proportional, "Proportional");
    ui.selectable_value(&mut self.font_style, FontStyle::Monospace, "Monospace");
  });

Three pieces:

  • from_id_salt("...") — a unique ID for this combobox so egui can track its open/closed state. Required.
  • selected_text(...) — the text shown on the closed combobox button. Usually the label of the currently selected variant.
  • show_ui(ui, |ui| { ... }) — the contents of the dropdown popup. Inside, you typically use selectable_value.

ui.selectable_value(&mut field, candidate, label) — the same pattern as radio buttons. Three arguments: a mutable reference to the field, the value this entry represents, and the label. Clicking it sets the field to the candidate value.

When to choose ComboBox over radio buttons:

  • Radio buttons for 2–4 options that are always visible.
  • ComboBox for 5+ options or when you want a compact "selected option" display.

The semantics are identical (exclusive choice into one field); the visual is different.

RichText and FontId

let font = egui::FontId::new(
  self.font_size.points(),
  self.font_style.family(),
);

ui.label(
  egui::RichText::new(&self.preview_text).font(font)
);

FontId::new(size, family) builds a font identifier from a size in points and a FontFamily (Proportional, Monospace, or a named custom family).

RichText::new(text).font(font) builds a styled string. The RichText API has many builders for color, weight, size, italics, underline, background. We are only using .font() here.

ui.label(rich_text) accepts RichText directly, so styling is just an extra builder on the text before passing to label.

Useful RichText methods worth knowing:

  • .color(Color32::RED) — text color.
  • .background_color(Color32::YELLOW) — highlight.
  • .size(24.0) — font size in points.
  • .strong() / .italics() / .underline() / .strikethrough().
  • .code() / .heading() — apply the predefined code or heading style.

For most styling you can build the text once and pass it where you would have passed a &str.

ui.text_edit_multiline

ui.text_edit_multiline(&mut self.preview_text);

Like text_edit_singleline but with a textarea. Multiple lines, soft-wrap, scrollable if it gets long. Edit the preview text and the central panel re-renders immediately.

Use multiline whenever the user might want to type more than one line — descriptions, paragraphs, code, JSON.

Three controls, one preview

egui::SidePanel::left("controls").min_width(200.0).show(ctx, |ui| {
  // font style dropdown
  // font size dropdown
  // preview text multiline edit
});

egui::CentralPanel::default().show(ctx, |ui| {
  let font = egui::FontId::new(self.font_size.points(), self.font_style.family());
  ui.label(egui::RichText::new(&self.preview_text).font(font));
  // current settings summary at the bottom
});

Two panels coordinate through self. Update any control on the left, the central panel re-renders on the next frame.

Running it

cargo run. The left panel has two dropdowns (Proportional + Medium by default) and a multi-line preview text. The central panel renders the preview in that font.

Open the Font Style dropdown — the popup shows two options. Click "Monospace" — the preview re-renders in monospace. Open the Font Size dropdown, click "Extra Large" — the text grows. Edit the preview text — the central panel updates as you type.

Common mistakes

Forgetting from_id_salt (or using the same salt for two combos). Two combos with the same ID share open/closed state — opening one would open both. Use unique IDs.

Trying to put non-PartialEq types in selectable_value. The widget compares values; the type must implement PartialEq.

Building FontId outside update. FontId is cheap to construct; build it where you need it. Caching adds complexity for no gain.

Storing the font identifier instead of the source values. Save font_style: FontStyle and font_size: FontSize (the intent); compute the FontId on the fly. That way persistence (Episode 23) is clean.

What's next

Next episode: sliders. A sliders-driven image filter app. We will use egui::Slider for continuous numeric input — the right widget when range matters more than discrete options.

Recap

egui::ComboBox::from_id_salt(id).selected_text(...).show_ui(ui, |ui| { ... }) for dropdowns, with selectable_value inside. egui::FontId::new(size, family) for fonts. egui::RichText::new(text).font(font) for styled labels. The enum-with-helpers pattern keeps egui mapping in one place.

Next episode: sliders. See you in the next one.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.