#include "ItemIconCache.h" #include #include #include #include #include #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::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::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 key(cacheKey, sizePx); const std::map, 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(); }