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:
1 parent
3b274ed853
commit
6fb594ed28
16 files changed
+904
-463
No files matched your search
@@ -90,68 +90,70 @@ pub fn show(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
// The same numbers as a table, with the margin.
|
||||
let w = ui.available_width();
|
||||
let cw = 130.0;
|
||||
let header = |ui: &mut Ui, cells: [&str; 5]| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, 24.0), Sense::hover());
|
||||
ui.painter().text(
|
||||
r.left_center() + vec2(4.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
cells[0],
|
||||
theme::semibold(12.0),
|
||||
p.muted,
|
||||
);
|
||||
for (i, c) in cells[1..].iter().enumerate() {
|
||||
let cw = widgets::amount_col_w(ui.available_width(), 4, 100.0, 130.0);
|
||||
let w = ui.available_width().max(100.0 + cw * 4.0);
|
||||
widgets::min_width(ui, "by_month_table", w, |ui| {
|
||||
let header = |ui: &mut Ui, cells: [&str; 5]| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, 24.0), Sense::hover());
|
||||
ui.painter().text(
|
||||
egui::pos2(r.right() - cw * (3 - i) as f32 - 4.0, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
*c,
|
||||
r.left_center() + vec2(4.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
cells[0],
|
||||
theme::semibold(12.0),
|
||||
p.muted,
|
||||
);
|
||||
}
|
||||
};
|
||||
header(ui, ["Month", "Income", "Spending", "Net", "Margin"]);
|
||||
for m in months.iter().rev() {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 26.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), p.hover);
|
||||
}
|
||||
ui.painter().text(
|
||||
r.left_center() + vec2(4.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
fmt::month(m.start),
|
||||
theme::regular(13.0),
|
||||
p.text,
|
||||
);
|
||||
let vals = [
|
||||
fmt::money(m.revenue),
|
||||
fmt::money(m.expenses),
|
||||
fmt::accounting(m.net()),
|
||||
pct(m.net(), m.revenue)
|
||||
.map(|x| format!("{x}%"))
|
||||
.unwrap_or_else(|| "—".into()),
|
||||
];
|
||||
for (i, v) in vals.iter().enumerate() {
|
||||
let color = if i == 2 && m.net() < Decimal::ZERO {
|
||||
p.negative
|
||||
} else {
|
||||
p.text
|
||||
};
|
||||
for (i, c) in cells[1..].iter().enumerate() {
|
||||
ui.painter().text(
|
||||
egui::pos2(r.right() - cw * (3 - i) as f32 - 4.0, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
*c,
|
||||
theme::semibold(12.0),
|
||||
p.muted,
|
||||
);
|
||||
}
|
||||
};
|
||||
header(ui, ["Month", "Income", "Spending", "Net", "Margin"]);
|
||||
for m in months.iter().rev() {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 26.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), p.hover);
|
||||
}
|
||||
ui.painter().text(
|
||||
egui::pos2(r.right() - cw * (3 - i) as f32 - 4.0, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
v,
|
||||
r.left_center() + vec2(4.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
fmt::month(m.start),
|
||||
theme::regular(13.0),
|
||||
color,
|
||||
p.text,
|
||||
);
|
||||
let vals = [
|
||||
fmt::money(m.revenue),
|
||||
fmt::money(m.expenses),
|
||||
fmt::accounting(m.net()),
|
||||
pct(m.net(), m.revenue)
|
||||
.map(|x| format!("{x}%"))
|
||||
.unwrap_or_else(|| "—".into()),
|
||||
];
|
||||
for (i, v) in vals.iter().enumerate() {
|
||||
let color = if i == 2 && m.net() < Decimal::ZERO {
|
||||
p.negative
|
||||
} else {
|
||||
p.text
|
||||
};
|
||||
ui.painter().text(
|
||||
egui::pos2(r.right() - cw * (3 - i) as f32 - 4.0, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
v,
|
||||
theme::regular(13.0),
|
||||
color,
|
||||
);
|
||||
}
|
||||
if resp.clicked() {
|
||||
shell.state.reports.range =
|
||||
crate::pages::reports::Range::Custom(m.start, m.end);
|
||||
shell.go(Route::Reports(ReportKind::ProfitAndLoss));
|
||||
}
|
||||
}
|
||||
if resp.clicked() {
|
||||
shell.state.reports.range =
|
||||
crate::pages::reports::Range::Custom(m.start, m.end);
|
||||
shell.go(Route::Reports(ReportKind::ProfitAndLoss));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.add_space(16.0);
|
||||
@@ -210,66 +212,70 @@ fn spending_changes(ui: &mut Ui, b: &Books, shell: &mut Shell<'_>) {
|
||||
widgets::empty(ui, icon::RECEIPT, "No spending yet", "");
|
||||
return;
|
||||
}
|
||||
let w = ui.available_width();
|
||||
let cw = 130.0;
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, 24.0), Sense::hover());
|
||||
for (i, c) in ["This month", "Last month", "3-month avg"]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
ui.painter().text(
|
||||
egui::pos2(r.right() - cw * (2 - i) as f32 - 4.0, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
*c,
|
||||
theme::semibold(12.0),
|
||||
p.muted,
|
||||
);
|
||||
}
|
||||
ui.painter().text(
|
||||
r.left_center() + vec2(4.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
"Account",
|
||||
theme::semibold(12.0),
|
||||
p.muted,
|
||||
);
|
||||
for (code, (name, now, last, avg)) in &rows {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 28.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), p.hover);
|
||||
let label_min = 180.0;
|
||||
let cw = widgets::amount_col_w(ui.available_width(), 3, label_min, 130.0);
|
||||
let w = ui.available_width().max(label_min + cw * 3.0);
|
||||
widgets::min_width(ui, "spending_table", w, |ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(w, 24.0), Sense::hover());
|
||||
for (i, c) in ["This month", "Last month", "3-month avg"]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
ui.painter().text(
|
||||
egui::pos2(r.right() - cw * (2 - i) as f32 - 4.0, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
*c,
|
||||
theme::semibold(12.0),
|
||||
p.muted,
|
||||
);
|
||||
}
|
||||
ui.painter().text(
|
||||
r.left_center() + vec2(4.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
format!("{code} {name}"),
|
||||
theme::regular(13.0),
|
||||
p.text,
|
||||
"Account",
|
||||
theme::semibold(12.0),
|
||||
p.muted,
|
||||
);
|
||||
// Flag a category already running well above its average.
|
||||
let hot = *avg > Decimal::ZERO && *now > *avg * Decimal::new(125, 2);
|
||||
let vals = [(*now, hot), (*last, false), (*avg, false)];
|
||||
for (i, (v, flag)) in vals.iter().enumerate() {
|
||||
let x = r.right() - cw * (2 - i) as f32 - 4.0;
|
||||
ui.painter().text(
|
||||
egui::pos2(x, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
fmt::money(v.round_dp(2)),
|
||||
for (code, (name, now, last, avg)) in &rows {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(w, 28.0), Sense::click());
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(r, CornerRadius::same(4), p.hover);
|
||||
}
|
||||
widgets::clipped_text(
|
||||
ui.painter(),
|
||||
r.left_center() + vec2(4.0, 0.0),
|
||||
&format!("{code} {name}"),
|
||||
theme::regular(13.0),
|
||||
if *flag { p.warning } else { p.text },
|
||||
p.text,
|
||||
w - cw * 3.0 - 8.0,
|
||||
);
|
||||
if *flag {
|
||||
// Flag a category already running well above its average.
|
||||
let hot = *avg > Decimal::ZERO && *now > *avg * Decimal::new(125, 2);
|
||||
let vals = [(*now, hot), (*last, false), (*avg, false)];
|
||||
for (i, (v, flag)) in vals.iter().enumerate() {
|
||||
let x = r.right() - cw * (2 - i) as f32 - 4.0;
|
||||
ui.painter().text(
|
||||
egui::pos2(x - 86.0, r.center().y),
|
||||
egui::pos2(x, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
icon::ARROW_UP_RIGHT,
|
||||
egui::FontId::proportional(13.0),
|
||||
p.warning,
|
||||
fmt::money(v.round_dp(2)),
|
||||
theme::regular(13.0),
|
||||
if *flag { p.warning } else { p.text },
|
||||
);
|
||||
if *flag {
|
||||
ui.painter().text(
|
||||
egui::pos2(x - 86.0, r.center().y),
|
||||
egui::Align2::RIGHT_CENTER,
|
||||
icon::ARROW_UP_RIGHT,
|
||||
egui::FontId::proportional(13.0),
|
||||
p.warning,
|
||||
);
|
||||
}
|
||||
}
|
||||
if resp.clicked() {
|
||||
go = b.ledger.chart().find_by_code(code).map(|a| a.id);
|
||||
}
|
||||
}
|
||||
if resp.clicked() {
|
||||
go = b.ledger.chart().find_by_code(code).map(|a| a.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
if let Some(id) = go {
|
||||
|
||||
Reference in new issue
Block a user