katabastird/src/timer/mod.rs
2022-10-22 17:25:52 -07:00

253 lines
7.3 KiB
Rust

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<Vec<u8>>,
}
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;
}
}
}