diff --git a/crates/ledger-gui/src/attention.rs b/crates/ledger-gui/src/attention.rs index a334f78..7acb22e 100644 --- a/crates/ledger-gui/src/attention.rs +++ b/crates/ledger-gui/src/attention.rs @@ -51,9 +51,9 @@ pub fn find(b: &Books) -> Vec { severity: Severity::Problem, title: format!("{} without receipts", fmt::count(missing.len(), "expense", "expenses")), detail: format!( - "{} of spending over the {} threshold has no receipt attached.", + "{} of spending has no receipt attached ({}).", fmt::dollars(total), - fmt::dollars(b.receipt_threshold) + b.receipt_rule() ), target: Some(Route::Transactions), prompt: format!( @@ -67,11 +67,11 @@ pub fn find(b: &Books) -> Vec { severity: Severity::Problem, title: format!("No receipt: {}", e.description), detail: format!( - "#{} · {} · {} is over the {} receipt threshold.", + "#{} · {} · {}: {}.", e.id, fmt::date(e.date), fmt::dollars(e.amount), - fmt::dollars(b.receipt_threshold) + b.receipt_rule() ), target: Some(Route::Entry(e.id)), prompt: format!( diff --git a/crates/ledger-gui/src/books.rs b/crates/ledger-gui/src/books.rs index d6a2cdc..ddb033f 100644 --- a/crates/ledger-gui/src/books.rs +++ b/crates/ledger-gui/src/books.rs @@ -316,6 +316,19 @@ impl Books { Ok(books) } + /// Why an entry needs a receipt, in words: "every entry needs a + /// receipt" or "over the $25.00 receipt threshold". + pub fn receipt_rule(&self) -> String { + if self.receipt_threshold.is_zero() { + "every entry needs a receipt".to_string() + } else { + format!( + "over the {} receipt threshold", + crate::fmt::dollars(self.receipt_threshold) + ) + } + } + pub fn seq(&self) -> u64 { self.journal.len() as u64 } diff --git a/crates/ledger-gui/src/pages/entry.rs b/crates/ledger-gui/src/pages/entry.rs index aeef7cc..b70784c 100644 --- a/crates/ledger-gui/src/pages/entry.rs +++ b/crates/ledger-gui/src/pages/entry.rs @@ -243,10 +243,7 @@ pub fn detail(ui: &mut Ui, b: &Books, e: &EntryInfo, shell: &mut Shell<'_>) { ); if all_for_entry.is_empty() { let msg = if e.missing_receipt { - format!( - "None attached, and this entry is over the {} receipt threshold.", - fmt::dollars(b.receipt_threshold) - ) + format!("None attached, and {}.", b.receipt_rule()) } else { "None attached.".to_string() }; diff --git a/crates/ledger-gui/src/pages/overview.rs b/crates/ledger-gui/src/pages/overview.rs index 41d5c02..45346b5 100644 --- a/crates/ledger-gui/src/pages/overview.rs +++ b/crates/ledger-gui/src/pages/overview.rs @@ -283,41 +283,69 @@ fn attention(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) { ); return; } - egui::ScrollArea::vertical() - .id_salt("attention") - .max_height(260.0) - .show(ui, |ui| { - for (i, a) in b.attention.iter().enumerate() { - if i > 0 { - ui.add_space(4.0); - widgets::rule(ui); - ui.add_space(4.0); + // No inner scrolling: the card grows to fit. Past a handful of + // items, the rest sit behind a button that says how many. + const FIRST: usize = 6; + let id = ui.id().with("attention-expanded"); + let mut expanded: bool = ui.ctx().data(|d| d.get_temp(id)).unwrap_or(false); + let shown = if expanded { + b.attention.len() + } else { + b.attention.len().min(FIRST) + }; + for (i, a) in b.attention.iter().take(shown).enumerate() { + if i > 0 { + ui.add_space(4.0); + widgets::rule(ui); + ui.add_space(4.0); + } + let (glyph, color) = match a.severity { + Severity::Problem => (icon::WARNING_CIRCLE, p.negative), + Severity::Todo => (icon::CIRCLE_DASHED, p.warning), + Severity::Info => (icon::INFO, p.muted), + }; + ui.horizontal_top(|ui| { + ui.label(RichText::new(glyph).size(16.0).color(color)); + ui.vertical(|ui| { + let t = RichText::new(&a.title).font(theme::semibold(13.0)); + let r = widgets::link(ui, t); + if r.clicked() + && let Some(to) = &a.target + { + shell.go(to.clone()); } - let (glyph, color) = match a.severity { - Severity::Problem => (icon::WARNING_CIRCLE, p.negative), - Severity::Todo => (icon::CIRCLE_DASHED, p.warning), - Severity::Info => (icon::INFO, p.muted), - }; - ui.horizontal_top(|ui| { - ui.label(RichText::new(glyph).size(16.0).color(color)); - ui.vertical(|ui| { - let t = RichText::new(&a.title).font(theme::semibold(13.0)); - let r = widgets::link(ui, t); - if r.clicked() - && let Some(to) = &a.target - { - shell.go(to.clone()); - } - ui.label( - RichText::new(&a.detail) - .font(theme::regular(12.0)) - .color(p.muted), - ); - widgets::copy_prompt(ui, &a.prompt); - }); - }); - } + ui.label( + RichText::new(&a.detail) + .font(theme::regular(12.0)) + .color(p.muted), + ); + widgets::copy_prompt(ui, &a.prompt); + }); }); + } + let hidden = b.attention.len() - shown; + if hidden > 0 || expanded && b.attention.len() > FIRST { + ui.add_space(8.0); + let label = if hidden > 0 { + format!("{} Show {} more", icon::CARET_DOWN, hidden) + } else { + format!("{} Show fewer", icon::CARET_UP) + }; + if ui + .add( + egui::Button::new( + RichText::new(label) + .font(theme::semibold(12.5)) + .color(p.accent), + ) + .frame(false), + ) + .clicked() + { + expanded = !expanded; + ui.ctx().data_mut(|d| d.insert_temp(id, expanded)); + } + } }, ); } diff --git a/crates/ledger-gui/tests/screens.rs b/crates/ledger-gui/tests/screens.rs index 1ba1bae..394614f 100644 --- a/crates/ledger-gui/tests/screens.rs +++ b/crates/ledger-gui/tests/screens.rs @@ -77,7 +77,10 @@ fn screenshots() { let books = data::load_once(&source).unwrap(); let mut harness = Harness::builder() .with_size(egui::vec2( - 1440.0, + std::env::var("SHOT_WIDTH") + .ok() + .and_then(|w| w.parse().ok()) + .unwrap_or(1440.0), std::env::var("SHOT_HEIGHT") .ok() .and_then(|h| h.parse().ok())