draw the scrap of a debris card as an icon, not a word

The debris card's row now reads "Remaining" with the amount and the bare
scrap icon after it, and the count summary's indented sub-row states its
total the same way; both fall back to naming the item in words when no
scrap icon file exists.

StatRow gained the value-plus-icon form: a label draws either text or a
pixmap, so the two are composed into one via renderCaptionWithIcon, with
the color passed in because a pixmap does not follow the palette. That
also closes a gap in the building multi-selection, whose "Total cost" was
specified to carry the building_block icon and showed the bare number.

ItemIconCache::getInlineIcon() packages the sizing rule of the bare inline
icon. It sits on the cache rather than in IconCaption so StatRow keeps
pulling in nothing but the caption helper: StatRow.cpp is compiled into
DotaFactory_balancing, which links neither the ui library nor QtSvg, and
that target now compiles IconCaption.cpp along with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x
This commit is contained in:
2026-08-12 10:14:04 +02:00
parent 3a4b69b520
commit 344d18a5d1
11 changed files with 123 additions and 16 deletions

View File

@@ -21,8 +21,11 @@ QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
// their raw size is not the size to lay them out at.
QSize getLogicalSize(const QPixmap& pixmap);
// Item id of the building blocks resource, whose icon stands in for the "Blocks"
// word wherever a cost or stock is captioned (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST,
// REQ-UI-EXPAND-BUTTON). It lives next to the caption helper because every caller of
// one is a caller of the other.
// The items whose icons stand in for their names beside a number, drawn bare and without
// their colored square (REQ-UI-ITEM-ICON): building blocks beside a cost or a stock
// (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST, REQ-UI-EXPAND-BUTTON), and scrap beside the
// amount left in debris (REQ-UI-DEBRIS-PANEL, REQ-UI-FIELD-MULTI-SELECTION). They live
// next to the caption helper because every caller of one is a caller of the other;
// ItemIconCache::getInlineIcon() is what turns an id here into that icon.
const char* const kBlockItemId = "building_block";
const char* const kScrapItemId = "scrap";

View File

@@ -1,6 +1,7 @@
#include "ItemIconCache.h"
#include <QFile>
#include <QFontMetrics>
#include <QPainter>
#include <QRectF>
#include <QSvgRenderer>
@@ -94,6 +95,12 @@ QPixmap ItemIconCache::getPixmap(const std::string& itemId, int sizePx)
return getPixmap(itemId, itemId, sizePx, false);
}
QPixmap ItemIconCache::getInlineIcon(const std::string& itemId, const QFont& font)
{
if (!hasIcon(itemId)) { return QPixmap(); }
return getPixmap(itemId, QFontMetrics(font).height());
}
QPixmap ItemIconCache::getPixmap(const std::string& cacheKey, const std::string& itemId,
int sizePx, bool withSquare)
{

View File

@@ -5,6 +5,7 @@
#include <utility>
#include <QByteArray>
#include <QFont>
#include <QPixmap>
#include <QString>
@@ -26,9 +27,11 @@ struct VisualsConfig;
// two forms: paintItem() for the world's fractional geometry, getSquarePixmap() for
// widgets that want a ready-made pixmap.
//
// The bare icon of getPixmap() has one remaining use: the inline building_block icon
// that stands in for the word "Blocks" beside a number (REQ-UI-BLOCKS-ICON), which is a
// decoration on a line of text rather than an item display and takes no square.
// The bare icon of getPixmap() has one remaining use: an inline icon standing in for the
// item's name beside a number -- building blocks beside a cost or a stock
// (REQ-UI-BLOCKS-ICON), scrap beside the amount left in debris (REQ-UI-DEBRIS-PANEL) --
// which is a decoration on a line of text rather than an item display and takes no
// square. getInlineItemIcon() in IconCaption.h is how callers ask for that form.
//
// A missing icon file is not an error: hasIcon() returns false for it and the item shows
// its colored square alone.
@@ -64,6 +67,11 @@ public:
// icon file (callers should gate on hasIcon()).
QPixmap getPixmap(const std::string& itemId, int sizePx);
// The same bare icon sized to the height of `font`'s text, for the inline form above:
// an icon standing in for the item's name on a line of text. Null when the item has
// no icon file, which is not an error -- the caller names the item in words instead.
QPixmap getInlineIcon(const std::string& itemId, const QFont& font);
// Drops every rasterized pixmap. Called when the visuals are reloaded on a restart
// (REQ-CFG-RELOAD), because the composed squares carry the colors they were painted
// with; they are re-rasterized on next use.

View File

@@ -3,6 +3,8 @@
#include <QVBoxLayout>
#include "DebrisScrap.h"
#include "IconCaption.h"
#include "ItemIconCache.h"
#include "Simulation.h"
#include "StatRow.h"
@@ -10,8 +12,16 @@ DebrisContent::DebrisContent(const SelectionContext& context,
const SelectionRequest& request, QWidget* parent)
: SelectionContent(context, std::nullopt, parent)
, m_debris(request.debris)
, m_scrapIcon(context.itemIcons->getInlineIcon(kScrapItemId, font()))
{
m_scrapRow = new StatRow(tr("Scrap remaining"), this);
// The row states the amount and the bare scrap icon, the icon standing in for the
// word so the row spells out neither the item nor its unit -- the form the header bar
// states the block stock in (REQ-UI-DEBRIS-PANEL, REQ-UI-ITEM-ICON,
// REQ-UI-BLOCKS-ICON). Without an icon file the caption has to name the item again,
// since nothing else on the row would.
m_scrapRow = new StatRow(
m_scrapIcon.isNull() ? tr("Scrap remaining") : tr("Remaining"), this);
m_scrapRow->setValueIcon(m_scrapIcon);
m_scrapRow->setValueEmphasized(true);
getRuntimeLayout()->addWidget(m_scrapRow);

View File

@@ -2,6 +2,8 @@
#include <vector>
#include <QPixmap>
#include "entt/entity/entity.hpp"
#include "SelectionContent.h"
@@ -28,4 +30,8 @@ protected:
private:
std::vector<entt::entity> m_debris;
StatRow* m_scrapRow;
// The bare scrap icon the row states its amount with, or null where there is no icon
// file and the row names the item in words instead (REQ-UI-DEBRIS-PANEL). Taken once:
// it depends on the card's font, not on the value.
QPixmap m_scrapIcon;
};

View File

@@ -10,6 +10,8 @@
#include "DisplayName.h"
#include "EntityAdmin.h"
#include "FactionComponent.h"
#include "IconCaption.h"
#include "ItemIconCache.h"
#include "ShipIdentityComponent.h"
#include "Simulation.h"
#include "StatRow.h"
@@ -20,6 +22,7 @@ FieldMultiContent::FieldMultiContent(const SelectionContext& context,
: SelectionContent(context, std::nullopt, parent)
, m_debris(request.debris)
, m_scrapRow(nullptr)
, m_scrapIcon(context.itemIcons->getInlineIcon(kScrapItemId, font()))
{
setIdentity(QPixmap(), tr("Mixed selection"));
setCountSlot(static_cast<int>(request.actors.size() + request.debris.size()));
@@ -84,9 +87,11 @@ void FieldMultiContent::buildSummary(const std::vector<entt::entity>& actors)
getRuntimeLayout()->addWidget(new CountRow(
QPixmap(), tr("Debris"), static_cast<int>(m_debris.size()), this));
// Indented under the debris row, so the total reads as belonging to it
// (REQ-UI-DEBRIS-PANEL).
// Indented under the debris row, so the total reads as belonging to it, and
// stated in the same amount-plus-bare-icon form the debris card uses
// (REQ-UI-DEBRIS-PANEL, REQ-UI-FIELD-MULTI-SELECTION).
m_scrapRow = new StatRow(tr("holding"), this);
m_scrapRow->setValueIcon(m_scrapIcon);
m_scrapRow->setIndented(true);
m_scrapRow->setValueEmphasized(true);
getRuntimeLayout()->addWidget(m_scrapRow);
@@ -99,7 +104,9 @@ void FieldMultiContent::refreshRuntime()
// selection and rebuilds this card -- but the scrap falls as the debris is collected.
if (m_scrapRow)
{
m_scrapRow->setValue(tr("%1 scrap")
.arg(sumDebrisScrap(getContext().sim->getAdmin(), m_debris)));
const int scrap = sumDebrisScrap(getContext().sim->getAdmin(), m_debris);
// The icon names the item; only without one does the value have to say the word.
m_scrapRow->setValue(m_scrapIcon.isNull() ? tr("%1 scrap").arg(scrap)
: QString::number(scrap));
}
}

View File

@@ -2,6 +2,8 @@
#include <vector>
#include <QPixmap>
#include "entt/entity/entity.hpp"
#include "SelectionContent.h"
@@ -30,4 +32,7 @@ private:
// Null unless debris is part of the selection; the only value here that changes
// while the selection stands.
StatRow* m_scrapRow;
// The bare scrap icon the sub-row states its total with, or null where there is no
// icon file and the total names the item in words instead (REQ-UI-DEBRIS-PANEL).
QPixmap m_scrapIcon;
};

View File

@@ -10,6 +10,8 @@
#include "CountRow.h"
#include "FactoryQueries.h"
#include "GameConfig.h"
#include "IconCaption.h"
#include "ItemIconCache.h"
#include "SelectionNames.h"
#include "Simulation.h"
#include "StatRow.h"
@@ -92,7 +94,13 @@ void MultiBuildingContent::buildSummary()
}
}
// The block icon stands beside the total in place of the word, as it does on a build
// button and a blueprint card (REQ-UI-MULTI-SELECTION, REQ-UI-BLOCKS-ICON). With no
// icon file the total is the bare number, as it is there
// (REQ-UI-BUILD-COST, REQ-UI-BLUEPRINT-CARD).
StatRow* totalRow = new StatRow(tr("Total cost"), this);
totalRow->setValueIcon(
getContext().itemIcons->getInlineIcon(kBlockItemId, font()));
totalRow->setValue(QString::number(totalCost));
totalRow->setValueEmphasized(true);
getRuntimeLayout()->addWidget(totalRow);

View File

@@ -4,6 +4,8 @@
#include <QLabel>
#include <QPalette>
#include "IconCaption.h"
namespace
{
@@ -15,6 +17,7 @@ const int kIndentPx = 12;
StatRow::StatRow(const QString& label, QWidget* parent)
: QWidget(parent)
, m_valueEmphasized(false)
{
QHBoxLayout* layout = new QHBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
@@ -36,19 +39,48 @@ void StatRow::setLabel(const QString& label)
void StatRow::setValue(const QString& value)
{
m_valueLabel->setText(value);
m_value = value;
updateValue();
}
void StatRow::setValueIcon(const QPixmap& icon)
{
m_valueIcon = icon;
updateValue();
}
void StatRow::setValueEmphasized(bool emphasized)
{
m_valueEmphasized = emphasized;
QPalette valuePalette = m_valueLabel->palette();
valuePalette.setColor(QPalette::WindowText,
palette().color(emphasized ? QPalette::Highlight
: QPalette::WindowText));
valuePalette.setColor(QPalette::WindowText, getValueColor());
m_valueLabel->setPalette(valuePalette);
updateValue();
}
void StatRow::setIndented(bool indented)
{
layout()->setContentsMargins(indented ? kIndentPx : 0, 0, 0, 0);
}
void StatRow::updateValue()
{
if (m_valueIcon.isNull())
{
m_valueLabel->setText(m_value);
return;
}
// A label shows either text or a pixmap, so the two are composed into one -- the same
// way the header bar states the block stock (REQ-UI-BLOCKS-ICON). The color is passed
// in rather than left to the palette, which a pixmap does not follow.
m_valueLabel->setPixmap(renderCaptionWithIcon(m_value, m_valueIcon,
m_valueLabel->font(), getValueColor()));
}
QColor StatRow::getValueColor() const
{
return palette().color(m_valueEmphasized ? QPalette::Highlight
: QPalette::WindowText);
}

View File

@@ -1,5 +1,7 @@
#pragma once
#include <QColor>
#include <QPixmap>
#include <QString>
#include <QWidget>
@@ -20,6 +22,12 @@ public:
void setLabel(const QString& label);
void setValue(const QString& value);
// Draws an item icon after the value, bare and without its colored square: the icon
// stands in for the item's name beside the number rather than displaying the item
// (REQ-UI-ITEM-ICON), which is how a debris card states its scrap
// (REQ-UI-DEBRIS-PANEL). A null pixmap -- what a missing icon file yields -- leaves
// the value plain text, and the caller names the item in the label instead.
void setValueIcon(const QPixmap& icon);
// Draws the value in the palette's highlight color rather than its text color, for
// the one value a card is really about.
void setValueEmphasized(bool emphasized);
@@ -28,6 +36,15 @@ public:
void setIndented(bool indented);
private:
// Re-states the value in whichever form it currently takes. Needed because the icon
// form is one composed pixmap, which has to be built anew whenever the text or the
// color it is drawn in changes.
void updateValue();
QColor getValueColor() const;
QLabel* m_labelLabel;
QLabel* m_valueLabel;
QString m_value;
QPixmap m_valueIcon;
bool m_valueEmphasized;
};