REQ-UI-CONTROLS-PANEL said the content scrolls once it outgrows the space
available; only the height cap was implemented, so the surplus rows were simply
cut off. Silent row loss is the one failure this panel must not have, and it
was reachable: the Selection context runs to fifteen rows, and a panel that has
also had to rise above the build button bar can be left with less room than
that.
The rows move into a scroll area. The heading stays outside it, so it remains
visible and remains the collapse control whatever the rows are doing.
The size can no longer come from the panel's own layout -- a scroll area's hint
describes a viewport, not its contents -- so the heading and the rows are
measured directly and the chrome added. Verified against a forced rebuild and
an artificially cramped view:
roomy: heading 56x13 + rows 124x188 -> 142x223, no scrollbar
collapsed: -> 74x31, same bottom edge
cramped: wanted 223, band 124 -> 159x124, +17 for the scrollbar
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
It was confined to the band above the build button bar's strip, borrowing the
selection panel's rule. That rule exists because the selection panel is
full-height on the right, where the bar's strip is genuinely in the way. This
panel is short and in the corner, and the bar is centered and sized to its
buttons, so the corner is normally free -- reserving the whole strip pushed the
panel up for a collision that was not happening.
It now sits in the bottom-left corner and shares the view's bottom edge with
the bar, rising only when the panel's rectangle would actually intersect the
bar's, in which case it clears the bar's top by the usual margin and caps its
height at what is left. So it moves for a wide bar and a wide panel, and drops
back into the corner as soon as they no longer meet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
The label was painted palette(bright-text), which is not a destructive color at
all: Qt has no such role, and bright-text is white by design, meant for text
over dark highlights. On this panel's light chrome it was white on grey.
The chips carry the warning now and the label keeps the ordinary text color, so
the row stays legible whatever the palette and only the binding is marked --
which is what REQ-UI-CONTROLS-CARD asked for in the first place, and what the
mockup shows. The red is a literal because no palette role means it, chosen to
read on a light and a dark background alike, and it is widget chrome, so like
the rest of this stylesheet it is deliberately not a visuals.toml color.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
A widget created under an already-visible parent starts hidden, and a layout
counts a hidden item as empty -- it adds nothing to the size hint. Nothing
showed the freshly built rows until the event loop next ran, long after the
panel had measured itself, so every rebuild measured an empty card.
That one fact explains both symptoms. Originally refit() read a stale cached
hint, which described the previous context and so drew the card one rebuild
behind. Invalidating that cache to fix it replaced a plausible wrong answer
with the true one for a card whose rows were all still hidden, which is why the
panel then collapsed to its heading on every mode change.
Measured on a forced rebuild, with the rows shown and without:
shown: rowItems=10 hidden=0 rowsHint=188 needed=140x221
hidden: rowItems=10 hidden=10 rowsHint=6 needed=72x39
39px being heading plus margins -- the collapsed card exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
The previous fix went too far: it activated the panel's own layout before
resizing, which lays the heading and the rows out inside the geometry left over
from the previous context. The panel then took that stale frame as its answer
and collapsed to almost nothing whenever the mode changed.
Only the rows layout is activated now -- that part was right, and is what makes
freshly added rows visible and so measurable. The size comes from
layout()->totalSizeHint(), which says how big the content needs to be without
reference to how big the panel currently is; setGeometry re-runs the outer
layout afterwards on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
The card was drawn one rebuild behind: selecting something for the first time
sized it for the context before it, and the next selection sized it for that
one. refit() activated the panel's outer layout but never the rows layout
underneath it, so it read a size hint describing rows that were no longer
there, while the freshly added ones were not yet shown and so counted for
nothing. Both layouts are now invalidated and re-run innermost first, and the
rows are polished before being measured -- the badge chips carry border and
padding, which a label reports only once the stylesheet has reached it.
Also drops letter-spacing from the stylesheet. Qt has no such property and
warned once per widget it was applied to, which was most of the console. The
heading and the caption set it on their QFont, where it works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
The General card was the one that ran its context rows and the global ones
together as a flat list. That made it the exception a player has to notice: the
same six rows sit under a caption everywhere else, so leaving them uncaptioned
here asks the reader to work out that they are the same six.
Requirement wording corrected with it -- it claimed the always-available rows
were the General context's entire content, which was never true. General has
Select, Select area and Deconstruct mode of its own above the divider, exactly
like the other contexts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG
The panel decides nothing about what the controls are: it asks the same
resolver the key handling and the mouse dispatch ask, and renders each badge
from the binding that resolver matches. A chip cannot claim a key that does
nothing, and a label cannot describe a click that does something else, because
neither is written here.
Display text is the one part that is not shared -- lib/core says what is
available and what triggers it, ControlActionText.cpp says what it is called.
That split is why the table can stay free of UI strings and still be the single
source of the pairing.
It refreshes on a timer rather than by subscribing. Two things that change a
row -- a belt drag starting, the ghost crossing a transfer target -- happen on
mouse movement and publish nothing, and they must still show while the game is
paused, so there is no event and no tick to hang it on. Resolving is comparing
two vectors of enums, and the rebuild is skipped unless they differ.
Left edge, bottom-aligned in the same band the selection panel is confined to,
so it clears the build bar's strip and never meets the panel on the opposite
edge. Clicking the heading collapses it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHcUerKAZKWNvSKJxYKbnG