keep the selection panel half a tile off what it describes

The panel had one distance for the view edges, the widgets it steps around,
and the selection alike, so it stood eight pixels from a building and touched
it outright along the top. The gap from the selection is now its own value,
half a tile, and it is horizontal only -- the top edges stay level.

It is sampled where the tile size is known, in the same moment as the anchor
rectangle, and travels with it: a rectangle frozen in one moment has no
meaningful distance to a tile size measured in another. chooseSide now takes
the gap in place of the margin, the band having already taken the margin off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-17 16:24:14 +02:00
parent abaccc45b5
commit 392a2b8d00
7 changed files with 115 additions and 61 deletions

View File

@@ -57,12 +57,13 @@ int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRe
}
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
int marginPx)
int selectionGapPx)
{
// What each side offers: the gap between the anchor and that edge of the band, less
// the margin the panel keeps from the anchor.
const int roomRightPx = band.right() - anchorRect.right() - marginPx;
const int roomLeftPx = anchorRect.left() - band.left() - marginPx;
// What each side offers: the room between the anchor and that edge of the band, less
// the gap the panel keeps from the anchor. The band's own inset from the view has
// already taken the edge margin off.
const int roomRightPx = band.right() - anchorRect.right() - selectionGapPx;
const int roomLeftPx = anchorRect.left() - band.left() - selectionGapPx;
if (roomRightPx >= widthPx)
{
@@ -79,20 +80,20 @@ PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
QSize wantedSize, const std::vector<QRect>& occupiedRects,
int marginPx)
int selectionGapPx, int marginPx)
{
const int widthPx = std::min(wantedSize.width(), band.width());
// Against the anchor on the chosen side, growing away from it: the edge facing the
// A gap from the anchor on the chosen side, growing away from it: the edge facing the
// selection is the one that stays put as the panel's content resizes. A panel that
// does not fit there is pushed back inside the view rather than hanging off it, which
// is what puts it over the selection when neither side had room.
const int wantedLeftPx = (side == PanelSide::Right)
? anchorRect.right() + marginPx + 1
: anchorRect.left() - marginPx - widthPx;
? anchorRect.right() + selectionGapPx + 1
: anchorRect.left() - selectionGapPx - widthPx;
// Top-aligned with the anchor, then lifted by however much of it hangs below what is
// free.
// Top-aligned with the anchor -- the gap separates the two horizontally and plays no
// part here -- then lifted by however much of the panel hangs below what is free.
return fitInBand(band, wantedLeftPx, anchorRect.top(), wantedSize, occupiedRects,
marginPx);
}

View File

@@ -10,6 +10,12 @@
// owner places them in one ordered pass, each into the space the earlier ones left free,
// and these are the rules they place themselves by. Pure geometry -- no widget is
// involved, which is what lets the rules be tested without a display.
//
// Two distances run through these rules and are deliberately different (see
// REQ-UI-SELECTION-PANEL). marginPx is the edge margin: what a widget keeps from the
// view's edges and from the widgets it steps around. selectionGapPx is the gap the
// selection panel keeps from the selection it describes -- half a tile, which is the
// wider of the two, so the panel stands clear of the objects rather than touching them.
// The lowest bottom edge available to a widget occupying the horizontal span
// [leftPx, rightPx] inside band: the band's own bottom, or marginPx above the topmost
@@ -29,19 +35,21 @@ enum class PanelSide
// The side a panel widthPx wide takes beside anchorRect: the right of it where it fits
// within band, otherwise the left, and where it fits on neither, whichever side leaves
// more room -- the one case in which the panel ends up over the selection
// (REQ-UI-SELECTION-PANEL). Decided once when the selection starts and kept for as long
// as it lasts, so a card that grows later never flips the panel across the object.
// (REQ-UI-SELECTION-PANEL). The room a side offers is what is left of it once the panel's
// gap from the selection is taken off. Decided once when the selection starts and kept for
// as long as it lasts, so a card that grows later never flips the panel across the object.
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
int marginPx);
int selectionGapPx);
// Where a panel of wantedSize stands beside anchorRect on the given side: separated from
// it by marginPx and growing away from it, its top edge on the anchor's top edge, pushed
// inside band and above whatever occupies it. The returned height is short of
// wantedSize's when there was not enough room, which is the caller's cue to scroll its
// content (REQ-UI-SELECTION-PANEL).
// it by selectionGapPx and growing away from it, its top edge on the anchor's top edge,
// pushed inside band and above whatever occupies it. The gap is horizontal only -- the
// panel's top sits level with the anchor's, however wide the gap. The returned height is
// short of wantedSize's when there was not enough room, which is the caller's cue to
// scroll its content (REQ-UI-SELECTION-PANEL).
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
QSize wantedSize, const std::vector<QRect>& occupiedRects,
int marginPx);
int selectionGapPx, int marginPx);
// Where a panel of wantedSize stands once the player has dragged it to desiredTopLeftPx:
// at that point, by the same rules that place it beside a selection -- pushed inside band,