Reports
NAME
App::Moneymoor::Screen::Reports - the reports tab: the period's cash-flow strip and the bar chart of what it went on.
SYNOPSIS
use App::Moneymoor::Screen::Reports;
# Built by Screen::Main's content-host seam, never directly:
my $tab = App::Moneymoor::Screen::Reports.new(main => $main);
$tab.build($content-host); # mounts the strip and the chart
$tab.install-subscriptions; # digest / period / by-group β repaint
$tab.install-keybinds; # b
$tab.chart.data.head<label>; # 'Β£412.50 Groceries'
$tab.by-group; # False β b flips it
$tab.summary.parent.sizing.value; # 3 β the strip fits on one line
DESCRIPTION
VBox (the content host)
β Border(fixed 3 or 4, 'March 2026 summary') β RichText strip
β Border(flex, 'Spending by category',
bottom '+3 moreβ¦') β BarChart (horizontal)
Every number on screen comes out of View::ReportRow; this file is
the wiring. The tab is period-scoped and shares the budget tab's [ /
] navigation β the period lives in app/period, so switching tabs
keeps the user where they were.
One pane takes focus, and it is the chart
mount-pane registers a pane's hint context and makes the first one
mounted the tab's focus target. The summary strip is a caption for the
chart, not a thing to drive, so it is added to the host directly (with
its store pushed down by hand β a fixed-size child does not get
set-store cascaded) and only the chart is mounted. That makes the
chart the tab's focus target, the reports hint context's pane, and
the widget b is bound on.
The chart is constructed focusable for exactly that reason. A
BarChart has nothing to do with a keystroke of its own, but a pane
that cannot take focus leaves the hint footer resolving to generic
and the tab's one keybind unreachable.
The cut is a function of the pane's height
Horizontal bars do not scroll and do not compress below one row each,
so a chart with more categories than rows draws the overflow on top of
its own axis. The tab therefore asks View::ReportRow::top-spending
for as many bars as it has rows for (less the one the value axis
takes), and puts '+3 moreβ¦' in the pane's bottom title β where
it costs no row, which is the whole problem being solved.
The height is read at every repaint rather than cached, and
handle-resize re-runs the cut when a resize changes how many bars
fit. Screen::Main owns the app's single App.on-resize callback
and fans it out here (resize callbacks accumulate with no way to
remove them, so a per-tab registration would leak one per visit).
The strip's box is as deep as the strip
The summary pane is three rows on any terminal wide enough for Β§4.6's five figures β frame, one line, frame β and four only when the strip has to wrap. A fourth row held in reserve for a wrap that never happens is a blank line inside a box, which reads as a bug rather than as spacing, and it is a row taken off the chart.
View::ReportRow::summary-rows-needed owns the rule and
!fit-summary-height applies it, from both of the places the answer
can change: the strip's own repaint (a period whose totals gained a
digit) and a resize (the same strip in a narrower box). Applying it is
Selkie::Widget::MultiLineInput's re-measure idiom β set-sizing
followed by a dirty mark, because the parent layout re-reads a child's
sizing on its next render and never on its own β with
Widget.mark-screen-dirty as the mark, since the row that moves here
is a row the chart below gains or loses.
The height is only touched when it actually changes;
$!summary-rows-painted is to the strip what $!limit-painted is
to the chart.
Why the digest is enough here
The register keys on ws/rev because neither change token can see a
memo or a cleared state. Reports has the opposite problem and the
opposite answer: every figure on this tab β spending, inflow, outflow,
assigned β moves the derivation, so budget/digest covers all of
them, and a payee rename that leaves the derivation identical genuinely
changes nothing here.
METHODS
build($host)β construct and mount.install-subscriptions()/install-keybinds()β the two seamsScreen::Main::Subscriptions::install-contentandScreen::Main::Keybinds::install-contentcall.refresh()β repaint both panes; the subscription callback.refresh-summary()/refresh-chart()β one pane each, and the two subscription callbacks.refresh-summaryalso fits the strip's box to the strip.handle-resize(Int $rows, Int $cols)β re-fit the strip's box and re-cut the bars.chart,summary,by-group,rows,hidden-count.
SEE ALSO
App::Moneymoor::View::ReportRow β every figure and every label.
App::Moneymoor::StoreHandlers β
reports/toggle-by-group.Selkie::Widget::BarChart β the chart, in horizontal mode.