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 3b274ed853
commit 6fb594ed28
16 files changed
+904 -463

No files matched your search

+73 -2
View File
@@ -75,8 +75,17 @@ pub fn pill(ui: &mut Ui, text: &str, fg: Color32, bg: Color32) -> Response {
/// Text that looks like plain text until you hover it, then like a link.
pub fn link(ui: &mut Ui, text: RichText) -> Response {
link_label(ui, egui::Label::new(text))
}
/// [`link`] on one line, cut with an ellipsis where the space ends.
pub fn link_truncated(ui: &mut Ui, text: RichText) -> Response {
link_label(ui, egui::Label::new(text).truncate())
}
fn link_label(ui: &mut Ui, label: egui::Label) -> Response {
let p = pal(ui.ctx());
let r = ui.add(egui::Label::new(text.clone()).sense(Sense::click()));
let r = ui.add(label.sense(Sense::click()));
if r.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
let rect = r.rect;
@@ -163,7 +172,15 @@ pub fn kpi(ui: &mut Ui, k: Kpi<'_>) -> Response {
.color(p.muted),
);
ui.add_space(4.0);
ui.label(RichText::new(&k.value).font(theme::semibold(24.0)));
let font = fit_font(
ui,
&k.value,
theme::semibold,
24.0,
15.0,
ui.available_width(),
);
ui.add(egui::Label::new(RichText::new(&k.value).font(font)).truncate());
if let Some((text, color)) = &k.sub {
ui.add_space(2.0);
ui.label(RichText::new(text).font(theme::regular(12.0)).color(*color));
@@ -313,3 +330,57 @@ pub fn clipped_text(
let y = pos.y - galley.size().y / 2.0;
painter.galley(egui::pos2(pos.x, y), galley, color);
}
/// How many columns at least `min_width` wide (with `gap` between) fit
/// in `avail`, never fewer than one nor more than `max`.
pub fn fit_columns(avail: f32, min_width: f32, gap: f32, max: usize) -> usize {
(((avail + gap) / (min_width + gap)).floor() as usize).clamp(1, max.max(1))
}
/// The width of each of `n` right-aligned amount columns in a painted
/// table `width` wide, leaving at least `label_min` for the label on the
/// left: `max` when there's room, shrinking to a floor that still fits an
/// amount. Pair with [`min_width`] so a table that can't fit scrolls.
pub fn amount_col_w(width: f32, n: usize, label_min: f32, max: f32) -> f32 {
((width - label_min) / n.max(1) as f32).clamp(AMOUNT_MIN, max)
}
/// The narrowest an amount column gets ("(1,234,567.89)" at 13.5px).
pub const AMOUNT_MIN: f32 = 92.0;
/// Lay out `add` at least `width` wide: as is when there's room, in a
/// sideways scroll when the window is narrower, so a table never paints
/// over itself or past the edge.
pub fn min_width<R>(ui: &mut Ui, id: &str, width: f32, add: impl FnOnce(&mut Ui) -> R) -> R {
if ui.available_width() >= width {
return add(ui);
}
egui::ScrollArea::horizontal()
.id_salt(id)
.show(ui, |ui| {
ui.set_width(width);
add(ui)
})
.inner
}
/// The largest size up to `size` at which `text` fits on one line in
/// `width` (down to `floor`): big numbers shrink rather than wrap.
pub fn fit_font(
ui: &Ui,
text: &str,
font: impl Fn(f32) -> egui::FontId,
size: f32,
floor: f32,
width: f32,
) -> egui::FontId {
let measured = ui
.painter()
.layout_no_wrap(text.to_string(), font(size), Color32::WHITE)
.size()
.x;
if measured <= width {
return font(size);
}
font((size * width / measured).floor().max(floor))
}