Files
dota_factory/src/test/FloatingPanelPlacementTest.cpp
Malte Langkabel 392a2b8d00 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
2026-08-17 16:24:14 +02:00

259 lines
12 KiB
C++

#include "catch.hpp"
#include <vector>
#include <QRect>
#include "FloatingPanelPlacement.h"
// The band every case below places into: 1000x600, so a bottom edge of 599.
static QRect makeBand()
{
return QRect(0, 0, 1000, 600);
}
// The two distances the rules run on, deliberately different (REQ-UI-SELECTION-PANEL):
// the edge margin the panel keeps from the view and from the widgets it steps around, and
// the wider gap it keeps from the selection -- half a tile, so 20 px at a 40 px tile.
static const int kMarginPx = 8;
static const int kSelectionGapPx = 20;
TEST_CASE("With nothing in the way a widget may use the whole band", "[layout]")
{
// REQ-UI-SELECTION-PANEL: the band's own bottom is the limit when no other floating
// widget has been placed yet.
REQUIRE(getAvailableBottomPx(makeBand(), {}, 0, 999, kMarginPx) == 599);
}
TEST_CASE("A widget in the same column pushes the bottom above it", "[layout]")
{
// The build button bar sitting at the bottom center leaves the space above it, less
// the margin kept between the two (REQ-UI-BUILD-BAR).
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72) };
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 350, 650, kMarginPx) == 511);
}
TEST_CASE("A widget beside the column does not shorten it", "[layout]")
{
// The controls panel in the bottom-left corner is not in the way of a panel standing
// at the right edge, however tall it is (REQ-UI-CONTROLS-PANEL).
const std::vector<QRect> occupied = { QRect(0, 100, 200, 499) };
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 700, 999, kMarginPx) == 599);
// Touching columns do count as meeting: the panel starts exactly where the widget
// ends, which is an overlap of one pixel.
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 199, 999, kMarginPx) == 91);
}
TEST_CASE("The topmost widget in the column decides", "[layout]")
{
// Several widgets meet the column: the one that reaches highest is the binding one,
// whatever order they are given in.
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72),
QRect(0, 300, 500, 299),
QRect(900, 560, 100, 40) };
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 450, 550, kMarginPx) == 291);
}
TEST_CASE("An empty rectangle occupies nothing", "[layout]")
{
// A floating widget that is hidden contributes a null rect rather than being left
// out of the pass.
const std::vector<QRect> occupied = { QRect(), QRect(400, 520, 200, 0) };
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 0, 999, kMarginPx) == 599);
}
TEST_CASE("A widget filling the column leaves nothing", "[layout]")
{
// The caller is expected to notice that the space left is not positive rather than
// being handed a floor of its own.
const std::vector<QRect> occupied = { QRect(0, 0, 1000, 600) };
REQUIRE(getAvailableBottomPx(makeBand(), occupied, 0, 999, kMarginPx) == -9);
}
// ---------------------------------------------------------------------------
// Which side of the selection the panel takes
// ---------------------------------------------------------------------------
TEST_CASE("The panel stands to the right of the selection where it fits", "[layout]")
{
// REQ-UI-SELECTION-PANEL: right of the anchor is the first choice.
REQUIRE(chooseSide(makeBand(), QRect(100, 100, 60, 60), 300, kSelectionGapPx)
== PanelSide::Right);
}
TEST_CASE("The panel goes left when the right cannot hold it", "[layout]")
{
// A selection near the right edge leaves 80 px there once the gap it keeps from the
// selection is taken off, not enough for a 300 px panel, and the left is wide open.
REQUIRE(chooseSide(makeBand(), QRect(880, 100, 20, 60), 300, kSelectionGapPx)
== PanelSide::Left);
}
TEST_CASE("The room a side offers is measured less the gap", "[layout]")
{
// A 200 px panel beside a selection whose right edge leaves 214 px to the band's: it
// fits there on the margin alone, but not once the wider gap is taken off.
const QRect anchorRect(766, 100, 20, 60);
REQUIRE(chooseSide(makeBand(), anchorRect, 200, kMarginPx) == PanelSide::Right);
REQUIRE(chooseSide(makeBand(), anchorRect, 200, kSelectionGapPx) == PanelSide::Left);
}
TEST_CASE("Fitting on neither side, the panel takes the roomier one", "[layout]")
{
// A bounding box spanning most of the view: 180 px free on the left, 80 on the
// right, and a 300 px panel fits in neither. It covers as little as it can.
REQUIRE(chooseSide(makeBand(), QRect(200, 100, 700, 200), 300, kSelectionGapPx)
== PanelSide::Left);
REQUIRE(chooseSide(makeBand(), QRect(100, 100, 700, 200), 300, kSelectionGapPx)
== PanelSide::Right);
}
// ---------------------------------------------------------------------------
// Where it then stands
// ---------------------------------------------------------------------------
TEST_CASE("The panel sits beside the anchor with its top edges aligned", "[layout]")
{
// REQ-UI-SELECTION-PANEL: separated by the gap it keeps from the selection, growing
// away from it, top edge on the anchor's top edge.
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 120, 60, 60),
PanelSide::Right, QSize(300, 200), {},
kSelectionGapPx, kMarginPx);
REQUIRE(placed == QRect(180, 120, 300, 200));
const QRect placedLeft = placeBesideAnchor(makeBand(), QRect(520, 120, 60, 60),
PanelSide::Left, QSize(300, 200), {},
kSelectionGapPx, kMarginPx);
REQUIRE(placedLeft == QRect(200, 120, 300, 200));
}
TEST_CASE("The gap separates the panel horizontally only", "[layout]")
{
// REQ-UI-SELECTION-PANEL: widening the gap moves the panel further from the selection
// sideways and nowhere else -- its top stays level with the top of what it describes.
const QRect anchorRect(100, 120, 60, 60);
REQUIRE(placeBesideAnchor(makeBand(), anchorRect, PanelSide::Right, QSize(300, 200),
{}, 0, kMarginPx)
== QRect(160, 120, 300, 200));
REQUIRE(placeBesideAnchor(makeBand(), anchorRect, PanelSide::Right, QSize(300, 200),
{}, kSelectionGapPx, kMarginPx)
== QRect(180, 120, 300, 200));
}
TEST_CASE("A panel that would hang below the view is lifted", "[layout]")
{
// Top-aligning with a selection low in the view would put the panel's bottom past
// the band, so it rises until it fits rather than overrunning it.
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 500, 60, 60),
PanelSide::Right, QSize(300, 200), {},
kSelectionGapPx, kMarginPx);
REQUIRE(placed == QRect(180, 400, 300, 200));
}
TEST_CASE("A panel standing over another widget rises above it", "[layout]")
{
// The controls panel in the bottom-left is in the way of a panel placed to the left
// of a selection: it clears the top of it by the edge margin, the gap from the
// selection having settled its left edge (REQ-UI-CONTROLS-PANEL).
const std::vector<QRect> occupied = { QRect(0, 300, 260, 300) };
const QRect placed = placeBesideAnchor(makeBand(), QRect(500, 250, 60, 60),
PanelSide::Left, QSize(300, 200), occupied,
kSelectionGapPx, kMarginPx);
REQUIRE(placed == QRect(180, 92, 300, 200));
}
TEST_CASE("A panel taller than the space left is capped", "[layout]")
{
// Capping is the caller's cue to scroll: it asked for 700 and got what there was.
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 100, 60, 60),
PanelSide::Right, QSize(300, 700), {},
kSelectionGapPx, kMarginPx);
REQUIRE(placed == QRect(180, 0, 300, 600));
}
TEST_CASE("A panel that fits on neither side is pushed inside the view", "[layout]")
{
// The one case where it covers part of the selection (REQ-UI-SELECTION-PANEL): it
// stands as far from the anchor as the band allows, not off the edge of it.
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 100, 700, 200),
PanelSide::Right, QSize(300, 200), {},
kSelectionGapPx, kMarginPx);
REQUIRE(placed == QRect(700, 100, 300, 200));
}
// ---------------------------------------------------------------------------
// Where it stands once the player has dragged it (REQ-UI-SELECTION-PANEL-DRAG)
// ---------------------------------------------------------------------------
TEST_CASE("A dragged panel stands where it was dropped", "[layout]")
{
// With room for it there, the desired top-left corner is the answer: nothing about
// the selection it describes is consulted any more.
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(420, 180),
QSize(300, 200), {}, kMarginPx);
REQUIRE(placed == QRect(420, 180, 300, 200));
}
TEST_CASE("A panel dragged past the view's edges is pushed back inside", "[layout]")
{
// Dropped off the right edge and below the bottom, it is brought back within the
// band, as an anchored panel is.
REQUIRE(placeAtDesiredTopLeft(makeBand(), QPoint(900, 500), QSize(300, 200), {},
kMarginPx)
== QRect(700, 400, 300, 200));
// And off the left edge and above the top, the other way.
REQUIRE(placeAtDesiredTopLeft(makeBand(), QPoint(-120, -60), QSize(300, 200), {},
kMarginPx)
== QRect(0, 0, 300, 200));
}
TEST_CASE("A dragged panel rises above the widget it was dropped over", "[layout]")
{
// The player drops it over the build button bar; it lands above the bar's top by the
// margin instead, the bar staying where it is (REQ-UI-BUILD-BAR).
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72) };
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(450, 480),
QSize(300, 200), occupied, kMarginPx);
REQUIRE(placed == QRect(450, 312, 300, 200));
}
TEST_CASE("A widget beside a dragged panel's column does not move it", "[layout]")
{
// Only what the panel's own column meets is in its way: the controls panel in the
// bottom-left corner leaves a panel dropped at the right edge alone
// (REQ-UI-CONTROLS-PANEL).
const std::vector<QRect> occupied = { QRect(0, 300, 260, 300) };
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(600, 380),
QSize(300, 200), occupied, kMarginPx);
REQUIRE(placed == QRect(600, 380, 300, 200));
}
TEST_CASE("A dragged panel too tall for the room left is capped", "[layout]")
{
// Capping is the caller's cue to scroll here too: a card asking for 700 px over a bar
// that leaves 512 px of band gets what there is, from the top of the band.
const std::vector<QRect> occupied = { QRect(400, 520, 200, 72) };
const QRect placed = placeAtDesiredTopLeft(makeBand(), QPoint(450, 100),
QSize(300, 700), occupied, kMarginPx);
REQUIRE(placed == QRect(450, 0, 300, 512));
}
TEST_CASE("A dragged panel returns to where it was dropped", "[layout]")
{
// The resolution never writes back to the desired position: a panel lifted above the
// build button bar goes back to the point the player dropped it at as soon as the bar
// stops meeting its column -- here because the bar's button set shrank.
const QPoint desired(450, 380);
const std::vector<QRect> wideBar = { QRect(400, 520, 200, 72) };
REQUIRE(placeAtDesiredTopLeft(makeBand(), desired, QSize(300, 200), wideBar,
kMarginPx)
== QRect(450, 312, 300, 200));
const std::vector<QRect> narrowBar = { QRect(400, 520, 40, 72) };
REQUIRE(placeAtDesiredTopLeft(makeBand(), desired, QSize(300, 200), narrowBar,
kMarginPx)
== QRect(450, 380, 300, 200));
}