Back to Blog

Rust Module System in egui — Separate UI from Logic | GUI Tutorial

Celest KimCelest Kim

Video: Rust Module System in egui — Separate UI from Logic | GUI Tutorial by Taught by Celeste AI - AI Coding Coach

Watch full page →

Rust Module System in egui — Separate UI from Logic

Organizing a Rust egui application by separating data models from UI logic improves code clarity and reusability. This example demonstrates how to structure a contact book app using file-based modules, pub visibility, and re-exports across src/models/ and src/ui/ directories.

Code

// src/main.rs
mod app;
mod models;
mod ui;

fn main() {
  let app = app::MyApp::default();
  eframe::run_native("Contact Book", Default::default(), Box::new(|_cc| Box::new(app)));
}

// src/models/contact.rs
pub struct Contact {
  pub name: String,
  pub email: String,
}

// src/models/mod.rs
pub use self::contact::Contact;
mod contact;

// src/ui/contact_list.rs
use eframe::egui;
use crate::models::Contact;

pub fn show_contact_list(ui: &mut egui::Ui, contacts: &[Contact], selected: &mut Option) {
  egui::SidePanel::left("contact_list").show(ui.ctx(), |ui| {
    ui.heading("Contacts");
    for (i, contact) in contacts.iter().enumerate() {
      if ui.selectable_label(selected == &Some(i), &contact.name).clicked() {
        *selected = Some(i);
      }
    }
  });
}

// src/ui/contact_form.rs
use eframe::egui;

pub fn show_contact_form(ui: &mut egui::Ui, name: &mut String, email: &mut String, on_add: impl Fn()) {
  egui::CentralPanel::default().show(ui.ctx(), |ui| {
    ui.heading("Add Contact");
    egui::Grid::new("form_grid").show(ui, |ui| {
      ui.label("Name:");
      ui.text_edit_singleline(name);
      ui.end_row();
      ui.label("Email:");
      ui.text_edit_singleline(email);
      ui.end_row();
    });
    if ui.button("Add").clicked() {
      on_add();
    }
  });
}

// src/ui/mod.rs
pub use self::{contact_list::show_contact_list, contact_form::show_contact_form};
mod contact_list;
mod contact_form;

// src/models/mod.rs (already shown above)
// src/app.rs
use eframe::egui;
use crate::models::Contact;
use crate::ui::{show_contact_list, show_contact_form};

pub struct MyApp {
  contacts: Vec,
  selected: Option,
  new_name: String,
  new_email: String,
}

impl Default for MyApp {
  fn default() -> Self {
    Self {
      contacts: Vec::new(),
      selected: None,
      new_name: String::new(),
      new_email: String::new(),
    }
  }
}

impl eframe::App for MyApp {
  fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
    egui::SidePanel::left("contacts_panel").show(ctx, |ui| {
      show_contact_list(ui, &self.contacts, &mut self.selected);
    });

    egui::CentralPanel::default().show(ctx, |ui| {
      if let Some(idx) = self.selected {
        let contact = &self.contacts[idx];
        ui.label(format!("Name: {}", contact.name));
        ui.label(format!("Email: {}", contact.email));
      } else {
        ui.label("No contact selected");
      }
      show_contact_form(ui, &mut self.new_name, &mut self.new_email, || {
        if !self.new_name.is_empty() && !self.new_email.is_empty() {
          self.contacts.push(Contact {
            name: self.new_name.clone(),
            email: self.new_email.clone(),
          });
          self.new_name.clear();
          self.new_email.clear();
        }
      });
    });
  }
}

Key Points

  • Use mod declarations in main.rs to include app, models, and ui modules for clear project structure.
  • Leverage pub and pub use in mod.rs files to re-export structs and functions for easy cross-module access.
  • Separate data models (Contact struct) from UI components (contact list and form) to keep logic organized.
  • Use crate:: paths to import modules across directories, enabling modular and maintainable code.
  • Build reusable UI functions like show_contact_list and show_contact_form to compose the egui interface cleanly.