Back to Blog

egui Floating Windows & Dialogs | Rust GUI Ep 22

Sandy LaneSandy Lane

Video: egui Floating Windows & Dialogs | Rust GUI Ep 22 by Taught by Celeste AI - AI Coding Coach

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

egui::Window::new("Title").open(&mut bool).show(ctx, |ui| ...) — modal-feeling pop-ups in two lines.

Today: floating, draggable, optionally-closable windows. The egui equivalent of a dialog or pop-up. We use it for "New Post" forms, "About" info, "Confirm Delete" prompts, and post-editor windows that persist while you work.

The mechanic is one type — egui::Window — and a bool per window that controls visibility. egui handles dragging, resizing, focus, and the close button.

What we are building

A post-manager app. A list of posts in the central panel. Click any post to open it in a floating window where you can edit its body. A "New Post" button opens a form window. A small "x" next to each post opens a "Confirm Delete" centred dialog. An "About" button opens an info window.

Several windows can be open simultaneously, each with its own state.

egui::Window basics

egui::Window::new("Title")
  .open(&mut self.show_about)
  .resizable(false)
  .collapsible(false)
  .show(ctx, |ui| {
    ui.heading("Post Manager");
    ui.label("A simple post management app.");
  });

Builder pattern.

  • Window::new(title) — the window's title (also its identifier; titles must be unique per ctx).
  • .open(&mut bool) — wires the window's "x" close button to a bool. Click x → bool becomes false → next frame, window does not appear.
  • .resizable(bool) — let the user drag the corners to resize. Default true.
  • .collapsible(bool) — give the title bar a triangle that collapses the window. Default true.
  • .default_size([w, h]) — initial size. Otherwise content-sized.
  • .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) — pin the window to a screen position.
  • .show(ctx, |ui| { ... }) — the content closure.

Window is a top-level construct: pass ctx, not a parent ui. It draws above all other panels.

Per-post windows

for i in 0..self.posts.len() {
  let mut open = self.open_posts[i];
  egui::Window::new(&self.posts[i].title)
    .open(&mut open)
    .resizable(true)
    .default_size([300.0, 200.0])
    .show(ctx, |ui| {
      ui.text_edit_multiline(&mut self.posts[i].body);
    });
  self.open_posts[i] = open;
}

A vector of bools (self.open_posts) parallel to self.posts. Each true index gets a window. Click a post button — set the corresponding bool to true — that post's window appears next frame.

The let mut open = self.open_posts[i]; ...; self.open_posts[i] = open; dance is a Rust borrow-check workaround. We can't borrow self.posts[i].title and &mut self.open_posts[i] at the same time, so we pull the bool out into a local before the call.

Each window edits its post's body via text_edit_multiline, which binds to the post's body field directly. Edits are live; close the window and the changes persist.

Confirm-delete dialog

if let Some(idx) = self.confirm_delete {
  let mut open = true;
  egui::Window::new("Confirm Delete")
    .open(&mut open)
    .resizable(false)
    .collapsible(false)
    .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
    .show(ctx, |ui| {
      ui.label(format!("Delete '{}'?", self.posts[idx].title));
      ui.horizontal(|ui| {
        if ui.button("Delete").clicked() {
          self.posts.remove(idx);
          self.open_posts.remove(idx);
          self.confirm_delete = None;
        }
        if ui.button("Cancel").clicked() {
          self.confirm_delete = None;
        }
      });
    });
  if !open {
    self.confirm_delete = None;
  }
}

A few patterns to notice.

.anchor(Align2::CENTER_CENTER, [0.0, 0.0]) pins the dialog to the centre of the screen. Right for confirmation prompts where the user shouldn't have to find them.

.resizable(false) and .collapsible(false) keep the dialog tight — modal-feeling.

The bool open here is local, initialised to true, and we only check it after the closure ends. If the user clicks the x button, open flips to false, and we set self.confirm_delete = None to close the dialog state.

The Delete and Cancel buttons inside the dialog do their own confirm_delete = None, so the flow is: click x → close; click Cancel → close; click Delete → remove the post then close. All three end up in the same state.

True modals

egui's Window is not a true modal — other windows still receive clicks. For a real modal (the rest of the UI is unclickable until the user dismisses), wrap the dialog in egui::Modal (egui 0.30+) or manually intercept input.

For most uses — confirmation prompts, settings dialogs, about info — the non-modal Window is enough.

Multiple open windows

The post-manager creates one Window per post that has its open_posts flag set to true. Multiple posts open at once means multiple windows on screen. Each is independently draggable and resizable. The user can have a New Post window open while editing an existing post.

This is one of egui's strengths: window management costs almost nothing. There is no z-order code to maintain, no focus tracking. Window stacking and focus is egui's problem.

Windows vs side panels

SidePanel (Episode 9) is permanent — it always claims its part of the window. Window is transient — it appears, can be moved, and can be closed.

Use windows for:

  • Forms (new post, settings).
  • Confirmations (delete, save changes).
  • Auxiliary info (about, help).
  • Editors that the user opens occasionally (post body editor).

Use side panels for:

  • Always-visible navigation.
  • Tool palettes.
  • Persistent inspectors.

Running it

cargo run. The window opens with two starter posts and a "New Post" / "About" toolbar. Click a post — its body opens in a floating window. Click "New Post" — a form opens. Type a title and body, click Publish — the post appears in the list.

Click the small "x" next to a post — a centred confirm-delete dialog appears. Click Delete — the post vanishes. Click Cancel — the dialog closes and the post stays.

Drag any window by its title bar. Resize it by dragging the corners. Click the title bar's collapse triangle — the body collapses to just the title bar.

Common mistakes

Two windows with the same title. egui tracks windows by title; identical titles fight for the same state. Make titles unique, or use .id_source(unique) to disambiguate.

Forgetting to thread the open flag back to state. If you use a local bool for .open(&mut open), you must read it back to update your model. Otherwise the close button has no effect.

Trying to nest a Window inside another panel's .show. Windows go directly under ctx, not inside another ui. Move the call up to the same level as the panels.

Stacking too many transient windows. A list of 50 posts each with its own window is a UI mess. Rein it in: only allow one editor open at a time, or use a different pattern.

What's next

Next episode: persistence with serde. Save the app's settings — every checkbox, slider, and text field — across restarts using eframe::set_value and the auto-save callback. Three lines of code add full persistence.

Recap

egui::Window::new(title).open(&mut bool).show(ctx, |ui| ...) for floating windows. Builder methods for resizable, collapsible, anchor, default size. Multiple windows simultaneously are cheap. Use windows for transient UI; side panels for permanent UI.

Next episode: persistence with serde. 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.