diff --git a/src/app.rs b/src/app.rs index a419696..7634bb0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -213,7 +213,7 @@ impl App { event: EventState::new(EventStateArgs { app_time, event_time: time::PrimitiveDateTime::parse( - "2025-10-09 19:54:29", + "2025-10-09 21:32:30", format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"), ) .unwrap(), diff --git a/src/duration.rs b/src/duration.rs index a872c8c..299b594 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -92,7 +92,7 @@ pub struct CalendarDuration { direction: CalendarDurationDirection, } -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Copy)] pub enum CalendarDurationDirection { Since, Until, @@ -124,8 +124,12 @@ impl CalendarDuration { } } - pub fn direction(&self) -> &CalendarDurationDirection { - &self.direction + pub fn direction(&self) -> CalendarDurationDirection { + self.direction + } + + pub fn is_since(&self) -> bool { + self.direction == CalendarDurationDirection::Since } pub fn start_time(&self) -> &OffsetDateTime { diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index 71ba3c5..7bd9229 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -161,7 +161,7 @@ pub fn count_by_mode(times: u32, mode: &Mode) -> Duration { } const RANGE_OF_DONE_COUNT: u64 = 4; -const MAX_DONE_COUNT: u64 = RANGE_OF_DONE_COUNT * 5; +pub const MAX_DONE_COUNT: u64 = RANGE_OF_DONE_COUNT * 5; pub struct ClockState { type_id: ClockTypeId, @@ -447,18 +447,17 @@ impl ClockState { /// `tick` won't be called again after `Mode::Done` event (e.g. in `widget::Countdown`). /// That's why `update_done_count` is called from "outside". pub fn update_done_count(&mut self) { - if let Some(count) = self.done_count { - if count > 0 { - let value = count - 1; - self.done_count = Some(value) - } else { - // None means we are done and no counting anymore. - self.done_count = None - } - } + self.done_count = count_clock_done(self.done_count); } } +/// Safe way to count a possible `done` value +pub fn count_clock_done(value: Option) -> Option { + // Safe substraction for `Some(value > 1)` + // `None` means `done` == no counting anymore. + value.and_then(|count| count.checked_sub(1)) +} + #[derive(Debug, Clone)] pub struct Countdown {} @@ -649,17 +648,17 @@ where 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) -> 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 check whether to blink the clock while rendering. +/// Its logic is based on a given `count` value. +pub fn should_blink(count_value: Option) -> 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 @@ -1494,9 +1493,9 @@ where 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) { + // To simulate a blink effect, just use an "empty" symbol (string) + // It's "empty" all digits and creates an "empty" render area + let symbol = if self.blink && should_blink(state.done_count) { " " } else { self.style.get_digit_symbol() diff --git a/src/widgets/event.rs b/src/widgets/event.rs index e3499ab..5d57a42 100644 --- a/src/widgets/event.rs +++ b/src/widgets/event.rs @@ -8,7 +8,7 @@ use time::{OffsetDateTime, macros::format_description}; use crate::{ common::{AppTime, Style}, - duration::{CalendarDuration, CalendarDurationDirection}, + duration::CalendarDuration, events::{AppEventTx, TuiEvent, TuiEventHandler}, utils::center, widgets::{clock, clock_elements::DIGIT_HEIGHT}, @@ -22,6 +22,9 @@ pub struct EventState { app_time: OffsetDateTime, start_time: OffsetDateTime, with_decis: bool, + /// counter to simulate `DONE` state + /// Default value: `None` + done_count: Option, } pub struct EventStateArgs { @@ -54,12 +57,17 @@ impl EventState { app_time: app_datetime, start_time: app_datetime, with_decis, + done_count: None, } } + // Sets `app_time` pub fn set_app_time(&mut self, app_time: AppTime) { let app_datetime = OffsetDateTime::from(app_time); self.app_time = app_datetime; + + // Since updating `app_time` is like a `Tick`, we check `done` state here + self.check_done(); } pub fn set_with_decis(&mut self, with_decis: bool) { @@ -69,6 +77,25 @@ impl EventState { pub fn get_percentage_done(&self) -> u16 { get_percentage(self.start_time, self.event_time, self.app_time) } + + pub fn get_duration(&mut self) -> CalendarDuration { + CalendarDuration::from_start_end_times(self.event_time, self.app_time) + } + + fn check_done(&mut self) { + let clock_duration = self.get_duration(); + if clock_duration.is_since() { + let duration: Duration = clock_duration.into(); + // give some offset to make sure we are around `Duration::ZERO` + // Without that we might miss it, because the app runs on its own FPS + if duration < Duration::from_millis(100) { + // reset `done_count` + self.done_count = Some(clock::MAX_DONE_COUNT); + } + // count (possible) `done` + self.done_count = clock::count_clock_done(self.done_count); + } + } } impl TuiEventHandler for EventState { @@ -104,8 +131,7 @@ impl StatefulWidget for EventWidget { type State = EventState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let with_decis = state.with_decis; - let clock_duration = - CalendarDuration::from_start_end_times(state.event_time, state.app_time); + let clock_duration = state.get_duration(); 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(); @@ -117,8 +143,7 @@ impl StatefulWidget for EventWidget { "[year]-[month]-[day] [hour]:[minute]:[second]" )) .unwrap_or_else(|e| format!("time format error: {}", e)); - - let time_prefix = if clock_duration.direction() == &CalendarDurationDirection::Since { + let time_prefix = if clock_duration.is_since() { let duration: Duration = clock_duration.clone().into(); // Show `done` for a short of time (1 sec) if duration < Duration::from_secs(1) { @@ -150,8 +175,9 @@ impl StatefulWidget for EventWidget { ])) .areas(area); - // TODO: Add logic to handle blink in `DONE` mode, similar to `ClockWidget::should_blink` - let symbol = if self.blink { + // To simulate a blink effect, just use an "empty" symbol (string) + // It's "empty" all digits and creates an "empty" render area + let symbol = if self.blink && clock::should_blink(state.done_count) { " " } else { self.style.get_digit_symbol()