253 lines
7.3 KiB
Rust
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;
|
|
}
|
|
}
|
|
}
|