Back to Blog

Build a Playlist Manager with egui — Vec State & CRUD | Rust GUI Ep 27

Celest KimCelest Kim

Video: Build a Playlist Manager with egui — Vec State & CRUD | Rust GUI Ep 27 by Taught by Celeste AI - AI Coding Coach

Watch full page →

Build a Playlist Manager with egui — Vec State & CRUD

Learn how to create a simple playlist manager GUI in Rust using the egui framework. This example demonstrates managing a dynamic list of songs with add, remove, and favorite features, leveraging a Vec for state management and common egui widgets like TextEdit, Buttons, Checkboxes, and ScrollArea for a smooth user experience.

Code

use eframe::{egui, epi};

#[derive(Default)]
struct Song {
  title: String,
  artist: String,
  favorite: bool,
}

struct MyApp {
  songs: Vec<Song>,
  new_title: String,
  new_artist: String,
  remove_index: Option<usize>,
}

impl Default for MyApp {
  fn default() -> Self {
    Self {
      songs: Vec::new(),
      new_title: String::new(),
      new_artist: String::new(),
      remove_index: None,
    }
  }
}

impl epi::App for MyApp {
  fn name(&self) -> &str {
    "Playlist Manager"
  }

  fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
    egui::TopBottomPanel::bottom("add_song_panel").show(ctx, |ui| {
      ui.horizontal(|ui| {
        // Fixed width inputs for title and artist
        ui.add(egui::TextEdit::singleline(&mut self.new_title).desired_width(150.0));
        ui.add(egui::TextEdit::singleline(&mut self.new_artist).desired_width(150.0));

        // Add button enabled only if both fields are filled
        let add_enabled = !self.new_title.trim().is_empty() && !self.new_artist.trim().is_empty();
        if ui.add_enabled(add_enabled, egui::Button::new("Add")).clicked() {
          self.songs.push(Song {
            title: self.new_title.trim().to_owned(),
            artist: self.new_artist.trim().to_owned(),
            favorite: false,
          });
          self.new_title.clear();
          self.new_artist.clear();
        }
      });
    });

    egui::CentralPanel::default().show(ctx, |ui| {
      ui.heading("Playlist");
      // Scrollable list of songs
      egui::ScrollArea::vertical().show(ui, |ui| {
        for (i, song) in self.songs.iter_mut().enumerate() {
          ui.horizontal(|ui| {
            // Favorite checkbox
            ui.checkbox(&mut song.favorite, "");

            // Song info with bold title
            ui.label(egui::RichText::new(&song.title).strong());
            ui.label(format!("by {}", &song.artist));

            // Right-aligned remove button
            ui.with_layout(egui::Layout::right_to_left(), |ui| {
              if ui.button("Remove").clicked() {
                self.remove_index = Some(i);
              }
            });
          });
          ui.separator();
        }
      });
    });

    // Deferred removal to avoid borrow conflicts
    if let Some(index) = self.remove_index {
      if index < self.songs.len() {
        self.songs.remove(index);
      }
      self.remove_index = None;
    }
  }
}

fn main() {
  let app = MyApp::default();
  let native_options = eframe::NativeOptions::default();
  eframe::run_native(Box::new(app), native_options);
}

Key Points

  • Use a Vec<Song> to store and manage the playlist state dynamically.
  • TextEdit::singleline with desired_width creates fixed-width input fields for consistent layout.
  • add_enabled() disables the Add button until both title and artist fields are non-empty.
  • ScrollArea::vertical() enables scrolling through a potentially long list of songs.
  • Deferred removal with an Option<usize> avoids mutable borrow conflicts during iteration.