Viewer: clickable breadcrumbs

Every crumb above the page you're on links to where it names: the
section goes to its home (Parties › Tobin Harlow → Parties), and a
reconciliation's trail runs Accounts › its account › it. Same look; a
pointer and "Go to …" on hover. A crumb naming the current page isn't a
link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GWVo7x3UHw5kwCUaWxiejX
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-16 08:41:39 -06:00
1 parent babb662eba
commit 149d108126
3 files changed
+184 -25

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
//! The breadcrumb links back up: every crumb but the page you're on goes
//! where it names.
//!
//! ```text
//! GOAL: from Parties › Tobin Harlow, clicking "Parties" goes back to the
//! directory; deeper trails link each level.
//! SOURCE: DECISION (owner, 2026-10-16: "I should be able to click
//! parties and it takes me back there ... when it goes deeper").
//! INVARIANTS: palette::crumbs 1-3, and
//! 4. Clicking a crumb goes to its route (a history step, so Back
//! returns).
//! ```
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use ledger_gui::nav::{ReportKind, Route, Section};
use ledger_gui::palette::{crumbs, describe};
use ledger_gui::{App, Source, data};
fn demo(tag: &str) -> Source {
let dir = std::env::temp_dir().join(format!("ledger-gui-crumbs-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let today = chrono::NaiveDate::from_ymd_opt(2026, 10, 12).unwrap();
ledger_gui::demo::build(&dir, today).unwrap();
Source {
data_dir: dir.join("books-data"),
books_toml: Some(dir.join("wanderbooks.toml")),
today: Some(today),
}
}
/// INVARIANTS 1-3 on every kind of page in the demo books.
#[test]
fn crumbs_link_every_level_but_the_last() {
let b = data::load_once(&demo("unit")).unwrap();
let party = b.ledger.parties().next().unwrap().id;
let rec = b.reconciliations.first().expect("the demo reconciles");
let entry = b.entries.first().unwrap().id;
let mut routes = vec![
Route::Party(party),
Route::Reconciliation(rec.id),
Route::Entry(entry),
Route::Reports(ReportKind::ProfitAndLoss),
Route::Reports(ReportKind::BalanceSheet),
];
routes.extend(Section::ALL.iter().map(|s| s.home()));
for r in &routes {
let c = crumbs(Some(&b), r);
assert_eq!(c[0].0, r.section().title(), "{r:?}");
assert!(c.last().unwrap().1.is_none(), "{r:?}: the page you're on");
for (_, to) in &c {
assert_ne!(to.as_ref(), Some(r), "{r:?} links to itself");
}
// Every crumb above is a link, except one naming this very page
// (Reports on Profit and loss, the Reports section's home).
for (i, (_, to)) in c[..c.len() - 1].iter().enumerate() {
let home_here = i == 0 && r.section().home() == *r;
assert_eq!(to.is_some(), !home_here, "{r:?}: crumb {i}");
}
let here = describe(&b, r);
if !here.is_empty() {
assert_eq!(c.last().unwrap().0, here);
}
}
assert_eq!(
crumbs(Some(&b), &Route::Party(party)),
[
("Parties".to_string(), Some(Route::Parties)),
(b.party_label(party), None),
]
);
// A reconciliation sits under its account.
let account = Route::Account(rec.account_id);
let c = crumbs(Some(&b), &Route::Reconciliation(rec.id));
assert_eq!(c.len(), 3);
assert_eq!(c[0].1, Some(Route::Accounts));
assert_eq!(c[1], (describe(&b, &account), Some(account)));
// A section's own page is one crumb, not a link.
assert_eq!(
crumbs(Some(&b), &Route::Parties),
[("Parties".to_string(), None)]
);
// P&L is the Reports section's home, so "Reports" there isn't a link.
assert_eq!(
crumbs(Some(&b), &Route::Reports(ReportKind::ProfitAndLoss))[0].1,
None
);
}
/// INVARIANT 4, through the top bar.
#[test]
fn clicking_a_crumb_goes_there() {
let source = demo("click");
let b = data::load_once(&source).unwrap();
let rec = b.reconciliations.first().unwrap().clone();
let account = describe(&b, &Route::Account(rec.account_id));
let mut h = Harness::builder()
.with_size(egui::vec2(1280.0, 900.0))
.build_eframe(|cc| App::with_books(&cc.egui_ctx, b, false, Route::Reconciliation(rec.id)));
h.run_steps(3);
h.get_all_by_label(&account).next().unwrap().click();
h.run_steps(3);
assert_eq!(h.state().route(), &Route::Account(rec.account_id));
h.get_by_label("Accounts").click();
h.run_steps(3);
assert_eq!(h.state().route(), &Route::Accounts);
}