From 9bfe4e528b65ee43d84bbf83e765c1c6156d1a20 Mon Sep 17 00:00:00 2001 From: "Jens K." <47693+sectore@users.noreply.github.com> Date: Sat, 28 Dec 2024 12:32:27 +0100 Subject: [PATCH] extract clock elements (#28) Few tests. Extract `Style` to `common`. --- src/app.rs | 3 +- src/args.rs | 2 +- src/common.rs | 51 +++ src/main.rs | 1 + src/storage.rs | 4 +- src/widgets.rs | 3 + src/widgets/clock.rs | 666 +++++------------------------ src/widgets/clock_elements.rs | 247 +++++++++++ src/widgets/clock_elements_test.rs | 92 ++++ src/widgets/clock_test.rs | 1 + src/widgets/countdown.rs | 3 +- src/widgets/pomodoro.rs | 3 +- src/widgets/timer.rs | 3 +- 13 files changed, 516 insertions(+), 563 deletions(-) create mode 100644 src/common.rs create mode 100644 src/widgets/clock_elements.rs create mode 100644 src/widgets/clock_elements_test.rs diff --git a/src/app.rs b/src/app.rs index d56accd..eaeab9e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,12 @@ use crate::{ args::Args, + common::Style, constants::TICK_VALUE_MS, events::{Event, EventHandler, Events}, storage::AppStorage, terminal::Terminal, widgets::{ - clock::{self, Clock, ClockArgs, Style}, + clock::{self, Clock, ClockArgs}, countdown::{Countdown, CountdownWidget}, footer::Footer, header::Header, diff --git a/src/args.rs b/src/args.rs index aa3bc7e..d246bd9 100644 --- a/src/args.rs +++ b/src/args.rs @@ -5,7 +5,7 @@ use color_eyre::{ }; use std::time::Duration; -use crate::{app::Content, widgets::clock::Style}; +use crate::{app::Content, common::Style}; #[derive(Parser)] pub struct Args { diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..d8ac792 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,51 @@ +use clap::ValueEnum; +use ratatui::symbols::shade; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Copy, Clone, ValueEnum, Default, Serialize, Deserialize)] +pub enum Style { + #[default] + #[value(name = "full", alias = "f")] + Full, + #[value(name = "light", alias = "l")] + Light, + #[value(name = "medium", alias = "m")] + Medium, + #[value(name = "dark", alias = "d")] + Dark, + #[value(name = "thick", alias = "t")] + Thick, + #[value(name = "cross", alias = "c")] + Cross, + /// https://en.wikipedia.org/wiki/Braille_Patterns + /// Note: Might not be supported in all terminals + /// see https://docs.rs/ratatui/latest/src/ratatui/symbols.rs.html#150 + #[value(name = "braille", alias = "b")] + Braille, +} + +impl Style { + pub fn next(&self) -> Self { + match self { + Style::Full => Style::Dark, + Style::Dark => Style::Medium, + Style::Medium => Style::Light, + Style::Light => Style::Braille, + Style::Braille => Style::Thick, + Style::Thick => Style::Cross, + Style::Cross => Style::Full, + } + } + + pub fn get_digit_symbol(&self) -> &str { + match &self { + Style::Full => shade::FULL, + Style::Light => shade::LIGHT, + Style::Medium => shade::MEDIUM, + Style::Dark => shade::DARK, + Style::Cross => "╬", + Style::Thick => "┃", + Style::Braille => "⣿", + } + } +} diff --git a/src/main.rs b/src/main.rs index a0e2228..5939d38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod common; mod config; mod constants; mod events; diff --git a/src/storage.rs b/src/storage.rs index 3c26319..a7e197b 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -1,7 +1,5 @@ use crate::{ - app::Content, - constants::APP_NAME, - widgets::{clock::Style, pomodoro::Mode as PomodoroMode}, + app::Content, common::Style, constants::APP_NAME, widgets::pomodoro::Mode as PomodoroMode, }; use color_eyre::eyre::Result; use serde::{Deserialize, Serialize}; diff --git a/src/widgets.rs b/src/widgets.rs index b3d47a4..b92dfd5 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -1,4 +1,7 @@ pub mod clock; +pub mod clock_elements; +#[cfg(test)] +pub mod clock_elements_test; #[cfg(test)] pub mod clock_test; pub mod countdown; diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index 75d8008..fffb6f9 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -1,5 +1,3 @@ -use clap::ValueEnum; -use serde::{Deserialize, Serialize}; use std::fmt; use std::marker::PhantomData; use std::time::Duration; @@ -7,17 +5,20 @@ use strum::Display; use ratatui::{ buffer::Buffer, - layout::{Constraint, Layout, Position, Rect}, - symbols::shade, - widgets::StatefulWidget, + layout::{Constraint, Layout, Rect}, + widgets::{StatefulWidget, Widget}, }; use crate::{ + common::Style, duration::{ DurationEx, MINS_PER_HOUR, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND, SECS_PER_MINUTE, }, utils::center_horizontal, + widgets::clock_elements::{ + Colon, Digit, Dot, COLON_WIDTH, DIGIT_HEIGHT, DIGIT_WIDTH, DOT_WIDTH, + }, }; // max. 99:59:59 @@ -71,42 +72,6 @@ pub enum Format { HhMmSs, } -#[derive(Debug, Copy, Clone, ValueEnum, Default, Serialize, Deserialize)] -pub enum Style { - #[default] - #[value(name = "full", alias = "f")] - Full, - #[value(name = "light", alias = "l")] - Light, - #[value(name = "medium", alias = "m")] - Medium, - #[value(name = "dark", alias = "d")] - Dark, - #[value(name = "thick", alias = "t")] - Thick, - #[value(name = "cross", alias = "c")] - Cross, - /// https://en.wikipedia.org/wiki/Braille_Patterns - /// Note: Might not be supported in all terminals - /// see https://docs.rs/ratatui/latest/src/ratatui/symbols.rs.html#150 - #[value(name = "braille", alias = "b")] - Braille, -} - -impl Style { - pub fn next(&self) -> Self { - match self { - Style::Full => Style::Dark, - Style::Dark => Style::Medium, - Style::Medium => Style::Light, - Style::Light => Style::Braille, - Style::Braille => Style::Thick, - Style::Thick => Style::Cross, - Style::Cross => Style::Full, - } - } -} - #[derive(Debug, Clone)] pub struct Clock { initial_value: DurationEx, @@ -494,111 +459,8 @@ impl Clock { } } -const DIGIT_SIZE: usize = 5; -const DIGIT_WIDTH: u16 = DIGIT_SIZE as u16; -const DIGIT_HEIGHT: u16 = DIGIT_SIZE as u16 + 1 /* border height */; -const COLON_WIDTH: u16 = 4; // incl. padding left + padding right const SPACE_WIDTH: u16 = 1; -#[rustfmt::skip] -const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_1: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_2: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 1, - 1, 1, 1, 1, 1, - 1, 1, 0, 0, 0, - 1, 1, 1, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_3: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 1, - 1, 1, 1, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_4: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 0, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_5: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 1, 1, 0, 0, 0, - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 1, - 1, 1, 1, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_6: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 1, 1, 0, 0, 0, - 1, 1, 1, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_7: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, - 0, 0, 0, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_8: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_9: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 1, 1, 0, 1, 1, - 1, 1, 1, 1, 1, - 0, 0, 0, 1, 1, - 1, 1, 1, 1, 1, -]; - -#[rustfmt::skip] -const DIGIT_ERROR: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ - 1, 1, 1, 1, 1, - 1, 1, 0, 0, 0, - 1, 1, 1, 1, 0, - 1, 1, 0, 0, 0, - 1, 1, 1, 1, 1, -]; - pub struct ClockWidget where T: std::fmt::Debug, @@ -616,23 +478,11 @@ where } } - fn get_digit_symbol(&self, style: &Style) -> &str { - match &style { - Style::Full => shade::FULL, - Style::Light => shade::LIGHT, - Style::Medium => shade::MEDIUM, - Style::Dark => shade::DARK, - Style::Cross => "╬", - Style::Thick => "┃", - Style::Braille => "⣿", - } - } - fn get_horizontal_lengths(&self, format: &Format, with_decis: bool) -> Vec { let add_decis = |mut lengths: Vec, with_decis: bool| -> Vec { if with_decis { lengths.extend_from_slice(&[ - COLON_WIDTH, // . + DOT_WIDTH, // . DIGIT_WIDTH, // ds ]) } @@ -716,108 +566,6 @@ where pub fn get_height(&self) -> u16 { DIGIT_HEIGHT } - - fn render_digit( - &self, - number: u64, - symbol: &str, - with_border: bool, - area: Rect, - buf: &mut Buffer, - ) { - let left = area.left(); - let top = area.top(); - - let symbols = match number { - 0 => DIGIT_0, - 1 => DIGIT_1, - 2 => DIGIT_2, - 3 => DIGIT_3, - 4 => DIGIT_4, - 5 => DIGIT_5, - 6 => DIGIT_6, - 7 => DIGIT_7, - 8 => DIGIT_8, - 9 => DIGIT_9, - _ => DIGIT_ERROR, - }; - - symbols.iter().enumerate().for_each(|(i, item)| { - let x = i % DIGIT_SIZE; - let y = i / DIGIT_SIZE; - if *item == 1 { - let p = Position { - x: left + x as u16, - y: top + y as u16, - }; - if let Some(cell) = buf.cell_mut(p) { - cell.set_symbol(symbol); - } - } - }); - - // Add border at the bottom - if with_border { - for x in 0..area.width { - let p = Position { - x: left + x, - y: top + area.height - 1, - }; - if let Some(cell) = buf.cell_mut(p) { - cell.set_symbol("─"); - } - } - } - } - - fn render_colon(&self, symbol: &str, area: Rect, buf: &mut Buffer) { - let left = area.left(); - let top = area.top(); - - let positions = [ - Position { - x: left + 1, - y: top + 1, - }, - Position { - x: left + 2, - y: top + 1, - }, - Position { - x: left + 1, - y: top + 3, - }, - Position { - x: left + 2, - y: top + 3, - }, - ]; - - for pos in positions { - if let Some(cell) = buf.cell_mut(pos) { - cell.set_symbol(symbol); - } - } - } - - fn render_dot(&self, symbol: &str, area: Rect, buf: &mut Buffer) { - let positions = [ - Position { - x: area.left() + 1, - y: area.top() + area.height - 2, - }, - Position { - x: area.left() + 2, - y: area.top() + area.height - 2, - }, - ]; - - for pos in positions { - if let Some(cell) = buf.cell_mut(pos) { - cell.set_symbol(symbol); - } - } - } } impl StatefulWidget for ClockWidget @@ -829,7 +577,7 @@ where fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let with_decis = state.with_decis; let format = state.format; - let symbol = self.get_digit_symbol(&state.style); + let symbol = state.style.get_digit_symbol(); let widths = self.get_horizontal_lengths(&format, with_decis); let area = center_horizontal( area, @@ -838,346 +586,154 @@ where let edit_hours = matches!(state.mode, Mode::Editable(Time::Hours, _)); let edit_minutes = matches!(state.mode, Mode::Editable(Time::Minutes, _)); let edit_secs = matches!(state.mode, Mode::Editable(Time::Seconds, _)); - let edit_deci = matches!(state.mode, Mode::Editable(Time::Decis, _)); + let edit_decis = matches!(state.mode, Mode::Editable(Time::Decis, _)); match format { Format::HhMmSs if with_decis => { let [hh, _, h, c_hm, mm, _, m, c_ms, ss, _, s, d, ds] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.hours() / 10, - symbol, - edit_hours, - hh, - buf, - ); - self.render_digit(state.current_value.hours() % 10, symbol, edit_hours, h, buf); - self.render_colon(symbol, c_hm, buf); - self.render_digit( - state.current_value.minutes_mod() / 10, - symbol, - edit_minutes, - mm, - buf, - ); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); - self.render_dot(symbol, d, buf); - self.render_digit(state.current_value.decis(), symbol, edit_deci, ds, buf); + Digit::new(state.current_value.hours() / 10, edit_hours, symbol).render(hh, buf); + Digit::new(state.current_value.hours() % 10, edit_hours, symbol).render(h, buf); + Colon::new(symbol).render(c_hm, buf); + Digit::new(state.current_value.minutes_mod() / 10, edit_minutes, symbol) + .render(mm, buf); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); + Dot::new(symbol).render(d, buf); + Digit::new(state.current_value.decis(), edit_decis, symbol).render(ds, buf); } Format::HhMmSs => { let [hh, _, h, c_hm, mm, _, m, c_ms, ss, _, s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.hours() / 10, - symbol, - edit_hours, - hh, - buf, - ); - self.render_digit(state.current_value.hours() % 10, symbol, edit_hours, h, buf); - self.render_colon(symbol, c_hm, buf); - self.render_digit( - state.current_value.minutes_mod() / 10, - symbol, - edit_minutes, - mm, - buf, - ); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); + Digit::new(state.current_value.hours() / 10, edit_hours, symbol).render(hh, buf); + Digit::new(state.current_value.hours() % 10, edit_hours, symbol).render(h, buf); + Colon::new(symbol).render(c_hm, buf); + Digit::new(state.current_value.minutes_mod() / 10, edit_minutes, symbol) + .render(mm, buf); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); } Format::HMmSs if with_decis => { let [h, c_hm, mm, _, m, c_ms, ss, _, s, d, ds] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit(state.current_value.hours() % 10, symbol, edit_hours, h, buf); - self.render_colon(symbol, c_hm, buf); - self.render_digit( - state.current_value.minutes_mod() / 10, - symbol, - edit_minutes, - mm, - buf, - ); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); - self.render_dot(symbol, d, buf); - self.render_digit(state.current_value.decis(), symbol, edit_deci, ds, buf); + Digit::new(state.current_value.hours() % 10, edit_hours, symbol).render(h, buf); + Colon::new(symbol).render(c_hm, buf); + Digit::new(state.current_value.minutes_mod() / 10, edit_minutes, symbol) + .render(mm, buf); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); + Dot::new(symbol).render(d, buf); + Digit::new(state.current_value.decis(), edit_decis, symbol).render(ds, buf); } Format::HMmSs => { let [h, c_hm, mm, _, m, c_ms, ss, _, s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit(state.current_value.hours() % 10, symbol, edit_hours, h, buf); - self.render_colon(symbol, c_hm, buf); - self.render_digit( - state.current_value.minutes_mod() / 10, - symbol, - edit_minutes, - mm, - buf, - ); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); + Digit::new(state.current_value.hours() % 10, edit_hours, symbol).render(h, buf); + Colon::new(symbol).render(c_hm, buf); + Digit::new(state.current_value.minutes_mod() / 10, edit_minutes, symbol) + .render(mm, buf); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); } Format::MmSs if with_decis => { let [mm, _, m, c_ms, ss, _, s, d, ds] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.minutes_mod() / 10, - symbol, - edit_minutes, - mm, - buf, - ); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); - self.render_dot(symbol, d, buf); - self.render_digit(state.current_value.decis(), symbol, edit_deci, ds, buf); + Digit::new(state.current_value.minutes_mod() / 10, edit_minutes, symbol) + .render(mm, buf); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); + Dot::new(symbol).render(d, buf); + Digit::new(state.current_value.decis(), edit_decis, symbol).render(ds, buf); } Format::MmSs => { let [mm, _, m, c_ms, ss, _, s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.minutes_mod() / 10, - symbol, - edit_minutes, - mm, - buf, - ); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); + Digit::new(state.current_value.minutes_mod() / 10, edit_minutes, symbol) + .render(mm, buf); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); } Format::MSs if with_decis => { let [m, c_ms, ss, _, s, d, ds] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); - self.render_dot(symbol, d, buf); - self.render_digit(state.current_value.decis(), symbol, edit_deci, ds, buf); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); + Dot::new(symbol).render(d, buf); + Digit::new(state.current_value.decis(), edit_decis, symbol).render(ds, buf); } Format::MSs => { let [m, c_ms, ss, _, s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.minutes_mod() % 10, - symbol, - edit_minutes, - m, - buf, - ); - self.render_colon(symbol, c_ms, buf); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); + Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol) + .render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); } Format::Ss if state.with_decis => { let [ss, _, s, d, ds] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); - self.render_dot(symbol, d, buf); - self.render_digit(state.current_value.decis(), symbol, edit_deci, ds, buf); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); + Dot::new(symbol).render(d, buf); + Digit::new(state.current_value.decis(), edit_decis, symbol).render(ds, buf); } Format::Ss => { let [ss, _, s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.seconds_mod() / 10, - symbol, - edit_secs, - ss, - buf, - ); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); + Digit::new(state.current_value.seconds_mod() / 10, edit_secs, symbol) + .render(ss, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); } Format::S if with_decis => { let [s, d, ds] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); - self.render_dot(symbol, d, buf); - self.render_digit(state.current_value.decis(), symbol, edit_deci, ds, buf); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); + Dot::new(symbol).render(d, buf); + Digit::new(state.current_value.decis(), edit_decis, symbol).render(ds, buf); } Format::S => { let [s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area); - self.render_digit( - state.current_value.seconds_mod() % 10, - symbol, - edit_secs, - s, - buf, - ); + Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol) + .render(s, buf); } } } diff --git a/src/widgets/clock_elements.rs b/src/widgets/clock_elements.rs new file mode 100644 index 0000000..1f9ace9 --- /dev/null +++ b/src/widgets/clock_elements.rs @@ -0,0 +1,247 @@ +use ratatui::{ + buffer::Buffer, + layout::{Position, Rect}, + widgets::Widget, +}; + +pub const DIGIT_SIZE: usize = 5; +pub const DIGIT_WIDTH: u16 = DIGIT_SIZE as u16; +pub const DIGIT_HEIGHT: u16 = DIGIT_SIZE as u16 + 1 /* border height */; +pub const COLON_WIDTH: u16 = 4; // incl. padding left + padding right +pub const DOT_WIDTH: u16 = 4; // incl. padding left + padding right + +#[rustfmt::skip] +const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_1: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_2: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_3: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_4: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_5: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_6: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_7: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_8: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_9: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const CHAR_E: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 0, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, +]; + +pub struct Digit<'a> { + digit: u64, + with_border: bool, + symbol: &'a str, +} + +impl<'a> Digit<'a> { + pub fn new(digit: u64, with_border: bool, symbol: &'a str) -> Self { + Self { + digit, + with_border, + symbol, + } + } +} + +impl<'a> Widget for Digit<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let left = area.left(); + let top = area.top(); + + let patterns = match self.digit { + 0 => DIGIT_0, + 1 => DIGIT_1, + 2 => DIGIT_2, + 3 => DIGIT_3, + 4 => DIGIT_4, + 5 => DIGIT_5, + 6 => DIGIT_6, + 7 => DIGIT_7, + 8 => DIGIT_8, + 9 => DIGIT_9, + _ => CHAR_E, + }; + + patterns.iter().enumerate().for_each(|(i, item)| { + let x = i % DIGIT_SIZE; + let y = i / DIGIT_SIZE; + if *item == 1 { + let p = Position { + x: left + x as u16, + y: top + y as u16, + }; + if let Some(cell) = buf.cell_mut(p) { + cell.set_symbol(self.symbol); + } + } + }); + + // Add border at the bottom + if self.with_border { + for x in 0..area.width { + let p = Position { + x: left + x, + y: top + area.height - 1, + }; + if let Some(cell) = buf.cell_mut(p) { + cell.set_symbol("─"); + } + } + } + } +} + +pub struct Dot<'a> { + symbol: &'a str, +} + +impl<'a> Dot<'a> { + pub fn new(symbol: &'a str) -> Self { + Self { symbol } + } +} + +impl<'a> Widget for Dot<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let positions = [ + Position { + x: area.left() + 1, + y: area.top() + area.height - 2, + }, + Position { + x: area.left() + 2, + y: area.top() + area.height - 2, + }, + ]; + + for pos in positions { + if let Some(cell) = buf.cell_mut(pos) { + cell.set_symbol(self.symbol); + } + } + } +} + +pub struct Colon<'a> { + symbol: &'a str, +} + +impl<'a> Colon<'a> { + pub fn new(symbol: &'a str) -> Self { + Self { symbol } + } +} + +impl<'a> Widget for Colon<'a> { + fn render(self, area: Rect, buf: &mut Buffer) { + let left = area.left(); + let top = area.top(); + + let positions = [ + Position { + x: left + 1, + y: top + 1, + }, + Position { + x: left + 2, + y: top + 1, + }, + Position { + x: left + 1, + y: top + 3, + }, + Position { + x: left + 2, + y: top + 3, + }, + ]; + + for pos in positions { + if let Some(cell) = buf.cell_mut(pos) { + cell.set_symbol(self.symbol); + } + } + } +} diff --git a/src/widgets/clock_elements_test.rs b/src/widgets/clock_elements_test.rs new file mode 100644 index 0000000..8999680 --- /dev/null +++ b/src/widgets/clock_elements_test.rs @@ -0,0 +1,92 @@ +use crate::widgets::clock_elements::*; +use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; + +const D_RECT: Rect = Rect::new(0, 0, DIGIT_WIDTH, DIGIT_HEIGHT); + +#[test] +fn test_d1() { + let mut b = Buffer::empty(D_RECT); + Digit::new(1, false, "█").render(D_RECT, &mut b); + #[rustfmt::skip] + let expected = Buffer::with_lines([ + " ██", + " ██", + " ██", + " ██", + " ██", + " ", + ]); + assert_eq!(b, expected, "w/o border"); + + Digit::new(1, true, "█").render(D_RECT, &mut b); + #[rustfmt::skip] + let expected = Buffer::with_lines([ + " ██", + " ██", + " ██", + " ██", + " ██", + "─────", + ]); + assert_eq!(b, expected, "w/ border"); +} + +#[test] +fn test_d2() { + let mut b = Buffer::empty(D_RECT); + Digit::new(2, false, "█").render(D_RECT, &mut b); + #[rustfmt::skip] + let expected = Buffer::with_lines([ + "█████", + " ██", + "█████", + "██ ", + "█████", + " ", + ]); + assert_eq!(b, expected, "w/o border"); + + Digit::new(2, true, "█").render(D_RECT, &mut b); + #[rustfmt::skip] + let expected = Buffer::with_lines([ + "█████", + " ██", + "█████", + "██ ", + "█████", + "─────", + ]); + assert_eq!(b, expected, "w/ border"); +} + +#[test] +fn test_dot() { + let mut b = Buffer::empty(D_RECT); + Dot::new("█").render(D_RECT, &mut b); + #[rustfmt::skip] + let expected = Buffer::with_lines([ + " ", + " ", + " ", + " ", + " ██ ", + " ", + ]); + assert_eq!(b, expected); +} + +#[test] +fn test_colon() { + let mut b = Buffer::empty(D_RECT); + Colon::new("█").render(D_RECT, &mut b); + #[rustfmt::skip] + let expected = Buffer::with_lines([ + " ", + " ██ ", + " ", + " ██ ", + " ", + " ", + ]); + assert_eq!(b, expected); +} diff --git a/src/widgets/clock_test.rs b/src/widgets/clock_test.rs index 1afeba8..4b4bf07 100644 --- a/src/widgets/clock_test.rs +++ b/src/widgets/clock_test.rs @@ -1,4 +1,5 @@ use crate::{ + common::Style, duration::{ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND}, widgets::clock::*, }; diff --git a/src/widgets/countdown.rs b/src/widgets/countdown.rs index cd964a6..53fa715 100644 --- a/src/widgets/countdown.rs +++ b/src/widgets/countdown.rs @@ -8,9 +8,10 @@ use ratatui::{ use std::cmp::max; use crate::{ + common::Style, events::{Event, EventHandler}, utils::center, - widgets::clock::{self, Clock, ClockWidget, Style}, + widgets::clock::{self, Clock, ClockWidget}, }; #[derive(Debug, Clone)] diff --git a/src/widgets/pomodoro.rs b/src/widgets/pomodoro.rs index 6e0f4ff..4d0ba2a 100644 --- a/src/widgets/pomodoro.rs +++ b/src/widgets/pomodoro.rs @@ -1,8 +1,9 @@ use crate::{ + common::Style, constants::TICK_VALUE_MS, events::{Event, EventHandler}, utils::center, - widgets::clock::{Clock, ClockWidget, Countdown, Style}, + widgets::clock::{Clock, ClockWidget, Countdown}, }; use ratatui::{ buffer::Buffer, diff --git a/src/widgets/timer.rs b/src/widgets/timer.rs index 7a1a08f..cc1fc3a 100644 --- a/src/widgets/timer.rs +++ b/src/widgets/timer.rs @@ -1,7 +1,8 @@ use crate::{ + common::Style, events::{Event, EventHandler}, utils::center, - widgets::clock::{self, Clock, ClockWidget, Style}, + widgets::clock::{self, Clock, ClockWidget}, }; use ratatui::{ buffer::Buffer,