From 6d2bf5ac09603d2e60136dbf48c8f17549e13511 Mon Sep 17 00:00:00 2001 From: Jens Krause <47693+sectore@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:44:56 +0100 Subject: [PATCH] Edit `countdown` by local time (#49) --- src/app.rs | 38 ++++- src/common.rs | 7 + src/duration.rs | 4 + src/widgets.rs | 1 + src/widgets/clock.rs | 88 ++++++----- src/widgets/clock_elements.rs | 1 + src/widgets/countdown.rs | 273 +++++++++++++++++++++++++++------- src/widgets/edit_time.rs | 231 ++++++++++++++++++++++++++++ src/widgets/footer.rs | 61 ++++---- 9 files changed, 572 insertions(+), 132 deletions(-) create mode 100644 src/widgets/edit_time.rs diff --git a/src/app.rs b/src/app.rs index b8abcc0..307392b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use crate::{ args::Args, - common::{AppTime, AppTimeFormat, Content, Style}, + common::{AppEditMode, AppTime, AppTimeFormat, Content, Style}, constants::TICK_VALUE_MS, events::{Event, EventHandler, Events}, storage::AppStorage, @@ -112,10 +112,11 @@ impl App { with_decis, pomodoro_mode, } = args; + let app_time = get_app_time(); Self { mode: Mode::Running, content, - app_time: get_app_time(), + app_time, style, with_decis, countdown: CountdownState::new( @@ -126,6 +127,7 @@ impl App { with_decis, }), elapsed_value_countdown, + app_time, ), timer: TimerState::new(ClockState::::new(ClockStateArgs { initial_value: Duration::ZERO, @@ -150,6 +152,7 @@ impl App { if let Some(event) = events.next().await { if matches!(event, Event::Tick) { self.app_time = get_app_time(); + self.countdown.set_app_time(self.app_time); } // Pipe events into subviews and handle only 'unhandled' events afterwards @@ -175,11 +178,32 @@ impl App { self.mode != Mode::Quit } - fn is_edit_mode(&self) -> bool { + fn get_edit_mode(&self) -> AppEditMode { match self.content { - Content::Countdown => self.countdown.get_clock().is_edit_mode(), - Content::Timer => self.timer.get_clock().is_edit_mode(), - Content::Pomodoro => self.pomodoro.get_clock().is_edit_mode(), + Content::Countdown => { + if self.countdown.is_clock_edit_mode() { + AppEditMode::Clock + } else if self.countdown.is_time_edit_mode() { + AppEditMode::Time + } else { + AppEditMode::None + } + } + + Content::Timer => { + if self.timer.get_clock().is_edit_mode() { + AppEditMode::Clock + } else { + AppEditMode::None + } + } + Content::Pomodoro => { + if self.pomodoro.get_clock().is_edit_mode() { + AppEditMode::Clock + } else { + AppEditMode::None + } + } } } @@ -298,7 +322,7 @@ impl StatefulWidget for AppWidget { Footer { running_clock: state.clock_is_running(), selected_content: state.content, - edit_mode: state.is_edit_mode(), + app_edit_mode: state.get_edit_mode(), app_time: state.app_time, } .render(v2, buf, &mut state.footer); diff --git a/src/common.rs b/src/common.rs index f807400..a5e435f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -128,6 +128,13 @@ impl AppTime { } } +#[derive(Debug)] +pub enum AppEditMode { + None, + Clock, + Time, +} + #[cfg(test)] mod tests { diff --git a/src/duration.rs b/src/duration.rs index 77a4821..816096f 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -20,6 +20,10 @@ pub const MINS_PER_HOUR: u64 = 60; // https://doc.rust-lang.org/src/core/time.rs.html#36 const HOURS_PER_DAY: u64 = 24; +// max. 99:59:59 +pub const MAX_DURATION: Duration = + Duration::from_secs(100 * MINS_PER_HOUR * SECS_PER_MINUTE).saturating_sub(ONE_SECOND); + #[derive(Debug, Clone, Copy, PartialOrd)] pub struct DurationEx { inner: Duration, diff --git a/src/widgets.rs b/src/widgets.rs index b92dfd5..acfc924 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -5,6 +5,7 @@ pub mod clock_elements_test; #[cfg(test)] pub mod clock_test; pub mod countdown; +pub mod edit_time; pub mod footer; pub mod header; pub mod pomodoro; diff --git a/src/widgets/clock.rs b/src/widgets/clock.rs index 08fad44..e297349 100644 --- a/src/widgets/clock.rs +++ b/src/widgets/clock.rs @@ -11,20 +11,13 @@ use ratatui::{ use crate::{ common::Style, - duration::{ - DurationEx, MINS_PER_HOUR, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND, - SECS_PER_MINUTE, - }, + duration::{DurationEx, MAX_DURATION, ONE_DECI_SECOND, ONE_HOUR, ONE_MINUTE, ONE_SECOND}, utils::center_horizontal, widgets::clock_elements::{ - Colon, Digit, Dot, COLON_WIDTH, DIGIT_HEIGHT, DIGIT_WIDTH, DOT_WIDTH, + Colon, Digit, Dot, COLON_WIDTH, DIGIT_HEIGHT, DIGIT_SPACE_WIDTH, DIGIT_WIDTH, DOT_WIDTH, }, }; -// max. 99:59:59 -const MAX_DURATION: Duration = - Duration::from_secs(100 * MINS_PER_HOUR * SECS_PER_MINUTE).saturating_sub(ONE_SECOND); - #[derive(Debug, Copy, Clone, Display, PartialEq, Eq)] pub enum Time { Decis, @@ -128,6 +121,11 @@ impl ClockState { &self.current_value } + pub fn set_current_value(&mut self, duration: DurationEx) { + self.current_value = duration; + self.update_format(); + } + pub fn toggle_edit(&mut self) { self.mode = match self.mode.clone() { Mode::Editable(_, prev) => { @@ -463,8 +461,6 @@ impl ClockState { } } -const SPACE_WIDTH: u16 = 1; - pub struct ClockWidget where T: std::fmt::Debug, @@ -498,61 +494,61 @@ where match format { Format::HhMmSs => add_decis( vec![ - DIGIT_WIDTH, // h - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // h - COLON_WIDTH, // : - DIGIT_WIDTH, // m - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // m - COLON_WIDTH, // : - DIGIT_WIDTH, // s - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // s + DIGIT_WIDTH, // h + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // h + COLON_WIDTH, // : + DIGIT_WIDTH, // m + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // m + COLON_WIDTH, // : + DIGIT_WIDTH, // s + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // s ], with_decis, ), Format::HMmSs => add_decis( vec![ - DIGIT_WIDTH, // h - COLON_WIDTH, // : - DIGIT_WIDTH, // m - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // m - COLON_WIDTH, // : - DIGIT_WIDTH, // s - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // s + DIGIT_WIDTH, // h + COLON_WIDTH, // : + DIGIT_WIDTH, // m + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // m + COLON_WIDTH, // : + DIGIT_WIDTH, // s + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // s ], with_decis, ), Format::MmSs => add_decis( vec![ - DIGIT_WIDTH, // m - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // m - COLON_WIDTH, // : - DIGIT_WIDTH, // s - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // s + DIGIT_WIDTH, // m + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // m + COLON_WIDTH, // : + DIGIT_WIDTH, // s + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // s ], with_decis, ), Format::MSs => add_decis( vec![ - DIGIT_WIDTH, // m - COLON_WIDTH, // : - DIGIT_WIDTH, // s - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // s + DIGIT_WIDTH, // m + COLON_WIDTH, // : + DIGIT_WIDTH, // s + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // s ], with_decis, ), Format::Ss => add_decis( vec![ - DIGIT_WIDTH, // s - SPACE_WIDTH, // (space) - DIGIT_WIDTH, // s + DIGIT_WIDTH, // s + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // s ], with_decis, ), diff --git a/src/widgets/clock_elements.rs b/src/widgets/clock_elements.rs index 923b9dc..c8c9765 100644 --- a/src/widgets/clock_elements.rs +++ b/src/widgets/clock_elements.rs @@ -9,6 +9,7 @@ pub const DIGIT_WIDTH: u16 = DIGIT_SIZE as u16; pub const DIGIT_HEIGHT: u16 = DIGIT_SIZE as u16 + 1 /* border height */; pub const COLON_WIDTH: u16 = 4; // incl. padding left + padding right pub const DOT_WIDTH: u16 = 4; // incl. padding left + padding right +pub const DIGIT_SPACE_WIDTH: u16 = 1; // space between digits #[rustfmt::skip] const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ diff --git a/src/widgets/countdown.rs b/src/widgets/countdown.rs index 4f869e0..492d18e 100644 --- a/src/widgets/countdown.rs +++ b/src/widgets/countdown.rs @@ -1,3 +1,15 @@ +use crate::{ + common::{AppTime, Style}, + constants::TICK_VALUE_MS, + duration::{DurationEx, MAX_DURATION}, + events::{Event, EventHandler}, + utils::center, + widgets::{ + clock::{self, ClockState, ClockStateArgs, ClockWidget, Mode as ClockMode}, + edit_time::EditTimeState, + }, +}; +use crossterm::event::KeyModifiers; use ratatui::{ buffer::Buffer, crossterm::event::KeyCode, @@ -5,16 +17,12 @@ use ratatui::{ text::Line, widgets::{StatefulWidget, Widget}, }; -use std::{cmp::max, time::Duration}; -use crate::{ - common::Style, - constants::TICK_VALUE_MS, - duration::DurationEx, - events::{Event, EventHandler}, - utils::center, - widgets::clock::{self, ClockState, ClockStateArgs, ClockWidget, Mode as ClockMode}, -}; +use std::ops::Sub; +use std::{cmp::max, time::Duration}; +use time::OffsetDateTime; + +use super::edit_time::{EditTimeStateArgs, EditTimeWidget}; /// State for Countdown Widget #[derive(Debug, Clone)] @@ -23,10 +31,17 @@ pub struct CountdownState { clock: ClockState, /// clock to count time after `DONE` - similar to Mission Elapsed Time (MET) elapsed_clock: ClockState, + app_time: AppTime, + /// Edit by local time + edit_time: Option, } impl CountdownState { - pub fn new(clock: ClockState, elapsed_value: Duration) -> Self { + pub fn new( + clock: ClockState, + elapsed_value: Duration, + app_time: AppTime, + ) -> Self { Self { clock, elapsed_clock: ClockState::::new(ClockStateArgs { @@ -43,6 +58,8 @@ impl CountdownState { } else { ClockMode::Initial }), + app_time, + edit_time: None, } } @@ -62,11 +79,55 @@ impl CountdownState { pub fn get_elapsed_value(&self) -> &DurationEx { self.elapsed_clock.get_current_value() } + + pub fn set_app_time(&mut self, app_time: AppTime) { + self.app_time = app_time; + } + + fn time_to_edit(&self) -> OffsetDateTime { + // get current value + let d: Duration = (*self.clock.get_current_value()).into(); + // transform + let dd = time::Duration::try_from(d).unwrap_or(time::Duration::ZERO); + // substract from `app_time` + OffsetDateTime::from(self.app_time).saturating_add(dd) + } + + pub fn min_time_to_edit(&self) -> OffsetDateTime { + OffsetDateTime::from(self.app_time) + } + + fn max_time_to_edit(&self) -> OffsetDateTime { + OffsetDateTime::from(self.app_time) + .saturating_add(time::Duration::try_from(MAX_DURATION).unwrap_or(time::Duration::ZERO)) + } + + fn edit_time_done(&mut self, edit_time: &mut EditTimeState) { + // get diff + let d: time::Duration = edit_time + .get_time() + .sub(OffsetDateTime::from(self.app_time)); + // transfrom + let dx: DurationEx = Duration::try_from(d).unwrap_or(Duration::ZERO).into(); + // update clock + self.clock.set_current_value(dx); + // remove `edit_time` + self.edit_time = None; + } + + pub fn is_clock_edit_mode(&self) -> bool { + self.clock.is_edit_mode() + } + + pub fn is_time_edit_mode(&self) -> bool { + self.edit_time.is_some() + } } impl EventHandler for CountdownState { fn update(&mut self, event: Event) -> Option { - let edit_mode = self.clock.is_edit_mode(); + let is_edit_clock = self.clock.is_edit_mode(); + let is_edit_time = self.edit_time.is_some(); match event { Event::Tick => { if !self.clock.is_done() { @@ -77,12 +138,24 @@ impl EventHandler for CountdownState { self.elapsed_clock.run(); } } + let min_time = self.min_time_to_edit(); + let max_time = self.max_time_to_edit(); + if let Some(edit_time) = &mut self.edit_time { + edit_time.set_min_time(min_time); + edit_time.set_max_time(max_time); + } } Event::Key(key) => match key.code { KeyCode::Char('r') => { - // reset both clocks + // reset both clocks to use intial values self.clock.reset(); self.elapsed_clock.reset(); + + // reset `edit_time` back initial value + let time = self.time_to_edit(); + if let Some(edit_time) = &mut self.edit_time { + edit_time.set_time(time); + } } KeyCode::Char('s') => { // toggle pause status depending on which clock is running @@ -91,30 +164,92 @@ impl EventHandler for CountdownState { } else { self.elapsed_clock.toggle_pause(); } + + // finish `edit_time` and continue for using `clock` + if let Some(edit_time) = &mut self.edit_time.clone() { + self.edit_time_done(edit_time); + } } - KeyCode::Char('e') => { - self.clock.toggle_edit(); - // stop + reset timer entering `edit` mode + // STRG + e => toggle edit time + KeyCode::Char('e') if key.modifiers.contains(KeyModifiers::CONTROL) => { + // stop editing clock + if self.clock.is_edit_mode() { + // toggle edit mode + self.clock.toggle_edit(); + } + + if let Some(edit_time) = &mut self.edit_time.clone() { + self.edit_time_done(edit_time) + } else { + // update `edit_time` + self.edit_time = Some(EditTimeState::new(EditTimeStateArgs { + time: self.time_to_edit(), + min: self.min_time_to_edit(), + max: self.max_time_to_edit(), + })); + } + + // stop `clock` + if self.clock.is_running() { + self.clock.toggle_pause(); + } + + // stop `elapsed_clock` if self.elapsed_clock.is_running() { self.elapsed_clock.toggle_pause(); } } - KeyCode::Left if edit_mode => { + // STRG + e => toggle edit clock + KeyCode::Char('e') => { + // toggle edit mode + self.clock.toggle_edit(); + + // stop `elapsed_clock` + if self.elapsed_clock.is_running() { + self.elapsed_clock.toggle_pause(); + } + + // finish `edit_time` and continue for using `clock` + if let Some(edit_time) = &mut self.edit_time.clone() { + self.edit_time_done(edit_time); + } + } + KeyCode::Left if is_edit_clock => { self.clock.edit_next(); } - KeyCode::Right if edit_mode => { + KeyCode::Left if is_edit_time => { + // safe unwrap because of previous check in `is_edit_time` + self.edit_time.as_mut().unwrap().next(); + } + KeyCode::Right if is_edit_clock => { self.clock.edit_prev(); } - KeyCode::Up if edit_mode => { + KeyCode::Right if is_edit_time => { + // safe unwrap because of previous check in `is_edit_time` + self.edit_time.as_mut().unwrap().prev(); + } + KeyCode::Up if is_edit_clock => { self.clock.edit_up(); // whenever `clock`'s value is changed, reset `elapsed_clock` self.elapsed_clock.reset(); } - KeyCode::Down if edit_mode => { + KeyCode::Up if is_edit_time => { + // safe unwrap because of previous check in `is_edit_time` + self.edit_time.as_mut().unwrap().up(); + // whenever `clock`'s value is changed, reset `elapsed_clock` + self.elapsed_clock.reset(); + } + KeyCode::Down if is_edit_clock => { self.clock.edit_down(); // whenever clock value is changed, reset timer self.elapsed_clock.reset(); } + KeyCode::Down if is_edit_time => { + // safe unwrap because of previous check in `is_edit_time` + self.edit_time.as_mut().unwrap().down(); + // whenever clock value is changed, reset timer + self.elapsed_clock.reset(); + } _ => return Some(event), }, _ => return Some(event), @@ -127,47 +262,77 @@ pub struct Countdown { pub style: Style, } +fn human_days_diff(a: &OffsetDateTime, b: &OffsetDateTime) -> String { + let days_diff = (a.date() - b.date()).whole_days(); + match days_diff { + 0 => "today".to_owned(), + 1 => "tomorrow".to_owned(), + n => format!("+{}days", n), + } +} + impl StatefulWidget for Countdown { type State = CountdownState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - let clock = ClockWidget::new(self.style); + // render `edit_time` OR `clock` + if let Some(edit_time) = &mut state.edit_time { + let label = Line::raw( + format!( + "Countdown {} {}", + edit_time.get_selected().clone(), + human_days_diff(edit_time.get_time(), &state.app_time.into()) + ) + .to_uppercase(), + ); + let widget = EditTimeWidget::new(self.style); + let area = center( + area, + Constraint::Length(max(widget.get_width(), label.width() as u16)), + Constraint::Length(widget.get_height() + 1 /* height of label */), + ); + let [v1, v2] = + Layout::vertical(Constraint::from_lengths([widget.get_height(), 1])).areas(area); - let label = Line::raw( - if state.clock.is_done() { - if state.clock.with_decis { - format!( - "Countdown {} +{}", - state.clock.get_mode(), - state - .elapsed_clock - .get_current_value() - .to_string_with_decis() - ) + widget.render(v1, buf, edit_time); + label.centered().render(v2, buf); + } else { + let label = Line::raw( + if state.clock.is_done() { + if state.clock.with_decis { + format!( + "Countdown {} +{}", + state.clock.get_mode(), + state + .elapsed_clock + .get_current_value() + .to_string_with_decis() + ) + } else { + format!( + "Countdown {} +{}", + state.clock.get_mode(), + state.elapsed_clock.get_current_value() + ) + } } else { - format!( - "Countdown {} +{}", - state.clock.get_mode(), - state.elapsed_clock.get_current_value() - ) + format!("Countdown {}", state.clock.get_mode()) } - } else { - format!("Countdown {}", state.clock.get_mode()) - } - .to_uppercase(), - ); + .to_uppercase(), + ); + let widget = ClockWidget::new(self.style); + let area = center( + area, + Constraint::Length(max( + widget.get_width(&state.clock.get_format(), state.clock.with_decis), + label.width() as u16, + )), + Constraint::Length(widget.get_height() + 1 /* height of label */), + ); + let [v1, v2] = + Layout::vertical(Constraint::from_lengths([widget.get_height(), 1])).areas(area); - let area = center( - area, - Constraint::Length(max( - clock.get_width(&state.clock.get_format(), state.clock.with_decis), - label.width() as u16, - )), - Constraint::Length(clock.get_height() + 1 /* height of label */), - ); - let [v1, v2] = - Layout::vertical(Constraint::from_lengths([clock.get_height(), 1])).areas(area); - - clock.render(v1, buf, &mut state.clock); - label.centered().render(v2, buf); + widget.render(v1, buf, &mut state.clock); + label.centered().render(v2, buf); + } } } diff --git a/src/widgets/edit_time.rs b/src/widgets/edit_time.rs new file mode 100644 index 0000000..9a709bd --- /dev/null +++ b/src/widgets/edit_time.rs @@ -0,0 +1,231 @@ +use std::fmt; +use time::OffsetDateTime; + +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + widgets::{StatefulWidget, Widget}, +}; + +use crate::{ + common::Style, + widgets::clock_elements::{Colon, Digit, COLON_WIDTH, DIGIT_SPACE_WIDTH, DIGIT_WIDTH}, +}; + +use super::clock_elements::DIGIT_HEIGHT; + +#[derive(Debug, Clone)] +pub enum Selected { + Seconds, + Minutes, + Hours, +} + +impl Selected { + pub fn next(&self) -> Self { + match self { + Selected::Seconds => Selected::Minutes, + Selected::Minutes => Selected::Hours, + Selected::Hours => Selected::Seconds, + } + } + + pub fn prev(&self) -> Self { + match self { + Selected::Seconds => Selected::Hours, + Selected::Minutes => Selected::Seconds, + Selected::Hours => Selected::Minutes, + } + } +} + +impl fmt::Display for Selected { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Selected::Seconds => write!(f, "[edit seconds]"), + Selected::Minutes => write!(f, "[edit minutes]"), + Selected::Hours => write!(f, "[edit hours]"), + } + } +} + +#[derive(Debug, Clone)] +pub struct EditTimeState { + selected: Selected, + time: OffsetDateTime, + min: OffsetDateTime, + max: OffsetDateTime, +} + +#[derive(Debug, Clone)] +pub struct EditTimeStateArgs { + pub time: OffsetDateTime, + pub min: OffsetDateTime, + pub max: OffsetDateTime, +} + +impl EditTimeState { + pub fn new(args: EditTimeStateArgs) -> Self { + EditTimeState { + time: args.time, + min: args.min, + max: args.max, + selected: Selected::Minutes, + } + } + + pub fn set_time(&mut self, time: OffsetDateTime) { + self.time = time; + } + + pub fn set_min_time(&mut self, min: OffsetDateTime) { + self.min = min; + } + + pub fn set_max_time(&mut self, min: OffsetDateTime) { + self.max = min; + } + + pub fn get_time(&mut self) -> &OffsetDateTime { + &self.time + } + + pub fn get_selected(&mut self) -> &Selected { + &self.selected + } + + pub fn next(&mut self) { + self.selected = self.selected.next(); + } + + pub fn prev(&mut self) { + self.selected = self.selected.prev(); + } + + pub fn up(&mut self) { + self.time = match self.selected { + Selected::Seconds => { + if self + .time + .lt(&self.max.saturating_sub(time::Duration::new(1, 0))) + { + self.time.saturating_add(time::Duration::new(1, 0)) + } else { + self.time + } + } + Selected::Minutes => { + if self + .time + .lt(&self.max.saturating_sub(time::Duration::new(60, 0))) + { + self.time.saturating_add(time::Duration::new(60, 0)) + } else { + self.time + } + } + Selected::Hours => { + if self + .time + .lt(&self.max.saturating_sub(time::Duration::new(60 * 60, 0))) + { + self.time.saturating_add(time::Duration::new(60 * 60, 0)) + } else { + self.time + } + } + } + } + + pub fn down(&mut self) { + self.time = match self.selected { + Selected::Seconds => { + if self + .time + .ge(&self.min.saturating_add(time::Duration::new(1, 0))) + { + self.time.saturating_sub(time::Duration::new(1, 0)) + } else { + self.time + } + } + Selected::Minutes => { + if self + .time + .ge(&self.min.saturating_add(time::Duration::new(60, 0))) + { + self.time.saturating_sub(time::Duration::new(60, 0)) + } else { + self.time + } + } + Selected::Hours => { + if self + .time + .ge(&self.min.saturating_add(time::Duration::new(60 * 60, 0))) + { + self.time.saturating_sub(time::Duration::new(60 * 60, 0)) + } else { + self.time + } + } + } + } +} + +#[derive(Debug, Clone)] +pub struct EditTimeWidget { + style: Style, +} + +impl EditTimeWidget { + pub fn new(style: Style) -> Self { + Self { style } + } + + fn get_horizontal_lengths(&self) -> Vec { + vec![ + DIGIT_WIDTH, // h + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // h + COLON_WIDTH, // : + DIGIT_WIDTH, // m + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // m + COLON_WIDTH, // : + DIGIT_WIDTH, // s + DIGIT_SPACE_WIDTH, // (space) + DIGIT_WIDTH, // s + ] + } + + pub fn get_width(&self) -> u16 { + self.get_horizontal_lengths().iter().sum() + } + + pub fn get_height(&self) -> u16 { + DIGIT_HEIGHT + } +} + +impl StatefulWidget for EditTimeWidget { + type State = EditTimeState; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let symbol = self.style.get_digit_symbol(); + let edit_hours = matches!(state.selected, Selected::Hours); + let edit_minutes = matches!(state.selected, Selected::Minutes); + let edit_secs = matches!(state.selected, Selected::Seconds); + + let [hh, _, h, c_hm, mm, _, m, c_ms, ss, _, s] = + Layout::horizontal(Constraint::from_lengths(self.get_horizontal_lengths())).areas(area); + + Digit::new((state.time.hour() as u64) / 10, edit_hours, symbol).render(hh, buf); + Digit::new((state.time.hour() as u64) % 10, edit_hours, symbol).render(h, buf); + Colon::new(symbol).render(c_hm, buf); + Digit::new((state.time.minute() as u64) / 10, edit_minutes, symbol).render(mm, buf); + Digit::new((state.time.minute() as u64) % 10, edit_minutes, symbol).render(m, buf); + Colon::new(symbol).render(c_ms, buf); + Digit::new((state.time.second() as u64) / 10, edit_secs, symbol).render(ss, buf); + Digit::new((state.time.second() as u64) % 10, edit_secs, symbol).render(s, buf); + } +} diff --git a/src/widgets/footer.rs b/src/widgets/footer.rs index 8a22f66..9fc6810 100644 --- a/src/widgets/footer.rs +++ b/src/widgets/footer.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use crate::common::{AppTime, AppTimeFormat, Content}; +use crate::common::{AppEditMode, AppTime, AppTimeFormat, Content}; use ratatui::{ buffer::Buffer, layout::{Constraint, Layout, Rect}, @@ -45,7 +45,7 @@ impl FooterState { pub struct Footer { pub running_clock: bool, pub selected_content: Content, - pub edit_mode: bool, + pub app_edit_mode: AppEditMode, pub app_time: AppTime, } @@ -139,9 +139,39 @@ impl StatefulWidget for Footer { Style::default().add_modifier(Modifier::BOLD), )), Cell::from(Line::from({ - if self.edit_mode { - vec![ - Span::from("[e]dit done"), + match self.app_edit_mode { + AppEditMode::None => { + let mut spans = vec![ + Span::from(if self.running_clock { + "[s]top" + } else { + "[s]tart" + }), + Span::from(SPACE), + Span::from("[r]eset"), + Span::from(SPACE), + Span::from("[e]dit"), + ]; + if self.selected_content == Content::Countdown { + spans.extend_from_slice(&[ + Span::from(SPACE), + Span::from("[ctrl+e]dit by local time"), + ]); + } + if self.selected_content == Content::Pomodoro { + spans.extend_from_slice(&[ + Span::from(SPACE), + Span::from("[← →]switch work/pause"), + ]); + } + spans + } + others => vec![ + Span::from(match others { + AppEditMode::Clock => "[e]dit done", + AppEditMode::Time => "[ctrl+e]dit done", + _ => "", + }), Span::from(SPACE), Span::from(format!( "[{} {}]edit selection", @@ -152,26 +182,7 @@ impl StatefulWidget for Footer { Span::from(format!("[{}]edit up", scrollbar::VERTICAL.begin)), // ↑ Span::from(SPACE), Span::from(format!("[{}]edit up", scrollbar::VERTICAL.end)), // ↓, - ] - } else { - let mut spans = vec![ - Span::from(if self.running_clock { - "[s]top" - } else { - "[s]tart" - }), - Span::from(SPACE), - Span::from("[r]eset"), - Span::from(SPACE), - Span::from("[e]dit"), - ]; - if self.selected_content == Content::Pomodoro { - spans.extend_from_slice(&[ - Span::from(SPACE), - Span::from("[← →]switch work/pause"), - ]); - } - spans + ], } })), ]),