From 399f0e7b013961cd40eed3fcb75367f9d73dbce1 Mon Sep 17 00:00:00 2001 From: Hivin Diyagama <49328442+SilverCrocus@users.noreply.github.com> Date: Fri, 6 Nov 2026 00:21:57 +1100 Subject: [PATCH 1/2] Dashboard: show calories left inside the Today ring The Today card showed eaten / target, leaving you to work out what was left. The ring's empty centre now shows the calories left, or how far over once you pass the target. It only reads "over" once that rounds to at least 1, so a fraction of a calorie past the target shows "0 left". Fixes #1 --- crates/openmacro-app/assets/main.css | 5 +++ .../openmacro-app/src/components/dashboard.rs | 45 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) 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..8036744 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,18 @@ pub fn Dashboard() -> Element { } } +/// What is left of today's target, or how far past it, in the user's energy +/// unit. It only reads as over once that rounds to at least 1, so a fraction +/// of a calorie past the target shows "0 left" rather than "0 over". +fn remaining_text(eaten: f64, target: f64) -> (String, bool) { + let over = fmt0(eaten - target); + if over == "0" || over.starts_with('-') { + (fmt0(target - eaten), false) + } else { + (over, true) + } +} + #[component] fn WeighInCard() -> Element { let state = use_context::(); @@ -401,3 +424,23 @@ 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 a_fraction_over_is_not_over() { + assert_eq!(remaining_text(1894.3, 1894.0), ("0".into(), false)); + assert_eq!(remaining_text(1893.8, 1894.0), ("0".into(), false)); + assert_eq!(remaining_text(1894.7, 1894.0), ("1".into(), true)); + } +} From 663e7ffcd7fde541b08cb189bcbab86c8c0afb1e Mon Sep 17 00:00:00 2001 From: Hivin Diyagama <49328442+SilverCrocus@users.noreply.github.com> Date: Fri, 6 Nov 2026 00:24:51 +1100 Subject: [PATCH 2/2] Dashboard: round the calories left like the figures beside it The ring rounded the difference, while "eaten / target" rounds each value, so they could disagree by 1: 1000.4 of 1894.6 showed "1000 / 1895" but 894 left. Round each value first, ties to even as `{:.0}` does, so the ring always matches. --- .../openmacro-app/src/components/dashboard.rs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/crates/openmacro-app/src/components/dashboard.rs b/crates/openmacro-app/src/components/dashboard.rs index 8036744..3429e15 100644 --- a/crates/openmacro-app/src/components/dashboard.rs +++ b/crates/openmacro-app/src/components/dashboard.rs @@ -365,15 +365,12 @@ pub fn Dashboard() -> Element { } /// What is left of today's target, or how far past it, in the user's energy -/// unit. It only reads as over once that rounds to at least 1, so a fraction -/// of a calorie past the target shows "0 left" rather than "0 over". +/// 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 over = fmt0(eaten - target); - if over == "0" || over.starts_with('-') { - (fmt0(target - eaten), false) - } else { - (over, true) - } + let left = target.round_ties_even() - eaten.round_ties_even(); + (fmt0(left.abs()), left < 0.0) } #[component] @@ -438,9 +435,25 @@ mod tests { } #[test] - fn a_fraction_over_is_not_over() { + 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(1893.8, 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}"); + } + } + } }