place the floating widgets in one ordered pass
The three widgets over the game world view each cached a rect handed to them by MainWindow's resize, then re-placed themselves from it. But the build button bar re-centers on a building unlock and the controls panel re-fits on a 50 ms timer, neither of which goes through MainWindow, so the rects the others held went stale -- and each widget re-implemented its own avoidance against them. They now implement FloatingPanel and are placed in one ordered pass: the bar takes what it wants, the controls panel steps around the bar, and the selection panel keeps clear of both. A widget that changed size or visibility publishes FloatingLayoutInvalidatedEvent instead of moving itself, because what it may take depends on the widgets placed before it. The rule they step around each other by is one function in lib, where it can be tested without a display -- the only way any of this geometry gets automated cover, screen capture of the world view being blank here. The selection panel keeps its right edge and its vertical centering, but the space it centers in is now what its own column has left free rather than the full-width strip the bar used to reserve. It therefore sits lower than before where the centered bar does not reach it, and it now clears the controls panel, which it previously ignored. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
@@ -16,6 +16,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.h
|
||||
@@ -32,6 +33,7 @@ SET(SRCS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/TunnelCompletion.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCoordinates.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldCamera.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanelPlacement.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionController.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildModeController.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlAction.cpp
|
||||
|
||||
24
src/lib/core/FloatingPanelPlacement.cpp
Normal file
24
src/lib/core/FloatingPanelPlacement.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#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;
|
||||
}
|
||||
18
src/lib/core/FloatingPanelPlacement.h
Normal file
18
src/lib/core/FloatingPanelPlacement.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QRect>
|
||||
|
||||
// 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);
|
||||
@@ -43,6 +43,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BeamFiredEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DebugDrawToggledEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/CommandRequestedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FloatingLayoutInvalidatedEvent.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/PlayerCommandsAppliedEvent.h
|
||||
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
|
||||
{
|
||||
};
|
||||
@@ -14,6 +14,7 @@ add_files(
|
||||
TunnelCompletionTest.cpp
|
||||
WorldCoordinatesTest.cpp
|
||||
WorldCameraTest.cpp
|
||||
FloatingPanelPlacementTest.cpp
|
||||
SelectionControllerTest.cpp
|
||||
BuildModeControllerTest.cpp
|
||||
ControlActionTest.cpp
|
||||
|
||||
68
src/test/FloatingPanelPlacementTest.cpp
Normal file
68
src/test/FloatingPanelPlacementTest.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#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);
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "DisplayName.h"
|
||||
#include "EventManager.h"
|
||||
#include "ExitBuilderModeRequestedEvent.h"
|
||||
#include "FloatingLayoutInvalidatedEvent.h"
|
||||
#include "IconCaption.h"
|
||||
#include "InputMapper.h"
|
||||
#include "ItemIconCache.h"
|
||||
@@ -270,15 +271,25 @@ BuildButtonBar::~BuildButtonBar()
|
||||
unregisterForEvents();
|
||||
}
|
||||
|
||||
void BuildButtonBar::anchorTo(const QRect& worldViewRect)
|
||||
void BuildButtonBar::placeIn(const QRect& viewRect,
|
||||
const std::vector<QRect>& /*occupiedRects*/)
|
||||
{
|
||||
m_viewRect = worldViewRect;
|
||||
recenter();
|
||||
}
|
||||
if (viewRect.isNull())
|
||||
{
|
||||
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
|
||||
{
|
||||
return height() + kBottomMarginPx;
|
||||
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(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()
|
||||
@@ -328,29 +339,11 @@ void BuildButtonBar::updateVisibility()
|
||||
{
|
||||
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
|
||||
// re-center on it (REQ-UI-BUILD-BAR).
|
||||
recenter();
|
||||
}
|
||||
|
||||
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));
|
||||
// A hidden button leaves the row, so the bar takes up a new width and has to be
|
||||
// re-centered on it -- and the widgets that keep clear of the bar have to be placed
|
||||
// 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::onBuildButton(int index)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "BuildingType.h"
|
||||
#include "DeconstructModeChangedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "FloatingPanel.h"
|
||||
#include "GameConfig.h"
|
||||
#include "UnlockedBuildingsChangedEvent.h"
|
||||
|
||||
@@ -24,10 +25,12 @@ class BuildingIconCache;
|
||||
class ItemIconCache;
|
||||
|
||||
// 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
|
||||
// world view's rect through anchorTo() and it centers itself along that rect's
|
||||
// bottom edge.
|
||||
// view (REQ-UI-BUILD-BAR). The bar is sized to its buttons and centers itself along the
|
||||
// bottom edge of the rect its owner places it in. It is placed first of the floating
|
||||
// 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,
|
||||
public FloatingPanel,
|
||||
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||
DeconstructModeChangedEvent,
|
||||
BuildHotkeyPressedEvent,
|
||||
@@ -45,15 +48,11 @@ public:
|
||||
QWidget* parent = nullptr);
|
||||
~BuildButtonBar() override;
|
||||
|
||||
// Centers the bar along the bottom edge of the game world view's rect, given in
|
||||
// the bar's parent coordinates (REQ-UI-BUILD-BAR). The rect is remembered, so a
|
||||
// re-center later driven by an unlock needs no second call from the owner.
|
||||
void anchorTo(const QRect& worldViewRect);
|
||||
|
||||
// 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;
|
||||
// Centers the bar along the bottom edge of the game world view's rect, given in the
|
||||
// bar's parent coordinates (REQ-UI-BUILD-BAR). Nothing is occupied yet when the bar
|
||||
// is placed, so it ignores what it is handed there.
|
||||
void placeIn(const QRect& viewRect,
|
||||
const std::vector<QRect>& occupiedRects) override;
|
||||
|
||||
void clearActiveButton();
|
||||
|
||||
@@ -67,11 +66,6 @@ private:
|
||||
// unlock state (REQ-LOCK-BUILDING); a locked building type's button is hidden.
|
||||
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 DeconstructModeChangedEvent> event) override;
|
||||
void handleEvent(std::shared_ptr<const BuildHotkeyPressedEvent> event) override;
|
||||
@@ -91,5 +85,4 @@ private:
|
||||
std::map<BuildingType, int> m_costs;
|
||||
std::optional<std::size_t> m_activeIndex;
|
||||
QPushButton* m_deconstructButton;
|
||||
QRect m_viewRect;
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ SET(HDRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldPrimitives.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FloatingPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ControlsPanel.h
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "ControlActionText.h"
|
||||
#include "EventManager.h"
|
||||
#include "FloatingLayoutInvalidatedEvent.h"
|
||||
#include "FloatingPanelPlacement.h"
|
||||
#include "GameWorldView.h"
|
||||
#include "selection/SelectionNames.h"
|
||||
|
||||
@@ -159,11 +162,10 @@ ControlsPanel::ControlsPanel(const GameWorldView* view, QWidget* parent)
|
||||
refresh();
|
||||
}
|
||||
|
||||
void ControlsPanel::anchorTo(const QRect& worldRect, const QRect& buildBarRect)
|
||||
void ControlsPanel::invalidateLayout()
|
||||
{
|
||||
m_worldRect = worldRect;
|
||||
m_buildBarRect = buildBarRect;
|
||||
refit();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||
}
|
||||
|
||||
void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
||||
@@ -174,7 +176,7 @@ void ControlsPanel::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
m_collapsed = !m_collapsed;
|
||||
m_scrollArea->setVisible(!m_collapsed);
|
||||
refit();
|
||||
invalidateLayout();
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
@@ -241,7 +243,7 @@ void ControlsPanel::rebuild(const ControlContext& context)
|
||||
}
|
||||
|
||||
m_scrollArea->setVisible(!m_collapsed);
|
||||
refit();
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
||||
@@ -277,9 +279,9 @@ QString ControlsPanel::getHeadingText(const ControlContext& context) const
|
||||
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
|
||||
// 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->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; }
|
||||
|
||||
// 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
|
||||
// (REQ-UI-CONTROLS-PANEL).
|
||||
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
||||
int heightPx = qMin(wantedHeightPx, band.height());
|
||||
int topPx = band.bottom() - heightPx + 1;
|
||||
int widthPx = qMin(contentWidthPx + chromePx, band.width());
|
||||
|
||||
// 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
|
||||
// two would actually overlap does the panel rise, clearing the bar's top by the
|
||||
// same margin it keeps from the view's edges (REQ-UI-BUILD-BAR).
|
||||
const QRect wanted(band.left(), topPx, widthPx, heightPx);
|
||||
if (!m_buildBarRect.isNull() && wanted.intersects(m_buildBarRect))
|
||||
{
|
||||
const int availablePx = m_buildBarRect.top() - kMarginPx - band.top();
|
||||
heightPx = qMin(heightPx, qMax(0, availablePx));
|
||||
topPx = m_buildBarRect.top() - kMarginPx - heightPx;
|
||||
}
|
||||
// this corner free and the panel can share the bottom edge with it. Only where the
|
||||
// two would actually meet does the panel rise, clearing the bar's top by the same
|
||||
// margin it keeps from the view's edges (REQ-UI-BUILD-BAR). The bar is the only
|
||||
// widget placed before this one, so it is the only rect that can be in the way.
|
||||
const int bottomPx = getAvailableBottomPx(band, occupiedRects, band.left(),
|
||||
band.left() + widthPx - 1, kMarginPx);
|
||||
int heightPx = qMin(wantedHeightPx, qMax(0, bottomPx - band.top() + 1));
|
||||
int topPx = bottomPx - heightPx + 1;
|
||||
|
||||
// Whatever the rows lost to either cap, they scroll for. The scrollbar needs its
|
||||
// own width, or it would appear over the labels.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QWidget>
|
||||
|
||||
#include "ControlAction.h"
|
||||
#include "FloatingPanel.h"
|
||||
|
||||
class GameWorldView;
|
||||
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
|
||||
// skipped unless the resolved content actually differs, which is a vector of enums to
|
||||
// compare.
|
||||
class ControlsPanel : public QWidget
|
||||
class ControlsPanel : public QWidget, public FloatingPanel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -42,10 +43,11 @@ public:
|
||||
// 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
|
||||
// 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
|
||||
// the bar's current geometry in the same coordinates, or a null rect when there is
|
||||
// none to avoid (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR).
|
||||
void anchorTo(const QRect& worldRect, const QRect& buildBarRect);
|
||||
// rises above the bar only when the two would otherwise overlap
|
||||
// (REQ-UI-CONTROLS-PANEL, REQ-UI-BUILD-BAR). The bar is the only thing placed before
|
||||
// it, so it is the only rect it ever has to rise above.
|
||||
void placeIn(const QRect& viewRect,
|
||||
const std::vector<QRect>& occupiedRects) override;
|
||||
|
||||
protected:
|
||||
// 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
|
||||
// context has none.
|
||||
QString getHeadingText(const ControlContext& context) const;
|
||||
// Re-fits the panel to its content within the anchored band.
|
||||
void refit();
|
||||
// Asks for the placement pass to be re-run, the panel's own size having changed.
|
||||
void invalidateLayout();
|
||||
|
||||
const GameWorldView* m_view;
|
||||
|
||||
@@ -82,9 +84,4 @@ private:
|
||||
// Survives context changes and simulation restarts; presentation only, never a
|
||||
// command (REQ-UI-CONTROLS-PANEL).
|
||||
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;
|
||||
};
|
||||
@@ -172,19 +172,50 @@ void MainWindow::layoutPanels()
|
||||
const QRect worldRect(0, headerH, totalW, totalH - headerH);
|
||||
m_headerBar->setGeometry(0, 0, totalW, headerH);
|
||||
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);
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "BuildingId.h"
|
||||
#include "EscapeMenuRequestedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "FloatingLayoutInvalidatedEvent.h"
|
||||
#include "GameConfig.h"
|
||||
#include "GameOverEvent.h"
|
||||
#include "LayoutDialogRequestedEvent.h"
|
||||
@@ -45,7 +46,8 @@ class MainWindow : public QWidget,
|
||||
LayoutDialogRequestedEvent,
|
||||
RecipeSelectionRequestedEvent,
|
||||
BlueprintSaveRequestedEvent,
|
||||
BlueprintSelectionRequestedEvent>
|
||||
BlueprintSelectionRequestedEvent,
|
||||
FloatingLayoutInvalidatedEvent>
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
@@ -67,6 +69,7 @@ private:
|
||||
void handleEvent(std::shared_ptr<const RecipeSelectionRequestedEvent> 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 FloatingLayoutInvalidatedEvent> event) override;
|
||||
|
||||
// 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
|
||||
@@ -85,6 +88,8 @@ private:
|
||||
// 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).
|
||||
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();
|
||||
|
||||
private:
|
||||
@@ -109,4 +114,8 @@ private:
|
||||
|
||||
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "BuildingIconCache.h"
|
||||
#include "EventManager.h"
|
||||
#include "FloatingLayoutInvalidatedEvent.h"
|
||||
#include "FloatingPanelPlacement.h"
|
||||
#include "ItemIconCache.h"
|
||||
#include "Simulation.h"
|
||||
#include "VisualsConfig.h"
|
||||
@@ -13,8 +16,8 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
// Distance kept between the panel and the edges of the band it is anchored to
|
||||
// (REQ-UI-SELECTION-PANEL).
|
||||
// Distance kept between the panel and the edges of the game world view, and between it
|
||||
// and the widgets it steps around (REQ-UI-SELECTION-PANEL).
|
||||
const int kMarginPx = 8;
|
||||
|
||||
// Upper bound on the card width. The panel is content-sized, but several of the cards'
|
||||
@@ -94,10 +97,10 @@ SelectionPanel::~SelectionPanel()
|
||||
unregisterForEvents();
|
||||
}
|
||||
|
||||
void SelectionPanel::anchorTo(const QRect& bandRect)
|
||||
void SelectionPanel::invalidateLayout()
|
||||
{
|
||||
m_bandRect = bandRect;
|
||||
updateVisibility();
|
||||
EventManager::getInstance()->sendEventImmediately(
|
||||
std::make_shared<FloatingLayoutInvalidatedEvent>());
|
||||
}
|
||||
|
||||
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
|
||||
@@ -178,7 +181,7 @@ void SelectionPanel::refreshContent()
|
||||
}
|
||||
|
||||
m_content->refresh();
|
||||
updateVisibility();
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
void SelectionPanel::rebuildContent()
|
||||
@@ -208,36 +211,28 @@ void SelectionPanel::rebuildContent()
|
||||
m_content->show();
|
||||
m_content->refresh();
|
||||
}
|
||||
updateVisibility();
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
void SelectionPanel::updateVisibility()
|
||||
void SelectionPanel::placeIn(const QRect& viewRect, const std::vector<QRect>& occupiedRects)
|
||||
{
|
||||
// Nothing selected in either category means no panel at all rather than an empty one
|
||||
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible. Before the
|
||||
// owner has anchored the panel there is nowhere to put it either, so it stays hidden
|
||||
// until then.
|
||||
const bool shouldShow = (m_content != nullptr) && !m_bandRect.isNull();
|
||||
setVisible(shouldShow);
|
||||
if (shouldShow)
|
||||
// (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible.
|
||||
setVisible(m_content != nullptr);
|
||||
if (m_content == nullptr || viewRect.isNull())
|
||||
{
|
||||
refit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SelectionPanel::refit()
|
||||
{
|
||||
const QRect band =
|
||||
m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||
const QRect band = viewRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
|
||||
|
||||
// The panel's border is drawn around the scroll area rather than around the card, so
|
||||
// it is added to whatever the card asks for. Spelled out here instead of read back
|
||||
// from contentsMargins() because the stylesheet box is what sets it, and asking the
|
||||
// style for it before the first show is unreliable.
|
||||
const int borderPx = 1;
|
||||
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
||||
const int maxHeightPx = band.height() - 2 * borderPx;
|
||||
if (maxWidthPx <= 0 || maxHeightPx <= 0)
|
||||
const int borderPx = 1;
|
||||
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
|
||||
if (maxWidthPx <= 0 || band.height() <= 2 * borderPx)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -273,9 +268,26 @@ void SelectionPanel::refit()
|
||||
// First at the cap, the most room the card can ever get, to learn how wide it
|
||||
// wants to be; then at that width for the height that follows from it.
|
||||
int contentWidthPx = qMin(measureAt(maxWidthPx).width(), maxWidthPx);
|
||||
|
||||
// How much height there is depends on where the panel stands: of the widgets
|
||||
// placed before it, only those whose rectangles meet its own column are in its
|
||||
// way (REQ-UI-SELECTION-PANEL). Its column follows from the width just measured,
|
||||
// so this cannot be settled before it -- and where the scroll bar below widens
|
||||
// the panel, the second pass settles it again against the wider column.
|
||||
const int leftPx =
|
||||
band.right() - (contentWidthPx + 2 * borderPx) + 1;
|
||||
const int bottomPx = getAvailableBottomPx(band, occupiedRects, leftPx,
|
||||
band.right(), kMarginPx);
|
||||
const int freeHeightPx = bottomPx - band.top() + 1;
|
||||
const int maxHeightPx = freeHeightPx - 2 * borderPx;
|
||||
if (maxHeightPx <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int contentHeightPx = measureAt(contentWidthPx).height();
|
||||
|
||||
// A card taller than the band is capped there and scrolls
|
||||
// A card taller than the space left is capped there and scrolls
|
||||
// (REQ-UI-SELECTION-PANEL). The bar is laid out beside the card, so the panel
|
||||
// widens by its width to leave the card the width its height was measured for --
|
||||
// and where the cap does not allow that, the card is measured again at what is
|
||||
@@ -301,11 +313,11 @@ void SelectionPanel::refit()
|
||||
const int panelWidthPx = contentWidthPx + 2 * borderPx;
|
||||
const int panelHeightPx = contentHeightPx + 2 * borderPx;
|
||||
|
||||
// Right-aligned in the band and centered on it vertically. The band excludes the
|
||||
// build button bar's strip, so centering here never puts the panel over the bar
|
||||
// (REQ-UI-SELECTION-PANEL).
|
||||
// Right-aligned in the band and centered vertically on the free part of it, so
|
||||
// the panel sits above whatever the widgets placed before it are occupying rather
|
||||
// than over them (REQ-UI-SELECTION-PANEL).
|
||||
setGeometry(band.right() - panelWidthPx + 1,
|
||||
band.top() + (band.height() - panelHeightPx) / 2,
|
||||
band.top() + (freeHeightPx - panelHeightPx) / 2,
|
||||
panelWidthPx, panelHeightPx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
#include <QRect>
|
||||
#include <QWidget>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "DebrisSelectionChangedEvent.h"
|
||||
#include "DebugDrawToggledEvent.h"
|
||||
#include "EntitySelectionChangedEvent.h"
|
||||
#include "EventHandler.h"
|
||||
#include "FloatingPanel.h"
|
||||
#include "PlayerCommandsAppliedEvent.h"
|
||||
#include "SelectionChangedEvent.h"
|
||||
#include "TickAdvancedEvent.h"
|
||||
@@ -28,10 +31,11 @@ class QVBoxLayout;
|
||||
// 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
|
||||
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, anchors to the right edge of
|
||||
// the band its owner hands it, and hides itself entirely while nothing is selected
|
||||
// (REQ-UI-EMPTY-SELECTION).
|
||||
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, places itself in what the build
|
||||
// button bar and the controls panel have left free, and hides itself entirely while
|
||||
// nothing is selected (REQ-UI-EMPTY-SELECTION).
|
||||
class SelectionPanel : public QWidget,
|
||||
public FloatingPanel,
|
||||
public CombinedEventHandler<TickAdvancedEvent,
|
||||
PlayerCommandsAppliedEvent,
|
||||
EntitySelectionChangedEvent,
|
||||
@@ -49,11 +53,12 @@ public:
|
||||
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
|
||||
~SelectionPanel() override;
|
||||
|
||||
// Confines the panel to the given band of the game world view: it right-aligns
|
||||
// within it and centers vertically in it. The band is the world view less the strip
|
||||
// the build button bar occupies, so the two never overlap and the bar never has to
|
||||
// move (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
|
||||
void anchorTo(const QRect& bandRect);
|
||||
// Sizes the panel to its card and places it in the game world view, right-aligned and
|
||||
// vertically centered in what the widgets placed before it have left free. Keeping
|
||||
// clear of them is entirely this panel's job; neither of them ever moves for it
|
||||
// (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR, REQ-UI-CONTROLS-PANEL).
|
||||
void placeIn(const QRect& viewRect,
|
||||
const std::vector<QRect>& occupiedRects) override;
|
||||
|
||||
private:
|
||||
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
|
||||
@@ -69,11 +74,9 @@ private:
|
||||
void refreshContent();
|
||||
// Replaces the card with the one the current selection calls for.
|
||||
void rebuildContent();
|
||||
// Shows the panel while a card exists and hides it otherwise
|
||||
// (REQ-UI-EMPTY-SELECTION), re-fitting it to the card while it is shown.
|
||||
void updateVisibility();
|
||||
// Re-fits the panel to its card within the anchored band.
|
||||
void refit();
|
||||
// Asks for the placement pass to be re-run, the card having changed size or the
|
||||
// panel having gained or lost its reason to be shown at all.
|
||||
void invalidateLayout();
|
||||
|
||||
SelectionContext m_context;
|
||||
// Read through m_context by the cards that need it, so a toggle reaches the card on
|
||||
@@ -84,12 +87,8 @@ private:
|
||||
ContentKey m_contentKey;
|
||||
SelectionContent* m_content = nullptr;
|
||||
|
||||
// The band the panel confines itself to, in the coordinates of its parent; null
|
||||
// until the owner has anchored it for the first time.
|
||||
QRect m_bandRect;
|
||||
|
||||
// Scrolls the card once it outgrows the band (REQ-UI-SELECTION-PANEL). The card is a
|
||||
// child of m_body, not of the panel itself.
|
||||
// Scrolls the card once it outgrows the space the panel has (REQ-UI-SELECTION-PANEL).
|
||||
// The card is a child of m_body, not of the panel itself.
|
||||
QScrollArea* m_scrollArea;
|
||||
QWidget* m_body;
|
||||
QVBoxLayout* m_bodyLayout;
|
||||
|
||||
Reference in New Issue
Block a user