use egui::{Color32, Direction, FontId, Layout, RichText, Ui}; use egui_extras::{Size, StripBuilder}; use std::time::{Duration, Instant}; use eframe::App; #[derive(Debug, Clone, Copy)] pub enum CountDirection { Up, Down, } #[derive(Debug, Clone, Copy)] pub struct Timer { direction: CountDirection, duration: Duration, state: State, } #[derive(Debug, Clone, Copy)] enum State { Unstarted, Paused(ChronoState), // time remaining Running(ChronoState), // time remaining } impl PartialEq for State { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Paused(_), Self::Paused(_)) => true, (Self::Running(_), Self::Running(_)) => true, (Self::Unstarted, Self::Unstarted) => true, _ => false, } } } impl Eq for State {} #[derive(Debug, Clone, Copy)] struct ChronoState { started: Instant, remaining: Duration, } impl Default for Timer { fn default() -> Self { let dur = Duration::from_secs(30); Self { direction: CountDirection::Down, duration: dur, state: State::Unstarted, } } } impl App for Timer { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { ctx.request_repaint_after(Duration::from_secs(1)); egui::CentralPanel::default().show(ctx, |ui| { let size = ctx.used_size(); let vsize = if size[1].is_normal() { size[1].abs() } else { 600.0 }; match self.state { State::Unstarted => self.unstarted(ui, vsize), _ => self.running(ui, vsize), } }); } } impl Timer { pub fn new(duration: Duration, direction: CountDirection, paused: bool) -> Self { if paused { Timer { duration, direction, state: State::Unstarted, } } else { let cs = ChronoState { remaining: duration, started: Instant::now(), }; Timer { duration, direction, state: State::Running(cs), } } } fn unstarted(&mut self, ui: &mut Ui, size: f32) { let start = RichText::new("START") .font(FontId::monospace(size * 0.9)) .color(Color32::WHITE) .background_color(Color32::LIGHT_GREEN); StripBuilder::new(ui) .size(Size::remainder()) .cell_layout(Layout::centered_and_justified(egui::Direction::TopDown)) .horizontal(|mut strip| { strip.cell(|ui| { if ui.button(start).clicked() { let dur = self.duration; self.state = State::Running(ChronoState { started: Instant::now(), remaining: dur, }); } }); }); } fn running(&mut self, ui: &mut Ui, size: f32) { let tsize = size * 0.3; let text = RichText::new("PAUSE") .font(FontId::monospace(tsize)) .color(Color32::GOLD); let elapsed; let started; let mut is_paused; if let State::Running(rs) = self.state { elapsed = Instant::now() - rs.started; started = rs.started; is_paused = false; } else if let State::Paused(cs) = self.state { elapsed = cs.remaining; started = Instant::now() - (self.duration - elapsed); is_paused = true; } else { unreachable!() } let remaining = if is_paused { elapsed } else { self.duration.saturating_sub(elapsed) }; StripBuilder::new(ui) .size(Size::relative(0.3333)) .size(Size::remainder()) .cell_layout(Layout::centered_and_justified(Direction::LeftToRight)) .vertical(|mut strip| { if is_paused { strip.strip(|pstrip| { pstrip .sizes(Size::remainder(), 2) .cell_layout(Layout::centered_and_justified(Direction::TopDown)) .horizontal(|mut pstrip| { pstrip.cell(|ui| { let resume = RichText::new("RESUME") .color(Color32::GREEN) .font(FontId::monospace(tsize)); if ui.button(resume).clicked() { is_paused = false; } }); pstrip.cell(|ui| { let reset = RichText::new("RESET") .color(Color32::RED) .font(FontId::monospace(tsize)); if ui.button(reset).clicked() { self.state = State::Unstarted; is_paused = true; } }); }); }); } else { // first the pause strip.cell(|ui| { if ui.button(text).clicked() { is_paused = true; } }); } let remaining = match self.direction { CountDirection::Down => remaining, _ => self.duration - remaining, }; // now the numbers let hours = remaining.as_secs() / 3600; let minutes = (remaining.as_secs() / 60) % 60; let seconds = remaining.as_secs() % 60; let tsize = size * 0.7; let hours = RichText::new(format!("{:02}", hours)).font(FontId::monospace(tsize)); let minutes = RichText::new(format!("{:02}", minutes)).font(FontId::monospace(tsize)); let seconds = RichText::new(format!("{:02}", seconds)).font(FontId::monospace(tsize)); strip.strip(|strip| { strip .sizes(Size::relative(0.33), 3) .cell_layout(Layout::centered_and_justified(Direction::TopDown)) .horizontal(|mut strip| { strip.cell(|ui| { ui.label(hours); }); strip.cell(|ui| { ui.label(minutes); }); strip.cell(|ui| { ui.label(seconds); }); }); }); }); let cs = ChronoState { remaining, started }; if is_paused { if self.state == State::Unstarted { return; } self.state = State::Paused(cs); } else { self.state = State::Running(cs); } } }