From 344d18a5d172ea74bab1d293644185f100209282 Mon Sep 17 00:00:00 2001 From: Malte Langkabel Date: Wed, 12 Aug 2026 10:14:04 +0200 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Ne3mejABZoLWKLh8fgpM3x --- src/balancing/CMakeLists.txt | 4 +++ src/ui/IconCaption.h | 11 ++++--- src/ui/ItemIconCache.cpp | 7 ++++ src/ui/ItemIconCache.h | 14 ++++++-- src/ui/selection/DebrisContent.cpp | 12 ++++++- src/ui/selection/DebrisContent.h | 6 ++++ src/ui/selection/FieldMultiContent.cpp | 15 ++++++--- src/ui/selection/FieldMultiContent.h | 5 +++ src/ui/selection/MultiBuildingContent.cpp | 8 +++++ src/ui/selection/StatRow.cpp | 40 ++++++++++++++++++++--- src/ui/selection/StatRow.h | 17 ++++++++++ 11 files changed, 123 insertions(+), 16 deletions(-) diff --git a/src/balancing/CMakeLists.txt b/src/balancing/CMakeLists.txt index 5f35ea0..b21e2a1 100644 --- a/src/balancing/CMakeLists.txt +++ b/src/balancing/CMakeLists.txt @@ -13,6 +13,9 @@ SET(HDRS ${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/BarRow.h ${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SectionBox.h ${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SelectionNames.h + # A stat row states a value with an inline item icon (REQ-UI-ITEM-ICON), which this + # composes; it needs nothing but Qt's painting. + ${CMAKE_CURRENT_SOURCE_DIR}/../ui/IconCaption.h ${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.h # Shared world-space shapes so the arena keeps looking like the game @@ -36,6 +39,7 @@ SET(SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/BarRow.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SectionBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../ui/selection/SelectionNames.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../ui/IconCaption.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../ui/VisualsLoader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../ui/WorldPrimitives.cpp PARENT_SCOPE diff --git a/src/ui/IconCaption.h b/src/ui/IconCaption.h index c9bca08..56800ce 100644 --- a/src/ui/IconCaption.h +++ b/src/ui/IconCaption.h @@ -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"; diff --git a/src/ui/ItemIconCache.cpp b/src/ui/ItemIconCache.cpp index d7e6991..42920f3 100644 --- a/src/ui/ItemIconCache.cpp +++ b/src/ui/ItemIconCache.cpp @@ -1,6 +1,7 @@ #include "ItemIconCache.h" #include +#include #include #include #include @@ -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) { diff --git a/src/ui/ItemIconCache.h b/src/ui/ItemIconCache.h index 6552d93..f20c1ca 100644 --- a/src/ui/ItemIconCache.h +++ b/src/ui/ItemIconCache.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -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. diff --git a/src/ui/selection/DebrisContent.cpp b/src/ui/selection/DebrisContent.cpp index 852ddff..63ea333 100644 --- a/src/ui/selection/DebrisContent.cpp +++ b/src/ui/selection/DebrisContent.cpp @@ -3,6 +3,8 @@ #include #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); diff --git a/src/ui/selection/DebrisContent.h b/src/ui/selection/DebrisContent.h index 00b7716..7872a8a 100644 --- a/src/ui/selection/DebrisContent.h +++ b/src/ui/selection/DebrisContent.h @@ -2,6 +2,8 @@ #include +#include + #include "entt/entity/entity.hpp" #include "SelectionContent.h" @@ -28,4 +30,8 @@ protected: private: std::vector 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; }; diff --git a/src/ui/selection/FieldMultiContent.cpp b/src/ui/selection/FieldMultiContent.cpp index 350764f..44ca298 100644 --- a/src/ui/selection/FieldMultiContent.cpp +++ b/src/ui/selection/FieldMultiContent.cpp @@ -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(request.actors.size() + request.debris.size())); @@ -84,9 +87,11 @@ void FieldMultiContent::buildSummary(const std::vector& actors) getRuntimeLayout()->addWidget(new CountRow( QPixmap(), tr("Debris"), static_cast(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)); } } diff --git a/src/ui/selection/FieldMultiContent.h b/src/ui/selection/FieldMultiContent.h index 75b7e57..3d6c4f3 100644 --- a/src/ui/selection/FieldMultiContent.h +++ b/src/ui/selection/FieldMultiContent.h @@ -2,6 +2,8 @@ #include +#include + #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; }; diff --git a/src/ui/selection/MultiBuildingContent.cpp b/src/ui/selection/MultiBuildingContent.cpp index dc892a2..d905be0 100644 --- a/src/ui/selection/MultiBuildingContent.cpp +++ b/src/ui/selection/MultiBuildingContent.cpp @@ -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); diff --git a/src/ui/selection/StatRow.cpp b/src/ui/selection/StatRow.cpp index 6970c27..3da8c59 100644 --- a/src/ui/selection/StatRow.cpp +++ b/src/ui/selection/StatRow.cpp @@ -4,6 +4,8 @@ #include #include +#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); +} diff --git a/src/ui/selection/StatRow.h b/src/ui/selection/StatRow.h index b88b4fe..652ccf0 100644 --- a/src/ui/selection/StatRow.h +++ b/src/ui/selection/StatRow.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include @@ -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; };