diff --git a/crates/openmacro-app/assets/main.css b/crates/openmacro-app/assets/main.css index 07ef25c..d532124 100644 --- a/crates/openmacro-app/assets/main.css +++ b/crates/openmacro-app/assets/main.css @@ -445,6 +445,11 @@ h3 { font-size: 14px; margin: 0 0 8px; color: var(--muted); font-weight: 600; } .hero { display: flex; align-items: center; gap: 18px; } .hero-num { font-size: 26px; font-weight: 700; font-variant-numeric: tabular-nums; } +.hero-ring { display: grid; place-items: center; flex-shrink: 0; } +.hero-ring > * { grid-area: 1 / 1; } +.hero-ring-label { display: flex; flex-direction: column; align-items: center; line-height: 1.15; } +.hero-ring-num { font-size: 18px; font-weight: 700; font-variant-numeric: tabular-nums; } +.hero-ring-label .muted { font-size: 11px; } .week-bars { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; height: 120px; } .week-col { display: flex; flex-direction: column; align-items: center; gap: 6px; font-size: 11px; } diff --git a/crates/openmacro-app/src/components/dashboard.rs b/crates/openmacro-app/src/components/dashboard.rs index 86e572a..3429e15 100644 --- a/crates/openmacro-app/src/components/dashboard.rs +++ b/crates/openmacro-app/src/components/dashboard.rs @@ -39,6 +39,11 @@ pub fn Dashboard() -> Element { let today_kcal = day_totals(&log, today).get_or_zero(Nutrient::Energy); let today_target = coach.target_for(today).kcal; + let (left_text, is_over) = remaining_text( + prefs::energy_value(today_kcal), + prefs::energy_value(today_target), + ); + let left_word = if is_over { "over" } else { "left" }; // Expenditure tile. let expenditure = coach.expenditure_kcal(); @@ -233,7 +238,13 @@ pub fn Dashboard() -> Element { } } section { class: "card hero", - Ring { fraction: today_kcal / today_target.max(1.0), size: 88 } + div { class: "hero-ring", + Ring { fraction: today_kcal / today_target.max(1.0), size: 88 } + div { class: "hero-ring-label", + span { class: "hero-ring-num", "{left_text}" } + span { class: "muted", "{energy_unit} {left_word}" } + } + } div { div { class: "muted", "Today" } div { class: "hero-num", "{today_text}" } @@ -353,6 +364,15 @@ pub fn Dashboard() -> Element { } } +/// What is left of today's target, or how far past it, in the user's energy +/// unit. Both are rounded before subtracting, so the number always matches +/// the "eaten / target" figures shown beside it. `{:.0}` rounds ties to +/// even, so this rounds the same way. +fn remaining_text(eaten: f64, target: f64) -> (String, bool) { + let left = target.round_ties_even() - eaten.round_ties_even(); + (fmt0(left.abs()), left < 0.0) +} + #[component] fn WeighInCard() -> Element { let state = use_context::(); @@ -401,3 +421,39 @@ fn WeighInCard() -> Element { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn remaining_against_target() { + assert_eq!(remaining_text(988.0, 1894.0), ("906".into(), false)); + assert_eq!(remaining_text(0.0, 1894.0), ("1894".into(), false)); + assert_eq!(remaining_text(1894.0, 1894.0), ("0".into(), false)); + assert_eq!(remaining_text(2014.0, 1894.0), ("120".into(), true)); + } + + #[test] + fn matches_the_rounded_figures() { + // Shown as "1000 / 1895", so 895 left, not 894. + assert_eq!(remaining_text(1000.4, 1894.6), ("895".into(), false)); + // Shown as "1894 / 1894": ties round to even, like `{:.0}`. + assert_eq!(remaining_text(1894.5, 1893.6), ("0".into(), false)); + assert_eq!(remaining_text(1894.3, 1894.0), ("0".into(), false)); + assert_eq!(remaining_text(1894.7, 1894.0), ("1".into(), true)); + } + + #[test] + fn rounds_like_the_figures_beside_it() { + for eaten in [0.0, 0.5, 1.5, 2.5, 987.5, 1000.4, 1894.49, 1894.5, 2013.6] { + for target in [1893.5, 1894.0, 1894.5, 1894.6, 2633.2] { + let (text, over) = remaining_text(eaten, target); + let diff = + fmt0(target).parse::().unwrap() - fmt0(eaten).parse::().unwrap(); + assert_eq!(text, fmt0(diff.abs()), "{eaten} of {target}"); + assert_eq!(over, diff < 0.0, "{eaten} of {target}"); + } + } + } +}