diff --git a/src/ui/selection/SelectionContent.cpp b/src/ui/selection/SelectionContent.cpp index 2831f8d..ef74f00 100644 --- a/src/ui/selection/SelectionContent.cpp +++ b/src/ui/selection/SelectionContent.cpp @@ -32,6 +32,38 @@ const int kSymbolSizePx = 20; const int kCardSpacingPx = 6; const int kHeaderSpacingPx = 6; +// The Salvage Bay has no recipe and no cycle: its two states say whether it is holding +// scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY, REQ-UI-SELECTION-STATUS). +QString getStatusCaption(ProductionStatus status, bool isSalvageBay) +{ + switch (status) + { + case ProductionStatus::Unconfigured: return QObject::tr("no recipe"); + case ProductionStatus::Producing: + return isSalvageBay ? QObject::tr("holding scrap") + : QObject::tr("producing"); + case ProductionStatus::Starved: + return isSalvageBay ? QObject::tr("empty") : QObject::tr("missing input"); + case ProductionStatus::Blocked: return QObject::tr("output full"); + } + return QString(); +} + +// Every caption the status slot can end up showing for this building, so the header can +// keep its width as the state changes rather than the panel jumping with it. +QStringList getAllStatusCaptions(bool isSalvageBay) +{ + QStringList captions; + for (ProductionStatus status : { ProductionStatus::Unconfigured, + ProductionStatus::Producing, + ProductionStatus::Starved, + ProductionStatus::Blocked }) + { + captions << getStatusCaption(status, isSalvageBay); + } + return captions; +} + } // namespace @@ -168,25 +200,26 @@ void SelectionContent::setProductionStatusSlot(const Building& building) return; } - const StatusLightVisuals& colors = m_context.visuals->statusLight; - // The Salvage Bay has no recipe and no cycle: its two states say whether it is - // holding scrap, not whether it is producing (REQ-BLD-SALVAGE-BAY). const bool isSalvageBay = (building.type == BuildingType::SalvageBay); + // Room for every caption this building can show, so the panel keeps its width as the + // building's state changes rather than jumping with it. + reserveSlotFor(getAllStatusCaptions(isSalvageBay)); + + const StatusLightVisuals& colors = m_context.visuals->statusLight; + QColor dotColor; switch (*status) { - case ProductionStatus::Unconfigured: - setSlot(colors.grey, tr("no recipe")); - break; - case ProductionStatus::Producing: - setSlot(colors.green, isSalvageBay ? tr("holding scrap") : tr("producing")); - break; - case ProductionStatus::Starved: - setSlot(colors.red, isSalvageBay ? tr("empty") : tr("missing input")); - break; - case ProductionStatus::Blocked: - setSlot(colors.yellow, tr("output full")); - break; + case ProductionStatus::Unconfigured: dotColor = colors.grey; break; + case ProductionStatus::Producing: dotColor = colors.green; break; + case ProductionStatus::Starved: dotColor = colors.red; break; + case ProductionStatus::Blocked: dotColor = colors.yellow; break; } + setSlot(dotColor, getStatusCaption(*status, isSalvageBay)); +} + +void SelectionContent::reserveSlotFor(const QStringList& captions) +{ + m_statusPill->reserveFor(captions); } void SelectionContent::refreshConstruction() diff --git a/src/ui/selection/SelectionContent.h b/src/ui/selection/SelectionContent.h index 6d8139a..78188f3 100644 --- a/src/ui/selection/SelectionContent.h +++ b/src/ui/selection/SelectionContent.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "BuildingId.h" @@ -79,6 +80,11 @@ protected: void setCountSlot(int count); void clearSlot(); + // Reserves header room for the widest caption the slot will ever show. The panel is + // sized to its card (REQ-UI-SELECTION-PANEL), so without this it changes width every + // time the caption does -- and a card whose chips wrap changes height with it. + void reserveSlotFor(const QStringList& captions); + // Fills the right slot from the building's production status, mapped to the same // colors and states the world's status light uses (REQ-UI-SELECTION-STATUS). Leaves // the slot empty for a type that has no status light. diff --git a/src/ui/selection/SelectionNames.cpp b/src/ui/selection/SelectionNames.cpp index c41589a..55e49b8 100644 --- a/src/ui/selection/SelectionNames.cpp +++ b/src/ui/selection/SelectionNames.cpp @@ -33,3 +33,16 @@ QString getBehaviorLabel(BehaviorKind kind) } return QString(); } + +QStringList getAllBehaviorLabels() +{ + QStringList labels; + for (BehaviorKind kind : { BehaviorKind::Retreat, BehaviorKind::Attack, + BehaviorKind::SalvageScrap, BehaviorKind::Repair, + BehaviorKind::Rally, BehaviorKind::Standby, + BehaviorKind::Advance }) + { + labels << getBehaviorLabel(kind); + } + return labels; +} diff --git a/src/ui/selection/SelectionNames.h b/src/ui/selection/SelectionNames.h index 335639f..d8064a0 100644 --- a/src/ui/selection/SelectionNames.h +++ b/src/ui/selection/SelectionNames.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "BehaviorKind.h" #include "BuildingType.h" @@ -13,3 +14,8 @@ QString getBuildingTypeName(BuildingType type); // Name of the behavior currently governing a ship, for the ship card's header slot // (REQ-UI-SHIP-BEHAVIOR). Empty when no behavior has won yet, which shows no slot. QString getBehaviorLabel(BehaviorKind kind); + +// Every name getBehaviorLabel can return. A ship's behavior changes as it fights, so the +// header reserves room for the widest of these rather than letting the panel change +// width each time (REQ-UI-SELECTION-PANEL). +QStringList getAllBehaviorLabels(); diff --git a/src/ui/selection/ShipContent.cpp b/src/ui/selection/ShipContent.cpp index 535b2d2..0b0a7c4 100644 --- a/src/ui/selection/ShipContent.cpp +++ b/src/ui/selection/ShipContent.cpp @@ -21,6 +21,10 @@ ShipContent::ShipContent(const SelectionContext& context, m_statsPanel = new ShipStatsPanel(context.config, this); getRuntimeLayout()->addWidget(m_statsPanel); + // A ship's behavior changes as it fights, so the header holds room for the longest + // name rather than the panel resizing under the player each time it does. + reserveSlotFor(getAllBehaviorLabels()); + EntityAdmin& admin = context.sim->getAdmin(); if (admin.isValid(m_entity) && admin.hasAll(m_entity)) { diff --git a/src/ui/selection/StatusPill.cpp b/src/ui/selection/StatusPill.cpp index 576bed7..666cc24 100644 --- a/src/ui/selection/StatusPill.cpp +++ b/src/ui/selection/StatusPill.cpp @@ -1,5 +1,6 @@ #include "StatusPill.h" +#include #include #include #include @@ -49,6 +50,23 @@ StatusPill::StatusPill(QWidget* parent) hide(); } +void StatusPill::reserveFor(const QStringList& captions) +{ + if (captions == m_reservedFor) + { + return; + } + m_reservedFor = captions; + + const QFontMetrics metrics(m_captionLabel->font()); + int widestPx = 0; + for (const QString& caption : captions) + { + widestPx = qMax(widestPx, metrics.horizontalAdvance(caption)); + } + m_captionLabel->setMinimumWidth(widestPx); +} + void StatusPill::setStatus(const QColor& dotColor, const QColor& outlineColor, const QString& caption) { diff --git a/src/ui/selection/StatusPill.h b/src/ui/selection/StatusPill.h index 1dfc947..a913636 100644 --- a/src/ui/selection/StatusPill.h +++ b/src/ui/selection/StatusPill.h @@ -2,6 +2,7 @@ #include #include +#include #include class QLabel; @@ -23,7 +24,16 @@ public: void setStatus(const QColor& dotColor, const QColor& outlineColor, const QString& caption); + // Reserves room for the widest of the captions this pill can show, so the header -- + // and with it the panel, which is sized to its content (REQ-UI-SELECTION-PANEL) -- + // keeps its width as the caption changes. Without it the whole panel jumps every + // time a building's status does, and a card whose chips wrap changes height with it. + void reserveFor(const QStringList& captions); + private: QLabel* m_dotLabel; QLabel* m_captionLabel; + // What the width is currently reserved for, so re-reserving the same set on every + // refresh costs nothing. + QStringList m_reservedFor; };