split the selection panel into one card per kind of selection

The panel rendered every selection out of a single pool of member widgets,
hidden and shown per branch, so each build path had to remember to hide the
other branches' widgets. That coupling produced the two defects fixed in
668ce0f, and the per-type detail the requirements now ask for would only add
more of it.

The pool is gone. SelectionPanel keeps the category arbitration, the float and
the hide-when-empty behaviour, and hosts exactly one SelectionContent at a
time; SelectionContentFactory picks which one from the selection alone. Each
card is one row of the catalog in REQ-UI-SELECTION-CONTENT and owns only its
own widgets.

The card structure (REQ-UI-SELECTION-CARD) lives in the base class: a header
with an identity symbol, a name and one right slot, then a configuration group
and a runtime group. A construction site keeps its configuration and has its
whole runtime group replaced by the construction progress, decided once there
rather than in every card (REQ-BLD-SITE-CONFIG).

Also implemented here:
- REQ-UI-SELECTION-STATUS: the header status dot, taken from the simulation's
  own getProductionStatus() so the panel and the world's status light cannot
  disagree.
- REQ-UI-SELECTION-AGGREGATE: belt-subsystem tiles and debris-only selections
  collapse into one card with a count instead of a count summary.
- REQ-UI-HQ-PANEL: the HQ shows the global block stock and its HP, neither of
  which is a buffer.
- BuildingIconCache, extracted from BuildButtonBar's file-local chip loading so
  the card headers and the build buttons rasterize the same SVGs once.

FieldSelectionPanel is deleted: ships, stations, debris and the field count
summary are four more cards in the same factory, so the two-panel arbitration
collapses into one decision.

The card parts are still today's labels and buttons; the item chips, bars,
recipe summary and stat rows follow.

Build clean, 541 tests pass, app runs with no Qt warnings. Visual check
pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-07 12:18:33 +02:00
parent 90e40dddbc
commit 89e984ec76
59 changed files with 2966 additions and 1549 deletions

View File

@@ -0,0 +1,187 @@
#include "SelectionContentFactory.h"
#include "AutoProductionContent.h"
#include "BeltContent.h"
#include "DebrisContent.h"
#include "FactoryQueries.h"
#include "FieldMultiContent.h"
#include "HqContent.h"
#include "MultiBuildingContent.h"
#include "RecipeProductionContent.h"
#include "ShipContent.h"
#include "ShipIdentityComponent.h"
#include "ShipyardContent.h"
#include "SplitterContent.h"
#include "StationBodyComponent.h"
#include "StationContent.h"
#include "StorageContent.h"
#include "Simulation.h"
namespace
{
// The catalog row a single building type belongs to (REQ-UI-SELECTION-CONTENT).
SelectionContentKind getKindForType(BuildingType type)
{
switch (type)
{
case BuildingType::Miner:
case BuildingType::Assembler:
return SelectionContentKind::RecipeProduction;
case BuildingType::Smelter:
case BuildingType::ReprocessingPlant:
return SelectionContentKind::AutoProduction;
case BuildingType::Shipyard:
return SelectionContentKind::Shipyard;
case BuildingType::SalvageBay:
return SelectionContentKind::Storage;
case BuildingType::Hq:
return SelectionContentKind::Hq;
case BuildingType::Belt:
case BuildingType::TunnelEntry:
case BuildingType::TunnelExit:
return SelectionContentKind::Belt;
case BuildingType::Splitter:
return SelectionContentKind::Splitter;
case BuildingType::PlayerDefenceStation:
case BuildingType::EnemyDefenceStation:
// Defence stations are field objects backed by entities; these two enum
// values exist only for cost and visuals lookup and never reach the panel as
// a building selection. The count summary is the harmless fallback.
return SelectionContentKind::MultiBuilding;
}
return SelectionContentKind::MultiBuilding;
}
// A belt, tunnel entry or tunnel exit -- the types whose card is the clear action alone
// and therefore aggregates (REQ-UI-SELECTION-AGGREGATE). The splitter is deliberately
// excluded: it carries per-object output filters, which have no aggregate.
bool isAggregatableBeltType(BuildingType type)
{
return type == BuildingType::Belt
|| type == BuildingType::TunnelEntry
|| type == BuildingType::TunnelExit;
}
ContentKey chooseBuildingContent(const SelectionRequest& request, Simulation& sim)
{
const FactoryState& state = sim.getFactoryState();
if (request.buildings.size() == 1)
{
const BuildingId id = request.buildings.front();
const Building* building = findBuilding(state, id);
if (building)
{
return { getKindForType(building->type), false };
}
const ConstructionSite* site = findSite(state, id);
if (site)
{
return { getKindForType(site->type), true };
}
// The building went away under the panel; the selection is stale and there is
// nothing to show (REQ-UI-EMPTY-SELECTION).
return {};
}
// Several buildings aggregate into one card only when every part of that card
// aggregates (REQ-UI-SELECTION-AGGREGATE). Construction sites are excluded from the
// belt case: a site's card is its own construction progress, which several sites
// cannot share.
bool allAggregatableBelts = true;
for (BuildingId id : request.buildings)
{
const Building* building = findBuilding(state, id);
if (!building || !isAggregatableBeltType(building->type))
{
allAggregatableBelts = false;
break;
}
}
if (allAggregatableBelts)
{
return { SelectionContentKind::Belt, false };
}
return { SelectionContentKind::MultiBuilding, false };
}
ContentKey chooseFieldContent(const SelectionRequest& request, Simulation& sim)
{
// A full single-object card is shown for a lone actor, and for debris whether one
// piece or several -- debris is the field category's other aggregating content
// (REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-SELECTION-AGGREGATE).
if (request.actors.size() == 1 && request.debris.empty())
{
EntityAdmin& admin = sim.getAdmin();
const entt::entity actor = request.actors.front();
if (admin.isValid(actor) && admin.hasAll<ShipIdentityComponent>(actor))
{
return { SelectionContentKind::Ship, false };
}
if (admin.isValid(actor) && admin.hasAll<StationBodyComponent>(actor))
{
return { SelectionContentKind::Station, false };
}
return {};
}
if (request.actors.empty() && !request.debris.empty())
{
return { SelectionContentKind::Debris, false };
}
return { SelectionContentKind::FieldMulti, false };
}
} // namespace
ContentKey chooseContent(const SelectionRequest& request, Simulation& sim)
{
if (request.isEmpty())
{
return {};
}
// Buildings win, so a non-empty building selection is the whole selection
// (REQ-UI-SELECTION-CATEGORIES).
if (!request.buildings.empty())
{
return chooseBuildingContent(request, sim);
}
return chooseFieldContent(request, sim);
}
SelectionContent* createContent(const ContentKey& key, const SelectionRequest& request,
const SelectionContext& context, QWidget* parent)
{
switch (key.kind)
{
case SelectionContentKind::None:
return nullptr;
case SelectionContentKind::RecipeProduction:
return new RecipeProductionContent(context, request, parent);
case SelectionContentKind::AutoProduction:
return new AutoProductionContent(context, request, parent);
case SelectionContentKind::Shipyard:
return new ShipyardContent(context, request, parent);
case SelectionContentKind::Storage:
return new StorageContent(context, request, parent);
case SelectionContentKind::Hq:
return new HqContent(context, request, parent);
case SelectionContentKind::Belt:
return new BeltContent(context, request, parent);
case SelectionContentKind::Splitter:
return new SplitterContent(context, request, parent);
case SelectionContentKind::MultiBuilding:
return new MultiBuildingContent(context, request, parent);
case SelectionContentKind::Ship:
return new ShipContent(context, request, parent);
case SelectionContentKind::Station:
return new StationContent(context, request, parent);
case SelectionContentKind::Debris:
return new DebrisContent(context, request, parent);
case SelectionContentKind::FieldMulti:
return new FieldMultiContent(context, request, parent);
}
return nullptr;
}