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 {
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(),

View File

@ -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 {

View File

@ -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<T> {
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`).
/// 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<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)]
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<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 check whether to blink the clock while rendering.
/// Its logic is based on a given `count` value.
pub fn should_blink(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
@ -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()

View File

@ -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<u64>,
}
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<T>::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()