fix issue where the selection panel sometimes collapses to very small size

This commit is contained in:
2026-08-07 20:59:55 +02:00
parent e5d689c8d8
commit 9f574fa05e
3 changed files with 98 additions and 37 deletions

View File

@@ -62,7 +62,13 @@ SelectionPanel::SelectionPanel(Simulation* sim, const GameConfig* config,
m_scrollArea->setFrameShape(QFrame::NoFrame);
m_scrollArea->setWidgetResizable(true);
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_body->setAutoFillBackground(false);
m_scrollArea->setWidget(m_body);
@@ -193,6 +199,13 @@ void SelectionPanel::rebuildContent()
if (m_content)
{
m_bodyLayout->addWidget(m_content);
// The show is what makes the card count. A widget created under an
// already-visible parent starts hidden, and a layout treats a hidden item as
// empty -- it adds nothing to the size hint until something shows it, which
// otherwise does not happen until the event loop next runs, long after refit()
// has measured the panel. The panel then fits itself to an empty body and
// collapses to its scroll bar.
m_content->show();
m_content->refresh();
}
updateVisibility();
@@ -214,11 +227,6 @@ void SelectionPanel::updateVisibility()
void SelectionPanel::refit()
{
// The layout drops hidden widgets from its size hint, but only once it has been
// re-run: a card hides and shows its parts as it refreshes, before Qt would get
// around to it on its own.
m_body->layout()->activate();
const QRect band =
m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
@@ -234,34 +242,70 @@ void SelectionPanel::refit()
return;
}
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)
// What the card asks for at a given width. The width has to be applied before
// asking, because a card's height depends on the room it is given -- and so, once
// laid out, does the width it reports. Measuring at whatever width the panel
// happens to have carries the previous card's shape into this one.
//
// Each measurement re-runs the body layout. Cards are built and discarded whole, so
// its cached hint describes the card before this one until it is invalidated, and
// re-running it is also what accounts for the parts a card hides and shows as it
// refreshes. The polish belongs to the same step: a freshly created chip reports an
// unstyled hint until the stylesheet has reached it, and the chips carry border and
// padding that change their size.
auto measureAt = [this](int widthPx) -> QSize
{
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
// (REQ-UI-SELECTION-PANEL). The scroll bar is laid out beside the card, so the
// panel widens by its width to keep the card as wide as the height was computed
// for.
contentHeightPx = maxHeightPx;
contentWidthPx = qMin(
contentWidthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(),
maxWidthPx);
// (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel
// widens by its width to leave the card the width its height was measured for --
// and where the cap does not allow that, the card is measured again at what is
// left over.
const bool scrolls = (contentHeightPx > maxHeightPx);
int viewportWidthPx = contentWidthPx;
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);
}