place the floating widgets in one ordered pass

The three widgets over the game world view each cached a rect handed to them by
MainWindow's resize, then re-placed themselves from it. But the build button bar
re-centers on a building unlock and the controls panel re-fits on a 50 ms timer,
neither of which goes through MainWindow, so the rects the others held went
stale -- and each widget re-implemented its own avoidance against them.

They now implement FloatingPanel and are placed in one ordered pass: the bar
takes what it wants, the controls panel steps around the bar, and the selection
panel keeps clear of both. A widget that changed size or visibility publishes
FloatingLayoutInvalidatedEvent instead of moving itself, because what it may
take depends on the widgets placed before it.

The rule they step around each other by is one function in lib, where it can be
tested without a display -- the only way any of this geometry gets automated
cover, screen capture of the world view being blank here.

The selection panel keeps its right edge and its vertical centering, but the
space it centers in is now what its own column has left free rather than the
full-width strip the bar used to reserve. It therefore sits lower than before
where the centered bar does not reach it, and it now clears the controls panel,
which it previously ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-08 19:10:00 +02:00
parent 37899ea964
commit e66eb7a81f
17 changed files with 329 additions and 143 deletions

View File

@@ -5,6 +5,9 @@
#include <QVBoxLayout>
#include "BuildingIconCache.h"
#include "EventManager.h"
#include "FloatingLayoutInvalidatedEvent.h"
#include "FloatingPanelPlacement.h"
#include "ItemIconCache.h"
#include "Simulation.h"
#include "VisualsConfig.h"
@@ -13,8 +16,8 @@
namespace
{
// Distance kept between the panel and the edges of the band it is anchored to
// (REQ-UI-SELECTION-PANEL).
// Distance kept between the panel and the edges of the game world view, and between it
// and the widgets it steps around (REQ-UI-SELECTION-PANEL).
const int kMarginPx = 8;
// Upper bound on the card width. The panel is content-sized, but several of the cards'
@@ -94,10 +97,10 @@ SelectionPanel::~SelectionPanel()
unregisterForEvents();
}
void SelectionPanel::anchorTo(const QRect& bandRect)
void SelectionPanel::invalidateLayout()
{
m_bandRect = bandRect;
updateVisibility();
EventManager::getInstance()->sendEventImmediately(
std::make_shared<FloatingLayoutInvalidatedEvent>());
}
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
@@ -178,7 +181,7 @@ void SelectionPanel::refreshContent()
}
m_content->refresh();
updateVisibility();
invalidateLayout();
}
void SelectionPanel::rebuildContent()
@@ -208,36 +211,28 @@ void SelectionPanel::rebuildContent()
m_content->show();
m_content->refresh();
}
updateVisibility();
invalidateLayout();
}
void SelectionPanel::updateVisibility()
void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
{
// Nothing selected in either category means no panel at all rather than an empty one
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible. Before the
// owner has anchored the panel there is nowhere to put it either, so it stays hidden
// until then.
const bool shouldShow = (m_content != nullptr) && !m_bandRect.isNull();
setVisible(shouldShow);
if (shouldShow)
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible.
setVisible(m_content != nullptr);
if (m_content == nullptr || viewRect.isNull())
{
refit();
return;
}
}
void SelectionPanel::refit()
{
const QRect band =
m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
// The panel's border is drawn around the scroll area rather than around the card, so
// it is added to whatever the card asks for. Spelled out here instead of read back
// from contentsMargins() because the stylesheet box is what sets it, and asking the
// style for it before the first show is unreliable.
const int borderPx = 1;
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
const int maxHeightPx = band.height() - 2 * borderPx;
if (maxWidthPx <= 0 || maxHeightPx <= 0)
const int borderPx = 1;
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
if (maxWidthPx <= 0 || band.height() <= 2 * borderPx)
{
return;
}
@@ -273,9 +268,26 @@ void SelectionPanel::refit()
// 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);
// How much height there is depends on where the panel stands: of the widgets
// placed before it, only those whose rectangles meet its own column are in its
// way (REQ-UI-SELECTION-PANEL). Its column follows from the width just measured,
// so this cannot be settled before it -- and where the scroll bar below widens
// the panel, the second pass settles it again against the wider column.
const int leftPx =
band.right() - (contentWidthPx + 2 * borderPx) + 1;
const int bottomPx = getAvailableBottomPx(band, occupiedRects, leftPx,
band.right(), kMarginPx);
const int freeHeightPx = bottomPx - band.top() + 1;
const int maxHeightPx = freeHeightPx - 2 * borderPx;
if (maxHeightPx <= 0)
{
return;
}
int contentHeightPx = measureAt(contentWidthPx).height();
// A card taller than the band is capped there and scrolls
// A card taller than the space left is capped there and scrolls
// (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
@@ -301,11 +313,11 @@ void SelectionPanel::refit()
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).
// Right-aligned in the band and centered vertically on the free part of it, so
// the panel sits above whatever the widgets placed before it are occupying rather
// than over them (REQ-UI-SELECTION-PANEL).
setGeometry(band.right() - panelWidthPx + 1,
band.top() + (band.height() - panelHeightPx) / 2,
band.top() + (freeHeightPx - panelHeightPx) / 2,
panelWidthPx, panelHeightPx);
}
}