Habit Tracker App in Rust egui — Grid, Checkboxes & CRUD | Learn egui Ep29
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::topandTopBottomPanel::bottomfor 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.