use std::sync::mpsc::channel; use std::time::{Duration, Instant}; use clap::Parser; use egui::{Color32, FontId, Layout, RichText, Ui}; use egui_extras::{Size, StripBuilder}; use crate::{cli::Cli, util::*, AIRHORN, DIGIT_FACTOR, PREDATOR_FONT, TEXT_FACTOR}; mod state; use state::{ChronoState, NextTimerState, TimerState}; mod eframe_app; mod gui; use gui::*; #[derive(Debug, Clone, Copy)] pub enum CountDirection { Up, Down, } #[derive(Debug, Clone)] pub struct Timer { direction: CountDirection, duration: Duration, state: TimerState, tstart: Instant, // so we can blink alarm: Option>, } impl Timer { pub fn new(ctx: &eframe::CreationContext) -> Self { let cli = Cli::parse(); let predator = cli.predator; let seconds = cli.hours.unwrap_or(0) * 3600 + cli.minutes.unwrap_or(0) * 60 + cli.seconds.unwrap_or(0); let duration = Duration::from_secs(seconds); let direction = if cli.count_up { CountDirection::Up } else { CountDirection::Down }; let alarm = if let Some(path) = cli.alarm { let buffer = std::fs::read(&path) .unwrap_or_else(|_| panic!("Could not open {:?} for reading.", path)); Some(buffer) } else if cli.airhorn { Some(AIRHORN.to_owned()) } else { None }; if predator { let mut fonts = egui::FontDefinitions::default(); fonts.font_data.insert( "predator".to_owned(), egui::FontData::from_static(PREDATOR_FONT), ); fonts .families .entry(egui::FontFamily::Monospace) .or_default() .insert(0, "predator".to_owned()); ctx.egui_ctx.set_fonts(fonts); } ctx.egui_ctx.request_repaint_after(Duration::from_secs(1)); let mut timer = Timer { duration, direction, state: TimerState::Unstarted, tstart: Instant::now(), alarm, }; if cli.running { let cs = ChronoState { remaining: duration, updated: Instant::now(), }; timer.state = TimerState::Running(cs); } timer } fn unstarted(&mut self, ui: &mut Ui, size: f32) { let tsize = size * 0.5; let start = RichText::new("START") .font(FontId::monospace(tsize)) .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 = TimerState::Running(ChronoState { updated: Instant::now(), remaining: dur, }); } }); }); } fn running(&mut self, ui: &mut Ui, size: f32, cs: ChronoState) { let tsize = size * TEXT_FACTOR; let text = RichText::new("PAUSE") .font(FontId::monospace(tsize)) .color(Color32::GOLD); let elapsed = Instant::now() - cs.updated; let remaining = cs.remaining.saturating_sub(elapsed); if remaining.is_zero() { if let Some(alarm_file) = &self.alarm { let alarm_file = alarm_file.to_owned(); std::thread::spawn(move || alarm(alarm_file)); } self.state = TimerState::Finished; return; } let (sender, rx) = channel(); { // if we're counting up, do the right thing let remaining = match self.direction { CountDirection::Down => remaining, CountDirection::Up => self.duration - remaining, }; // now the numbers let color = Color32::DARK_GRAY; let tsize = size * DIGIT_FACTOR; one_button( ui, text, state::NextTimerState::Paused, sender, remaining, color, tsize, ) } let cs = ChronoState { remaining, updated: Instant::now(), }; if rx.recv().is_ok() { self.state = TimerState::Paused(cs); } else { self.state = TimerState::Running(cs); } } fn paused(&mut self, ui: &mut Ui, vsize: f32, cs: ChronoState) { let tsize = vsize * TEXT_FACTOR; let remaining = cs.remaining; let resume = RichText::new("RESUME") .color(Color32::GREEN) .font(FontId::monospace(tsize)); let reset = RichText::new("RESET") .color(Color32::RED) .font(FontId::monospace(tsize)); let (sender, rx) = channel(); { let color = { let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0; if blink { Color32::BLACK } else { Color32::DARK_GRAY } }; // if we're counting up, do the right thing let remaining = match self.direction { CountDirection::Down => remaining, CountDirection::Up => self.duration - remaining, }; two_button( ui, resume, reset, NextTimerState::Running, NextTimerState::Unstarted, sender, remaining, color, vsize * DIGIT_FACTOR, ); } let cs = ChronoState { remaining, updated: Instant::now(), }; if let Ok(s) = rx.recv() { match s { NextTimerState::Running => { self.state = TimerState::Running(cs); } NextTimerState::Unstarted => { self.state = TimerState::Unstarted; } _ => unreachable!(), } } else { self.state = TimerState::Paused(cs); } } fn finished(&mut self, ui: &mut Ui, vsize: f32) { let remaining = match self.direction { CountDirection::Up => self.duration, CountDirection::Down => Duration::from_nanos(0), }; let tsize = vsize * 0.3; let reset = RichText::new("RESTART") .color(Color32::DARK_GREEN) .font(FontId::monospace(tsize)); let color = { let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0; if blink { Color32::BLACK } else { Color32::DARK_GRAY } }; let (sender, rx) = channel(); one_button( ui, reset, NextTimerState::Unstarted, sender, remaining, color, tsize, ); if rx.recv().is_ok() { self.state = TimerState::Unstarted; } } }