Compare commits
2 Commits
37899ea964
...
997cbc65d3
| Author | SHA1 | Date | |
|---|---|---|---|
| 997cbc65d3 | |||
| e66eb7a81f |
@@ -16,6 +16,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
||||||
@@ -32,6 +33,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
||||||
|
|||||||
76
src/lib/core/FloatingPanelPlacement.cpp
Normal file
76
src/lib/core/FloatingPanelPlacement.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
||||||
|
int leftPx, int rightPx, int marginPx)
|
||||||
|
{
|
||||||
|
int bottomPx = band.bottom();
|
||||||
|
for (const QRect& occupied : occupiedRects)
|
||||||
|
{
|
||||||
|
if (occupied.isEmpty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Only what is actually in the way counts: a widget entirely to one side of this
|
||||||
|
// span is not below it, however tall it is.
|
||||||
|
if (occupied.right() < leftPx || occupied.left() > rightPx)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bottomPx = std::min(bottomPx, occupied.top() - marginPx - 1);
|
||||||
|
}
|
||||||
|
return bottomPx;
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
|
||||||
|
int marginPx)
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
if (roomRightPx >= widthPx)
|
||||||
|
{
|
||||||
|
return PanelSide::Right;
|
||||||
|
}
|
||||||
|
if (roomLeftPx >= widthPx)
|
||||||
|
{
|
||||||
|
return PanelSide::Left;
|
||||||
|
}
|
||||||
|
// Neither side can hold it without covering the selection, so it goes where it
|
||||||
|
// covers the least of it.
|
||||||
|
return (roomRightPx >= roomLeftPx) ? PanelSide::Right : PanelSide::Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
||||||
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
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
|
||||||
|
// selection is the one that stays put as the panel's content resizes.
|
||||||
|
int leftPx = (side == PanelSide::Right) ? anchorRect.right() + marginPx + 1
|
||||||
|
: anchorRect.left() - marginPx - widthPx;
|
||||||
|
// 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.
|
||||||
|
leftPx = std::min(leftPx, band.right() - widthPx + 1);
|
||||||
|
leftPx = std::max(leftPx, band.left());
|
||||||
|
|
||||||
|
// Only the widgets its own column meets can shorten it.
|
||||||
|
const int bottomPx = getAvailableBottomPx(band, occupiedRects, leftPx,
|
||||||
|
leftPx + widthPx - 1, marginPx);
|
||||||
|
const int heightPx =
|
||||||
|
std::min(wantedSize.height(), std::max(0, bottomPx - band.top() + 1));
|
||||||
|
|
||||||
|
// Top-aligned with the anchor, then lifted by however much of it hangs below what is
|
||||||
|
// free. Never above the band: a panel taller than the space left is capped instead,
|
||||||
|
// and scrolls.
|
||||||
|
int topPx = std::max(anchorRect.top(), band.top());
|
||||||
|
topPx = std::min(topPx, bottomPx - heightPx + 1);
|
||||||
|
topPx = std::max(topPx, band.top());
|
||||||
|
|
||||||
|
return QRect(leftPx, topPx, widthPx, heightPx);
|
||||||
|
}
|
||||||
43
src/lib/core/FloatingPanelPlacement.h
Normal file
43
src/lib/core/FloatingPanelPlacement.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
#include <QSize>
|
||||||
|
|
||||||
|
// Geometry for the widgets floating over the game world view (REQ-UI-WORLD-SIZE). Their
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// occupied rectangle whose horizontal extent meets that span. A rectangle beside the
|
||||||
|
// span is not in the way and does not shorten it (REQ-UI-SELECTION-PANEL,
|
||||||
|
// REQ-UI-CONTROLS-PANEL). The result is inclusive, as QRect::bottom() is.
|
||||||
|
int getAvailableBottomPx(const QRect& band, const std::vector<QRect>& occupiedRects,
|
||||||
|
int leftPx, int rightPx, int marginPx);
|
||||||
|
|
||||||
|
// Which side of the selection the panel stands on (REQ-UI-SELECTION-PANEL).
|
||||||
|
enum class PanelSide
|
||||||
|
{
|
||||||
|
Right,
|
||||||
|
Left
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
PanelSide chooseSide(const QRect& band, const QRect& anchorRect, int widthPx,
|
||||||
|
int marginPx);
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
QRect placeBesideAnchor(const QRect& band, const QRect& anchorRect, PanelSide side,
|
||||||
|
QSize wantedSize, const std::vector<QRect>& occupiedRects,
|
||||||
|
int marginPx);
|
||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BossWaveUpdatedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoicesAvailableEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionChangedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionAnchorChangedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameOverEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/GameResetEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WinEvent.h
|
||||||
@@ -43,6 +44,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingLayoutInvalidatedEvent.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
12
src/lib/eventsystem/event/FloatingLayoutInvalidatedEvent.h
Normal file
12
src/lib/eventsystem/event/FloatingLayoutInvalidatedEvent.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Asks the owner of the widgets floating over the game world view to re-run its placement
|
||||||
|
// pass (see ui/FloatingPanel.h). Published by a floating widget whose content or
|
||||||
|
// visibility changed: what space that widget may take depends on the ones placed before
|
||||||
|
// it, so it cannot re-place itself alone (REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL,
|
||||||
|
// REQ-UI-SELECTION-PANEL). Carries no payload -- the pass re-reads every widget.
|
||||||
|
class FloatingLayoutInvalidatedEvent : public Event
|
||||||
|
{
|
||||||
|
};
|
||||||
25
src/lib/eventsystem/event/SelectionAnchorChangedEvent.h
Normal file
25
src/lib/eventsystem/event/SelectionAnchorChangedEvent.h
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Where on the screen the selection that is about to be made sits: the bounds of the one
|
||||||
|
// object selected, or of all of them when the selection starts as a multi-selection
|
||||||
|
// (REQ-UI-SELECTION-PANEL). The rectangle is in the game world view's own widget
|
||||||
|
// coordinates.
|
||||||
|
//
|
||||||
|
// Published only when a selection *starts* -- a plain click or drag, or an additive one
|
||||||
|
// onto an empty selection -- and always immediately before the selection itself. Adding
|
||||||
|
// to a selection publishes nothing, which is what leaves the selection panel where it
|
||||||
|
// is while the selection grows; and because the rectangle is screen space frozen at that
|
||||||
|
// moment, scrolling the view or a selected ship flying off does not move the panel
|
||||||
|
// either.
|
||||||
|
class SelectionAnchorChangedEvent : public Event
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SelectionAnchorChangedEvent(QRect rectPx)
|
||||||
|
: rectPx(rectPx) {}
|
||||||
|
|
||||||
|
const QRect rectPx;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ add_files(
|
|||||||
TunnelCompletionTest.cpp
|
TunnelCompletionTest.cpp
|
||||||
WorldCoordinatesTest.cpp
|
WorldCoordinatesTest.cpp
|
||||||
WorldCameraTest.cpp
|
WorldCameraTest.cpp
|
||||||
|
FloatingPanelPlacementTest.cpp
|
||||||
SelectionControllerTest.cpp
|
SelectionControllerTest.cpp
|
||||||
BuildModeControllerTest.cpp
|
BuildModeControllerTest.cpp
|
||||||
ControlActionTest.cpp
|
ControlActionTest.cpp
|
||||||
|
|||||||
156
src/test/FloatingPanelPlacementTest.cpp
Normal file
156
src/test/FloatingPanelPlacementTest.cpp
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const int kMarginPx = 8;
|
||||||
|
|
||||||
|
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, kMarginPx)
|
||||||
|
== PanelSide::Right);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The panel goes left when the right cannot hold it", "[layout]")
|
||||||
|
{
|
||||||
|
// A selection near the right edge leaves 100 px there, not enough for a 300 px
|
||||||
|
// panel, and the left is wide open.
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(880, 100, 20, 60), 300, kMarginPx)
|
||||||
|
== PanelSide::Left);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Fitting on neither side, the panel takes the roomier one", "[layout]")
|
||||||
|
{
|
||||||
|
// A bounding box spanning most of the view: 192 px free on the left, 92 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, kMarginPx)
|
||||||
|
== PanelSide::Left);
|
||||||
|
REQUIRE(chooseSide(makeBand(), QRect(100, 100, 700, 200), 300, kMarginPx)
|
||||||
|
== 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 margin, growing away from the selection,
|
||||||
|
// top edge on the anchor's top edge.
|
||||||
|
const QRect placed = placeBesideAnchor(makeBand(), QRect(100, 120, 60, 60),
|
||||||
|
PanelSide::Right, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 120, 300, 200));
|
||||||
|
|
||||||
|
const QRect placedLeft = placeBesideAnchor(makeBand(), QRect(500, 120, 60, 60),
|
||||||
|
PanelSide::Left, QSize(300, 200), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placedLeft == QRect(192, 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), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 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 margin (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,
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(192, 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), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(168, 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), {},
|
||||||
|
kMarginPx);
|
||||||
|
REQUIRE(placed == QRect(700, 100, 300, 200));
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "DisplayName.h"
|
#include "DisplayName.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "ExitBuilderModeRequestedEvent.h"
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
#include "IconCaption.h"
|
#include "IconCaption.h"
|
||||||
#include "InputMapper.h"
|
#include "InputMapper.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
@@ -270,15 +271,25 @@ BuildButtonBar::~BuildButtonBar()
|
|||||||
unregisterForEvents();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::anchorTo(const QRect& worldViewRect)
|
void BuildButtonBar::placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& /*occupiedRects*/)
|
||||||
{
|
{
|
||||||
m_viewRect = worldViewRect;
|
if (viewRect.isNull())
|
||||||
recenter();
|
{
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
// The layout drops hidden buttons from its size hint, but only once it has been
|
||||||
|
// re-run: an unlock changes which buttons are shown, and Qt would not get around to
|
||||||
|
// it before the bar is measured here.
|
||||||
|
layout()->activate();
|
||||||
|
|
||||||
int BuildButtonBar::getStripHeightPx() const
|
const QSize barSize = sizeHint();
|
||||||
{
|
// Centered, except that a bar wider than the view stays flush with its left edge
|
||||||
return height() + kBottomMarginPx;
|
// rather than hanging off both sides.
|
||||||
|
const int x = qMax(viewRect.left(),
|
||||||
|
viewRect.left() + (viewRect.width() - barSize.width()) / 2);
|
||||||
|
const int y = viewRect.bottom() - kBottomMarginPx - barSize.height() + 1;
|
||||||
|
setGeometry(QRect(QPoint(x, y), barSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::clearActiveButton()
|
void BuildButtonBar::clearActiveButton()
|
||||||
@@ -328,29 +339,11 @@ void BuildButtonBar::updateVisibility()
|
|||||||
{
|
{
|
||||||
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
|
m_buttons[i]->setVisible(m_sim->isBuildingUnlocked(m_types[i]));
|
||||||
}
|
}
|
||||||
// A hidden button leaves the row, so the bar has to take up its new width and
|
// A hidden button leaves the row, so the bar takes up a new width and has to be
|
||||||
// re-center on it (REQ-UI-BUILD-BAR).
|
// re-centered on it -- and the widgets that keep clear of the bar have to be placed
|
||||||
recenter();
|
// against that new rect too, so the whole pass is re-run (REQ-UI-BUILD-BAR).
|
||||||
}
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
void BuildButtonBar::recenter()
|
|
||||||
{
|
|
||||||
if (m_viewRect.isNull())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// The layout drops hidden buttons from its size hint, but only once it has been
|
|
||||||
// re-run: updateVisibility() calls this straight after setVisible(), before Qt
|
|
||||||
// would get around to it on its own.
|
|
||||||
layout()->activate();
|
|
||||||
|
|
||||||
const QSize barSize = sizeHint();
|
|
||||||
// Centered, except that a bar wider than the view stays flush with its left edge
|
|
||||||
// rather than hanging off both sides.
|
|
||||||
const int x = qMax(m_viewRect.left(),
|
|
||||||
m_viewRect.left() + (m_viewRect.width() - barSize.width()) / 2);
|
|
||||||
const int y = m_viewRect.bottom() - kBottomMarginPx - barSize.height() + 1;
|
|
||||||
setGeometry(QRect(QPoint(x, y), barSize));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BuildButtonBar::onBuildButton(int index)
|
void BuildButtonBar::onBuildButton(int index)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "DeconstructModeChangedEvent.h"
|
#include "DeconstructModeChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "UnlockedBuildingsChangedEvent.h"
|
#include "UnlockedBuildingsChangedEvent.h"
|
||||||
|
|
||||||
@@ -24,10 +25,12 @@ class BuildingIconCache;
|
|||||||
class ItemIconCache;
|
class ItemIconCache;
|
||||||
|
|
||||||
// The build menu: one horizontal row of build buttons floating over the game world
|
// The build menu: one horizontal row of build buttons floating over the game world
|
||||||
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons; its owner hands it the
|
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons and centers itself along the
|
||||||
// world view's rect through anchorTo() and it centers itself along that rect's
|
// bottom edge of the rect its owner places it in. It is placed first of the floating
|
||||||
// bottom edge.
|
// widgets and so takes the space it wants outright: nothing ever moves it aside, and the
|
||||||
|
// widgets placed after it keep out of its way instead.
|
||||||
class BuildButtonBar : public QWidget,
|
class BuildButtonBar : public QWidget,
|
||||||
|
public FloatingPanel,
|
||||||
public CombinedEventHandler<BuilderModeExitedEvent,
|
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
DeconstructModeChangedEvent,
|
DeconstructModeChangedEvent,
|
||||||
BuildHotkeyPressedEvent,
|
BuildHotkeyPressedEvent,
|
||||||
@@ -45,15 +48,11 @@ public:
|
|||||||
QWidget* parent = nullptr);
|
QWidget* parent = nullptr);
|
||||||
~BuildButtonBar() override;
|
~BuildButtonBar() override;
|
||||||
|
|
||||||
// Centers the bar along the bottom edge of the game world view's rect, given in
|
// Centers the bar along the bottom edge of the game world view's rect, given in the
|
||||||
// the bar's parent coordinates (REQ-UI-BUILD-BAR). The rect is remembered, so a
|
// bar's parent coordinates (REQ-UI-BUILD-BAR). Nothing is occupied yet when the bar
|
||||||
// re-center later driven by an unlock needs no second call from the owner.
|
// is placed, so it ignores what it is handed there.
|
||||||
void anchorTo(const QRect& worldViewRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
// Height of the strip the bar occupies along the bottom of the world view: its own
|
|
||||||
// height plus the margin below it. The selection panel keeps out of this strip, and
|
|
||||||
// the bar never moves for the panel in return (REQ-UI-BUILD-BAR).
|
|
||||||
int getStripHeightPx() const;
|
|
||||||
|
|
||||||
void clearActiveButton();
|
void clearActiveButton();
|
||||||
|
|
||||||
@@ -67,11 +66,6 @@ private:
|
|||||||
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
|
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
|
||||||
void updateVisibility();
|
void updateVisibility();
|
||||||
|
|
||||||
// Shrinks the bar to its currently shown buttons and re-centers it in the
|
|
||||||
// anchored rect (REQ-UI-BUILD-BAR). Does nothing until anchorTo() supplied that
|
|
||||||
// rect, so the construction-time call is harmless.
|
|
||||||
void recenter();
|
|
||||||
|
|
||||||
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuilderModeExitedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DeconstructModeChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
||||||
@@ -91,5 +85,4 @@ private:
|
|||||||
std::map<BuildingType, int> m_costs;
|
std::map<BuildingType, int> m_costs;
|
||||||
std::optional<std::size_t> m_activeIndex;
|
std::optional<std::size_t> m_activeIndex;
|
||||||
QPushButton* m_deconstructButton;
|
QPushButton* m_deconstructButton;
|
||||||
QRect m_viewRect;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
|
||||||
@@ -47,6 +49,7 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/SelectionBounds.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ControlActionText.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "ControlActionText.h"
|
#include "ControlActionText.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "GameWorldView.h"
|
#include "GameWorldView.h"
|
||||||
#include "selection/SelectionNames.h"
|
#include "selection/SelectionNames.h"
|
||||||
|
|
||||||
@@ -159,11 +162,10 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::anchorTo(const QRect& worldRect, const QRect& buildBarRect)
|
void ControlsPanel::invalidateLayout()
|
||||||
{
|
{
|
||||||
m_worldRect = worldRect;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
m_buildBarRect = buildBarRect;
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
refit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
||||||
@@ -174,7 +176,7 @@ void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
|||||||
{
|
{
|
||||||
m_collapsed = !m_collapsed;
|
m_collapsed = !m_collapsed;
|
||||||
m_scrollArea->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
event->accept();
|
event->accept();
|
||||||
}
|
}
|
||||||
@@ -241,7 +243,7 @@ void ControlsPanel::rebuild(const ControlContext& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_scrollArea->setVisible(!m_collapsed);
|
m_scrollArea->setVisible(!m_collapsed);
|
||||||
refit();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
||||||
@@ -277,9 +279,9 @@ QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
|||||||
return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
|
return name + QStringLiteral(" ") + kHeadingSeparator + QStringLiteral(" ") + detail;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ControlsPanel::refit()
|
void ControlsPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
|
||||||
{
|
{
|
||||||
if (m_worldRect.isNull()) { return; }
|
if (viewRect.isNull()) { return; }
|
||||||
|
|
||||||
// Rows are torn down and rebuilt wholesale, and a widget added to a layout is only
|
// Rows are torn down and rebuilt wholesale, and a widget added to a layout is only
|
||||||
// shown once that layout runs -- without this the new rows count for nothing and
|
// shown once that layout runs -- without this the new rows count for nothing and
|
||||||
@@ -290,7 +292,7 @@ void ControlsPanel::refit()
|
|||||||
m_rowsLayout->invalidate();
|
m_rowsLayout->invalidate();
|
||||||
m_rowsLayout->activate();
|
m_rowsLayout->activate();
|
||||||
|
|
||||||
const QRect band = m_worldRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
if (band.width() <= 0 || band.height() <= 0) { return; }
|
if (band.width() <= 0 || band.height() <= 0) { return; }
|
||||||
|
|
||||||
// Measured from the heading and the rows directly rather than from the panel's own
|
// Measured from the heading and the rows directly rather than from the panel's own
|
||||||
@@ -314,21 +316,17 @@ void ControlsPanel::refit()
|
|||||||
|
|
||||||
// The bottom-left corner of the view, growing upward as rows are added
|
// The bottom-left corner of the view, growing upward as rows are added
|
||||||
// (REQ-UI-CONTROLS-PANEL).
|
// (REQ-UI-CONTROLS-PANEL).
|
||||||
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
||||||
int heightPx = qMin(wantedHeightPx, band.height());
|
|
||||||
int topPx = band.bottom() - heightPx + 1;
|
|
||||||
|
|
||||||
// The build button bar is centered and sized to its buttons, so it usually leaves
|
// The build button bar is centered and sized to its buttons, so it usually leaves
|
||||||
// this corner free and the panel can share the bottom edge with it. Only when the
|
// this corner free and the panel can share the bottom edge with it. Only where the
|
||||||
// two would actually overlap does the panel rise, clearing the bar's top by the
|
// two would actually meet does the panel rise, clearing the bar's top by the same
|
||||||
// same margin it keeps from the view's edges (REQ-UI-BUILD-BAR).
|
// margin it keeps from the view's edges (REQ-UI-BUILD-BAR). The bar is the only
|
||||||
const QRect wanted(band.left(), topPx, widthPx, heightPx);
|
// widget placed before this one, so it is the only rect that can be in the way.
|
||||||
if (!m_buildBarRect.isNull() && wanted.intersects(m_buildBarRect))
|
const int bottomPx = getAvailableBottomPx(band, occupiedRects, band.left(),
|
||||||
{
|
band.left() + widthPx - 1, kMarginPx);
|
||||||
const int availablePx = m_buildBarRect.top() - kMarginPx - band.top();
|
int heightPx = qMin(wantedHeightPx, qMax(0, bottomPx - band.top() + 1));
|
||||||
heightPx = qMin(heightPx, qMax(0, availablePx));
|
int topPx = bottomPx - heightPx + 1;
|
||||||
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
|
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
|
||||||
// own width, or it would appear over the labels.
|
// own width, or it would appear over the labels.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "ControlAction.h"
|
#include "ControlAction.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
|
|
||||||
class GameWorldView;
|
class GameWorldView;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
@@ -29,7 +30,7 @@ class QVBoxLayout;
|
|||||||
// while the game is paused, so there is no tick to hang it on either. The rebuild is
|
// while the game is paused, so there is no tick to hang it on either. The rebuild is
|
||||||
// skipped unless the resolved content actually differs, which is a vector of enums to
|
// skipped unless the resolved content actually differs, which is a vector of enums to
|
||||||
// compare.
|
// compare.
|
||||||
class ControlsPanel : public QWidget
|
class ControlsPanel : public QWidget, public FloatingPanel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -42,10 +43,11 @@ public:
|
|||||||
// Places the panel in the bottom-left corner of the game world view. It shares the
|
// Places the panel in the bottom-left corner of the game world view. It shares the
|
||||||
// bottom edge with the build button bar rather than clearing its strip, the bar
|
// bottom edge with the build button bar rather than clearing its strip, the bar
|
||||||
// being centered and sized to its buttons and so usually leaving the left free; it
|
// being centered and sized to its buttons and so usually leaving the left free; it
|
||||||
// rises above the bar only when the two would otherwise overlap. `buildBarRect` is
|
// rises above the bar only when the two would otherwise overlap
|
||||||
// the bar's current geometry in the same coordinates, or a null rect when there is
|
// (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR). The bar is the only thing placed before
|
||||||
// none to avoid (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR).
|
// it, so it is the only rect it ever has to rise above.
|
||||||
void anchorTo(const QRect& worldRect, const QRect& buildBarRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Clicking the heading collapses and expands the panel (REQ-UI-CONTROLS-PANEL).
|
// Clicking the heading collapses and expands the panel (REQ-UI-CONTROLS-PANEL).
|
||||||
@@ -59,8 +61,8 @@ private:
|
|||||||
// The heading's "<name> * <detail>" text for the context, detail omitted when the
|
// The heading's "<name> * <detail>" text for the context, detail omitted when the
|
||||||
// context has none.
|
// context has none.
|
||||||
QString getHeadingText(const ControlContext& context) const;
|
QString getHeadingText(const ControlContext& context) const;
|
||||||
// Re-fits the panel to its content within the anchored band.
|
// Asks for the placement pass to be re-run, the panel's own size having changed.
|
||||||
void refit();
|
void invalidateLayout();
|
||||||
|
|
||||||
const GameWorldView* m_view;
|
const GameWorldView* m_view;
|
||||||
|
|
||||||
@@ -82,9 +84,4 @@ private:
|
|||||||
// Survives context changes and simulation restarts; presentation only, never a
|
// Survives context changes and simulation restarts; presentation only, never a
|
||||||
// command (REQ-UI-CONTROLS-PANEL).
|
// command (REQ-UI-CONTROLS-PANEL).
|
||||||
bool m_collapsed = false;
|
bool m_collapsed = false;
|
||||||
|
|
||||||
// The world view the panel sits in, and the build button bar it steps around, both
|
|
||||||
// in the coordinates of its parent. Null until the owner has anchored it.
|
|
||||||
QRect m_worldRect;
|
|
||||||
QRect m_buildBarRect;
|
|
||||||
};
|
};
|
||||||
|
|||||||
27
src/ui/FloatingPanel.h
Normal file
27
src/ui/FloatingPanel.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
// A widget floating over the game world view (REQ-UI-WORLD-SIZE). MainWindow places all
|
||||||
|
// of them in one ordered pass -- build button bar, then controls panel, then selection
|
||||||
|
// panel -- and each places itself into the space the earlier ones have not taken. The
|
||||||
|
// order is the priority the requirements state: the bar never moves for anyone
|
||||||
|
// (REQ-UI-BUILD-BAR), the controls panel steps around the bar (REQ-UI-CONTROLS-PANEL),
|
||||||
|
// and keeping clear of both is the selection panel's job (REQ-UI-SELECTION-PANEL).
|
||||||
|
//
|
||||||
|
// A widget never re-places itself, because what it may take depends on the widgets placed
|
||||||
|
// before it. It instead publishes FloatingLayoutInvalidatedEvent whenever its content or
|
||||||
|
// its visibility changed, and the owner re-runs the whole pass.
|
||||||
|
class FloatingPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~FloatingPanel() = default;
|
||||||
|
|
||||||
|
// Sets this widget's own geometry within viewRect, keeping clear of occupiedRects --
|
||||||
|
// the geometry of every floating widget already placed in this pass, in the same
|
||||||
|
// coordinates. A widget with nothing to show hides itself and takes no space.
|
||||||
|
virtual void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) = 0;
|
||||||
|
};
|
||||||
@@ -47,6 +47,8 @@
|
|||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "DebrisSystem.h"
|
#include "DebrisSystem.h"
|
||||||
|
#include "SelectionAnchorChangedEvent.h"
|
||||||
|
#include "SelectionBounds.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
@@ -1255,6 +1257,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
|
if (!buildingHit.has_value()) { buildingHit = siteAtTile(tile); }
|
||||||
if (buildingHit.has_value())
|
if (buildingHit.has_value())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {*buildingHit}, {}, {});
|
||||||
m_selection.selectBuildings({*buildingHit}, mode);
|
m_selection.selectBuildings({*buildingHit}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1262,6 +1265,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
const entt::entity actorHit = entityAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||||
if (actorHit != entt::null)
|
if (actorHit != entt::null)
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, {actorHit}, {});
|
||||||
m_selection.selectFieldObjects({actorHit}, {}, mode);
|
m_selection.selectFieldObjects({actorHit}, {}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1269,6 +1273,7 @@ bool GameWorldView::selectAtPoint(QPoint tile, QVector2D worldPos, bool additive
|
|||||||
const entt::entity debrisHit = debrisAtWorldPos(m_sim->getAdmin(), worldPos);
|
const entt::entity debrisHit = debrisAtWorldPos(m_sim->getAdmin(), worldPos);
|
||||||
if (debrisHit != entt::null)
|
if (debrisHit != entt::null)
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, {}, {debrisHit});
|
||||||
m_selection.selectFieldObjects({}, {debrisHit}, mode);
|
m_selection.selectFieldObjects({}, {debrisHit}, mode);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1289,6 +1294,7 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
buildingsInBox(m_sim->getFactoryState(), m_boxStartTile, m_boxCurrentTile);
|
||||||
if (!boxIds.empty())
|
if (!boxIds.empty())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, boxIds, {}, {});
|
||||||
m_selection.selectBuildings(boxIds, mode);
|
m_selection.selectBuildings(boxIds, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1305,7 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
debrisInBox(m_sim->getAdmin(), m_boxStartTile, m_boxCurrentTile);
|
||||||
if (!boxActors.empty() || !boxDebris.empty())
|
if (!boxActors.empty() || !boxDebris.empty())
|
||||||
{
|
{
|
||||||
|
publishSelectionAnchor(mode, {}, boxActors, boxDebris);
|
||||||
m_selection.selectFieldObjects(boxActors, boxDebris, mode);
|
m_selection.selectFieldObjects(boxActors, boxDebris, mode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1307,6 +1314,35 @@ void GameWorldView::selectInBox(bool additive)
|
|||||||
if (!additive) { m_selection.clearAll(); }
|
if (!additive) { m_selection.clearAll(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameWorldView::publishSelectionAnchor(SelectionMode mode,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
// An additive gesture onto something already selected is growing that selection, not
|
||||||
|
// starting one, and the panel stays where it was put (REQ-UI-SELECTION-PANEL).
|
||||||
|
const bool startsSelection =
|
||||||
|
(mode == SelectionMode::Replace) || (m_selection.getSelectedBuildings().empty()
|
||||||
|
&& m_selection.getSelectedActors().empty()
|
||||||
|
&& m_selection.getSelectedDebris().empty());
|
||||||
|
if (!startsSelection)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen space, frozen here: the panel is placed against where the selection is at
|
||||||
|
// this moment and stays there, however far the view scrolls or the objects move
|
||||||
|
// afterwards.
|
||||||
|
const QRect anchorRect = getSelectionWidgetRect(*m_sim, getCoordinates(),
|
||||||
|
buildings, actors, debris);
|
||||||
|
if (anchorRect.isNull())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
|
std::make_shared<SelectionAnchorChangedEvent>(anchorRect));
|
||||||
|
}
|
||||||
|
|
||||||
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
void GameWorldView::mouseMoveEvent(QMouseEvent* event)
|
||||||
{
|
{
|
||||||
const WorldCoordinates coordinates = getCoordinates();
|
const WorldCoordinates coordinates = getCoordinates();
|
||||||
|
|||||||
@@ -226,6 +226,15 @@ private:
|
|||||||
// which is the only case that goes on to start a box drag.
|
// which is the only case that goes on to start a box drag.
|
||||||
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
bool selectAtPoint(QPoint tile, QVector2D worldPos, bool additive);
|
||||||
void selectInBox(bool additive);
|
void selectInBox(bool additive);
|
||||||
|
// Publishes where on the screen the selection about to be made sits, so the
|
||||||
|
// selection panel can be placed beside it (REQ-UI-SELECTION-PANEL). Called with
|
||||||
|
// what is about to be selected, immediately before selecting it, and publishes
|
||||||
|
// nothing unless that selection is starting rather than growing.
|
||||||
|
void publishSelectionAnchor(
|
||||||
|
SelectionMode mode,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris);
|
||||||
void stepSpeed(int delta);
|
void stepSpeed(int delta);
|
||||||
void placeAtTile(QPoint tile);
|
void placeAtTile(QPoint tile);
|
||||||
|
|
||||||
|
|||||||
@@ -172,19 +172,50 @@ void MainWindow::layoutPanels()
|
|||||||
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
||||||
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
||||||
m_gameWorldView->setGeometry(worldRect);
|
m_gameWorldView->setGeometry(worldRect);
|
||||||
// Sizes itself to its buttons and centers along the bottom of the world view
|
|
||||||
// (REQ-UI-BUILD-BAR).
|
|
||||||
m_buildButtonBar->anchorTo(worldRect);
|
|
||||||
// The panel confines itself to what the bar leaves free, so the bar never has to
|
|
||||||
// move for it (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
|
||||||
m_selectionPanel->anchorTo(
|
|
||||||
worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
|
|
||||||
// The opposite corner: bottom-left of the view, sharing the bottom edge with the
|
|
||||||
// bar rather than clearing its whole strip, and rising only if the two would meet.
|
|
||||||
// Anchored after the bar so the geometry it steps around is the current one
|
|
||||||
// (REQ-UI-CONTROLS-PANEL).
|
|
||||||
m_controlsPanel->anchorTo(worldRect, m_buildButtonBar->geometry());
|
|
||||||
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
|
||||||
|
|
||||||
|
// The floating widgets are placed in one ordered pass, each into the space the
|
||||||
|
// earlier ones have not taken (FloatingPanel.h). The order is the priority the
|
||||||
|
// requirements state: the build button bar takes what it wants and never moves for
|
||||||
|
// anyone (REQ-UI-BUILD-BAR), the controls panel steps around the bar
|
||||||
|
// (REQ-UI-CONTROLS-PANEL), and the selection panel keeps clear of both
|
||||||
|
// (REQ-UI-SELECTION-PANEL). A widget with nothing to show hides itself in placeIn()
|
||||||
|
// and takes no space.
|
||||||
|
//
|
||||||
|
// Re-entry is refused rather than queued: setGeometry() on a widget in the pass can
|
||||||
|
// reach code that asks for another pass, and the one already running is about to
|
||||||
|
// produce the same answer.
|
||||||
|
if (m_layingOut)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_layingOut = true;
|
||||||
|
|
||||||
|
const std::vector<QWidget*> floatingWidgets = {
|
||||||
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
||||||
|
const std::vector<FloatingPanel*> floatingPanels = {
|
||||||
|
m_buildButtonBar, m_controlsPanel, m_selectionPanel };
|
||||||
|
|
||||||
|
std::vector<QRect> occupiedRects;
|
||||||
|
for (std::size_t i = 0; i < floatingPanels.size(); ++i)
|
||||||
|
{
|
||||||
|
floatingPanels[i]->placeIn(worldRect, occupiedRects);
|
||||||
|
if (floatingWidgets[i]->isVisible())
|
||||||
|
{
|
||||||
|
occupiedRects.push_back(floatingWidgets[i]->geometry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_layingOut = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::handleEvent(
|
||||||
|
std::shared_ptr<const FloatingLayoutInvalidatedEvent> /*event*/)
|
||||||
|
{
|
||||||
|
// One of the floating widgets changed size or visibility. What each of them may take
|
||||||
|
// depends on the ones placed before it, so the answer is the whole pass rather than
|
||||||
|
// that one widget re-placing itself.
|
||||||
|
layoutPanels();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
void MainWindow::handleEvent(std::shared_ptr<const SchematicChoicesAvailableEvent> event)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "EscapeMenuRequestedEvent.h"
|
#include "EscapeMenuRequestedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
#include "GameOverEvent.h"
|
#include "GameOverEvent.h"
|
||||||
#include "LayoutDialogRequestedEvent.h"
|
#include "LayoutDialogRequestedEvent.h"
|
||||||
@@ -45,7 +46,8 @@ class MainWindow : public QWidget,
|
|||||||
LayoutDialogRequestedEvent,
|
LayoutDialogRequestedEvent,
|
||||||
RecipeSelectionRequestedEvent,
|
RecipeSelectionRequestedEvent,
|
||||||
BlueprintSaveRequestedEvent,
|
BlueprintSaveRequestedEvent,
|
||||||
BlueprintSelectionRequestedEvent>
|
BlueprintSelectionRequestedEvent,
|
||||||
|
FloatingLayoutInvalidatedEvent>
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -67,6 +69,7 @@ private:
|
|||||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BlueprintSaveRequestedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
void handleEvent(std::shared_ptr<const BlueprintSelectionRequestedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const FloatingLayoutInvalidatedEvent> event) override;
|
||||||
|
|
||||||
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
// Reloads the game config and visuals.toml from disk (REQ-CFG-RELOAD), shared
|
||||||
// by every restart path. On success the reloaded visuals are applied to this
|
// by every restart path. On success the reloaded visuals are applied to this
|
||||||
@@ -85,6 +88,8 @@ private:
|
|||||||
// both callers already hold theirs, which is what keeps the dim continuous when a
|
// both callers already hold theirs, which is what keeps the dim continuous when a
|
||||||
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
// confirmed save hands straight over to this dialog (REQ-UI-MODAL-DIM).
|
||||||
void showBlueprintSelectionDialog();
|
void showBlueprintSelectionDialog();
|
||||||
|
// Places the widgets floating over the game world view, in one ordered pass
|
||||||
|
// (FloatingPanel.h). Runs on a resize and on every FloatingLayoutInvalidatedEvent.
|
||||||
void layoutPanels();
|
void layoutPanels();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -109,4 +114,8 @@ private:
|
|||||||
|
|
||||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||||
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
std::shared_ptr<ParsedReplay> m_replay; // non-null => view-only playback
|
||||||
|
|
||||||
|
// Set while the placement pass runs, so a widget placed in it cannot start a second
|
||||||
|
// pass from inside the first.
|
||||||
|
bool m_layingOut = false;
|
||||||
};
|
};
|
||||||
|
|||||||
115
src/ui/SelectionBounds.cpp
Normal file
115
src/ui/SelectionBounds.cpp
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
#include "SelectionBounds.h"
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "DebrisComponent.h"
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "PositionComponent.h"
|
||||||
|
#include "Simulation.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
|
#include "WorldCoordinates.h"
|
||||||
|
#include "WorldPrimitives.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// The widget rectangle of a footprint anchored at a tile, both kinds of body being
|
||||||
|
// described the same way.
|
||||||
|
QRectF getFootprintWidgetRect(const WorldCoordinates& coordinates, QPoint anchor,
|
||||||
|
QSize footprint)
|
||||||
|
{
|
||||||
|
const QPointF topLeft = coordinates.tileToWidget(anchor);
|
||||||
|
return QRectF(topLeft.x(), topLeft.y(),
|
||||||
|
footprint.width() * static_cast<qreal>(coordinates.getTilePx()),
|
||||||
|
footprint.height() * static_cast<qreal>(coordinates.getTilePx()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
BuildingId id)
|
||||||
|
{
|
||||||
|
if (const Building* building = findBuilding(state, id))
|
||||||
|
{
|
||||||
|
return getFootprintWidgetRect(coordinates, building->anchor, building->footprint);
|
||||||
|
}
|
||||||
|
if (const ConstructionSite* site = findSite(state, id))
|
||||||
|
{
|
||||||
|
return getFootprintWidgetRect(coordinates, site->anchor, site->footprint);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QRectF> getActorWidgetRect(EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity actor)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(actor))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (admin.hasAll<StationBodyComponent>(actor))
|
||||||
|
{
|
||||||
|
const StationBodyComponent& body = admin.get<StationBodyComponent>(actor);
|
||||||
|
return getFootprintWidgetRect(coordinates, body.anchor, body.footprint);
|
||||||
|
}
|
||||||
|
if (admin.hasAll<PositionComponent>(actor))
|
||||||
|
{
|
||||||
|
// A ship is a triangle about its center; the square its longest extent fits in
|
||||||
|
// is what the panel is placed beside.
|
||||||
|
const QPointF center =
|
||||||
|
coordinates.worldToWidget(admin.get<PositionComponent>(actor).value);
|
||||||
|
const qreal extent = static_cast<qreal>(getShipForwardExtentPx(coordinates));
|
||||||
|
return QRectF(center.x() - extent, center.y() - extent,
|
||||||
|
2.0 * extent, 2.0 * extent);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QRectF> getDebrisWidgetRect(EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity debris)
|
||||||
|
{
|
||||||
|
if (!admin.isValid(debris) || !admin.hasAll<PositionComponent, DebrisComponent>(debris))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
const QPointF center =
|
||||||
|
coordinates.worldToWidget(admin.get<PositionComponent>(debris).value);
|
||||||
|
const qreal radius = static_cast<qreal>(getDebrisRadiusPx(coordinates));
|
||||||
|
return QRectF(center.x() - radius, center.y() - radius, 2.0 * radius, 2.0 * radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect getSelectionWidgetRect(Simulation& sim, const WorldCoordinates& coordinates,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris)
|
||||||
|
{
|
||||||
|
QRectF bounds;
|
||||||
|
// A null rect unites to nothing of its own, so the first object found sets the box
|
||||||
|
// and every later one grows it.
|
||||||
|
auto add = [&bounds](const std::optional<QRectF>& rect)
|
||||||
|
{
|
||||||
|
if (rect.has_value())
|
||||||
|
{
|
||||||
|
bounds = bounds.isNull() ? *rect : bounds.united(*rect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (BuildingId id : buildings)
|
||||||
|
{
|
||||||
|
add(getBuildingWidgetRect(sim.getFactoryState(), coordinates, id));
|
||||||
|
}
|
||||||
|
for (entt::entity actor : actors)
|
||||||
|
{
|
||||||
|
add(getActorWidgetRect(sim.getAdmin(), coordinates, actor));
|
||||||
|
}
|
||||||
|
for (entt::entity piece : debris)
|
||||||
|
{
|
||||||
|
add(getDebrisWidgetRect(sim.getAdmin(), coordinates, piece));
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds.isNull() ? QRect() : bounds.toAlignedRect();
|
||||||
|
}
|
||||||
50
src/ui/SelectionBounds.h
Normal file
50
src/ui/SelectionBounds.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
|
||||||
|
class EntityAdmin;
|
||||||
|
class Simulation;
|
||||||
|
class WorldCoordinates;
|
||||||
|
struct FactoryState;
|
||||||
|
|
||||||
|
// Where selectable objects are on the screen. The world renderer draws every selection
|
||||||
|
// outline from these rectangles, and the selection panel is placed beside the one that
|
||||||
|
// covers the whole selection (REQ-UI-SELECTION-PANEL).
|
||||||
|
//
|
||||||
|
// All of them are in the game world view's own widget coordinates, and all of them are
|
||||||
|
// only true for the WorldCoordinates they were asked for: the transform is a value built
|
||||||
|
// per frame or per event, so a rectangle does not survive a scroll or a resize.
|
||||||
|
|
||||||
|
// The footprint of a building or of a construction site (REQ-UI-SELECTION-CARD). Null
|
||||||
|
// when the id names neither, which is what a deconstruction under the caller looks like.
|
||||||
|
std::optional<QRectF> getBuildingWidgetRect(const FactoryState& state,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
BuildingId id);
|
||||||
|
|
||||||
|
// The body of a ship or of a defence station (REQ-UI-ENTITY-CLICK-SELECT): a station's
|
||||||
|
// footprint, or the square the ship's triangle is drawn in. The admin is taken by
|
||||||
|
// non-const reference only because EntityAdmin::hasAll() is not const; nothing here
|
||||||
|
// writes to it.
|
||||||
|
std::optional<QRectF> getActorWidgetRect(EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity actor);
|
||||||
|
|
||||||
|
// The circle a piece of debris is drawn as (REQ-UI-DEBRIS-CLICK-SELECT).
|
||||||
|
std::optional<QRectF> getDebrisWidgetRect(EntityAdmin& admin,
|
||||||
|
const WorldCoordinates& coordinates,
|
||||||
|
entt::entity debris);
|
||||||
|
|
||||||
|
// The rectangle covering a whole selection -- one object, or the bounding box of all of
|
||||||
|
// them (REQ-UI-SELECTION-PANEL). Objects that no longer resolve are skipped; the result
|
||||||
|
// is null when none of them does.
|
||||||
|
QRect getSelectionWidgetRect(Simulation& sim, const WorldCoordinates& coordinates,
|
||||||
|
const std::vector<BuildingId>& buildings,
|
||||||
|
const std::vector<entt::entity>& actors,
|
||||||
|
const std::vector<entt::entity>& debris);
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
#include "BuildingIconCache.h"
|
#include "BuildingIconCache.h"
|
||||||
|
#include "EventManager.h"
|
||||||
|
#include "FloatingLayoutInvalidatedEvent.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "ItemIconCache.h"
|
#include "ItemIconCache.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "VisualsConfig.h"
|
#include "VisualsConfig.h"
|
||||||
@@ -13,8 +16,8 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
// Distance kept between the panel and the edges of the band it is anchored to
|
// Distance kept between the panel and the edges of the game world view, and between it
|
||||||
// (REQ-UI-SELECTION-PANEL).
|
// and the widgets it steps around (REQ-UI-SELECTION-PANEL).
|
||||||
const int kMarginPx = 8;
|
const int kMarginPx = 8;
|
||||||
|
|
||||||
// Upper bound on the card width. The panel is content-sized, but several of the cards'
|
// Upper bound on the card width. The panel is content-sized, but several of the cards'
|
||||||
@@ -94,10 +97,10 @@ SelectionPanel::~SelectionPanel()
|
|||||||
unregisterForEvents();
|
unregisterForEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::anchorTo(const QRect& bandRect)
|
void SelectionPanel::invalidateLayout()
|
||||||
{
|
{
|
||||||
m_bandRect = bandRect;
|
EventManager::getInstance()->sendEventImmediately(
|
||||||
updateVisibility();
|
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||||
@@ -113,6 +116,18 @@ void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> ev
|
|||||||
rebuildContent();
|
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(
|
void SelectionPanel::handleEvent(
|
||||||
std::shared_ptr<const EntitySelectionChangedEvent> event)
|
std::shared_ptr<const EntitySelectionChangedEvent> event)
|
||||||
{
|
{
|
||||||
@@ -178,7 +193,7 @@ void SelectionPanel::refreshContent()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_content->refresh();
|
m_content->refresh();
|
||||||
updateVisibility();
|
invalidateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SelectionPanel::rebuildContent()
|
void SelectionPanel::rebuildContent()
|
||||||
@@ -208,36 +223,37 @@ void SelectionPanel::rebuildContent()
|
|||||||
m_content->show();
|
m_content->show();
|
||||||
m_content->refresh();
|
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
|
// 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
|
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible.
|
||||||
// owner has anchored the panel there is nowhere to put it either, so it stays hidden
|
setVisible(m_content != nullptr);
|
||||||
// until then.
|
if (m_content == nullptr || viewRect.isNull())
|
||||||
const bool shouldShow = (m_content != nullptr) && !m_bandRect.isNull();
|
|
||||||
setVisible(shouldShow);
|
|
||||||
if (shouldShow)
|
|
||||||
{
|
{
|
||||||
refit();
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void SelectionPanel::refit()
|
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||||
{
|
|
||||||
const QRect band =
|
// The anchor is published in the world view's coordinates and this panel is placed in
|
||||||
m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
// 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
|
// 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
|
// 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
|
// from contentsMargins() because the stylesheet box is what sets it, and asking the
|
||||||
// style for it before the first show is unreliable.
|
// style for it before the first show is unreliable.
|
||||||
const int borderPx = 1;
|
const int borderPx = 1;
|
||||||
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
||||||
const int maxHeightPx = band.height() - 2 * borderPx;
|
if (maxWidthPx <= 0 || band.height() <= 2 * borderPx)
|
||||||
if (maxWidthPx <= 0 || maxHeightPx <= 0)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -273,9 +289,35 @@ void SelectionPanel::refit()
|
|||||||
// First at the cap, the most room the card can ever get, to learn how wide it
|
// 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.
|
// wants to be; then at that width for the height that follows from it.
|
||||||
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
|
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
int contentHeightPx = measureAt(contentWidthPx).height();
|
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
|
// (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 --
|
// 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
|
// and where the cap does not allow that, the card is measured again at what is
|
||||||
@@ -301,11 +343,8 @@ void SelectionPanel::refit()
|
|||||||
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
||||||
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
||||||
|
|
||||||
// Right-aligned in the band and centered on it vertically. The band excludes the
|
setGeometry(placeBesideAnchor(band, anchorRect, *m_side,
|
||||||
// build button bar's strip, so centering here never puts the panel over the bar
|
QSize(panelWidthPx, panelHeightPx),
|
||||||
// (REQ-UI-SELECTION-PANEL).
|
occupiedRects, kMarginPx));
|
||||||
setGeometry(band.right() - panelWidthPx + 1,
|
|
||||||
band.top() + (band.height() - panelHeightPx) / 2,
|
|
||||||
panelWidthPx, panelHeightPx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,17 @@
|
|||||||
#include <QRect>
|
#include <QRect>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "DebrisSelectionChangedEvent.h"
|
#include "DebrisSelectionChangedEvent.h"
|
||||||
#include "DebugDrawToggledEvent.h"
|
#include "DebugDrawToggledEvent.h"
|
||||||
#include "EntitySelectionChangedEvent.h"
|
#include "EntitySelectionChangedEvent.h"
|
||||||
#include "EventHandler.h"
|
#include "EventHandler.h"
|
||||||
|
#include "FloatingPanel.h"
|
||||||
|
#include "FloatingPanelPlacement.h"
|
||||||
#include "PlayerCommandsAppliedEvent.h"
|
#include "PlayerCommandsAppliedEvent.h"
|
||||||
|
#include "SelectionAnchorChangedEvent.h"
|
||||||
#include "SelectionChangedEvent.h"
|
#include "SelectionChangedEvent.h"
|
||||||
#include "TickAdvancedEvent.h"
|
#include "TickAdvancedEvent.h"
|
||||||
#include "selection/SelectionContentFactory.h"
|
#include "selection/SelectionContentFactory.h"
|
||||||
@@ -28,14 +34,16 @@ class QVBoxLayout;
|
|||||||
// What each card looks like lives in src/ui/selection/.
|
// What each card looks like lives in src/ui/selection/.
|
||||||
//
|
//
|
||||||
// The panel floats over the game world view rather than occupying a column of its own
|
// The panel floats over the game world view rather than occupying a column of its own
|
||||||
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, anchors to the right edge of
|
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, places itself in what the build
|
||||||
// the band its owner hands it, and hides itself entirely while nothing is selected
|
// button bar and the controls panel have left free, and hides itself entirely while
|
||||||
// (REQ-UI-EMPTY-SELECTION).
|
// nothing is selected (REQ-UI-EMPTY-SELECTION).
|
||||||
class SelectionPanel : public QWidget,
|
class SelectionPanel : public QWidget,
|
||||||
|
public FloatingPanel,
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
PlayerCommandsAppliedEvent,
|
PlayerCommandsAppliedEvent,
|
||||||
EntitySelectionChangedEvent,
|
EntitySelectionChangedEvent,
|
||||||
SelectionChangedEvent,
|
SelectionChangedEvent,
|
||||||
|
SelectionAnchorChangedEvent,
|
||||||
DebrisSelectionChangedEvent,
|
DebrisSelectionChangedEvent,
|
||||||
DebugDrawToggledEvent>
|
DebugDrawToggledEvent>
|
||||||
{
|
{
|
||||||
@@ -49,14 +57,16 @@ public:
|
|||||||
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
||||||
~SelectionPanel() override;
|
~SelectionPanel() override;
|
||||||
|
|
||||||
// Confines the panel to the given band of the game world view: it right-aligns
|
// Sizes the panel to its card and places it beside the selection it describes, in
|
||||||
// within it and centers vertically in it. The band is the world view less the strip
|
// what the widgets placed before it have left free. Keeping clear of them is entirely
|
||||||
// the build button bar occupies, so the two never overlap and the bar never has to
|
// this panel's job; neither of them ever moves for it (REQ-UI-SELECTION-PANEL,
|
||||||
// move (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
// REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL).
|
||||||
void anchorTo(const QRect& bandRect);
|
void placeIn(const QRect& viewRect,
|
||||||
|
const std::vector<QRect>& occupiedRects) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||||
|
void handleEvent(std::shared_ptr<const SelectionAnchorChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
|
||||||
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
|
||||||
@@ -69,11 +79,9 @@ private:
|
|||||||
void refreshContent();
|
void refreshContent();
|
||||||
// Replaces the card with the one the current selection calls for.
|
// Replaces the card with the one the current selection calls for.
|
||||||
void rebuildContent();
|
void rebuildContent();
|
||||||
// Shows the panel while a card exists and hides it otherwise
|
// Asks for the placement pass to be re-run, the card having changed size or the
|
||||||
// (REQ-UI-EMPTY-SELECTION), re-fitting it to the card while it is shown.
|
// panel having gained or lost its reason to be shown at all.
|
||||||
void updateVisibility();
|
void invalidateLayout();
|
||||||
// Re-fits the panel to its card within the anchored band.
|
|
||||||
void refit();
|
|
||||||
|
|
||||||
SelectionContext m_context;
|
SelectionContext m_context;
|
||||||
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
||||||
@@ -84,12 +92,18 @@ private:
|
|||||||
ContentKey m_contentKey;
|
ContentKey m_contentKey;
|
||||||
SelectionContent* m_content = nullptr;
|
SelectionContent* m_content = nullptr;
|
||||||
|
|
||||||
// The band the panel confines itself to, in the coordinates of its parent; null
|
// Where the current selection was on the screen when it started, in the game world
|
||||||
// until the owner has anchored it for the first time.
|
// view's coordinates, and which side of it the panel took. Both are frozen for as
|
||||||
QRect m_bandRect;
|
// long as the selection lasts: the anchor because the panel does not chase a
|
||||||
|
// scrolling view or a moving ship, the side because a card that grows must not flip
|
||||||
|
// the panel across the object (REQ-UI-SELECTION-PANEL). The side is resolved on the
|
||||||
|
// first placement after a new anchor, being the first point at which the panel's
|
||||||
|
// width is known.
|
||||||
|
QRect m_anchorRect;
|
||||||
|
std::optional<PanelSide> m_side;
|
||||||
|
|
||||||
// Scrolls the card once it outgrows the band (REQ-UI-SELECTION-PANEL). The card is a
|
// Scrolls the card once it outgrows the space the panel has (REQ-UI-SELECTION-PANEL).
|
||||||
// child of m_body, not of the panel itself.
|
// The card is a child of m_body, not of the panel itself.
|
||||||
QScrollArea* m_scrollArea;
|
QScrollArea* m_scrollArea;
|
||||||
QWidget* m_body;
|
QWidget* m_body;
|
||||||
QVBoxLayout* m_bodyLayout;
|
QVBoxLayout* m_bodyLayout;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "ProductionRules.h"
|
#include "ProductionRules.h"
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "SalvageScrapBehavior.h"
|
#include "SalvageScrapBehavior.h"
|
||||||
|
#include "SelectionBounds.h"
|
||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "ShipIdentityComponent.h"
|
#include "ShipIdentityComponent.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
@@ -451,30 +452,6 @@ void WorldRenderer::drawBuildings(QPainter& painter, const WorldCoordinates& coo
|
|||||||
drawSelectionHighlights(painter, coordinates, frame);
|
drawSelectionHighlights(painter, coordinates, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<QRectF> WorldRenderer::footprintWidgetRect(
|
|
||||||
const WorldCoordinates& coordinates, BuildingId id) const
|
|
||||||
{
|
|
||||||
std::optional<QPoint> anchor;
|
|
||||||
std::optional<QSize> footprint;
|
|
||||||
|
|
||||||
if (const Building* b = findBuilding(m_sim.getFactoryState(), id))
|
|
||||||
{
|
|
||||||
anchor = b->anchor;
|
|
||||||
footprint = b->footprint;
|
|
||||||
}
|
|
||||||
else if (const ConstructionSite* s = findSite(m_sim.getFactoryState(), id))
|
|
||||||
{
|
|
||||||
anchor = s->anchor;
|
|
||||||
footprint = s->footprint;
|
|
||||||
}
|
|
||||||
if (!anchor.has_value() || !footprint.has_value()) { return std::nullopt; }
|
|
||||||
|
|
||||||
const QPointF tl = coordinates.tileToWidget(*anchor);
|
|
||||||
return QRectF(tl.x(), tl.y(),
|
|
||||||
footprint->width() * static_cast<qreal>(coordinates.getTilePx()),
|
|
||||||
footprint->height() * static_cast<qreal>(coordinates.getTilePx()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
|
void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordinates& coordinates,
|
||||||
const WorldRenderFrame& frame)
|
const WorldRenderFrame& frame)
|
||||||
{
|
{
|
||||||
@@ -483,7 +460,8 @@ void WorldRenderer::drawSelectionHighlights(QPainter& painter, const WorldCoordi
|
|||||||
|
|
||||||
for (BuildingId selId : frame.selection.getSelectedBuildings())
|
for (BuildingId selId : frame.selection.getSelectedBuildings())
|
||||||
{
|
{
|
||||||
const std::optional<QRectF> rect = footprintWidgetRect(coordinates, selId);
|
const std::optional<QRectF> rect =
|
||||||
|
getBuildingWidgetRect(m_sim.getFactoryState(), coordinates, selId);
|
||||||
if (!rect.has_value()) { continue; }
|
if (!rect.has_value()) { continue; }
|
||||||
// Outline sits 1px outside the footprint (into adjacent tiles).
|
// Outline sits 1px outside the footprint (into adjacent tiles).
|
||||||
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
painter.drawRect(rect->adjusted(-1, -1, 1, 1));
|
||||||
|
|||||||
@@ -139,11 +139,6 @@ private:
|
|||||||
BuildingType type, const QRectF& box,
|
BuildingType type, const QRectF& box,
|
||||||
const QColor& fill) const;
|
const QColor& fill) const;
|
||||||
|
|
||||||
// Widget-space rectangle covering a building or construction site's footprint,
|
|
||||||
// or nullopt if the id resolves to neither. Used by the selection highlight.
|
|
||||||
std::optional<QRectF> footprintWidgetRect(const WorldCoordinates& coordinates,
|
|
||||||
BuildingId id) const;
|
|
||||||
|
|
||||||
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
std::optional<QVector2D> entityPosition(entt::entity entity) const;
|
||||||
|
|
||||||
// Non-const only because EntityAdmin's component accessors are; the renderer
|
// Non-const only because EntityAdmin's component accessors are; the renderer
|
||||||
|
|||||||
Reference in New Issue
Block a user