Compare commits

...

10 commits

Author SHA1 Message Date
Joe Ardent
3b71b0d5c0 checkpoint 2024-12-27 11:01:03 -08:00
Joe Ardent
ec3f36779c minor cleanup 2022-10-29 15:53:49 -07:00
Joe Ardent
4957f3f30b tweaks 2022-10-23 23:21:49 -07:00
Joe Ardent
033db88b8e just tweak the constants of exponentiation 2022-10-23 22:14:47 -07:00
Joe Ardent
4383b703f1 better exponential accumulation 2022-10-23 19:44:33 -07:00
Joe Ardent
7a8097d035 make the background get redder as it progresses 2022-10-23 18:14:12 -07:00
Joe Ardent
7a8b950fbd get ready for publish 2022-10-23 12:28:41 -07:00
Joe Ardent
83eb23f1d3 tweaks 2022-10-22 22:14:35 -07:00
Joe Ardent
58319dffff use channels to send signals 2022-10-22 17:25:52 -07:00
Joe Ardent
4ccc2e4738 move some shit around 2022-10-22 13:52:13 -07:00
15 changed files with 1329 additions and 891 deletions

1415
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,19 @@
[package]
name = "katabastird"
version = "1.0.0"
version = "1.6.1"
edition = "2021"
rust-version = "1.61"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "A simple graphical countdown timer that is configured and launched from the commandline."
repository = "https://gitlab.com/nebkor/katabastird"
readme = "README.md"
keywords = ["unix", "commandline", "timer", "gui", "cli"]
license-file = "LICENSE.md"
[dependencies]
clap = { version = "4", features = ["derive", "env", "unicode", "suggestions", "usage"] }
eframe = { version = "0.19", features = ["wgpu"] }
egui = { version = "0.19", features = ["bytemuck"] }
egui_extras = "0.19"
naga = { version = "0.10", features = ["spv-out", "wgsl-out", "wgsl-in"] }
rodio = { version = "0.16" }
wgpu = { version = "0.14", features = ["naga", "spirv"] }
# naga = { version = "0.10", features = ["spv-out", "wgsl-out", "wgsl-in"] }
# wgpu = { version = "0.14", features = ["naga", "spirv"] }

View file

@ -6,15 +6,20 @@ Katabastird is a simple countdown timer that is configured and launched from the
Usage: katabastird [OPTIONS]
Options:
--hours <HOURS> Hours to count down [default: 0]
-m, --minutes <MINUTES> Minutes to count down [default: 0]
-s, --seconds <SECONDS> Seconds to count down [default: 0]
-h, --hours <HOURS> Hours to count down
-m, --minutes <MINUTES> Minutes to count down
-s, --seconds <SECONDS> Seconds to count down
-a, --alarm <ALARM> Audio file to play at the end of the countdown
-A
-r, --running Begin countdown immediately
-u, --count-up Count up from zero, actually
-p, --predator Use the Predator font
-h, --help Print help information
-H, --help Print this help
-V, --version Print version information
```
![a Predator's view of the timer while it's paused](./predator_timer_small.png)
![a Predator's view of the timer while it's paused](./predator_timer_small.png "a Predator's view of rthe timer while it's paused")
"[Katabasis](https://en.wikipedia.org/wiki/Katabasis)" is the descent into the Underworld.
All content is licensed under the Chaos License; see [LICENSE.md](./LICENSE.md) for details.

1
VERSION Normal file
View file

@ -0,0 +1 @@
1.61

32
VERSIONING.md Normal file
View file

@ -0,0 +1,32 @@
# Golden Versioning
```katabastird``` is versioned under a scheme I call "goldver", as an homage to the
vastly inferior [semver](https://semver.org).
## What does "goldver" mean?
When projects are versioned with goldver, the first version is "1". Note that it
is not "1.0", or, "1.0-prealpha-release-preview", or anything nonsensical like
that. As new versions are released, decimals from *phi*, the [Golden
Ratio](https://en.wikipedia.org/wiki/Golden_ratio), are appended after an
initial decimal point. So the second released version will be "1.6", the third
would be "1.61", etc., and on until perfection is asymptotically approached as
the number of released versions goes to infinity.
## Wait, didn't Donald Knuth do this?
No! He uses [pi for TeX and e for MetaFont](https://texfaq.org/FAQ-TeXfuture),
obviously COMPLETELY different.
## Ok.
Cool.
## What version is katabastird now?
Canonically, see the ```VERSION``` file. Heretically, once there have been
at least three releases, the version string in the ```Cargo.toml``` file will
always be of the form "1.6.x", where *x* is at least one digit long, starting
with "1". Each subsequent release will append the next digit of *phi* to
*x*. The number of releases can be calculated by counting the number of digits
in *x* and adding 2 to that.

View file

@ -1,5 +1,13 @@
pub const AIRHORN: &[u8] = include_bytes!("../airhorn_alarm.mp3");
pub const PREDATOR_FONT: &[u8] = include_bytes!("../Predator.ttf");
use std::time::Duration;
pub const AIRHORN: &[u8] = include_bytes!("../resources/airhorn_alarm.mp3");
pub const PREDATOR_FONT: &[u8] = include_bytes!("../resources/Predator.ttf");
pub(crate) const MIN_REPAINT: Duration = Duration::from_millis(100);
pub(crate) const MAX_REPAINT: Duration = Duration::from_millis(250);
pub(crate) const DIGIT_FACTOR: f32 = 0.4;
pub(crate) const TEXT_FACTOR: f32 = 0.2;
pub mod cli;
pub mod timer;

View file

@ -1,4 +1,4 @@
use egui::Vec2;
use eframe::egui::Vec2;
use katabastird::timer::Timer;
fn main() {

View file

@ -1,316 +0,0 @@
use std::time::{Duration, Instant};
use clap::Parser;
use eframe::{App, CreationContext};
use egui::{Color32, Direction, FontId, Layout, RichText, Ui};
use egui_extras::{Size, StripBuilder};
use crate::{cli::Cli, util::*, AIRHORN, PREDATOR_FONT};
const MIN_REPAINT: Duration = Duration::from_millis(100);
const MAX_REPAINT: Duration = Duration::from_millis(250);
const DIGIT_FACTOR: f32 = 0.4;
const TEXT_FACTOR: f32 = 0.2;
#[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>>,
}
#[derive(Debug, Clone, Copy)]
enum TimerState {
Unstarted,
Paused(ChronoState),
Running(ChronoState),
Finished,
}
impl PartialEq for TimerState {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(Self::Paused(_), Self::Paused(_))
| (Self::Running(_), Self::Running(_))
| (Self::Unstarted, Self::Unstarted)
| (Self::Finished, Self::Finished)
)
}
}
impl Eq for TimerState {}
#[derive(Debug, Clone, Copy)]
struct ChronoState {
updated: Instant,
remaining: Duration,
}
impl App for Timer {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
ctx.request_repaint_after(MAX_REPAINT);
let height = ctx.input().screen_rect().height();
egui::CentralPanel::default().show(ctx, |ui| {
match self.state {
TimerState::Unstarted => self.unstarted(ui, height),
TimerState::Running(cs) => {
let dur = Instant::now() - cs.updated;
let dur = MIN_REPAINT.saturating_sub(dur);
ctx.request_repaint_after(dur);
self.running(ui, height, cs);
}
TimerState::Paused(cs) => self.paused(ui, height, cs),
TimerState::Finished => self.finished(ui, height),
}
// check for quit key
if ui.input().key_pressed(egui::Key::Q) || ui.input().key_pressed(egui::Key::Escape) {
frame.close();
}
});
}
}
impl Timer {
pub fn new(ctx: &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 mut is_paused = false;
let elapsed = Instant::now() - cs.updated;
let remaining = cs.remaining.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| {
// first the pause
strip.cell(|ui| {
if ui.button(text).clicked() {
is_paused = true;
}
});
// 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;
display_digits(&mut strip, remaining, color, tsize);
});
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 cs = ChronoState {
remaining,
updated: Instant::now(),
};
if is_paused {
// did we click the reset button?
if self.state == TimerState::Unstarted {
return;
}
self.state = TimerState::Paused(cs);
} else {
self.state = TimerState::Running(cs);
}
}
fn paused(&mut self, ui: &mut Ui, vsize: f32, cs: ChronoState) {
let mut is_running = false;
let tsize = vsize * TEXT_FACTOR;
let remaining = cs.remaining;
StripBuilder::new(ui)
.size(Size::relative(0.33333))
.size(Size::remainder())
.cell_layout(Layout::centered_and_justified(Direction::LeftToRight))
.vertical(|mut strip| {
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_running = true;
}
});
pstrip.cell(|ui| {
let reset = RichText::new("RESET")
.color(Color32::RED)
.font(FontId::monospace(tsize));
if ui.button(reset).clicked() {
self.state = TimerState::Unstarted;
}
});
});
});
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,
};
display_digits(&mut strip, remaining, color, vsize * DIGIT_FACTOR);
});
if is_running {
let cs = ChronoState {
remaining,
updated: Instant::now(),
};
self.state = TimerState::Running(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),
};
StripBuilder::new(ui)
.size(Size::relative(0.33333))
.size(Size::remainder())
.cell_layout(Layout::centered_and_justified(Direction::LeftToRight))
.vertical(|mut strip| {
strip.strip(|pstrip| {
pstrip
.size(Size::remainder())
.cell_layout(Layout::centered_and_justified(Direction::TopDown))
.horizontal(|mut pstrip| {
pstrip.cell(|ui| {
let tsize = vsize * 0.3;
let reset = RichText::new("RESTART")
.color(Color32::DARK_GREEN)
.font(FontId::monospace(tsize));
if ui.button(reset).clicked() {
self.state = TimerState::Unstarted;
}
});
});
});
let color = {
let blink = (Instant::now() - self.tstart).as_secs() % 2 == 0;
if blink {
Color32::BLACK
} else {
Color32::DARK_GRAY
}
};
display_digits(&mut strip, remaining, color, vsize * DIGIT_FACTOR);
});
}
}

57
src/timer/eframe_app.rs Normal file
View file

@ -0,0 +1,57 @@
use std::time::Instant;
use eframe::{
egui::{self, Frame},
epaint::Color32,
Frame as EFrame,
};
use super::{state::*, Timer};
use crate::{MAX_REPAINT, MIN_REPAINT};
const STARTING_COLOR: &[f32; 3] = &[10.0, 4.0, 14.0];
const UNIT_COLOR: &[f32; 3] = &[0.986, 0.154, 0.055]; // [160, 125, 9].normalize()
impl eframe::App for Timer {
fn update(&mut self, ctx: &egui::Context, frame: &mut EFrame) {
ctx.request_repaint_after(MAX_REPAINT);
let height = ctx.input().screen_rect().height();
let t = self.done.powi(3);
let color = get_color(t);
egui::CentralPanel::default()
.frame(Frame::none().fill(color))
.show(ctx, |ui| {
match self.state {
TimerState::Unstarted => self.unstarted(ui, height),
TimerState::Running(cs) => {
let dur = Instant::now() - cs.updated;
let dur = MIN_REPAINT.saturating_sub(dur);
ctx.request_repaint_after(dur);
self.running(ui, height, cs);
}
TimerState::Paused(cs) => self.paused(ui, height, cs),
TimerState::Finished => self.finished(ui, height),
}
// check for quit key
if ui.input().key_pressed(egui::Key::Q) || ui.input().key_pressed(egui::Key::Escape)
{
frame.close();
}
});
}
}
fn get_color(t: f32) -> Color32 {
let [sr, sg, sb] = STARTING_COLOR;
let [ur, ug, ub] = UNIT_COLOR;
let mag = t * 162.0;
let (r, g, b) = (
(sr + (mag * ur).round()) as u8,
(sg + (mag * ug).round()) as u8,
(sb + (mag * ub).round()) as u8,
);
// when t is 1.0, then the final color is roughly 170, 29, 23.
Color32::from_rgb(r, g, b)
}

58
src/timer/gui.rs Normal file
View file

@ -0,0 +1,58 @@
use std::{sync::mpsc::Sender, time::Duration};
use eframe::egui::{Color32, Direction, Layout, RichText, Ui};
use egui_extras::{Size, StripBuilder};
use super::state::NextTimerState;
use crate::util::display_digits;
pub(crate) fn two_rows(
ui: &mut Ui,
buttons: &[(RichText, NextTimerState)],
sender: Sender<NextTimerState>,
remaining: Duration,
digit_color: Color32,
digit_size: f32,
) {
StripBuilder::new(ui)
.size(Size::relative(0.33333))
.size(Size::remainder())
.cell_layout(Layout::centered_and_justified(Direction::LeftToRight))
.vertical(|mut strip| {
strip.strip(|pstrip| {
pstrip
.sizes(Size::remainder(), buttons.len())
.cell_layout(Layout::centered_and_justified(Direction::TopDown))
.horizontal(|mut pstrip| {
for (button, signal) in buttons.iter() {
pstrip.cell(|ui| {
if ui.button(button.clone()).clicked() {
sender.send(*signal).unwrap();
}
});
}
});
});
display_digits(&mut strip, remaining, digit_color, digit_size);
});
}
pub(crate) fn one_row(
ui: &mut Ui,
buttons: &[(RichText, NextTimerState)],
sender: Sender<NextTimerState>,
) {
StripBuilder::new(ui)
.sizes(Size::remainder(), buttons.len())
.cell_layout(Layout::centered_and_justified(Direction::TopDown))
.horizontal(|mut strip| {
for (button, signal) in buttons.iter() {
strip.cell(|ui| {
if ui.button(button.clone()).clicked() {
sender.send(*signal).unwrap();
}
});
}
});
}

257
src/timer/mod.rs Normal file
View file

@ -0,0 +1,257 @@
use std::sync::mpsc::channel;
use std::time::{Duration, Instant};
use clap::Parser;
use eframe::egui::{self, Color32, FontId, RichText, Ui};
use crate::{cli::Cli, util::*, AIRHORN, DIGIT_FACTOR, MAX_REPAINT, PREDATOR_FONT, TEXT_FACTOR};
mod state;
use state::{ChronoState, NextTimerState, TimerState};
mod eframe_app;
mod gui;
#[derive(Debug, Clone, Copy)]
pub enum CountDirection {
Up,
Down,
}
#[derive(Clone)]
pub struct Timer {
direction: CountDirection,
duration: Duration,
state: TimerState,
tstart: Instant, // so we can blink
alarm: Option<Vec<u8>>,
done: f32,
}
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 alarm sound file {:?} 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(MAX_REPAINT);
let updated = Instant::now();
let mut timer = Timer {
duration,
direction,
state: TimerState::Unstarted,
tstart: updated,
alarm,
done: 0.0,
};
if cli.running {
let cs = ChronoState {
remaining: duration,
updated,
};
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);
self.done = 0.0;
let (sender, rx) = channel();
gui::one_row(ui, &[(start, NextTimerState::Running)], sender);
if rx.recv().is_ok() {
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_sound) = &self.alarm {
let alarm_sound = alarm_sound.to_owned();
std::thread::spawn(move || alarm(alarm_sound));
}
self.state = TimerState::Finished;
self.done = 1.0;
return;
}
self.done = 1.0 - (remaining.as_secs_f32() / self.duration.as_secs_f32());
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,
};
let color = Color32::DARK_GRAY;
let tsize = size * DIGIT_FACTOR;
gui::two_rows(
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,
};
gui::two_rows(
ui,
&[
(resume, NextTimerState::Running),
(reset, 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();
gui::two_rows(
ui,
&[(reset, NextTimerState::Running)], // technically we can send anything but let's try not to be misleading
sender,
remaining,
color,
vsize * DIGIT_FACTOR,
);
if rx.recv().is_ok() {
let cs = ChronoState {
remaining: self.duration,
updated: Instant::now(),
};
self.state = TimerState::Running(cs);
}
}
}

40
src/timer/state.rs Normal file
View file

@ -0,0 +1,40 @@
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy)]
pub(crate) enum TimerState {
Unstarted,
Paused(ChronoState),
Running(ChronoState),
Finished,
}
// this is a bit of a hack to deat with not being able to have const instances of the regular
// timerstate.
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum NextTimerState {
Unstarted,
Paused,
Running,
// no need for finished, there will never be a button to click on that says "finished"
}
impl PartialEq for TimerState {
fn eq(&self, other: &Self) -> bool {
matches!(
(self, other),
(Self::Paused(_), Self::Paused(_))
| (Self::Running(_), Self::Running(_))
| (Self::Unstarted, Self::Unstarted)
| (Self::Finished, Self::Finished)
)
}
}
impl Eq for TimerState {}
impl Eq for NextTimerState {}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ChronoState {
pub updated: Instant,
pub remaining: Duration,
}

View file

@ -1,6 +1,6 @@
use std::{io::Cursor, time::Duration};
use egui::{Color32, Direction, FontId, Layout, RichText};
use eframe::egui::{Color32, Direction, FontId, Layout, RichText};
use egui_extras::{Size, Strip};
use rodio::{source::Source, Decoder, OutputStream};