From e2cd5360792c1365f4e2218e983dab12d460e7c7 Mon Sep 17 00:00:00 2001 From: Jens Krause <47693+sectore@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:51:34 +0200 Subject: [PATCH] Introduce `CalendarDuration` (#120) * trait ClockDuration, CalendarDuration, tests * make clock rendering more generic * remove `should_blink` from `RenderClockState` * pass less down: `mode` -> `editable_time` * simplify `event` duration states * remove deprecated `DirectedDuration` * fix comments --- src/app.rs | 5 +- src/duration.rs | 530 ++++++++---- src/widgets/clock.rs | 1644 +++++++++++++++++++------------------ src/widgets/clock_test.rs | 113 ++- src/widgets/event.rs | 102 ++- src/widgets/local_time.rs | 2 +- 6 files changed, 1315 insertions(+), 1081 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0add061..fbbc012 100644 --- a/src/app.rs +++ b/src/app.rs @@ -415,7 +415,8 @@ impl App { Content::Countdown => self.countdown.is_running(), Content::Timer => self.timer.get_clock().is_running(), Content::Pomodoro => self.pomodoro.get_clock().is_running(), - Content::Event => self.event.get_clock().is_running(), + // Event clock runs forever + Content::Event => true, // `LocalTime` does not use a `Clock` Content::LocalTime => false, } @@ -426,7 +427,7 @@ impl App { Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()), Content::Timer => None, Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()), - Content::Event => Some(self.event.get_percentage_done()), + Content::Event => None, Content::LocalTime => None, } } diff --git a/src/duration.rs b/src/duration.rs index d16760c..a872c8c 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -7,8 +7,6 @@ use std::fmt; use std::time::Duration; use time::OffsetDateTime; -use crate::common::AppTime; - // unstable // https://doc.rust-lang.org/src/core/time.rs.html#32 pub const SECS_PER_MINUTE: u64 = 60; @@ -38,35 +36,194 @@ pub const MAX_DURATION: Duration = ONE_YEAR .saturating_mul(1000) .saturating_sub(ONE_DECI_SECOND); -/// `Duration` with direction in time (past or future) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DirectedDuration { - /// Time `until` a future moment (positive `Duration`) - Until(Duration), - /// Time `since` a past moment (negative duration, but still represented as positive `Duration`) - Since(Duration), +/// Trait for duration types that can be displayed in clock widgets. +/// +/// This trait abstracts over different duration calculation strategies: +/// - `DurationEx`: Uses fixed 365-day years (fast, simple) +/// - `CalendarDuration`: Uses actual calendar dates (accounts for leap years) +pub trait ClockDuration { + /// Total years + fn years(&self) -> u64; + + /// Total days + fn days(&self) -> u64; + + /// Days within the current year (0-364 or 0-365 for leap years) + fn days_mod(&self) -> u64; + + /// Total hours + fn hours(&self) -> u64; + + /// Hours within the current day (0-23) + fn hours_mod(&self) -> u64; + + /// Hours as 12-hour clock (1-12) + fn hours_mod_12(&self) -> u64; + + /// Total minutes + fn minutes(&self) -> u64; + + /// Minutes within the current hour (0-59) + fn minutes_mod(&self) -> u64; + + /// Total seconds + fn seconds(&self) -> u64; + + /// Seconds within the current minute (0-59) + fn seconds_mod(&self) -> u64; + + /// Deciseconds (tenths of a second, 0-9) + fn decis(&self) -> u64; + + /// Total milliseconds + fn millis(&self) -> u128; } -impl From for Duration { - fn from(directed: DirectedDuration) -> Self { - match directed { - DirectedDuration::Until(d) => d, - DirectedDuration::Since(d) => d, +/// Calendar-aware duration that accounts for leap years. +/// +/// Unlike `DurationEx` which uses fixed 365-day years, this calculates +/// years and days based on actual calendar dates, properly handling leap years. +/// +/// All calculations are performed on-demand from the stored dates. +#[derive(Debug, Clone)] +pub struct CalendarDuration { + earlier: OffsetDateTime, + later: OffsetDateTime, + direction: CalendarDurationDirection, +} + +#[derive(PartialEq, Debug, Clone)] +pub enum CalendarDurationDirection { + Since, + Until, +} + +impl CalendarDuration { + /// Create a new CalendarDuration by given two `OffsetDateTime`. + /// + /// The order of arguments matters: + /// First: `start_time` - `OffsetDateTime` to start from + /// Second: `end_time` - `OffsetDateTime` for expected end + pub fn from_start_end_times(start_time: OffsetDateTime, end_time: OffsetDateTime) -> Self { + // To avoid negative values by calculating differences of `start` and `end` times, + // we might switch those values internally by storing it as `earlier` and `later` values + // It simplifies all calculations in `ClockDuration` trait later. + // And `direction` will still help to still get original `start` and `end` times later. + if start_time <= end_time { + Self { + earlier: start_time, + later: end_time, + direction: CalendarDurationDirection::Since, + } + } else { + Self { + earlier: end_time, + later: start_time, + direction: CalendarDurationDirection::Until, + } + } + } + + pub fn direction(&self) -> &CalendarDurationDirection { + &self.direction + } + + pub fn start_time(&self) -> &OffsetDateTime { + match self.direction { + CalendarDurationDirection::Since => &self.earlier, + CalendarDurationDirection::Until => &self.later, + } + } + + pub fn end_time(&self) -> &OffsetDateTime { + match self.direction { + CalendarDurationDirection::Since => &self.later, + CalendarDurationDirection::Until => &self.earlier, } } } -impl DirectedDuration { - pub fn from_offset_date_times(value_a: OffsetDateTime, value_b: OffsetDateTime) -> Self { - let diff = value_a - value_b; +impl From for Duration { + fn from(cal_duration: CalendarDuration) -> Self { + let diff = cal_duration.later - cal_duration.earlier; + Duration::from_millis(diff.whole_milliseconds().max(0) as u64) + } +} - if diff.is_negative() { - Self::Since(Duration::from_millis( - diff.whole_milliseconds().unsigned_abs() as u64, - )) - } else { - Self::Until(Duration::from_millis(diff.whole_milliseconds() as u64)) +impl ClockDuration for CalendarDuration { + fn years(&self) -> u64 { + let mut years = (self.later.year() - self.earlier.year()) as i64; + + // Check if we've completed a full year by comparing month/day/time + let intermediate = self + .earlier + .replace_year(self.later.year()) + .unwrap_or(self.earlier); + + if intermediate > self.later { + years -= 1; } + + years.max(0) as u64 + } + + fn days_mod(&self) -> u64 { + let year_count = self.years(); + + // Calculate intermediate date after adding complete years + let target_year = self.earlier.year() + year_count as i32; + let intermediate = self + .earlier + .replace_year(target_year) + .unwrap_or(self.earlier); + + let remaining = self.later - intermediate; + remaining.whole_days().max(0) as u64 + } + + fn days(&self) -> u64 { + (self.later - self.earlier).whole_days().max(0) as u64 + } + + fn hours_mod(&self) -> u64 { + let total_hours = (self.later - self.earlier).whole_hours(); + (total_hours % 24).max(0) as u64 + } + + fn hours(&self) -> u64 { + (self.later - self.earlier).whole_hours().max(0) as u64 + } + + fn hours_mod_12(&self) -> u64 { + let hours = self.hours_mod(); + (hours + 11) % 12 + 1 + } + + fn minutes_mod(&self) -> u64 { + let total_minutes = (self.later - self.earlier).whole_minutes(); + (total_minutes % 60).max(0) as u64 + } + + fn minutes(&self) -> u64 { + (self.later - self.earlier).whole_minutes().max(0) as u64 + } + + fn seconds_mod(&self) -> u64 { + let total_seconds = (self.later - self.earlier).whole_seconds(); + (total_seconds % 60).max(0) as u64 + } + + fn seconds(&self) -> u64 { + (self.later - self.earlier).whole_seconds().max(0) as u64 + } + + fn decis(&self) -> u64 { + let total_millis = (self.later - self.earlier).whole_milliseconds(); + ((total_millis % 1000) / 100).max(0) as u64 + } + + fn millis(&self) -> u128 { + (self.later - self.earlier).whole_milliseconds().max(0) as u128 } } @@ -93,62 +250,60 @@ impl From for Duration { } } -impl DurationEx { - pub fn years(&self) -> u64 { +impl ClockDuration for DurationEx { + fn years(&self) -> u64 { self.days() / DAYS_PER_YEAR } - pub fn days(&self) -> u64 { + fn days(&self) -> u64 { self.hours() / HOURS_PER_DAY } - /// Days in a year - pub fn days_mod(&self) -> u64 { + fn days_mod(&self) -> u64 { self.days() % DAYS_PER_YEAR } - pub fn hours(&self) -> u64 { + fn hours(&self) -> u64 { self.seconds() / (SECS_PER_MINUTE * MINS_PER_HOUR) } - /// Hours as 24-hour clock - pub fn hours_mod(&self) -> u64 { + fn hours_mod(&self) -> u64 { self.hours() % HOURS_PER_DAY } - /// Hours as 12-hour clock - pub fn hours_mod_12(&self) -> u64 { + fn hours_mod_12(&self) -> u64 { // 0 => 12, // 1..=12 => hours, // 13..=23 => hours - 12, (self.hours_mod() + 11) % 12 + 1 } - pub fn minutes(&self) -> u64 { + fn minutes(&self) -> u64 { self.seconds() / MINS_PER_HOUR } - pub fn minutes_mod(&self) -> u64 { + fn minutes_mod(&self) -> u64 { self.minutes() % SECS_PER_MINUTE } - pub fn seconds(&self) -> u64 { + fn seconds(&self) -> u64 { self.inner.as_secs() } - pub fn seconds_mod(&self) -> u64 { + fn seconds_mod(&self) -> u64 { self.seconds() % SECS_PER_MINUTE } - // deciseconds - pub fn decis(&self) -> u64 { + fn decis(&self) -> u64 { (self.inner.subsec_millis() / 100) as u64 } - // milliseconds - pub fn millis(&self) -> u128 { + + fn millis(&self) -> u128 { self.inner.as_millis() } +} +impl DurationEx { pub fn saturating_add(&self, ex: DurationEx) -> Self { let inner = self.inner.saturating_add(ex.inner); Self { inner } @@ -166,6 +321,7 @@ impl DurationEx { impl fmt::Display for DurationEx { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use ClockDuration as _; // Import trait methods if self.years() >= 1 { write!( f, @@ -233,85 +389,6 @@ fn parse_hours(h: &str) -> Result { Ok(hours) } -/// Parses `DirectedDuration` from following formats: -/// - `yyyy-mm-dd hh:mm:ss` -/// - `yyyy-mm-dd hh:mm` -/// - `hh:mm:ss` -/// - `hh:mm` -/// - `mm` -/// -/// Returns `DirectedDuration::Until` for future times, `DirectedDuration::Since` for past times -#[allow(dead_code)] -pub fn parse_duration_by_time(arg: &str) -> Result { - use time::{OffsetDateTime, PrimitiveDateTime, macros::format_description}; - - let now: OffsetDateTime = AppTime::new().into(); - - let target_time = if arg.contains('-') { - // First: `YYYY-MM-DD HH:MM:SS` - // Then: `YYYY-MM-DD HH:MM` - let format_with_seconds = - format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"); - let format_without_seconds = format_description!("[year]-[month]-[day] [hour]:[minute]"); - - let pdt = PrimitiveDateTime::parse(arg, format_with_seconds) - .or_else(|_| PrimitiveDateTime::parse(arg, format_without_seconds)) - .map_err(|e| { - eyre!("Invalid datetime '{}'. Use format 'yyyy-mm-dd hh:mm:ss' or 'yyyy-mm-dd hh:mm'. Error: {}", arg, e) - })?; - pdt.assume_offset(now.offset()) - } else { - // Parse time parts: interpret as HH:MM:SS, HH:MM, or SS - let parts: Vec<&str> = arg.split(':').collect(); - - let (hour, minute, second) = match parts.as_slice() { - [mm] => { - // Single part: treat as minutes in current hour - let m = parse_minutes(mm)?; - (now.hour(), m, 0) - } - [hh, mm] => { - // Two parts: treat as HH:MM (time of day) - let h = parse_hours(hh)?; - let m = parse_minutes(mm)?; - (h, m, 0) - } - [hh, mm, ss] => { - // Three parts: HH:MM:SS - let h = parse_hours(hh)?; - let m = parse_minutes(mm)?; - let s = parse_seconds(ss)?; - (h, m, s) - } - _ => { - return Err(eyre!( - "Invalid time format. Use 'hh:mm:ss', 'hh:mm', or 'mm'" - )); - } - }; - - now.replace_time( - time::Time::from_hms(hour, minute, second).map_err(|_| eyre!("Invalid time"))?, - ) - }; - - let mut duration_secs = (target_time - now).whole_seconds(); - - // `Since` for past times - if duration_secs < 0 { - duration_secs *= -1; - Ok(DirectedDuration::Since(Duration::from_secs( - duration_secs as u64, - ))) - } else - // `Until` for future times, - { - Ok(DirectedDuration::Until(Duration::from_secs( - duration_secs as u64, - ))) - } -} - /// Parses `Duration` from `hh:mm:ss`, `mm:ss` or `ss` pub fn parse_duration(arg: &str) -> Result { let parts: Vec<&str> = arg.split(':').collect(); @@ -396,6 +473,7 @@ pub fn parse_long_duration(arg: &str) -> Result { #[cfg(test)] mod tests { + use super::ClockDuration; use super::*; use std::time::Duration; @@ -497,31 +575,6 @@ mod tests { assert_eq!(result, 1); } - #[test] - fn test_from_offset_date_times() { - use time::macros::datetime; - - // Future time (Until) - let now = datetime!(2024-01-01 12:00:00).assume_utc(); - let future = datetime!(2024-01-01 13:00:00).assume_utc(); - assert!(matches!( - DirectedDuration::from_offset_date_times(future, now), - DirectedDuration::Until(_) - )); - - // Past time (Since) - assert!(matches!( - DirectedDuration::from_offset_date_times(now, future), - DirectedDuration::Since(_) - )); - - // Same time (Until with 0 duration) - assert!(matches!( - DirectedDuration::from_offset_date_times(now, now), - DirectedDuration::Until(_) - )); - } - #[test] fn test_parse_duration() { // ss @@ -543,42 +596,6 @@ mod tests { assert!(parse_duration("01:02:03:04").is_err()); // too many parts } - #[test] - fn test_parse_duration_by_time() { - // YYYY-MM-DD HH:MM:SS - future - assert!(matches!( - parse_duration_by_time("2050-06-15 14:30:45"), - Ok(DirectedDuration::Until(_)) - )); - - // YYYY-MM-DD HH:MM - future - assert!(matches!( - parse_duration_by_time("2050-06-15 14:30"), - Ok(DirectedDuration::Until(_)) - )); - - // HH:MM:SS - past - assert!(matches!( - parse_duration_by_time("2000-01-01 23:59:59"), - Ok(DirectedDuration::Since(_)) - )); - - // HH:MM - Until or Since depending on current time - assert!(parse_duration_by_time("18:00").is_ok()); - - // MM - Until or Since depending on current time - assert!(parse_duration_by_time("45").is_ok()); - - // errors - assert!(parse_duration_by_time("60").is_err()); // invalid minutes - assert!(parse_duration_by_time("24:00").is_err()); // invalid hours - assert!(parse_duration_by_time("24:00:00").is_err()); // invalid hours - assert!(parse_duration_by_time("2030-13-01 12:00:00").is_err()); // invalid month - assert!(parse_duration_by_time("2030-06-32 12:00:00").is_err()); // invalid day - assert!(parse_duration_by_time("abc").is_err()); // invalid input - assert!(parse_duration_by_time("01:02:03:04").is_err()); // too many parts - } - #[test] fn test_parse_long_duration() { // `Yy` @@ -677,4 +694,155 @@ mod tests { assert!(parse_long_duration("1y 2d 3d 4:00").is_err()); // too many parts (4 parts) assert!(parse_long_duration("1y 2d 3h 4m 5s").is_err()); // too many parts (5 parts) } + + #[test] + fn test_calendar_duration_leap_year() { + use time::macros::datetime; + + // 2024 is a leap year (366 days) + let start = datetime!(2024-01-01 00:00:00 UTC); + let end = datetime!(2025-01-01 00:00:00 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!(cal_dur.years(), 1, "Should be exactly 1 year"); + assert_eq!(cal_dur.days_mod(), 0, "Should be 0 remaining days"); + assert_eq!(cal_dur.days(), 366, "2024 has 366 days (leap year)"); + } + + #[test] + fn test_calendar_duration_non_leap_year() { + use time::macros::datetime; + + // 2023 is not a leap year (365 days) + let start = datetime!(2023-01-01 00:00:00 UTC); + let end = datetime!(2024-01-01 00:00:00 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!(cal_dur.years(), 1, "Should be exactly 1 year"); + assert_eq!(cal_dur.days_mod(), 0, "Should be 0 remaining days"); + assert_eq!(cal_dur.days(), 365, "2023 has 365 days (non-leap year)"); + } + + #[test] + fn test_calendar_duration_partial_year_with_leap_day() { + use time::macros::datetime; + + // Span including Feb 29, 2024 + let start = datetime!(2024-02-01 00:00:00 UTC); + let end = datetime!(2024-03-15 00:00:00 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!(cal_dur.years(), 0, "Should be 0 years"); + // Feb 2024 has 29 days, so: 29 days (rest of Feb) + 15 days (March) = 44 days + assert_eq!( + cal_dur.days(), + 43, + "Should be 43 days (29 in Feb + 14 partial March)" + ); + } + + #[test] + fn test_calendar_duration_partial_year_without_leap_day() { + use time::macros::datetime; + + // Same dates but in 2023 (non-leap year) + let start = datetime!(2023-02-01 00:00:00 UTC); + let end = datetime!(2023-03-15 00:00:00 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!(cal_dur.years(), 0, "Should be 0 years"); + // Feb 2023 has 28 days, so: 28 days (rest of Feb) + 15 days (March) = 43 days + assert_eq!( + cal_dur.days(), + 42, + "Should be 42 days (28 in Feb + 14 partial March)" + ); + } + + #[test] + fn test_calendar_duration_multiple_years_spanning_leap_years() { + use time::macros::datetime; + + // From 2023 (non-leap) through 2024 (leap) to 2025 + let start = datetime!(2023-03-01 10:00:00 UTC); + let end = datetime!(2025-03-01 10:00:00 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!(cal_dur.years(), 2, "Should be exactly 2 years"); + assert_eq!(cal_dur.days_mod(), 0, "Should be 0 remaining days"); + // Total days: 365 (2023 partial + 2024 partial) + 366 (full 2024 year conceptually included) + // Actually: From 2023-03-01 to 2025-03-01 = 365 + 366 = 731 days + assert_eq!(cal_dur.days(), 731, "Should be 731 total days"); + } + + #[test] + fn test_calendar_duration_year_boundary() { + use time::macros::datetime; + + // Test incomplete year - just before year boundary + let start = datetime!(2024-01-01 00:00:00 UTC); + let end = datetime!(2024-12-31 23:59:59 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!(cal_dur.years(), 0, "Should be 0 years (not complete)"); + assert_eq!(cal_dur.days(), 365, "Should be 365 days"); + } + + #[test] + fn test_calendar_duration_hours_minutes_seconds() { + use time::macros::datetime; + + let start = datetime!(2024-01-01 10:30:45 UTC); + let end = datetime!(2024-01-02 14:25:50 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!(cal_dur.years(), 0); + assert_eq!(cal_dur.days(), 1); + assert_eq!(cal_dur.hours_mod(), 3, "Should be 3 hours past midnight"); + assert_eq!(cal_dur.minutes_mod(), 55, "Should be 55 minutes"); + assert_eq!(cal_dur.seconds_mod(), 5, "Should be 5 seconds"); + } + + #[test] + fn test_calendar_duration_reversed_dates() { + use time::macros::datetime; + + // CalendarDuration::between should handle reversed order + let later = datetime!(2025-01-01 00:00:00 UTC); + let earlier = datetime!(2024-01-01 00:00:00 UTC); + let cal_dur = CalendarDuration::from_start_end_times(later, earlier); + + assert_eq!(cal_dur.years(), 1, "Should still calculate 1 year"); + assert_eq!(cal_dur.days(), 366, "Should still be 366 days"); + } + + #[test] + fn test_calendar_duration_same_date() { + use time::macros::datetime; + + let date = datetime!(2024-06-15 12:00:00 UTC); + let cal_dur = CalendarDuration::from_start_end_times(date, date); + + assert_eq!(cal_dur.years(), 0); + assert_eq!(cal_dur.days(), 0); + assert_eq!(cal_dur.hours(), 0); + assert_eq!(cal_dur.minutes(), 0); + assert_eq!(cal_dur.seconds(), 0); + } + + #[test] + fn test_calendar_duration_deciseconds() { + use time::macros::datetime; + + let start = datetime!(2024-01-01 00:00:00.000 UTC); + let end = datetime!(2024-01-01 00:00:00.750 UTC); + let cal_dur = CalendarDuration::from_start_end_times(start, end); + + assert_eq!( + cal_dur.decis(), + 7, + "Should be 7 deciseconds (750ms = 7.5 decis, truncated to 7)" + ); + assert_eq!(cal_dur.millis(), 750, "Should be 750 milliseconds"); + } } diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index f6355e8..71ba3c5 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -14,8 +14,8 @@ use ratatui::{ use crate::{ common::{ClockTypeId, Style as DigitStyle}, duration::{ - DurationEx, MAX_DURATION, ONE_DAY, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND, - ONE_YEAR, + ClockDuration, DurationEx, MAX_DURATION, ONE_DAY, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, + ONE_SECOND, ONE_YEAR, }, events::{AppEvent, AppEventTx}, utils::center_horizontal, @@ -90,7 +90,7 @@ pub enum Format { YyyDddHhMmSs, } -pub fn format_by_duration(d: &DurationEx) -> Format { +pub fn format_by_duration(d: &D) -> Format { if d.years() >= 100 && d.days_mod() >= 100 { Format::YyyDddHhMmSs } else if d.years() >= 100 && d.days_mod() >= 10 { @@ -437,7 +437,7 @@ impl ClockState { } fn update_format(&mut self) { - let d = self.get_current_value(); + let d: &DurationEx = self.get_current_value(); self.format = format_by_duration(d); } @@ -642,236 +642,8 @@ where } } - 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(&[ - DOT_WIDTH, // . - DIGIT_WIDTH, // ds - ]) - } - lengths - }; - - const LABEL_WIDTH: u16 = DIGIT_LABEL_WIDTH + DIGIT_SPACE_WIDTH; - - match format { - Format::YyyDddHhMmSs => add_decis( - vec![ - THREE_DIGITS_WIDTH, // y_y_y - LABEL_WIDTH, // _l__ - THREE_DIGITS_WIDTH, // d_d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YyyDdHhMmSs => add_decis( - vec![ - THREE_DIGITS_WIDTH, // y_y_y - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YyyDHhMmSs => add_decis( - vec![ - THREE_DIGITS_WIDTH, // y_y_y - LABEL_WIDTH, // _l__ - DIGIT_WIDTH, // d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YyDddHhMmSs => add_decis( - vec![ - TWO_DIGITS_WIDTH, // y_y - LABEL_WIDTH, // _l__ - THREE_DIGITS_WIDTH, // d_d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YyDdHhMmSs => add_decis( - vec![ - TWO_DIGITS_WIDTH, // y_y - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YyDHhMmSs => add_decis( - vec![ - TWO_DIGITS_WIDTH, // y_y - LABEL_WIDTH, // _l__ - DIGIT_WIDTH, // d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YDddHhMmSs => add_decis( - vec![ - DIGIT_WIDTH, // Y - LABEL_WIDTH, // _l__ - THREE_DIGITS_WIDTH, // d_d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YDdHhMmSs => add_decis( - vec![ - DIGIT_WIDTH, // Y - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::YDHhMmSs => add_decis( - vec![ - DIGIT_WIDTH, // Y - LABEL_WIDTH, // _l__ - DIGIT_WIDTH, // d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - - Format::DddHhMmSs => add_decis( - vec![ - THREE_DIGITS_WIDTH, // d_d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::DdHhMmSs => add_decis( - vec![ - TWO_DIGITS_WIDTH, // d_d - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::DHhMmSs => add_decis( - vec![ - DIGIT_WIDTH, // D - LABEL_WIDTH, // _l__ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::HhMmSs => add_decis( - vec![ - TWO_DIGITS_WIDTH, // h_h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::HMmSs => add_decis( - vec![ - DIGIT_WIDTH, // h - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::MmSs => add_decis( - vec![ - TWO_DIGITS_WIDTH, // m_m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::MSs => add_decis( - vec![ - DIGIT_WIDTH, // m - COLON_WIDTH, // : - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::Ss => add_decis( - vec![ - TWO_DIGITS_WIDTH, // s_s - ], - with_decis, - ), - Format::S => add_decis( - vec![ - DIGIT_WIDTH, // s - ], - with_decis, - ), - } - } - pub fn get_width(&self, format: &Format, with_decis: bool) -> u16 { - self.get_horizontal_lengths(format, with_decis).iter().sum() + clock_horizontal_lengths(format, with_decis).iter().sum() } pub fn get_height(&self) -> u16 { @@ -890,6 +662,827 @@ where } } +// Helper to get horizontal lengths of a clock +// depending on given `Format` and `with_decis` params +pub fn clock_horizontal_lengths(format: &Format, with_decis: bool) -> Vec { + let add_decis = |mut lengths: Vec, with_decis: bool| -> Vec { + if with_decis { + lengths.extend_from_slice(&[ + DOT_WIDTH, // . + DIGIT_WIDTH, // ds + ]) + } + lengths + }; + + const LABEL_WIDTH: u16 = DIGIT_LABEL_WIDTH + DIGIT_SPACE_WIDTH; + + match format { + Format::YyyDddHhMmSs => add_decis( + vec![ + THREE_DIGITS_WIDTH, // y_y_y + LABEL_WIDTH, // _l__ + THREE_DIGITS_WIDTH, // d_d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YyyDdHhMmSs => add_decis( + vec![ + THREE_DIGITS_WIDTH, // y_y_y + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YyyDHhMmSs => add_decis( + vec![ + THREE_DIGITS_WIDTH, // y_y_y + LABEL_WIDTH, // _l__ + DIGIT_WIDTH, // d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YyDddHhMmSs => add_decis( + vec![ + TWO_DIGITS_WIDTH, // y_y + LABEL_WIDTH, // _l__ + THREE_DIGITS_WIDTH, // d_d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YyDdHhMmSs => add_decis( + vec![ + TWO_DIGITS_WIDTH, // y_y + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YyDHhMmSs => add_decis( + vec![ + TWO_DIGITS_WIDTH, // y_y + LABEL_WIDTH, // _l__ + DIGIT_WIDTH, // d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YDddHhMmSs => add_decis( + vec![ + DIGIT_WIDTH, // Y + LABEL_WIDTH, // _l__ + THREE_DIGITS_WIDTH, // d_d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YDdHhMmSs => add_decis( + vec![ + DIGIT_WIDTH, // Y + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::YDHhMmSs => add_decis( + vec![ + DIGIT_WIDTH, // Y + LABEL_WIDTH, // _l__ + DIGIT_WIDTH, // d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + + Format::DddHhMmSs => add_decis( + vec![ + THREE_DIGITS_WIDTH, // d_d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::DdHhMmSs => add_decis( + vec![ + TWO_DIGITS_WIDTH, // d_d + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::DHhMmSs => add_decis( + vec![ + DIGIT_WIDTH, // D + LABEL_WIDTH, // _l__ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::HhMmSs => add_decis( + vec![ + TWO_DIGITS_WIDTH, // h_h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::HMmSs => add_decis( + vec![ + DIGIT_WIDTH, // h + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::MmSs => add_decis( + vec![ + TWO_DIGITS_WIDTH, // m_m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::MSs => add_decis( + vec![ + DIGIT_WIDTH, // m + COLON_WIDTH, // : + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::Ss => add_decis( + vec![ + TWO_DIGITS_WIDTH, // s_s + ], + with_decis, + ), + Format::S => add_decis( + vec![ + DIGIT_WIDTH, // s + ], + with_decis, + ), + } +} + +// State to render a clock +pub struct RenderClockState<'a, D: ClockDuration> { + pub format: Format, + pub editable_time: Option