gui: every screen lays itself out to fit, down to a 360-point tiling slot

Narrow windows overlapped and clipped: the top bar's crumb, date, and
search ran into each other; big numbers wrapped mid-figure; tables
painted columns over each other; the entry and document panes squeezed
their lists to slivers; anything too wide widened the whole page.

- Below 1000 points the sidebar becomes an icon rail (names, attention,
  and integrity checks in tooltips).
- The top bar lays out right to left: search shrinks to its icon, the
  date drops, the crumb truncates.
- Pages are pinned to the window width with narrower margins when tight,
  so one wide thing can't push everything off screen.
- Tiles (KPIs, cash accounts, documents) fit as many columns as the
  width allows, balanced across rows; KPI figures shrink rather than wrap.
- Painted tables shrink their amount columns, clip labels with an
  ellipsis, then scroll sideways rather than overlap.
- List/detail splits show only the detail when both won't fit.
- Reports: the side list becomes a picker; controls wrap by known width.
- Smaller fixes: chart labels thin out, company facts stack, journal
  timestamps yield, transaction filters become a dropdown.
- Min window size drops to 360x400; the screenshot test covers every
  report, a register, a document, and a journal event.

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-12 20:02:48 -06:00
1 parent 9eba3180b4
commit ea996f6e7e
16 files changed
+904 -463

No files matched your search

+53 -52
View File
@@ -162,38 +162,10 @@ fn row(ui: &mut Ui, b: &Books, e: &EntryInfo, on: bool, since_update: f64) -> eg
theme::semibold(16.0),
p.text,
);
// Description and accounts.
// Description and accounts, cut with an ellipsis before the receipt
// icon and the amount. Cash in reads green with a plus, cash out with
// a minus; entries that don't move cash show their size, muted.
let text_left = left + 44.0;
let amount_w = 120.0;
let max_w = (rect.right() - amount_w - 70.0 - text_left).max(60.0);
let desc = painter.layout(e.description.clone(), theme::semibold(13.5), p.text, max_w);
let desc = if desc.rows.len() > 1 {
painter.layout_no_wrap(
truncate(&e.description, (max_w / 7.5) as usize),
theme::semibold(13.5),
p.text,
)
} else {
desc
};
painter.galley(egui::pos2(text_left, rect.center().y - 17.0), desc, p.text);
let accounts: Vec<String> = e.accounts.iter().map(|a| b.account_name(*a)).collect();
let mut sub = format!("#{} · {} · {}", e.id, e.origin, accounts.join(" → "));
if let Some(v) = &e.party {
sub.push_str(&format!(" · {v}"));
}
let sub = truncate(&sub, (max_w / 6.3) as usize);
painter.text(
egui::pos2(text_left, rect.center().y + 10.0),
egui::Align2::LEFT_CENTER,
sub,
theme::regular(12.0),
p.muted,
);
// Amount and badges.
let right = rect.right() - 10.0;
// Cash in reads green with a plus, cash out with a minus; entries that
// don't move cash show their size, muted.
let (amount, color) = if e.cash_effect > rust_decimal::Decimal::ZERO {
(format!("+{}", fmt::money(e.cash_effect)), p.positive)
} else if e.cash_effect < rust_decimal::Decimal::ZERO {
@@ -201,6 +173,35 @@ fn row(ui: &mut Ui, b: &Books, e: &EntryInfo, on: bool, since_update: f64) -> eg
} else {
(fmt::money(e.amount), p.muted)
};
let amount_w = painter
.layout_no_wrap(amount.clone(), theme::semibold(14.0), color)
.size()
.x
.max(64.0);
let max_w = (rect.right() - 10.0 - amount_w - 44.0 - text_left).max(40.0);
widgets::clipped_text(
&painter,
egui::pos2(text_left, rect.center().y - 8.0),
&e.description,
theme::semibold(13.5),
p.text,
max_w,
);
let accounts: Vec<String> = e.accounts.iter().map(|a| b.account_name(*a)).collect();
let mut sub = format!("#{} · {} · {}", e.id, e.origin, accounts.join(" → "));
if let Some(v) = &e.party {
sub.push_str(&format!(" · {v}"));
}
widgets::clipped_text(
&painter,
egui::pos2(text_left, rect.center().y + 10.0),
&sub,
theme::regular(12.0),
p.muted,
max_w,
);
// Amount and badges.
let right = rect.right() - 10.0;
painter.text(
egui::pos2(right, rect.center().y - 6.0),
egui::Align2::RIGHT_CENTER,
@@ -234,7 +235,7 @@ fn row(ui: &mut Ui, b: &Books, e: &EntryInfo, on: bool, since_update: f64) -> eg
};
if let Some((g, c)) = clip {
painter.text(
egui::pos2(rect.right() - amount_w - 24.0, rect.center().y),
egui::pos2(rect.right() - 10.0 - amount_w - 20.0, rect.center().y),
egui::Align2::CENTER_CENTER,
g,
egui::FontId::proportional(16.0),
@@ -244,27 +245,27 @@ fn row(ui: &mut Ui, b: &Books, e: &EntryInfo, on: bool, since_update: f64) -> eg
resp
}
fn truncate(s: &str, n: usize) -> String {
if s.chars().count() <= n {
s.to_string()
} else {
let t: String = s.chars().take(n.saturating_sub(1)).collect();
format!("{t}…")
}
}
fn filters(ui: &mut Ui, b: &Books, st: &mut State) {
widgets::segmented(
ui,
&mut st.filter,
&[
(Filter::All, "All"),
(Filter::Posted, "Posted"),
(Filter::Drafts, "Drafts"),
(Filter::WithReceipt, "With receipt"),
(Filter::NoReceipt, "No receipt"),
],
);
let options = [
(Filter::All, "All"),
(Filter::Posted, "Posted"),
(Filter::Drafts, "Drafts"),
(Filter::WithReceipt, "With receipt"),
(Filter::NoReceipt, "No receipt"),
];
// The segmented control needs about 330 points; narrower, a dropdown.
if ui.available_width() >= 340.0 {
widgets::segmented(ui, &mut st.filter, &options);
} else {
let current = options.iter().find(|o| o.0 == st.filter).map(|o| o.1);
egui::ComboBox::from_id_salt("txn_filter")
.selected_text(format!("{} {}", icon::FUNNEL, current.unwrap_or("All")))
.show_ui(ui, |ui| {
for (f, label) in options {
ui.selectable_value(&mut st.filter, f, label);
}
});
}
if let Some(a) = st.account {
ui.add_space(6.0);
let label = format!("{} {} {}", icon::FUNNEL, b.account_label(a), icon::X);