fix(editable): auto jump to next possible editable while decreasing, but ignoring zero values (#109)

* extract `format_by_duration`

* fix(editable): switch to next ignoring zero values
This commit is contained in:
Jens Krause 2025-09-29 20:48:19 +02:00 committed by GitHub
parent 816741f842
commit 6dc7eb81c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 220 additions and 178 deletions

View File

@ -67,7 +67,7 @@ impl fmt::Display for Mode {
}
// Clock format:
// From `1 deciseconds` up to `999y 364d 23:59:59`
// From `1s` up to `999y 364d 23:59:59`
#[derive(Debug, Copy, Clone, PartialEq, Eq, Display, PartialOrd, Ord)]
pub enum Format {
S,
@ -90,6 +90,64 @@ pub enum Format {
YyyDddHhMmSs,
}
pub fn format_by_duration(d: &DurationEx) -> Format {
if d.years() >= 100 && d.days_mod() >= 100 {
Format::YyyDddHhMmSs
} else if d.years() >= 100 && d.days_mod() >= 10 {
Format::YyyDdHhMmSs
} else if d.years() >= 100 && d.days() >= 1 {
Format::YyyDHhMmSs
} else if d.years() >= 10 && d.days_mod() >= 100 {
Format::YyDddHhMmSs
} else if d.years() >= 10 && d.days_mod() >= 10 {
Format::YyDdHhMmSs
} else if d.years() >= 10 && d.days() >= 1 {
Format::YyDHhMmSs
} else if d.years() >= 1 && d.days_mod() >= 100 {
Format::YDddHhMmSs
} else if d.years() >= 1 && d.days_mod() >= 10 {
Format::YDdHhMmSs
} else if d.years() >= 1 && d.days() >= 1 {
Format::YDHhMmSs
} else if d.days() >= 100 {
Format::DddHhMmSs
} else if d.days() >= 10 {
Format::DdHhMmSs
} else if d.days() >= 1 {
Format::DHhMmSs
} else if d.hours() >= 10 {
Format::HhMmSs
} else if d.hours() >= 1 {
Format::HMmSs
} else if d.minutes() >= 10 {
Format::MmSs
} else if d.minutes() >= 1 {
Format::MSs
} else if d.seconds() >= 10 {
Format::Ss
} else {
Format::S
}
}
pub fn time_by_format(format: &Format) -> Time {
match format {
Format::YDddHhMmSs
| Format::YDdHhMmSs
| Format::YDHhMmSs
| Format::YyDddHhMmSs
| Format::YyDdHhMmSs
| Format::YyDHhMmSs
| Format::YyyDddHhMmSs
| Format::YyyDdHhMmSs
| Format::YyyDHhMmSs => Time::Years,
Format::DddHhMmSs | Format::DdHhMmSs | Format::DHhMmSs => Time::Days,
Format::HhMmSs | Format::HMmSs => Time::Hours,
Format::MmSs | Format::MSs => Time::Minutes,
Format::Ss | Format::S => Time::Seconds,
}
}
const RANGE_OF_DONE_COUNT: u64 = 4;
const MAX_DONE_COUNT: u64 = RANGE_OF_DONE_COUNT * 5;
@ -163,6 +221,10 @@ impl<T> ClockState<T> {
}
}
pub fn get_format(&self) -> &Format {
&self.format
}
pub fn get_initial_value(&self) -> &DurationEx {
&self.initial_value
}
@ -298,7 +360,8 @@ impl<T> ClockState<T> {
_ => self.current_value,
};
self.update_format();
self.update_mode();
let updated_format = *self.get_format();
self.downgrade_mode_by_format(&updated_format);
}
pub fn is_edit_mode(&self) -> bool {
@ -383,24 +446,23 @@ impl<T> ClockState<T> {
self.update_format();
}
fn update_mode(&mut self) {
// Note: Since `Format` does not include `deciseconds` for different reason,
// `Mode::Editable` can be downgraded up to `Time::Seconds` (but not to `Time::Decis`)
fn downgrade_mode_by_format(&mut self, format: &Format) {
let mode = self.mode.clone();
// FIXME: By editing an hour from 01:00:00 down,
// but `Editable` should be `Seconds`, but it's minutes
// Same for others (years, days, etc.).
let time = time_by_format(format);
self.mode = match mode {
Mode::Editable(Time::Years, prev) if self.format <= Format::DddHhMmSs => {
Mode::Editable(Time::Days, prev)
Mode::Editable(Time::Years, prev) if format <= &Format::DddHhMmSs => {
Mode::Editable(time, prev)
}
Mode::Editable(Time::Days, prev) if self.format <= Format::HhMmSs => {
Mode::Editable(Time::Hours, prev)
Mode::Editable(Time::Days, prev) if format <= &Format::HhMmSs => {
Mode::Editable(time, prev)
}
Mode::Editable(Time::Hours, prev) if self.format <= Format::MmSs => {
Mode::Editable(Time::Minutes, prev)
Mode::Editable(Time::Hours, prev) if format <= &Format::MmSs => {
Mode::Editable(time, prev)
}
Mode::Editable(Time::Minutes, prev) if self.format <= Format::Ss => {
Mode::Editable(Time::Seconds, prev)
Mode::Editable(Time::Minutes, prev) if format <= &Format::Ss => {
Mode::Editable(time, prev)
}
_ => mode,
}
@ -429,48 +491,8 @@ impl<T> ClockState<T> {
}
fn update_format(&mut self) {
self.format = self.get_format();
}
pub fn get_format(&self) -> Format {
let v = self.current_value;
if v.years() >= 100 && v.days_mod() >= 100 {
Format::YyyDddHhMmSs
} else if v.years() >= 100 && v.days_mod() >= 10 {
Format::YyyDdHhMmSs
} else if v.years() >= 100 && v.days() >= 1 {
Format::YyyDHhMmSs
} else if v.years() >= 10 && v.days_mod() >= 100 {
Format::YyDddHhMmSs
} else if v.years() >= 10 && v.days_mod() >= 10 {
Format::YyDdHhMmSs
} else if v.years() >= 10 && v.days() >= 1 {
Format::YyDHhMmSs
} else if v.years() >= 1 && v.days_mod() >= 100 {
Format::YDddHhMmSs
} else if v.years() >= 1 && v.days_mod() >= 10 {
Format::YDdHhMmSs
} else if v.years() >= 1 && v.days() >= 1 {
Format::YDHhMmSs
} else if v.days() >= 100 {
Format::DddHhMmSs
} else if v.days() >= 10 {
Format::DdHhMmSs
} else if v.days() >= 1 {
Format::DHhMmSs
} else if v.hours() >= 10 {
Format::HhMmSs
} else if v.hours() >= 1 {
Format::HMmSs
} else if v.minutes() >= 10 {
Format::MmSs
} else if v.minutes() >= 1 {
Format::MSs
} else if v.seconds() >= 10 {
Format::Ss
} else {
Format::S
}
let d = self.get_current_value();
self.format = format_by_duration(d);
}
/// Updates inner value of `done_count`.

View File

@ -28,17 +28,17 @@ fn test_type_id() {
#[test]
fn test_get_format_seconds() {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: Duration::from_secs(5),
current_value: Duration::from_secs(5),
initial_value: ONE_SECOND * 5,
current_value: ONE_SECOND * 5,
tick_value: ONE_DECI_SECOND,
with_decis: false,
app_tx: None,
});
// S
assert_eq!(c.get_format(), Format::S);
assert_eq!(c.get_format(), &Format::S);
// Ss
c.set_current_value(Duration::from_secs(15).into());
assert_eq!(c.get_format(), Format::Ss);
assert_eq!(c.get_format(), &Format::Ss);
}
#[test]
@ -51,10 +51,10 @@ fn test_get_format_minutes() {
app_tx: None,
});
// MSs
assert_eq!(c.get_format(), Format::MSs);
assert_eq!(c.get_format(), &Format::MSs);
// MmSs
c.set_current_value(Duration::from_secs(610).into()); // 10+ minutes
assert_eq!(c.get_format(), Format::MmSs);
c.set_current_value((ONE_MINUTE * 11).into()); // 10+ minutes
assert_eq!(c.get_format(), &Format::MmSs);
}
#[test]
@ -67,162 +67,186 @@ fn test_get_format_hours() {
app_tx: None,
});
// HMmSS
assert_eq!(c.get_format(), Format::HMmSs);
assert_eq!(c.get_format(), &Format::HMmSs);
// HhMmSs
c.set_current_value((10 * ONE_HOUR).into());
assert_eq!(c.get_format(), Format::HhMmSs);
assert_eq!(c.get_format(), &Format::HhMmSs);
}
#[test]
fn test_get_format_boundaries() {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_SECOND,
current_value: ONE_SECOND,
tick_value: ONE_DECI_SECOND,
with_decis: false,
app_tx: None,
});
fn test_format_by_duration_boundaries() {
// S
c.set_current_value(Duration::from_secs(9).into());
assert_eq!(c.get_format(), Format::S);
assert_eq!(format_by_duration(&(ONE_SECOND * 9).into()), Format::S);
// Ss
c.set_current_value((10 * ONE_SECOND).into());
assert_eq!(c.get_format(), Format::Ss);
assert_eq!(format_by_duration(&(10 * ONE_SECOND).into()), Format::Ss);
// Ss
c.set_current_value((59 * ONE_SECOND).into());
assert_eq!(c.get_format(), Format::Ss);
assert_eq!(format_by_duration(&(59 * ONE_SECOND).into()), Format::Ss);
// MSs
c.set_current_value(ONE_MINUTE.into());
assert_eq!(c.get_format(), Format::MSs);
assert_eq!(format_by_duration(&ONE_MINUTE.into()), Format::MSs);
// HhMmSs
c.set_current_value((ONE_DAY.saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::HhMmSs);
assert_eq!(
format_by_duration(&(ONE_DAY.saturating_sub(ONE_SECOND)).into()),
Format::HhMmSs
);
// DHhMmSs
c.set_current_value(ONE_DAY.into());
assert_eq!(c.get_format(), Format::DHhMmSs);
assert_eq!(format_by_duration(&ONE_DAY.into()), Format::DHhMmSs);
// DHhMmSs
c.set_current_value(((10 * ONE_DAY).saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::DHhMmSs);
assert_eq!(
format_by_duration(&((10 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
Format::DHhMmSs
);
// DdHhMmSs
c.set_current_value((10 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::DdHhMmSs);
assert_eq!(format_by_duration(&(10 * ONE_DAY).into()), Format::DdHhMmSs);
// DdHhMmSs
c.set_current_value(((100 * ONE_DAY).saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::DdHhMmSs);
assert_eq!(
format_by_duration(&((100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
Format::DdHhMmSs
);
// DddHhMmSs
c.set_current_value((100 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::DddHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_DAY).into()),
Format::DddHhMmSs
);
// DddHhMmSs
c.set_current_value((ONE_YEAR.saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::DddHhMmSs);
assert_eq!(
format_by_duration(&(ONE_YEAR.saturating_sub(ONE_SECOND).into())),
Format::DddHhMmSs
);
// YDHhMmSs
c.set_current_value(ONE_YEAR.into());
assert_eq!(c.get_format(), Format::YDHhMmSs);
assert_eq!(format_by_duration(&ONE_YEAR.into()), Format::YDHhMmSs);
// YDdHhMmSs
c.set_current_value((ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::YDdHhMmSs);
assert_eq!(
format_by_duration(&(ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
Format::YDdHhMmSs
);
// YDddHhMmSs
c.set_current_value((ONE_YEAR + 100 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YDddHhMmSs);
assert_eq!(
format_by_duration(&(ONE_YEAR + 100 * ONE_DAY).into()),
Format::YDddHhMmSs
);
// YDddHhMmSs
c.set_current_value(((10 * ONE_YEAR).saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::YDddHhMmSs);
assert_eq!(
format_by_duration(&((10 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
Format::YDddHhMmSs
);
// YyDHhMmSs
c.set_current_value((10 * ONE_YEAR).into());
assert_eq!(c.get_format(), Format::YyDHhMmSs);
assert_eq!(
format_by_duration(&(10 * ONE_YEAR).into()),
Format::YyDHhMmSs
);
// YyDdHhMmSs
c.set_current_value((10 * ONE_YEAR + 10 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyDdHhMmSs);
assert_eq!(
format_by_duration(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
Format::YyDdHhMmSs
);
// YyDdHhMmSs
c.set_current_value((10 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::YyDdHhMmSs);
assert_eq!(
format_by_duration(&(10 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
Format::YyDdHhMmSs
);
// YyDddHhMmSs
c.set_current_value((10 * ONE_YEAR + 100 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyDddHhMmSs);
assert_eq!(
format_by_duration(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
Format::YyDddHhMmSs
);
// YyDddHhMmSs
c.set_current_value(((100 * ONE_YEAR).saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::YyDddHhMmSs);
assert_eq!(
format_by_duration(&((100 * ONE_YEAR).saturating_sub(ONE_SECOND)).into()),
Format::YyDddHhMmSs
);
// YyyDHhMmSs
c.set_current_value((100 * ONE_YEAR).into());
assert_eq!(c.get_format(), Format::YyyDHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_YEAR).into()),
Format::YyyDHhMmSs
);
// YyyDdHhMmSs
c.set_current_value((100 * ONE_YEAR + 10 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyyDdHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
Format::YyyDdHhMmSs
);
// YyyDdHhMmSs
c.set_current_value((100 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into());
assert_eq!(c.get_format(), Format::YyyDdHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_YEAR + (100 * ONE_DAY).saturating_sub(ONE_SECOND)).into()),
Format::YyyDdHhMmSs
);
// YyyDddHhMmSs
c.set_current_value((100 * ONE_YEAR + 100 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyyDddHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
Format::YyyDddHhMmSs
);
}
#[test]
fn test_get_format_days() {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_DAY,
current_value: ONE_DAY,
tick_value: ONE_DECI_SECOND,
with_decis: false,
app_tx: None,
});
fn test_format_by_duration_days() {
// DHhMmSs
assert_eq!(c.get_format(), Format::DHhMmSs);
assert_eq!(format_by_duration(&ONE_DAY.into()), Format::DHhMmSs);
// DdHhMmSs
c.set_current_value((10 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::DdHhMmSs);
assert_eq!(format_by_duration(&(10 * ONE_DAY).into()), Format::DdHhMmSs);
// DddHhMmSs
c.set_current_value((101 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::DddHhMmSs);
assert_eq!(
format_by_duration(&(101 * ONE_DAY).into()),
Format::DddHhMmSs
);
}
#[test]
fn test_get_format_years() {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_YEAR,
current_value: ONE_YEAR,
tick_value: ONE_DECI_SECOND,
with_decis: false,
app_tx: None,
});
fn test_format_by_duration_years() {
// YDHhMmSs (1 year, 0 days)
assert_eq!(c.get_format(), Format::YDHhMmSs);
assert_eq!(format_by_duration(&ONE_YEAR.into()), Format::YDHhMmSs);
// YDHhMmSs (1 year, 1 day)
c.set_current_value((ONE_YEAR + ONE_DAY).into());
assert_eq!(c.get_format(), Format::YDHhMmSs);
assert_eq!(
format_by_duration(&(ONE_YEAR + ONE_DAY).into()),
Format::YDHhMmSs
);
// YDdHhMmSs (1 year, 10 days)
c.set_current_value((ONE_YEAR + 10 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YDdHhMmSs);
assert_eq!(
format_by_duration(&(ONE_YEAR + 10 * ONE_DAY).into()),
Format::YDdHhMmSs
);
// YDddHhMmSs (1 year, 100 days)
c.set_current_value((ONE_YEAR + 100 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YDddHhMmSs);
assert_eq!(
format_by_duration(&(ONE_YEAR + 100 * ONE_DAY).into()),
Format::YDddHhMmSs
);
// YyDHhMmSs (10 years)
c.set_current_value((10 * ONE_YEAR).into());
assert_eq!(c.get_format(), Format::YyDHhMmSs);
assert_eq!(
format_by_duration(&(10 * ONE_YEAR).into()),
Format::YyDHhMmSs
);
// YyDdHhMmSs (10 years, 10 days)
c.set_current_value((10 * ONE_YEAR + 10 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyDdHhMmSs);
assert_eq!(
format_by_duration(&(10 * ONE_YEAR + 10 * ONE_DAY).into()),
Format::YyDdHhMmSs
);
// YyDddHhMmSs (10 years, 100 days)
c.set_current_value((10 * ONE_YEAR + 100 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyDddHhMmSs);
assert_eq!(
format_by_duration(&(10 * ONE_YEAR + 100 * ONE_DAY).into()),
Format::YyDddHhMmSs
);
// YyyDHhMmSs (100 years)
c.set_current_value((100 * ONE_YEAR).into());
assert_eq!(c.get_format(), Format::YyyDHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_YEAR).into()),
Format::YyyDHhMmSs
);
// YyyDdHhMmSs (100 years, 10 days)
c.set_current_value((100 * ONE_YEAR + 10 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyyDdHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_YEAR + 10 * ONE_DAY).into()),
Format::YyyDdHhMmSs
);
// YyyDddHhMmSs (100 years, 100 days)
c.set_current_value((100 * ONE_YEAR + 100 * ONE_DAY).into());
assert_eq!(c.get_format(), Format::YyyDddHhMmSs);
assert_eq!(
format_by_duration(&(100 * ONE_YEAR + 100 * ONE_DAY).into()),
Format::YyyDddHhMmSs
);
}
#[test]
@ -371,8 +395,8 @@ fn test_edit_up_overflow_protection() {
#[test]
fn test_edit_down_years_to_days() {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_YEAR,
current_value: ONE_YEAR,
initial_value: ONE_YEAR + ONE_DAY,
current_value: ONE_YEAR + ONE_DAY,
tick_value: ONE_DECI_SECOND,
with_decis: false,
app_tx: None,
@ -390,8 +414,8 @@ fn test_edit_down_years_to_days() {
#[test]
fn test_edit_down_days_to_hours() {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_DAY,
current_value: ONE_DAY,
initial_value: ONE_DAY + ONE_HOUR,
current_value: ONE_DAY + ONE_HOUR,
tick_value: ONE_DECI_SECOND,
with_decis: false,
app_tx: None,
@ -408,8 +432,8 @@ fn test_edit_down_days_to_hours() {
#[test]
fn test_edit_down_hours_to_minutes() {
let mut c = ClockState::<Timer>::new(ClockStateArgs {
initial_value: ONE_HOUR,
current_value: ONE_HOUR,
initial_value: ONE_HOUR + ONE_MINUTE,
current_value: ONE_HOUR + ONE_MINUTE,
tick_value: ONE_DECI_SECOND,
with_decis: false,
app_tx: None,
@ -474,9 +498,6 @@ fn test_edit_hours_in_dhhmmss_format() {
app_tx: None,
});
// Should be in D HH:MM:SS format
assert_eq!(c.get_format(), Format::DHhMmSs);
c.toggle_edit();
c.edit_next(); // Move to Hours
assert!(matches!(c.get_mode(), Mode::Editable(Time::Hours, _)));

View File

@ -372,10 +372,11 @@ impl StatefulWidget for Countdown {
.to_uppercase(),
);
let widget = ClockWidget::new(self.style, self.blink);
let area = center(
area,
Constraint::Length(max(
widget.get_width(&state.clock.get_format(), state.clock.with_decis),
widget.get_width(state.clock.get_format(), state.clock.with_decis),
label.width() as u16,
)),
Constraint::Length(widget.get_height() + 1 /* height of label */),

View File

@ -250,10 +250,8 @@ impl StatefulWidget for PomodoroWidget {
let area = center(
area,
Constraint::Length(max(
clock_widget.get_width(
&state.get_clock().get_format(),
state.get_clock().with_decis,
),
clock_widget
.get_width(state.get_clock().get_format(), state.get_clock().with_decis),
label.width() as u16,
)),
Constraint::Length(

View File

@ -107,7 +107,7 @@ impl StatefulWidget for Timer {
let area = center(
area,
Constraint::Length(max(
clock_widget.get_width(&clock.get_format(), clock.with_decis),
clock_widget.get_width(clock.get_format(), clock.with_decis),
label.width() as u16,
)),
Constraint::Length(clock_widget.get_height() + 1 /* height of label */),