feat(event) blink effect at event time (#123)

Similar to `Countdown` and `Pomodoro` DONE effects.
This commit is contained in:
Jens Krause 2025-10-10 10:18:37 +02:00 committed by GitHub
parent 4594bc722e
commit 6b6221803c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 34 deletions

View File

@ -213,7 +213,7 @@ impl App {
event: EventState::new(EventStateArgs { event: EventState::new(EventStateArgs {
app_time, app_time,
event_time: time::PrimitiveDateTime::parse( 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]"), format_description!("[year]-[month]-[day] [hour]:[minute]:[second]"),
) )
.unwrap(), .unwrap(),

View File

@ -92,7 +92,7 @@ pub struct CalendarDuration {
direction: CalendarDurationDirection, direction: CalendarDurationDirection,
} }
#[derive(PartialEq, Debug, Clone)] #[derive(PartialEq, Debug, Clone, Copy)]
pub enum CalendarDurationDirection { pub enum CalendarDurationDirection {
Since, Since,
Until, Until,
@ -124,8 +124,12 @@ impl CalendarDuration {
} }
} }
pub fn direction(&self) -> &CalendarDurationDirection { pub fn direction(&self) -> CalendarDurationDirection {
&self.direction self.direction
}
pub fn is_since(&self) -> bool {
self.direction == CalendarDurationDirection::Since
} }
pub fn start_time(&self) -> &OffsetDateTime { pub fn start_time(&self) -> &OffsetDateTime {

View File

@ -161,7 +161,7 @@ pub fn count_by_mode(times: u32, mode: &Mode) -> Duration {
} }
const RANGE_OF_DONE_COUNT: u64 = 4; 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<T> { pub struct ClockState<T> {
type_id: ClockTypeId, type_id: ClockTypeId,
@ -447,18 +447,17 @@ impl<T> ClockState<T> {
/// `tick` won't be called again after `Mode::Done` event (e.g. in `widget::Countdown`). /// `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". /// That's why `update_done_count` is called from "outside".
pub fn update_done_count(&mut self) { pub fn update_done_count(&mut self) {
if let Some(count) = self.done_count { self.done_count = count_clock_done(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
}
}
} }
} }
/// Safe way to count a possible `done` value
pub fn count_clock_done(value: Option<u64>) -> Option<u64> {
// Safe substraction for `Some(value > 1)`
// `None` means `done` == no counting anymore.
value.and_then(|count| count.checked_sub(1))
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Countdown {} pub struct Countdown {}
@ -649,17 +648,17 @@ where
pub fn get_height(&self) -> u16 { pub fn get_height(&self) -> u16 {
DIGIT_HEIGHT DIGIT_HEIGHT
} }
}
/// Checks whether to blink the clock while rendering. /// Helper to check whether to blink the clock while rendering.
/// Its logic is based on a given `count` value. /// Its logic is based on a given `count` value.
fn should_blink(&self, count_value: &Option<u64>) -> bool { pub fn should_blink(count_value: Option<u64>) -> bool {
// Example: // Example:
// if `RANGE_OF_DONE_COUNT` is 4 // if `RANGE_OF_DONE_COUNT` is 4
// then for ranges `0..4`, `8..12` etc. it will return `true` // then for ranges `0..4`, `8..12` etc. it will return `true`
count_value count_value
.map(|b| (b % (RANGE_OF_DONE_COUNT * 2)) < RANGE_OF_DONE_COUNT) .map(|b| (b % (RANGE_OF_DONE_COUNT * 2)) < RANGE_OF_DONE_COUNT)
.unwrap_or(false) .unwrap_or(false)
}
} }
// Helper to get horizontal lengths of a clock // Helper to get horizontal lengths of a clock
@ -1494,9 +1493,9 @@ where
let format = state.format; let format = state.format;
let widths = clock_horizontal_lengths(&format, with_decis); let widths = clock_horizontal_lengths(&format, with_decis);
// to simulate a blink effect, just use an "empty" symbol (string) // To simulate a blink effect, just use an "empty" symbol (string)
// to "empty" all digits and to have an "empty" render area // It's "empty" all digits and creates an "empty" render area
let symbol = if self.blink && self.should_blink(&state.done_count) { let symbol = if self.blink && should_blink(state.done_count) {
" " " "
} else { } else {
self.style.get_digit_symbol() self.style.get_digit_symbol()

View File

@ -8,7 +8,7 @@ use time::{OffsetDateTime, macros::format_description};
use crate::{ use crate::{
common::{AppTime, Style}, common::{AppTime, Style},
duration::{CalendarDuration, CalendarDurationDirection}, duration::CalendarDuration,
events::{AppEventTx, TuiEvent, TuiEventHandler}, events::{AppEventTx, TuiEvent, TuiEventHandler},
utils::center, utils::center,
widgets::{clock, clock_elements::DIGIT_HEIGHT}, widgets::{clock, clock_elements::DIGIT_HEIGHT},
@ -22,6 +22,9 @@ pub struct EventState {
app_time: OffsetDateTime, app_time: OffsetDateTime,
start_time: OffsetDateTime, start_time: OffsetDateTime,
with_decis: bool, with_decis: bool,
/// counter to simulate `DONE` state
/// Default value: `None`
done_count: Option<u64>,
} }
pub struct EventStateArgs { pub struct EventStateArgs {
@ -54,12 +57,17 @@ impl EventState {
app_time: app_datetime, app_time: app_datetime,
start_time: app_datetime, start_time: app_datetime,
with_decis, with_decis,
done_count: None,
} }
} }
// Sets `app_time`
pub fn set_app_time(&mut self, app_time: AppTime) { pub fn set_app_time(&mut self, app_time: AppTime) {
let app_datetime = OffsetDateTime::from(app_time); let app_datetime = OffsetDateTime::from(app_time);
self.app_time = app_datetime; 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) { pub fn set_with_decis(&mut self, with_decis: bool) {
@ -69,6 +77,25 @@ impl EventState {
pub fn get_percentage_done(&self) -> u16 { pub fn get_percentage_done(&self) -> u16 {
get_percentage(self.start_time, self.event_time, self.app_time) 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 { impl TuiEventHandler for EventState {
@ -104,8 +131,7 @@ 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 with_decis = state.with_decis; let with_decis = state.with_decis;
let clock_duration = let clock_duration = state.get_duration();
CalendarDuration::from_start_end_times(state.event_time, state.app_time);
let clock_format = clock::format_by_duration(&clock_duration); let clock_format = clock::format_by_duration(&clock_duration);
let clock_widths = clock::clock_horizontal_lengths(&clock_format, with_decis); let clock_widths = clock::clock_horizontal_lengths(&clock_format, with_decis);
let clock_width = clock_widths.iter().sum(); let clock_width = clock_widths.iter().sum();
@ -117,8 +143,7 @@ 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 = if clock_duration.is_since() {
let time_prefix = if clock_duration.direction() == &CalendarDurationDirection::Since {
let duration: Duration = clock_duration.clone().into(); let duration: Duration = clock_duration.clone().into();
// Show `done` for a short of time (1 sec) // Show `done` for a short of time (1 sec)
if duration < Duration::from_secs(1) { if duration < Duration::from_secs(1) {
@ -150,8 +175,9 @@ impl StatefulWidget for EventWidget {
])) ]))
.areas(area); .areas(area);
// TODO: Add logic to handle blink in `DONE` mode, similar to `ClockWidget<T>::should_blink` // To simulate a blink effect, just use an "empty" symbol (string)
let symbol = if self.blink { // It's "empty" all digits and creates an "empty" render area
let symbol = if self.blink && clock::should_blink(state.done_count) {
" " " "
} else { } else {
self.style.get_digit_symbol() self.style.get_digit_symbol()