place the selection panel beside what it describes

The panel was anchored to the right edge of the view, which is nowhere near
whatever the player just clicked. It now stands beside the selection: right of
it where it fits, otherwise left, otherwise the roomier side pushed inside the
view -- the one case where it covers part of what it describes.

Nothing told the panel where the selection was. The selection events carry ids
only, and the mode that separates a fresh selection from an expanded one is
consumed inside SelectionController before they are built, so the view now
publishes the selection's screen bounds itself, immediately before selecting and
only when the selection is starting. Freezing that rectangle is what holds the
panel still: it does not chase a scrolling view, a ship flying off, or a
selection being added to. Only the panel's own size still moves it, and even
then it keeps its side and the edge facing the selection.

The rectangles come from what the renderer was already computing for the
selection outlines, now shared rather than duplicated.

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:18:58 +02:00
parent e66eb7a81f
commit 997cbc65d3
14 changed files with 469 additions and 51 deletions

View File

@@ -116,6 +116,18 @@ void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> ev
rebuildContent();
}
void SelectionPanel::handleEvent(
std::shared_ptr<const SelectionAnchorChangedEvent> event)
{
// A new selection is starting. Both the anchor and the side are settled against it
// and then left alone for as long as it lasts (REQ-UI-SELECTION-PANEL); the side is
// only reset here, being resolved on the next placement once the card's width is
// known. The rect arrives in the world view's coordinates and is translated when the
// panel is placed, the two widgets being siblings in the same parent.
m_anchorRect = event->rectPx;
m_side.reset();
}
void SelectionPanel::handleEvent(
std::shared_ptr<const EntitySelectionChangedEvent> event)
{
@@ -226,6 +238,15 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
// The anchor is published in the world view's coordinates and this panel is placed in
// its parent's; the two widgets are siblings, so the view's own origin is the whole
// difference. Without an anchor the panel falls back to the top-right corner, by
// standing beside a point just outside that corner -- in practice unreachable, every
// non-empty selection following a click or a drag that publishes one.
const QRect anchorRect =
m_anchorRect.isNull() ? QRect(band.right() + kMarginPx + 1, band.top(), 1, 1)
: m_anchorRect.translated(viewRect.topLeft());
// 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
@@ -269,17 +290,26 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
// 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;
// Which side of the selection the panel takes is settled on the first placement
// after a new anchor and kept for as long as that selection lasts, so a card that
// grows or shrinks never flips the panel across the object it describes
// (REQ-UI-SELECTION-PANEL). This is the first point at which its width is known.
if (!m_side.has_value())
{
m_side = chooseSide(band, anchorRect, contentWidthPx + 2 * borderPx,
kMarginPx);
}
// How much height there is depends on where the panel ends up standing: of the
// widgets placed before it, only those whose rectangles meet its own column are
// in its way. That 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. Asking for the whole band's
// height is what makes the answer the most the panel could have there.
const int maxHeightPx =
placeBesideAnchor(band, anchorRect, *m_side,
QSize(contentWidthPx + 2 * borderPx, band.height()),
occupiedRects, kMarginPx).height() - 2 * borderPx;
if (maxHeightPx <= 0)
{
return;
@@ -313,11 +343,8 @@ void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& oc
const int panelWidthPx = contentWidthPx + 2 * borderPx;
const int panelHeightPx = contentHeightPx + 2 * borderPx;
// 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() + (freeHeightPx - panelHeightPx) / 2,
panelWidthPx, panelHeightPx);
setGeometry(placeBesideAnchor(band, anchorRect, *m_side,
QSize(panelWidthPx, panelHeightPx),
occupiedRects, kMarginPx));
}
}