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
This commit is contained in:
parent
99032834be
commit
e2cd536079
@ -415,7 +415,8 @@ impl App {
|
|||||||
Content::Countdown => self.countdown.is_running(),
|
Content::Countdown => self.countdown.is_running(),
|
||||||
Content::Timer => self.timer.get_clock().is_running(),
|
Content::Timer => self.timer.get_clock().is_running(),
|
||||||
Content::Pomodoro => self.pomodoro.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`
|
// `LocalTime` does not use a `Clock`
|
||||||
Content::LocalTime => false,
|
Content::LocalTime => false,
|
||||||
}
|
}
|
||||||
@ -426,7 +427,7 @@ impl App {
|
|||||||
Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()),
|
Content::Countdown => Some(self.countdown.get_clock().get_percentage_done()),
|
||||||
Content::Timer => None,
|
Content::Timer => None,
|
||||||
Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()),
|
Content::Pomodoro => Some(self.pomodoro.get_clock().get_percentage_done()),
|
||||||
Content::Event => Some(self.event.get_percentage_done()),
|
Content::Event => None,
|
||||||
Content::LocalTime => None,
|
Content::LocalTime => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
532
src/duration.rs
532
src/duration.rs
@ -7,8 +7,6 @@ use std::fmt;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
use crate::common::AppTime;
|
|
||||||
|
|
||||||
// unstable
|
// unstable
|
||||||
// https://doc.rust-lang.org/src/core/time.rs.html#32
|
// https://doc.rust-lang.org/src/core/time.rs.html#32
|
||||||
pub const SECS_PER_MINUTE: u64 = 60;
|
pub const SECS_PER_MINUTE: u64 = 60;
|
||||||
@ -38,38 +36,197 @@ pub const MAX_DURATION: Duration = ONE_YEAR
|
|||||||
.saturating_mul(1000)
|
.saturating_mul(1000)
|
||||||
.saturating_sub(ONE_DECI_SECOND);
|
.saturating_sub(ONE_DECI_SECOND);
|
||||||
|
|
||||||
/// `Duration` with direction in time (past or future)
|
/// Trait for duration types that can be displayed in clock widgets.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
///
|
||||||
pub enum DirectedDuration {
|
/// This trait abstracts over different duration calculation strategies:
|
||||||
/// Time `until` a future moment (positive `Duration`)
|
/// - `DurationEx`: Uses fixed 365-day years (fast, simple)
|
||||||
Until(Duration),
|
/// - `CalendarDuration`: Uses actual calendar dates (accounts for leap years)
|
||||||
/// Time `since` a past moment (negative duration, but still represented as positive `Duration`)
|
pub trait ClockDuration {
|
||||||
Since(Duration),
|
/// 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<DirectedDuration> for Duration {
|
/// Calendar-aware duration that accounts for leap years.
|
||||||
fn from(directed: DirectedDuration) -> Self {
|
///
|
||||||
match directed {
|
/// Unlike `DurationEx` which uses fixed 365-day years, this calculates
|
||||||
DirectedDuration::Until(d) => d,
|
/// years and days based on actual calendar dates, properly handling leap years.
|
||||||
DirectedDuration::Since(d) => d,
|
///
|
||||||
}
|
/// All calculations are performed on-demand from the stored dates.
|
||||||
}
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CalendarDuration {
|
||||||
|
earlier: OffsetDateTime,
|
||||||
|
later: OffsetDateTime,
|
||||||
|
direction: CalendarDurationDirection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DirectedDuration {
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub fn from_offset_date_times(value_a: OffsetDateTime, value_b: OffsetDateTime) -> Self {
|
pub enum CalendarDurationDirection {
|
||||||
let diff = value_a - value_b;
|
Since,
|
||||||
|
Until,
|
||||||
|
}
|
||||||
|
|
||||||
if diff.is_negative() {
|
impl CalendarDuration {
|
||||||
Self::Since(Duration::from_millis(
|
/// Create a new CalendarDuration by given two `OffsetDateTime`.
|
||||||
diff.whole_milliseconds().unsigned_abs() as u64,
|
///
|
||||||
))
|
/// 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 {
|
} else {
|
||||||
Self::Until(Duration::from_millis(diff.whole_milliseconds() as u64))
|
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 From<CalendarDuration> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialOrd)]
|
||||||
pub struct DurationEx {
|
pub struct DurationEx {
|
||||||
inner: Duration,
|
inner: Duration,
|
||||||
@ -93,62 +250,60 @@ impl From<DurationEx> for Duration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DurationEx {
|
impl ClockDuration for DurationEx {
|
||||||
pub fn years(&self) -> u64 {
|
fn years(&self) -> u64 {
|
||||||
self.days() / DAYS_PER_YEAR
|
self.days() / DAYS_PER_YEAR
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn days(&self) -> u64 {
|
fn days(&self) -> u64 {
|
||||||
self.hours() / HOURS_PER_DAY
|
self.hours() / HOURS_PER_DAY
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Days in a year
|
fn days_mod(&self) -> u64 {
|
||||||
pub fn days_mod(&self) -> u64 {
|
|
||||||
self.days() % DAYS_PER_YEAR
|
self.days() % DAYS_PER_YEAR
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hours(&self) -> u64 {
|
fn hours(&self) -> u64 {
|
||||||
self.seconds() / (SECS_PER_MINUTE * MINS_PER_HOUR)
|
self.seconds() / (SECS_PER_MINUTE * MINS_PER_HOUR)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hours as 24-hour clock
|
fn hours_mod(&self) -> u64 {
|
||||||
pub fn hours_mod(&self) -> u64 {
|
|
||||||
self.hours() % HOURS_PER_DAY
|
self.hours() % HOURS_PER_DAY
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hours as 12-hour clock
|
fn hours_mod_12(&self) -> u64 {
|
||||||
pub fn hours_mod_12(&self) -> u64 {
|
|
||||||
// 0 => 12,
|
// 0 => 12,
|
||||||
// 1..=12 => hours,
|
// 1..=12 => hours,
|
||||||
// 13..=23 => hours - 12,
|
// 13..=23 => hours - 12,
|
||||||
(self.hours_mod() + 11) % 12 + 1
|
(self.hours_mod() + 11) % 12 + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn minutes(&self) -> u64 {
|
fn minutes(&self) -> u64 {
|
||||||
self.seconds() / MINS_PER_HOUR
|
self.seconds() / MINS_PER_HOUR
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn minutes_mod(&self) -> u64 {
|
fn minutes_mod(&self) -> u64 {
|
||||||
self.minutes() % SECS_PER_MINUTE
|
self.minutes() % SECS_PER_MINUTE
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seconds(&self) -> u64 {
|
fn seconds(&self) -> u64 {
|
||||||
self.inner.as_secs()
|
self.inner.as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seconds_mod(&self) -> u64 {
|
fn seconds_mod(&self) -> u64 {
|
||||||
self.seconds() % SECS_PER_MINUTE
|
self.seconds() % SECS_PER_MINUTE
|
||||||
}
|
}
|
||||||
|
|
||||||
// deciseconds
|
fn decis(&self) -> u64 {
|
||||||
pub fn decis(&self) -> u64 {
|
|
||||||
(self.inner.subsec_millis() / 100) as u64
|
(self.inner.subsec_millis() / 100) as u64
|
||||||
}
|
}
|
||||||
// milliseconds
|
|
||||||
pub fn millis(&self) -> u128 {
|
fn millis(&self) -> u128 {
|
||||||
self.inner.as_millis()
|
self.inner.as_millis()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DurationEx {
|
||||||
pub fn saturating_add(&self, ex: DurationEx) -> Self {
|
pub fn saturating_add(&self, ex: DurationEx) -> Self {
|
||||||
let inner = self.inner.saturating_add(ex.inner);
|
let inner = self.inner.saturating_add(ex.inner);
|
||||||
Self { inner }
|
Self { inner }
|
||||||
@ -166,6 +321,7 @@ impl DurationEx {
|
|||||||
|
|
||||||
impl fmt::Display for DurationEx {
|
impl fmt::Display for DurationEx {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
use ClockDuration as _; // Import trait methods
|
||||||
if self.years() >= 1 {
|
if self.years() >= 1 {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
@ -233,85 +389,6 @@ fn parse_hours(h: &str) -> Result<u8, Report> {
|
|||||||
Ok(hours)
|
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<DirectedDuration, Report> {
|
|
||||||
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`
|
/// Parses `Duration` from `hh:mm:ss`, `mm:ss` or `ss`
|
||||||
pub fn parse_duration(arg: &str) -> Result<Duration, Report> {
|
pub fn parse_duration(arg: &str) -> Result<Duration, Report> {
|
||||||
let parts: Vec<&str> = arg.split(':').collect();
|
let parts: Vec<&str> = arg.split(':').collect();
|
||||||
@ -396,6 +473,7 @@ pub fn parse_long_duration(arg: &str) -> Result<Duration, Report> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
use super::ClockDuration;
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -497,31 +575,6 @@ mod tests {
|
|||||||
assert_eq!(result, 1);
|
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]
|
#[test]
|
||||||
fn test_parse_duration() {
|
fn test_parse_duration() {
|
||||||
// ss
|
// ss
|
||||||
@ -543,42 +596,6 @@ mod tests {
|
|||||||
assert!(parse_duration("01:02:03:04").is_err()); // too many parts
|
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]
|
#[test]
|
||||||
fn test_parse_long_duration() {
|
fn test_parse_long_duration() {
|
||||||
// `Yy`
|
// `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 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)
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,8 @@ use ratatui::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::{ClockTypeId, Style as DigitStyle},
|
common::{ClockTypeId, Style as DigitStyle},
|
||||||
duration::{
|
duration::{
|
||||||
DurationEx, MAX_DURATION, ONE_DAY, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND,
|
ClockDuration, DurationEx, MAX_DURATION, ONE_DAY, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE,
|
||||||
ONE_YEAR,
|
ONE_SECOND, ONE_YEAR,
|
||||||
},
|
},
|
||||||
events::{AppEvent, AppEventTx},
|
events::{AppEvent, AppEventTx},
|
||||||
utils::center_horizontal,
|
utils::center_horizontal,
|
||||||
@ -90,7 +90,7 @@ pub enum Format {
|
|||||||
YyyDddHhMmSs,
|
YyyDddHhMmSs,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_by_duration(d: &DurationEx) -> Format {
|
pub fn format_by_duration<D: ClockDuration>(d: &D) -> Format {
|
||||||
if d.years() >= 100 && d.days_mod() >= 100 {
|
if d.years() >= 100 && d.days_mod() >= 100 {
|
||||||
Format::YyyDddHhMmSs
|
Format::YyyDddHhMmSs
|
||||||
} else if d.years() >= 100 && d.days_mod() >= 10 {
|
} else if d.years() >= 100 && d.days_mod() >= 10 {
|
||||||
@ -437,7 +437,7 @@ impl<T> ClockState<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn update_format(&mut self) {
|
fn update_format(&mut self) {
|
||||||
let d = self.get_current_value();
|
let d: &DurationEx = self.get_current_value();
|
||||||
self.format = format_by_duration(d);
|
self.format = format_by_duration(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -642,7 +642,29 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_horizontal_lengths(&self, format: &Format, with_decis: bool) -> Vec<u16> {
|
pub fn get_width(&self, format: &Format, with_decis: bool) -> u16 {
|
||||||
|
clock_horizontal_lengths(format, with_decis).iter().sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_height(&self) -> u16 {
|
||||||
|
DIGIT_HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks whether to blink the clock while rendering.
|
||||||
|
/// Its logic is based on a given `count` value.
|
||||||
|
fn should_blink(&self, count_value: &Option<u64>) -> bool {
|
||||||
|
// Example:
|
||||||
|
// if `RANGE_OF_DONE_COUNT` is 4
|
||||||
|
// then for ranges `0..4`, `8..12` etc. it will return `true`
|
||||||
|
count_value
|
||||||
|
.map(|b| (b % (RANGE_OF_DONE_COUNT * 2)) < RANGE_OF_DONE_COUNT)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<u16> {
|
||||||
let add_decis = |mut lengths: Vec<u16>, with_decis: bool| -> Vec<u16> {
|
let add_decis = |mut lengths: Vec<u16>, with_decis: bool| -> Vec<u16> {
|
||||||
if with_decis {
|
if with_decis {
|
||||||
lengths.extend_from_slice(&[
|
lengths.extend_from_slice(&[
|
||||||
@ -870,53 +892,36 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_width(&self, format: &Format, with_decis: bool) -> u16 {
|
// State to render a clock
|
||||||
self.get_horizontal_lengths(format, with_decis).iter().sum()
|
pub struct RenderClockState<'a, D: ClockDuration> {
|
||||||
|
pub format: Format,
|
||||||
|
pub editable_time: Option<Time>,
|
||||||
|
pub with_decis: bool,
|
||||||
|
pub symbol: &'a str,
|
||||||
|
pub widths: Vec<u16>,
|
||||||
|
pub duration: D,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_height(&self) -> u16 {
|
// Helper to render a clock
|
||||||
DIGIT_HEIGHT
|
pub fn render_clock<D: ClockDuration>(area: Rect, buf: &mut Buffer, state: RenderClockState<D>) {
|
||||||
}
|
let RenderClockState {
|
||||||
|
format,
|
||||||
|
with_decis,
|
||||||
|
symbol,
|
||||||
|
widths,
|
||||||
|
editable_time,
|
||||||
|
duration,
|
||||||
|
} = state;
|
||||||
|
|
||||||
/// Checks whether to blink the clock while rendering.
|
let width = widths.iter().sum();
|
||||||
/// Its logic is based on a given `count` value.
|
let area = center_horizontal(area, Constraint::Length(width));
|
||||||
fn should_blink(&self, count_value: &Option<u64>) -> bool {
|
|
||||||
// Example:
|
|
||||||
// if `RANGE_OF_DONE_COUNT` is 4
|
|
||||||
// then for ranges `0..4`, `8..12` etc. it will return `true`
|
|
||||||
count_value
|
|
||||||
.map(|b| (b % (RANGE_OF_DONE_COUNT * 2)) < RANGE_OF_DONE_COUNT)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> StatefulWidget for ClockWidget<T>
|
let edit_years = matches!(editable_time, Some(Time::Years));
|
||||||
where
|
let edit_days = matches!(editable_time, Some(Time::Days));
|
||||||
T: std::fmt::Debug,
|
let edit_hours = matches!(editable_time, Some(Time::Hours));
|
||||||
{
|
let edit_minutes = matches!(editable_time, Some(Time::Minutes));
|
||||||
type State = ClockState<T>;
|
let edit_secs = matches!(editable_time, Some(Time::Seconds));
|
||||||
|
let edit_decis = matches!(editable_time, Some(Time::Decis));
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
|
||||||
let with_decis = state.with_decis;
|
|
||||||
let format = state.format;
|
|
||||||
// to simulate a blink effect, just use an "empty" symbol (string)
|
|
||||||
// to "empty" all digits and to have an "empty" render area
|
|
||||||
let symbol = if self.blink && self.should_blink(&state.done_count) {
|
|
||||||
" "
|
|
||||||
} else {
|
|
||||||
self.style.get_digit_symbol()
|
|
||||||
};
|
|
||||||
let widths = self.get_horizontal_lengths(&format, with_decis);
|
|
||||||
let area = center_horizontal(
|
|
||||||
area,
|
|
||||||
Constraint::Length(self.get_width(&format, with_decis)),
|
|
||||||
);
|
|
||||||
let edit_years = matches!(state.mode, Mode::Editable(Time::Years, _));
|
|
||||||
let edit_days = matches!(state.mode, Mode::Editable(Time::Days, _));
|
|
||||||
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_decis = matches!(state.mode, Mode::Editable(Time::Decis, _));
|
|
||||||
|
|
||||||
let render_three_digits = |d1, d2, d3, editable, area, buf: &mut Buffer| {
|
let render_three_digits = |d1, d2, d3, editable, area, buf: &mut Buffer| {
|
||||||
let [a1, a2, a3] = Layout::horizontal(Constraint::from_lengths([
|
let [a1, a2, a3] = Layout::horizontal(Constraint::from_lengths([
|
||||||
@ -950,9 +955,9 @@ where
|
|||||||
|
|
||||||
let render_yyy = |area, buf| {
|
let render_yyy = |area, buf| {
|
||||||
render_three_digits(
|
render_three_digits(
|
||||||
(state.current_value.years() / 100) % 10,
|
(duration.years() / 100) % 10,
|
||||||
(state.current_value.years() / 10) % 10,
|
(duration.years() / 10) % 10,
|
||||||
state.current_value.years() % 10,
|
duration.years() % 10,
|
||||||
edit_years,
|
edit_years,
|
||||||
area,
|
area,
|
||||||
buf,
|
buf,
|
||||||
@ -961,8 +966,8 @@ where
|
|||||||
|
|
||||||
let render_yy = |area, buf| {
|
let render_yy = |area, buf| {
|
||||||
render_two_digits(
|
render_two_digits(
|
||||||
(state.current_value.years() / 10) % 10,
|
(duration.years() / 10) % 10,
|
||||||
state.current_value.years() % 10,
|
duration.years() % 10,
|
||||||
edit_years,
|
edit_years,
|
||||||
area,
|
area,
|
||||||
buf,
|
buf,
|
||||||
@ -970,14 +975,14 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let render_y = |area, buf| {
|
let render_y = |area, buf| {
|
||||||
Digit::new(state.current_value.years() % 10, edit_years, symbol).render(area, buf);
|
Digit::new(duration.years() % 10, edit_years, symbol).render(area, buf);
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_ddd = |area, buf| {
|
let render_ddd = |area, buf| {
|
||||||
render_three_digits(
|
render_three_digits(
|
||||||
(state.current_value.days_mod() / 100) % 10,
|
(duration.days_mod() / 100) % 10,
|
||||||
(state.current_value.days_mod() / 10) % 10,
|
(duration.days_mod() / 10) % 10,
|
||||||
state.current_value.days_mod() % 10,
|
duration.days_mod() % 10,
|
||||||
edit_days,
|
edit_days,
|
||||||
area,
|
area,
|
||||||
buf,
|
buf,
|
||||||
@ -986,8 +991,8 @@ where
|
|||||||
|
|
||||||
let render_dd = |area, buf| {
|
let render_dd = |area, buf| {
|
||||||
render_two_digits(
|
render_two_digits(
|
||||||
(state.current_value.days_mod() / 10) % 10,
|
(duration.days_mod() / 10) % 10,
|
||||||
state.current_value.days_mod() % 10,
|
duration.days_mod() % 10,
|
||||||
edit_days,
|
edit_days,
|
||||||
area,
|
area,
|
||||||
buf,
|
buf,
|
||||||
@ -995,13 +1000,13 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let render_d = |area, buf| {
|
let render_d = |area, buf| {
|
||||||
Digit::new(state.current_value.days_mod() % 10, edit_days, symbol).render(area, buf);
|
Digit::new(duration.days_mod() % 10, edit_days, symbol).render(area, buf);
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_hh = |area, buf| {
|
let render_hh = |area, buf| {
|
||||||
render_two_digits(
|
render_two_digits(
|
||||||
state.current_value.hours_mod() / 10,
|
duration.hours_mod() / 10,
|
||||||
state.current_value.hours_mod() % 10,
|
duration.hours_mod() % 10,
|
||||||
edit_hours,
|
edit_hours,
|
||||||
area,
|
area,
|
||||||
buf,
|
buf,
|
||||||
@ -1009,13 +1014,13 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let render_h = |area, buf| {
|
let render_h = |area, buf| {
|
||||||
Digit::new(state.current_value.hours_mod() % 10, edit_hours, symbol).render(area, buf);
|
Digit::new(duration.hours_mod() % 10, edit_hours, symbol).render(area, buf);
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_mm = |area, buf| {
|
let render_mm = |area, buf| {
|
||||||
render_two_digits(
|
render_two_digits(
|
||||||
state.current_value.minutes_mod() / 10,
|
duration.minutes_mod() / 10,
|
||||||
state.current_value.minutes_mod() % 10,
|
duration.minutes_mod() % 10,
|
||||||
edit_minutes,
|
edit_minutes,
|
||||||
area,
|
area,
|
||||||
buf,
|
buf,
|
||||||
@ -1023,14 +1028,13 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let render_m = |area, buf| {
|
let render_m = |area, buf| {
|
||||||
Digit::new(state.current_value.minutes_mod() % 10, edit_minutes, symbol)
|
Digit::new(duration.minutes_mod() % 10, edit_minutes, symbol).render(area, buf);
|
||||||
.render(area, buf);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_ss = |area, buf| {
|
let render_ss = |area, buf| {
|
||||||
render_two_digits(
|
render_two_digits(
|
||||||
state.current_value.seconds_mod() / 10,
|
duration.seconds_mod() / 10,
|
||||||
state.current_value.seconds_mod() % 10,
|
duration.seconds_mod() % 10,
|
||||||
edit_secs,
|
edit_secs,
|
||||||
area,
|
area,
|
||||||
buf,
|
buf,
|
||||||
@ -1038,11 +1042,11 @@ where
|
|||||||
};
|
};
|
||||||
|
|
||||||
let render_s = |area, buf| {
|
let render_s = |area, buf| {
|
||||||
Digit::new(state.current_value.seconds_mod() % 10, edit_secs, symbol).render(area, buf);
|
Digit::new(duration.seconds_mod() % 10, edit_secs, symbol).render(area, buf);
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_ds = |area, buf| {
|
let render_ds = |area, buf| {
|
||||||
Digit::new(state.current_value.decis(), edit_decis, symbol).render(area, buf);
|
Digit::new(duration.decis(), edit_decis, symbol).render(area, buf);
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_label = |l: &str, area, buf: &mut Buffer| {
|
let render_label = |l: &str, area, buf: &mut Buffer| {
|
||||||
@ -1436,8 +1440,7 @@ where
|
|||||||
render_ds(ds, buf);
|
render_ds(ds, buf);
|
||||||
}
|
}
|
||||||
Format::MmSs => {
|
Format::MmSs => {
|
||||||
let [m_m, c_ms, s_s] =
|
let [m_m, c_ms, s_s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area);
|
||||||
Layout::horizontal(Constraint::from_lengths(widths)).areas(area);
|
|
||||||
render_mm(m_m, buf);
|
render_mm(m_m, buf);
|
||||||
render_colon(c_ms, buf);
|
render_colon(c_ms, buf);
|
||||||
render_ss(s_s, buf);
|
render_ss(s_s, buf);
|
||||||
@ -1452,15 +1455,13 @@ where
|
|||||||
render_ds(ds, buf);
|
render_ds(ds, buf);
|
||||||
}
|
}
|
||||||
Format::MSs => {
|
Format::MSs => {
|
||||||
let [m, c_ms, s_s] =
|
let [m, c_ms, s_s] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area);
|
||||||
Layout::horizontal(Constraint::from_lengths(widths)).areas(area);
|
|
||||||
render_m(m, buf);
|
render_m(m, buf);
|
||||||
render_colon(c_ms, buf);
|
render_colon(c_ms, buf);
|
||||||
render_ss(s_s, buf);
|
render_ss(s_s, buf);
|
||||||
}
|
}
|
||||||
Format::Ss if state.with_decis => {
|
Format::Ss if state.with_decis => {
|
||||||
let [s_s, dot, ds] =
|
let [s_s, dot, ds] = Layout::horizontal(Constraint::from_lengths(widths)).areas(area);
|
||||||
Layout::horizontal(Constraint::from_lengths(widths)).areas(area);
|
|
||||||
render_ss(s_s, buf);
|
render_ss(s_s, buf);
|
||||||
render_dot(dot, buf);
|
render_dot(dot, buf);
|
||||||
render_ds(ds, buf);
|
render_ds(ds, buf);
|
||||||
@ -1481,4 +1482,37 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> StatefulWidget for ClockWidget<T>
|
||||||
|
where
|
||||||
|
T: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
type State = ClockState<T>;
|
||||||
|
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
let with_decis = state.with_decis;
|
||||||
|
let format = state.format;
|
||||||
|
let widths = clock_horizontal_lengths(&format, with_decis);
|
||||||
|
|
||||||
|
// to simulate a blink effect, just use an "empty" symbol (string)
|
||||||
|
// to "empty" all digits and to have an "empty" render area
|
||||||
|
let symbol = if self.blink && self.should_blink(&state.done_count) {
|
||||||
|
" "
|
||||||
|
} else {
|
||||||
|
self.style.get_digit_symbol()
|
||||||
|
};
|
||||||
|
|
||||||
|
let render_state = RenderClockState {
|
||||||
|
with_decis,
|
||||||
|
duration: state.current_value,
|
||||||
|
editable_time: match state.get_mode() {
|
||||||
|
Mode::Editable(time, _) => Some(*time),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
format,
|
||||||
|
symbol,
|
||||||
|
widths,
|
||||||
|
};
|
||||||
|
render_clock(area, buf, render_state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::ClockTypeId,
|
common::ClockTypeId,
|
||||||
duration::{
|
duration::{
|
||||||
MAX_DURATION, ONE_DAY, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND, ONE_YEAR,
|
DurationEx, MAX_DURATION, ONE_DAY, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND,
|
||||||
|
ONE_YEAR,
|
||||||
},
|
},
|
||||||
widgets::clock::*,
|
widgets::clock::*,
|
||||||
};
|
};
|
||||||
@ -76,102 +77,129 @@ fn test_get_format_hours() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_format_by_duration_boundaries() {
|
fn test_format_by_duration_boundaries() {
|
||||||
// S
|
// S
|
||||||
assert_eq!(format_by_duration(&(ONE_SECOND * 9).into()), Format::S);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&(ONE_SECOND * 9).into()),
|
||||||
|
Format::S
|
||||||
|
);
|
||||||
// Ss
|
// Ss
|
||||||
assert_eq!(format_by_duration(&(10 * ONE_SECOND).into()), Format::Ss);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&(10 * ONE_SECOND).into()),
|
||||||
|
Format::Ss
|
||||||
|
);
|
||||||
// Ss
|
// Ss
|
||||||
assert_eq!(format_by_duration(&(59 * ONE_SECOND).into()), Format::Ss);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&(59 * ONE_SECOND).into()),
|
||||||
|
Format::Ss
|
||||||
|
);
|
||||||
// MSs
|
// MSs
|
||||||
assert_eq!(format_by_duration(&ONE_MINUTE.into()), Format::MSs);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&ONE_MINUTE.into()),
|
||||||
|
Format::MSs
|
||||||
|
);
|
||||||
// HhMmSs
|
// HhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(ONE_DAY.saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(&(ONE_DAY.saturating_sub(ONE_SECOND)).into()),
|
||||||
Format::HhMmSs
|
Format::HhMmSs
|
||||||
);
|
);
|
||||||
// DHhMmSs
|
// DHhMmSs
|
||||||
assert_eq!(format_by_duration(&ONE_DAY.into()), Format::DHhMmSs);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&ONE_DAY.into()),
|
||||||
|
Format::DHhMmSs
|
||||||
|
);
|
||||||
// DHhMmSs
|
// DHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&((10 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(&((10 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
||||||
Format::DHhMmSs
|
Format::DHhMmSs
|
||||||
);
|
);
|
||||||
// DdHhMmSs
|
// DdHhMmSs
|
||||||
assert_eq!(format_by_duration(&(10 * ONE_DAY).into()), Format::DdHhMmSs);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&(10 * ONE_DAY).into()),
|
||||||
|
Format::DdHhMmSs
|
||||||
|
);
|
||||||
// DdHhMmSs
|
// DdHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&((100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(&((100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
||||||
Format::DdHhMmSs
|
Format::DdHhMmSs
|
||||||
);
|
);
|
||||||
// DddHhMmSs
|
// DddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(100 * ONE_DAY).into()),
|
||||||
Format::DddHhMmSs
|
Format::DddHhMmSs
|
||||||
);
|
);
|
||||||
// DddHhMmSs
|
// DddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(ONE_YEAR.saturating_sub(ONE_SECOND).into())),
|
format_by_duration::<DurationEx>(&(ONE_YEAR.saturating_sub(ONE_SECOND).into())),
|
||||||
Format::DddHhMmSs
|
Format::DddHhMmSs
|
||||||
);
|
);
|
||||||
// YDHhMmSs
|
// YDHhMmSs
|
||||||
assert_eq!(format_by_duration(&ONE_YEAR.into()), Format::YDHhMmSs);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&ONE_YEAR.into()),
|
||||||
|
Format::YDHhMmSs
|
||||||
|
);
|
||||||
// YDdHhMmSs
|
// YDdHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(
|
||||||
|
&(ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()
|
||||||
|
),
|
||||||
Format::YDdHhMmSs
|
Format::YDdHhMmSs
|
||||||
);
|
);
|
||||||
// YDddHhMmSs
|
// YDddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(ONE_YEAR + 100 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(ONE_YEAR + 100 * ONE_DAY).into()),
|
||||||
Format::YDddHhMmSs
|
Format::YDddHhMmSs
|
||||||
);
|
);
|
||||||
// YDddHhMmSs
|
// YDddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&((10 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(&((10 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
|
||||||
Format::YDddHhMmSs
|
Format::YDddHhMmSs
|
||||||
);
|
);
|
||||||
// YyDHhMmSs
|
// YyDHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(10 * ONE_YEAR).into()),
|
format_by_duration::<DurationEx>(&(10 * ONE_YEAR).into()),
|
||||||
Format::YyDHhMmSs
|
Format::YyDHhMmSs
|
||||||
);
|
);
|
||||||
// YyDdHhMmSs
|
// YyDdHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
|
||||||
Format::YyDdHhMmSs
|
Format::YyDdHhMmSs
|
||||||
);
|
);
|
||||||
// YyDdHhMmSs
|
// YyDdHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(10 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(
|
||||||
|
&(10 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()
|
||||||
|
),
|
||||||
Format::YyDdHhMmSs
|
Format::YyDdHhMmSs
|
||||||
);
|
);
|
||||||
// YyDddHhMmSs
|
// YyDddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
|
||||||
Format::YyDddHhMmSs
|
Format::YyDddHhMmSs
|
||||||
);
|
);
|
||||||
// YyDddHhMmSs
|
// YyDddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&((100 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(&((100 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
|
||||||
Format::YyDddHhMmSs
|
Format::YyDddHhMmSs
|
||||||
);
|
);
|
||||||
// YyyDHhMmSs
|
// YyyDHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_YEAR).into()),
|
format_by_duration::<DurationEx>(&(100 * ONE_YEAR).into()),
|
||||||
Format::YyyDHhMmSs
|
Format::YyyDHhMmSs
|
||||||
);
|
);
|
||||||
// YyyDdHhMmSs
|
// YyyDdHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
|
||||||
Format::YyyDdHhMmSs
|
Format::YyyDdHhMmSs
|
||||||
);
|
);
|
||||||
// YyyDdHhMmSs
|
// YyyDdHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
|
format_by_duration::<DurationEx>(
|
||||||
|
&(100 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()
|
||||||
|
),
|
||||||
Format::YyyDdHhMmSs
|
Format::YyyDdHhMmSs
|
||||||
);
|
);
|
||||||
// YyyDddHhMmSs
|
// YyyDddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
|
||||||
Format::YyyDddHhMmSs
|
Format::YyyDddHhMmSs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -179,12 +207,18 @@ fn test_format_by_duration_boundaries() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_format_by_duration_days() {
|
fn test_format_by_duration_days() {
|
||||||
// DHhMmSs
|
// DHhMmSs
|
||||||
assert_eq!(format_by_duration(&ONE_DAY.into()), Format::DHhMmSs);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&ONE_DAY.into()),
|
||||||
|
Format::DHhMmSs
|
||||||
|
);
|
||||||
// DdHhMmSs
|
// DdHhMmSs
|
||||||
assert_eq!(format_by_duration(&(10 * ONE_DAY).into()), Format::DdHhMmSs);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&(10 * ONE_DAY).into()),
|
||||||
|
Format::DdHhMmSs
|
||||||
|
);
|
||||||
// DddHhMmSs
|
// DddHhMmSs
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(101 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(101 * ONE_DAY).into()),
|
||||||
Format::DddHhMmSs
|
Format::DddHhMmSs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -192,59 +226,62 @@ fn test_format_by_duration_days() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_format_by_duration_years() {
|
fn test_format_by_duration_years() {
|
||||||
// YDHhMmSs (1 year, 0 days)
|
// YDHhMmSs (1 year, 0 days)
|
||||||
assert_eq!(format_by_duration(&ONE_YEAR.into()), Format::YDHhMmSs);
|
assert_eq!(
|
||||||
|
format_by_duration::<DurationEx>(&ONE_YEAR.into()),
|
||||||
|
Format::YDHhMmSs
|
||||||
|
);
|
||||||
|
|
||||||
// YDHhMmSs (1 year, 1 day)
|
// YDHhMmSs (1 year, 1 day)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(ONE_YEAR + ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(ONE_YEAR + ONE_DAY).into()),
|
||||||
Format::YDHhMmSs
|
Format::YDHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YDdHhMmSs (1 year, 10 days)
|
// YDdHhMmSs (1 year, 10 days)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(ONE_YEAR + 10 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(ONE_YEAR + 10 * ONE_DAY).into()),
|
||||||
Format::YDdHhMmSs
|
Format::YDdHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YDddHhMmSs (1 year, 100 days)
|
// YDddHhMmSs (1 year, 100 days)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(ONE_YEAR + 100 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(ONE_YEAR + 100 * ONE_DAY).into()),
|
||||||
Format::YDddHhMmSs
|
Format::YDddHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YyDHhMmSs (10 years)
|
// YyDHhMmSs (10 years)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(10 * ONE_YEAR).into()),
|
format_by_duration::<DurationEx>(&(10 * ONE_YEAR).into()),
|
||||||
Format::YyDHhMmSs
|
Format::YyDHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YyDdHhMmSs (10 years, 10 days)
|
// YyDdHhMmSs (10 years, 10 days)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
|
||||||
Format::YyDdHhMmSs
|
Format::YyDdHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YyDddHhMmSs (10 years, 100 days)
|
// YyDddHhMmSs (10 years, 100 days)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
|
||||||
Format::YyDddHhMmSs
|
Format::YyDddHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YyyDHhMmSs (100 years)
|
// YyyDHhMmSs (100 years)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_YEAR).into()),
|
format_by_duration::<DurationEx>(&(100 * ONE_YEAR).into()),
|
||||||
Format::YyyDHhMmSs
|
Format::YyyDHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YyyDdHhMmSs (100 years, 10 days)
|
// YyyDdHhMmSs (100 years, 10 days)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
|
||||||
Format::YyyDdHhMmSs
|
Format::YyyDdHhMmSs
|
||||||
);
|
);
|
||||||
|
|
||||||
// YyyDddHhMmSs (100 years, 100 days)
|
// YyyDddHhMmSs (100 years, 100 days)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
format_by_duration(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
|
format_by_duration::<DurationEx>(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
|
||||||
Format::YyyDddHhMmSs
|
Format::YyyDddHhMmSs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,10 @@ use time::{OffsetDateTime, macros::format_description};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{AppTime, Style},
|
common::{AppTime, Style},
|
||||||
constants::TICK_VALUE_MS,
|
duration::{CalendarDuration, CalendarDurationDirection},
|
||||||
duration::DirectedDuration,
|
|
||||||
events::{AppEventTx, TuiEvent, TuiEventHandler},
|
events::{AppEventTx, TuiEvent, TuiEventHandler},
|
||||||
utils::center,
|
utils::center,
|
||||||
widgets::clock::{self, ClockState, ClockStateArgs, ClockWidget},
|
widgets::{clock, clock_elements::DIGIT_HEIGHT},
|
||||||
};
|
};
|
||||||
use std::{cmp::max, time::Duration};
|
use std::{cmp::max, time::Duration};
|
||||||
|
|
||||||
@ -20,8 +19,8 @@ use std::{cmp::max, time::Duration};
|
|||||||
pub struct EventState {
|
pub struct EventState {
|
||||||
title: String,
|
title: String,
|
||||||
event_time: OffsetDateTime,
|
event_time: OffsetDateTime,
|
||||||
clock: ClockState<clock::Countdown>,
|
app_time: OffsetDateTime,
|
||||||
directed_duration: DirectedDuration,
|
with_decis: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct EventStateArgs {
|
pub struct EventStateArgs {
|
||||||
@ -42,52 +41,27 @@ impl EventState {
|
|||||||
app_tx,
|
app_tx,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
|
// TODO: Handle app Events
|
||||||
|
let _ = app_tx;
|
||||||
let app_datetime = OffsetDateTime::from(app_time);
|
let app_datetime = OffsetDateTime::from(app_time);
|
||||||
// assume event has as same `offset` as `app_time`
|
// assume event has as same `offset` as `app_time`
|
||||||
let event_offset = event_time.assume_offset(app_datetime.offset());
|
let event_offset = event_time.assume_offset(app_datetime.offset());
|
||||||
let directed_duration =
|
|
||||||
DirectedDuration::from_offset_date_times(event_offset, app_datetime);
|
|
||||||
let current_value = directed_duration.into();
|
|
||||||
|
|
||||||
let clock = ClockState::<clock::Countdown>::new(ClockStateArgs {
|
|
||||||
initial_value: current_value,
|
|
||||||
current_value,
|
|
||||||
tick_value: Duration::from_millis(TICK_VALUE_MS),
|
|
||||||
with_decis,
|
|
||||||
app_tx: Some(app_tx.clone()),
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
title: event_title,
|
title: event_title,
|
||||||
event_time: event_offset,
|
event_time: event_offset,
|
||||||
directed_duration,
|
app_time: app_datetime,
|
||||||
clock,
|
with_decis,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_clock(&self) -> &ClockState<clock::Countdown> {
|
|
||||||
&self.clock
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_app_time(&mut self, app_time: AppTime) {
|
pub fn set_app_time(&mut self, app_time: AppTime) {
|
||||||
// update `directed_duration`
|
|
||||||
let app_datetime = OffsetDateTime::from(app_time);
|
let app_datetime = OffsetDateTime::from(app_time);
|
||||||
self.directed_duration =
|
self.app_time = app_datetime;
|
||||||
DirectedDuration::from_offset_date_times(self.event_time, app_datetime);
|
|
||||||
// update clock
|
|
||||||
let duration: Duration = self.directed_duration.into();
|
|
||||||
self.clock.set_current_value(duration.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_with_decis(&mut self, with_decis: bool) {
|
pub fn set_with_decis(&mut self, with_decis: bool) {
|
||||||
self.clock.with_decis = with_decis;
|
self.with_decis = with_decis;
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_percentage_done(&self) -> u16 {
|
|
||||||
match self.directed_duration {
|
|
||||||
DirectedDuration::Since(_) => 100,
|
|
||||||
DirectedDuration::Until(_) => self.clock.get_percentage_done(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +80,13 @@ pub struct EventWidget {
|
|||||||
impl StatefulWidget for EventWidget {
|
impl StatefulWidget for EventWidget {
|
||||||
type State = EventState;
|
type State = EventState;
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
let clock = &mut state.clock;
|
let with_decis = state.with_decis;
|
||||||
let clock_widget = ClockWidget::new(self.style, self.blink);
|
let clock_duration =
|
||||||
|
CalendarDuration::from_start_end_times(state.event_time, state.app_time);
|
||||||
|
let clock_format = clock::format_by_duration(&clock_duration);
|
||||||
|
let clock_widths = clock::clock_horizontal_lengths(&clock_format, with_decis);
|
||||||
|
let clock_width = clock_widths.iter().sum();
|
||||||
|
|
||||||
let label_event = Line::raw(state.title.to_uppercase());
|
let label_event = Line::raw(state.title.to_uppercase());
|
||||||
let time_str = state
|
let time_str = state
|
||||||
.event_time
|
.event_time
|
||||||
@ -115,17 +94,19 @@ impl StatefulWidget for EventWidget {
|
|||||||
"[year]-[month]-[day] [hour]:[minute]:[second]"
|
"[year]-[month]-[day] [hour]:[minute]:[second]"
|
||||||
))
|
))
|
||||||
.unwrap_or_else(|e| format!("time format error: {}", e));
|
.unwrap_or_else(|e| format!("time format error: {}", e));
|
||||||
let time_prefix = match state.directed_duration {
|
|
||||||
DirectedDuration::Since(d) => {
|
let time_prefix = if clock_duration.direction() == &CalendarDurationDirection::Since {
|
||||||
// Show `done` for a short of time (1 sec.)
|
let duration: Duration = clock_duration.clone().into();
|
||||||
if d < Duration::from_secs(1) {
|
// Show `done` for a short of time (1 sec)
|
||||||
|
if duration < Duration::from_secs(1) {
|
||||||
"Done"
|
"Done"
|
||||||
} else {
|
} else {
|
||||||
"Since"
|
"Since"
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
DirectedDuration::Until(_) => "Until",
|
"Until"
|
||||||
};
|
};
|
||||||
|
|
||||||
let label_time = Line::raw(format!(
|
let label_time = Line::raw(format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
time_prefix.to_uppercase(),
|
time_prefix.to_uppercase(),
|
||||||
@ -135,21 +116,34 @@ impl StatefulWidget for EventWidget {
|
|||||||
|
|
||||||
let area = center(
|
let area = center(
|
||||||
area,
|
area,
|
||||||
Constraint::Length(max(
|
Constraint::Length(max(clock_width, max_label_width)),
|
||||||
clock_widget.get_width(clock.get_format(), clock.with_decis),
|
Constraint::Length(DIGIT_HEIGHT + 3 /* height of label */),
|
||||||
max_label_width,
|
|
||||||
)),
|
|
||||||
Constraint::Length(clock_widget.get_height() + 3 /* height of label */),
|
|
||||||
);
|
);
|
||||||
let [_, v1, v2, v3] = Layout::vertical(Constraint::from_lengths([
|
let [_, v1, v2, v3] = Layout::vertical(Constraint::from_lengths([
|
||||||
1, // empty (offset) to keep everything centered vertically comparing to "clock" widgets with one label only
|
1, // empty (offset) to keep everything centered vertically comparing to "clock" widgets with one label only
|
||||||
clock_widget.get_height(),
|
DIGIT_HEIGHT,
|
||||||
1, // event date
|
1, // event date
|
||||||
1, // event title
|
1, // event title
|
||||||
]))
|
]))
|
||||||
.areas(area);
|
.areas(area);
|
||||||
|
|
||||||
clock_widget.render(v1, buf, clock);
|
// TODO: Add logic to handle blink in `DONE` mode, similar to `ClockWidget<T>::should_blink`
|
||||||
|
let symbol = if self.blink {
|
||||||
|
" "
|
||||||
|
} else {
|
||||||
|
self.style.get_digit_symbol()
|
||||||
|
};
|
||||||
|
|
||||||
|
let render_clock_state = clock::RenderClockState {
|
||||||
|
with_decis,
|
||||||
|
duration: clock_duration,
|
||||||
|
editable_time: None,
|
||||||
|
format: clock_format,
|
||||||
|
symbol,
|
||||||
|
widths: clock_widths,
|
||||||
|
};
|
||||||
|
|
||||||
|
clock::render_clock(v1, buf, render_clock_state);
|
||||||
label_time.centered().render(v2, buf);
|
label_time.centered().render(v2, buf);
|
||||||
label_event.centered().render(v3, buf);
|
label_event.centered().render(v3, buf);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use ratatui::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{AppTime, AppTimeFormat, Style as DigitStyle},
|
common::{AppTime, AppTimeFormat, Style as DigitStyle},
|
||||||
duration::DurationEx,
|
duration::{ClockDuration, DurationEx},
|
||||||
events::{TuiEvent, TuiEventHandler},
|
events::{TuiEvent, TuiEventHandler},
|
||||||
utils::center,
|
utils::center,
|
||||||
widgets::clock_elements::{
|
widgets::clock_elements::{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user