Files
dota_factory/src/ui/ItemIconCache.cpp
Malte Langkabel 344d18a5d1 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
2026-08-12 10:14:04 +02:00

162 lines
5.3 KiB
C++

#include "ItemIconCache.h"
#include <QFile>
#include <QFontMetrics>
#include <QPainter>
#include <QRectF>
#include <QSvgRenderer>
#include "VisualsConfig.h"
namespace
{
// How far the icon is inset within the item's colored square, as a fraction of the
// square's size on each side (REQ-UI-ITEM-ICON). The inset is what keeps a frame of the
// square's color visible all around the icon: each icon's viewBox is cropped tight to
// its artwork, so an icon drawn at the full rect would cover the square entirely.
const double kIconInsetFraction = 0.15;
} // namespace
ItemIconCache::ItemIconCache(const QString& iconDir, const VisualsConfig* visuals)
: m_iconDir(iconDir)
, m_visuals(visuals)
{
}
const QByteArray& ItemIconCache::getSvg(const std::string& itemId)
{
const std::map<std::string, QByteArray>::const_iterator cached =
m_svgById.find(itemId);
if (cached != m_svgById.end())
{
return cached->second;
}
// An absent or unreadable file caches an empty byte array so it is not retried;
// a missing icon is not an error (REQ-UI-ITEM-ICON).
QByteArray svg;
QFile file(m_iconDir + "/" + QString::fromStdString(itemId) + ".svg");
if (file.open(QIODevice::ReadOnly))
{
svg = file.readAll();
}
return m_svgById.emplace(itemId, std::move(svg)).first->second;
}
bool ItemIconCache::hasIcon(const std::string& itemId)
{
return !getSvg(itemId).isEmpty();
}
void ItemIconCache::paintItem(QPainter& painter, const QRectF& rect,
const std::string& itemId)
{
// The colored square from visuals.toml backs every item, icon or not: it is what
// gives the item contrast against the tile beneath it in the world, and its outline
// is what separates neighbouring items where they overlap on a belt (REQ-GW-TILE-SIZE,
// REQ-UI-ITEM-ICON).
if (m_visuals != nullptr)
{
const std::map<std::string, ItemVisuals>::const_iterator it =
m_visuals->items.find(itemId);
if (it != m_visuals->items.end())
{
painter.fillRect(rect, it->second.fill);
painter.setPen(QPen(it->second.outline, 1));
painter.setBrush(Qt::NoBrush);
painter.drawRect(rect);
}
}
if (!hasIcon(itemId)) { return; }
// The icon goes on top, inset so a frame of the square's color stays visible all
// around it. It is rasterized once at the inset pixel size and cached, so this is a
// plain pixmap blit per frame.
const double inset = kIconInsetFraction * rect.width();
const QRectF iconRect = rect.adjusted(inset, inset, -inset, -inset);
int sizePx = qRound(iconRect.width());
if (sizePx < 1) { sizePx = 1; }
painter.drawPixmap(iconRect, getPixmap(itemId, sizePx),
QRectF(0, 0, sizePx, sizePx));
}
QPixmap ItemIconCache::getSquarePixmap(const std::string& itemId, int sizePx)
{
return getPixmap("square:" + itemId, itemId, sizePx, true);
}
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)
{
if (sizePx <= 0)
{
return QPixmap();
}
const std::pair<std::string, int> key(cacheKey, sizePx);
const std::map<std::pair<std::string, int>, QPixmap>::const_iterator cached =
m_pixmapCache.find(key);
if (cached != m_pixmapCache.end())
{
return cached->second;
}
QPixmap pixmap;
if (withSquare)
{
// Null unless the item has something to draw -- a square, an icon, or both --
// so a caller with nothing to show can fall back to text (REQ-UI-ITEM-ICON).
const bool hasSquare = m_visuals != nullptr
&& m_visuals->items.find(itemId) != m_visuals->items.end();
if (hasSquare || hasIcon(itemId))
{
pixmap = QPixmap(sizePx, sizePx);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
// No antialiasing: the square's edges are axis-aligned and land on pixel
// boundaries, and smoothing a 1-pixel outline only blurs it. The icon is
// blitted into a slightly smaller rect than it was rasterized at, which is
// what the smooth transform is for.
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
// One pixel short of the pixmap so the square's right and bottom edges land
// inside it rather than on its border.
paintItem(painter, QRectF(0, 0, sizePx - 1, sizePx - 1), itemId);
}
}
else
{
const QByteArray& svg = getSvg(itemId);
if (!svg.isEmpty())
{
QSvgRenderer renderer(svg);
pixmap = QPixmap(sizePx, sizePx);
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
renderer.render(&painter);
}
}
return m_pixmapCache.emplace(key, std::move(pixmap)).first->second;
}
void ItemIconCache::clearPixmapCache()
{
m_pixmapCache.clear();
}