Back to Blog

Habit Tracker App in Rust egui — Grid, Checkboxes & CRUD | Learn egui Ep29

Celest KimCelest Kim

Video: Habit Tracker App in Rust egui — Grid, Checkboxes & CRUD | Learn egui Ep29 by Taught by Celeste AI - AI Coding Coach

Watch full page →

Habit Tracker App in Rust egui — Grid, Checkboxes & CRUD

This example demonstrates how to build a weekly habit tracker app using Rust and the egui GUI library. It features a striped grid layout with toggle buttons for each day, streak counting with color-coded highlights, completion statistics, and full CRUD functionality to add and delete habits.

Code

use egui::{Color32, RichText, ScrollArea, TextEdit, TopBottomPanel, Ui, Vec2, WidgetText};
use egui::widgets::Grid;

struct Habit {
  name: String,
  days: [bool; 7], // one bool per day of the week
}

impl Habit {
  fn new(name: &str) -> Self {
    Self {
      name: name.to_owned(),
      days: [false; 7],
    }
  }

  // Count the current streak of consecutive completed days from the end
  fn streak(&self) -> usize {
    self.days.iter().rev().take_while(|&&done| done).count()
  }

  // Count total completed days this week
  fn completed(&self) -> usize {
    self.days.iter().filter(|&&done| done).count()
  }
}

struct HabitApp {
  habits: Vec,
  new_habit_name: String,
  habit_to_remove: Option,
}

impl Default for HabitApp {
  fn default() -> Self {
    Self {
      habits: vec![
        Habit::new("Exercise"),
        Habit::new("Read"),
        Habit::new("Meditate"),
      ],
      new_habit_name: String::new(),
      habit_to_remove: None,
    }
  }
}

impl HabitApp {
  fn ui(&mut self, ui: &mut Ui) {
    // Top panel with completion stats
    TopBottomPanel::top("header").show(ui.ctx(), |ui| {
      let total_habits = self.habits.len().max(1) as f32;
      let total_completed: usize = self.habits.iter().map(|h| h.completed()).sum();
      let total_possible = self.habits.len() * 7;
      let percent = (total_completed as f32 / total_possible as f32) * 100.0;
      ui.label(RichText::new(format!("Completion: {:.1}%", percent)).strong());
    });

    // Scrollable area for habits grid
    ScrollArea::vertical().show(ui, |ui| {
      Grid::new("habit_grid").striped(true).show(ui, |ui| {
        ui.label(RichText::new("Habit").strong());
        for day in &["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] {
          ui.label(RichText::new(*day).strong());
        }
        ui.label(RichText::new("Streak").strong());
        ui.label(RichText::new("Delete").strong());
        ui.end_row();

        for (idx, habit) in self.habits.iter_mut().enumerate() {
          ui.label(&habit.name);

          for day_done in habit.days.iter_mut() {
            let (color, label) = if *day_done {
              (Color32::LIGHT_GREEN, "✔")
            } else {
              (Color32::LIGHT_GRAY, " ")
            };
            if ui.button(RichText::new(label).background_color(color)).clicked() {
              *day_done = !*day_done;
            }
          }

          // Color-coded streak display
          let streak = habit.streak();
          let streak_color = if streak >= 5 {
            Color32::GOLD
          } else if streak >= 3 {
            Color32::LIGHT_GREEN
          } else {
            Color32::LIGHT_GRAY
          };
          ui.label(RichText::new(streak.to_string()).color(streak_color));

          // Delete button with deferred removal
          if ui.button("🗑").clicked() {
            self.habit_to_remove = Some(idx);
          }

          ui.end_row();
        }
      });
    });

    // Bottom panel with add habit form
    TopBottomPanel::bottom("add_habit").show(ui.ctx(), |ui| {
      ui.horizontal(|ui| {
        let add_enabled = !self.new_habit_name.trim().is_empty();
        ui.add_enabled(add_enabled, TextEdit::singleline(&mut self.new_habit_name).desired_width(200.0));
        if ui.add_enabled(add_enabled, ui.button("Add Habit")).clicked() {
          self.habits.push(Habit::new(&self.new_habit_name));
          self.new_habit_name.clear();
        }
      });
    });

    // Remove habit if requested
    if let Some(idx) = self.habit_to_remove.take() {
      if idx < self.habits.len() {
        self.habits.remove(idx);
      }
    }
  }
}

Key Points

  • Use Grid::new().striped(true) to create a table with alternating row backgrounds for better readability.
  • Toggle buttons with Button::new().fill(Color32) provide intuitive day completion checkboxes with color feedback.
  • Calculate streaks by iterating days in reverse and counting consecutive completions with .iter().rev().take_while().
  • Use TopBottomPanel::top and TopBottomPanel::bottom for placing header stats and input forms in distinct UI regions.
  • Implement safe deletion with an optional deferred removal pattern to avoid mutability conflicts during UI rendering.