feat(event) blink effect at event time (#123)
Similar to `Countdown` and `Pomodoro` DONE effects.
This commit is contained in:
parent
4594bc722e
commit
6b6221803c
@ -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(),
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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,16 +447,15 @@ 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)]
|
||||||
@ -649,10 +648,11 @@ 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`
|
||||||
@ -660,7 +660,6 @@ where
|
|||||||
.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
|
||||||
// depending on given `Format` and `with_decis` params
|
// depending on given `Format` and `with_decis` params
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user