let the selection panel decide its own scroll bar

Selecting a building after a piece of debris left a scroll bar on a card that
had nothing to scroll.

Two faults, both found by driving the panel through the reported sequences and
printing what it measured itself to:

The bar was Qt's decision. Asked for ScrollBarAsNeeded, the scroll area shows a
bar the moment the card is larger than its viewport -- which is true while the
card is being measured, since measuring means giving it a width -- and does not
take it back when the range turns out to be empty. The panel already works out
whether the card fits its band and widens itself for the bar when it does not,
so it now sets the policy itself: AlwaysOn when it decided to scroll, AlwaysOff
when it did not. The previous commit's resize-to-cap made this visible;
removing that alone traded the phantom bar for a real one, because the card's
height depends on the width it is measured at.

The measurement was also a pass short. A card's width follows from the room it
is given and its height from that width, so refit() measures twice: once at the
cap to learn the width the card wants, once at that width for the height. The
whole thing then runs twice, because parts of a freshly built card report an
unstyled size until the style reaches them during the first round -- that gap
was leaving the panel a few pixels short of what the card turned out to need,
which is a scroll bar over a card that looks like it fits.

Measured before and after, HQ card, roomy band:
  before  panel=113x114  card wants 111x121  bar visible, range empty
  after   panel=113x123  card wants 111x121  no bar
And in a 90px band it still scrolls: panel=130x74, range 47, bar shown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-07 21:13:32 +02:00
parent 6fa3ba7f0c
commit 44af3184d3

View File

@@ -62,7 +62,13 @@ SelectionPanel::SelectionPanel(Simulation* sim, const GameConfig* config,
m_scrollArea->setFrameShape(QFrame::NoFrame); m_scrollArea->setFrameShape(QFrame::NoFrame);
m_scrollArea->setWidgetResizable(true); m_scrollArea->setWidgetResizable(true);
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); // The panel works out for itself whether the card fits the band, and sizes itself to
// leave room for the bar when it does not (REQ-UI-SELECTION-PANEL), so refit() sets
// this policy rather than leaving the scroll area to decide. Asked to decide, it
// shows a bar the moment the card is momentarily larger than the viewport -- which
// happens while the card is being measured -- and does not take it back when the
// range turns out to be empty.
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scrollArea->viewport()->setAutoFillBackground(false); m_scrollArea->viewport()->setAutoFillBackground(false);
m_body->setAutoFillBackground(false); m_body->setAutoFillBackground(false);
m_scrollArea->setWidget(m_body); m_scrollArea->setWidget(m_body);
@@ -236,50 +242,70 @@ void SelectionPanel::refit()
return; return;
} }
// Measured at the cap rather than at whatever width the panel happens to have. A // What the card asks for at a given width. The width has to be applied before
// word-wrapped label's size hint follows its current width, so a panel that once // asking, because a card's height depends on the room it is given -- and so, once
// came out too narrow would keep reporting a narrow, tall hint and stay that way. // laid out, does the width it reports. Measuring at whatever width the panel
m_body->resize(maxWidthPx, m_body->height()); // happens to have carries the previous card's shape into this one.
//
// Cards are built and thrown away whole, so the body layout's cached hint describes // Each measurement re-runs the body layout. Cards are built and discarded whole, so
// the card before this one until it is discarded. Re-running the layout is what // its cached hint describes the card before this one until it is invalidated, and
// makes the current card's parts measurable: a card hides and shows its parts as it // re-running it is also what accounts for the parts a card hides and shows as it
// refreshes, and Qt would not get around to either on its own before the panel is // refreshes. The polish belongs to the same step: a freshly created chip reports an
// sized here. The polish belongs to the same step -- a freshly created chip reports // unstyled hint until the stylesheet has reached it, and the chips carry border and
// an unstyled hint until the stylesheet has reached it, and the chips carry border // padding that change their size.
// and padding that change their width. auto measureAt = [this](int widthPx) -> QSize
m_body->ensurePolished();
m_body->layout()->invalidate();
m_body->layout()->activate();
int contentWidthPx = qMin(m_body->sizeHint().width(), maxWidthPx);
// Word-wrapped labels only know their height once the width is fixed; the layout
// reports -1 when nothing in it wraps, in which case the plain hint is exact.
int contentHeightPx = m_body->heightForWidth(contentWidthPx);
if (contentHeightPx < 0)
{ {
contentHeightPx = m_body->sizeHint().height(); m_body->resize(widthPx, m_body->height());
} m_body->ensurePolished();
m_body->layout()->invalidate();
m_body->layout()->activate();
return m_body->sizeHint();
};
if (contentHeightPx > maxHeightPx) // Run twice. Parts of a card report an unstyled size until the style has actually
// reached them, which for a freshly built card happens during the first round of
// measuring; the second round then measures a card that is fully laid out and
// settles on the answer. Without it a card can end up a few pixels short of what it
// turns out to need, and the difference shows as a scroll bar over a card that
// looks like it fits.
for (int pass = 0; pass < 2; ++pass)
{ {
// First at the cap, the most room the card can ever get, to learn how wide it
// wants to be; then at that width for the height that follows from it.
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
int contentHeightPx = measureAt(contentWidthPx).height();
// A card taller than the band is capped there and scrolls // A card taller than the band is capped there and scrolls
// (REQ-UI-SELECTION-PANEL). The scroll bar is laid out beside the card, so the // (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel
// panel widens by its width to keep the card as wide as the height was computed // widens by its width to leave the card the width its height was measured for --
// for. // and where the cap does not allow that, the card is measured again at what is
contentHeightPx = maxHeightPx; // left over.
contentWidthPx = qMin( const bool scrolls = (contentHeightPx > maxHeightPx);
contentWidthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(), int viewportWidthPx = contentWidthPx;
maxWidthPx); if (scrolls)
{
const int scrollBarWidthPx =
m_scrollArea->verticalScrollBar()->sizeHint().width();
contentWidthPx = qMin(contentWidthPx + scrollBarWidthPx, maxWidthPx);
contentHeightPx = maxHeightPx;
viewportWidthPx = contentWidthPx - scrollBarWidthPx;
measureAt(viewportWidthPx);
}
m_scrollArea->setVerticalScrollBarPolicy(
scrolls ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
// Left at the size the scroll area is about to give it, so the card is not
// briefly wider than its viewport.
m_body->resize(viewportWidthPx, m_body->sizeHint().height());
const int panelWidthPx = contentWidthPx + 2 * borderPx;
const int panelHeightPx = contentHeightPx + 2 * borderPx;
// Right-aligned in the band and centered on it vertically. The band excludes the
// build button bar's strip, so centering here never puts the panel over the bar
// (REQ-UI-SELECTION-PANEL).
setGeometry(band.right() - panelWidthPx + 1,
band.top() + (band.height() - panelHeightPx) / 2,
panelWidthPx, panelHeightPx);
} }
const int panelWidthPx = contentWidthPx + 2 * borderPx;
const int panelHeightPx = contentHeightPx + 2 * borderPx;
// Right-aligned in the band and centered on it vertically. The band excludes the
// build button bar's strip, so centering here never puts the panel over the bar
// (REQ-UI-SELECTION-PANEL).
setGeometry(band.right() - panelWidthPx + 1,
band.top() + (band.height() - panelHeightPx) / 2,
panelWidthPx, panelHeightPx);
} }