color item icon backgrounds in the UI from visuals.toml
This commit is contained in:
@@ -2,10 +2,26 @@
|
||||
|
||||
#include <QFile>
|
||||
#include <QPainter>
|
||||
#include <QRectF>
|
||||
#include <QSvgRenderer>
|
||||
|
||||
ItemIconCache::ItemIconCache(const QString& iconDir)
|
||||
#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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -34,14 +50,59 @@ 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::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(itemId, sizePx);
|
||||
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())
|
||||
@@ -49,16 +110,45 @@ QPixmap ItemIconCache::getPixmap(const std::string& itemId, int sizePx)
|
||||
return cached->second;
|
||||
}
|
||||
|
||||
const QByteArray& svg = getSvg(itemId);
|
||||
QPixmap pixmap;
|
||||
if (!svg.isEmpty())
|
||||
if (withSquare)
|
||||
{
|
||||
QSvgRenderer renderer(svg);
|
||||
pixmap = QPixmap(sizePx, sizePx);
|
||||
pixmap.fill(Qt::transparent);
|
||||
QPainter painter(&pixmap);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
renderer.render(&painter);
|
||||
// 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-RECIPE-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();
|
||||
}
|
||||
|
||||
@@ -8,38 +8,76 @@
|
||||
#include <QPixmap>
|
||||
#include <QString>
|
||||
|
||||
// Rasterizes and caches per-item icon SVGs (REQ-UI-ITEM-ICON). Item icons are
|
||||
// self-contained, full-color SVGs loaded from a directory, one file per item type
|
||||
// named after the item's id (e.g. "iron_ore.svg"). Shared by the recipe-selection
|
||||
// dialog (REQ-UI-RECIPE-ICON) and the game world's belt/port item rendering so the
|
||||
// rasterization is not duplicated.
|
||||
class QPainter;
|
||||
class QRectF;
|
||||
struct VisualsConfig;
|
||||
|
||||
// Rasterizes and caches per-item icon SVGs, and composes them onto the item's colored
|
||||
// square (REQ-UI-ITEM-ICON). Item icons are self-contained, full-color SVGs loaded from
|
||||
// a directory, one file per item type named after the item's id (e.g. "iron_ore.svg").
|
||||
//
|
||||
// A missing icon file is not an error: hasIcon() returns false for it and the caller
|
||||
// falls back (a colored square in the world, a name caption in the dialog).
|
||||
// The square is the item's `fill` and `outline` from visuals.toml with the icon inset
|
||||
// within it, and it backs the icon both in the game world and wherever the UI displays
|
||||
// an item as an item: the recipe-selection dialog's option buttons (REQ-UI-RECIPE-ICON),
|
||||
// the selection panel's item chips (REQ-UI-SINGLE-SELECTION, REQ-UI-HQ-PANEL), and the
|
||||
// recipe summary (REQ-UI-RECIPE-SUMMARY). Defined here once rather than per widget, in
|
||||
// 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.
|
||||
//
|
||||
// A missing icon file is not an error: hasIcon() returns false for it and the item shows
|
||||
// its colored square alone.
|
||||
class ItemIconCache
|
||||
{
|
||||
public:
|
||||
// iconDir is the directory holding the "<item_id>.svg" icon files
|
||||
// (typically "<configDir>/../icons/items").
|
||||
explicit ItemIconCache(const QString& iconDir);
|
||||
// iconDir is the directory holding the "<item_id>.svg" icon files (typically
|
||||
// "<configDir>/../icons/items"). visuals supplies the per-item square colors and
|
||||
// must outlive the cache; its contents may be replaced on a restart (REQ-CFG-RELOAD),
|
||||
// which is what clearPixmapCache() is for.
|
||||
ItemIconCache(const QString& iconDir, const VisualsConfig* visuals);
|
||||
|
||||
// True if an icon SVG file exists for the given item id. Loads the file's bytes
|
||||
// on first query and remembers the result (including absence) so repeated calls
|
||||
// are cheap.
|
||||
bool hasIcon(const std::string& itemId);
|
||||
|
||||
// Returns the item's icon rasterized to a transparent sizePx*sizePx pixmap,
|
||||
// cached per (item id, size) so it is rendered once and reused across frames and
|
||||
// only re-rasterized when the target size changes (REQ-UI-ITEM-ICON). Returns a
|
||||
// null pixmap if the item has no icon file (callers should gate on hasIcon()).
|
||||
// Paints the item's colored square into rect and its icon inset within it
|
||||
// (REQ-UI-ITEM-ICON). Takes the rect as a QRectF so the world can keep painting at
|
||||
// sub-pixel geometry. An item with no visuals entry gets no square, one with no icon
|
||||
// file no icon; with neither, this paints nothing.
|
||||
void paintItem(QPainter& painter, const QRectF& rect, const std::string& itemId);
|
||||
|
||||
// The same composition rasterized to a sizePx*sizePx pixmap for widget use, cached
|
||||
// per (item id, size) so it is rendered once and reused. Returns a null pixmap only
|
||||
// when the item has neither a visuals entry nor an icon file, which is the one case
|
||||
// in which a caller has nothing to show and falls back to text.
|
||||
QPixmap getSquarePixmap(const std::string& itemId, int sizePx);
|
||||
|
||||
// Returns the item's icon alone, without its square, rasterized to a transparent
|
||||
// sizePx*sizePx pixmap and cached per (item id, size) so it is rendered once and
|
||||
// reused across frames (REQ-UI-ITEM-ICON). Returns a null pixmap if the item has no
|
||||
// icon file (callers should gate on hasIcon()).
|
||||
QPixmap getPixmap(const std::string& itemId, int sizePx);
|
||||
|
||||
// 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.
|
||||
void clearPixmapCache();
|
||||
|
||||
private:
|
||||
// Returns the raw SVG bytes for an item id, loading and caching them on first
|
||||
// access. An absent file caches an empty QByteArray so it is not retried.
|
||||
const QByteArray& getSvg(const std::string& itemId);
|
||||
// Shared rasterize-and-cache step. cacheKey distinguishes the bare and squared
|
||||
// variants of one item within the single pixmap cache.
|
||||
QPixmap getPixmap(const std::string& cacheKey, const std::string& itemId,
|
||||
int sizePx, bool withSquare);
|
||||
|
||||
QString m_iconDir;
|
||||
const VisualsConfig* m_visuals; // Not owned; lives in MainWindow.
|
||||
std::map<std::string, QByteArray> m_svgById;
|
||||
std::map<std::pair<std::string, int>, QPixmap> m_pixmapCache;
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
||||
// REQ-UI-BUILD-ICON).
|
||||
const QString configDirPath = QString::fromStdString(m_configDir);
|
||||
m_itemIcons = std::make_unique<ItemIconCache>(
|
||||
QDir::cleanPath(configDirPath + "/../icons/items"));
|
||||
QDir::cleanPath(configDirPath + "/../icons/items"), &m_visuals);
|
||||
m_buildingIcons = std::make_unique<BuildingIconCache>(
|
||||
QDir::cleanPath(configDirPath + "/../icons/buildings"));
|
||||
|
||||
@@ -283,6 +283,9 @@ std::optional<GameConfig> MainWindow::reloadConfig()
|
||||
VisualsConfig newVisuals = VisualsLoader::load(m_configDir + "/visuals.toml");
|
||||
m_visuals = std::move(newVisuals);
|
||||
m_dimOverlay->setDimColor(m_visuals.overlays.modalDim);
|
||||
// The composed item squares carry the colors they were painted with, so they
|
||||
// are dropped for the new ones to take effect (REQ-UI-ITEM-ICON).
|
||||
m_itemIcons->clearPixmapCache();
|
||||
return newConfig;
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
|
||||
@@ -127,12 +127,17 @@ RecipeSelectionDialog::RecipeSelectionDialog(
|
||||
const RecipeSelectionOption& option = options[static_cast<std::size_t>(i)];
|
||||
|
||||
QPushButton* button = new QPushButton(this);
|
||||
// Icon-only when the produced item has an icon (REQ-UI-RECIPE-ICON); otherwise
|
||||
// fall back to the caption. The name stays reachable via the tooltip.
|
||||
if (!option.iconItemId.empty() && itemIcons->hasIcon(option.iconItemId))
|
||||
// Icon-only for a recipe option: the produced item's icon on its colored square,
|
||||
// or the square alone when the item has no icon file (REQ-UI-RECIPE-ICON). The
|
||||
// name stays reachable via the tooltip. Options carrying no item at all -- the
|
||||
// "(None)" entry and the Shipyard's schematics -- keep their text caption, as
|
||||
// does an item with neither square nor icon to show.
|
||||
const QPixmap icon = option.iconItemId.empty()
|
||||
? QPixmap()
|
||||
: itemIcons->getSquarePixmap(option.iconItemId, kOptionIconSize.width());
|
||||
if (!icon.isNull())
|
||||
{
|
||||
button->setIcon(QIcon(itemIcons->getPixmap(
|
||||
option.iconItemId, kOptionIconSize.width())));
|
||||
button->setIcon(QIcon(icon));
|
||||
button->setIconSize(kOptionIconSize);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -541,37 +541,14 @@ void WorldRenderer::drawPortItems(QPainter& painter, const WorldCoordinates& coo
|
||||
void WorldRenderer::drawWorldItem(QPainter& painter, const std::string& itemId,
|
||||
QPointF center, float halfPx)
|
||||
{
|
||||
if (!m_itemIcons) { return; }
|
||||
|
||||
// The colored square carrying the item's icon (REQ-GW-TILE-SIZE, REQ-UI-ITEM-ICON).
|
||||
// The composition lives in the icon cache, which draws it the same way here and in
|
||||
// the UI's item displays, so an item reads the same on a belt as in a panel.
|
||||
const QRectF itemRect(center.x() - halfPx, center.y() - halfPx,
|
||||
halfPx * 2, halfPx * 2);
|
||||
|
||||
// The colored square from visuals.toml (REQ-GW-TILE-SIZE) backs every item, icon or
|
||||
// not: it is what gives the item contrast against the tile beneath it, and its dark
|
||||
// outline is what separates neighbouring items where they overlap on a belt.
|
||||
const std::map<std::string, ItemVisuals>::const_iterator it =
|
||||
m_visuals.items.find(itemId);
|
||||
if (it != m_visuals.items.end())
|
||||
{
|
||||
painter.fillRect(itemRect, it->second.fill);
|
||||
painter.setPen(QPen(it->second.outline, 1));
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.drawRect(itemRect);
|
||||
}
|
||||
|
||||
if (!m_itemIcons || !m_itemIcons->hasIcon(itemId)) { return; }
|
||||
|
||||
// The icon goes on top, inset so a frame of the square's color stays visible all
|
||||
// around it (REQ-UI-ITEM-ICON). The inset is needed because each icon's viewBox is
|
||||
// cropped tight to its artwork: drawn at the full rect, a solid icon would cover the
|
||||
// square entirely. It is rasterized once at the inset pixel size and cached, so this
|
||||
// is a plain pixmap blit per frame.
|
||||
constexpr double kIconInsetFraction = 0.15;
|
||||
const double inset = kIconInsetFraction * static_cast<double>(halfPx * 2.0f);
|
||||
const QRectF iconRect = itemRect.adjusted(inset, inset, -inset, -inset);
|
||||
|
||||
int sizePx = qRound(iconRect.width());
|
||||
if (sizePx < 1) { sizePx = 1; }
|
||||
painter.drawPixmap(iconRect, m_itemIcons->getPixmap(itemId, sizePx),
|
||||
QRectF(0, 0, sizePx, sizePx));
|
||||
m_itemIcons->paintItem(painter, itemRect, itemId);
|
||||
}
|
||||
|
||||
void WorldRenderer::drawBeltItems(QPainter& painter, const WorldCoordinates& coordinates,
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size the item icon is drawn at inside a chip, in device-independent pixels.
|
||||
const int kChipIconSizePx = 18;
|
||||
// Size the item's colored square is drawn at inside a chip, in device-independent
|
||||
// pixels. Larger than the artwork it carries, which the square insets (REQ-UI-ITEM-ICON).
|
||||
const int kChipIconSizePx = 22;
|
||||
|
||||
// Chips per row. Two fit the panel's capped width side by side; a third would force the
|
||||
// counts to shrink.
|
||||
@@ -64,11 +65,10 @@ void ItemChipRow::rebuildChips(const std::vector<Entry>& entries)
|
||||
for (std::size_t index = 0; index < entries.size(); ++index)
|
||||
{
|
||||
const std::string& itemId = entries[index].itemId;
|
||||
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the chip is then laid
|
||||
// out around its count alone.
|
||||
const QPixmap icon = m_itemIcons->hasIcon(itemId)
|
||||
? m_itemIcons->getPixmap(itemId, kChipIconSizePx)
|
||||
: QPixmap();
|
||||
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
|
||||
// is not an error: the chip then carries the square alone, and only an item with
|
||||
// no square either leaves the chip laid out around its count alone.
|
||||
const QPixmap icon = m_itemIcons->getSquarePixmap(itemId, kChipIconSizePx);
|
||||
|
||||
ItemChip* chip = new ItemChip(icon, this);
|
||||
m_layout->addWidget(chip, static_cast<int>(index) / kChipsPerRow,
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
namespace
|
||||
{
|
||||
|
||||
// Size the item icons are drawn at on the summary line, in device-independent pixels.
|
||||
const int kSummaryIconSizePx = 14;
|
||||
// Size the items' colored squares are drawn at on the summary line, in device-independent
|
||||
// pixels. Larger than the artwork they carry, which the square insets (REQ-UI-ITEM-ICON).
|
||||
const int kSummaryIconSizePx = 18;
|
||||
|
||||
// Adds a freshly built label to the summary and shows it.
|
||||
//
|
||||
@@ -101,13 +102,15 @@ void RecipeSummaryRow::addAmounts(const std::vector<Amount>& amounts)
|
||||
{
|
||||
for (const Amount& entry : amounts)
|
||||
{
|
||||
// A missing icon file is not an error (REQ-UI-ITEM-ICON): the item's id then
|
||||
// stands in for its icon.
|
||||
if (m_itemIcons->hasIcon(entry.itemId))
|
||||
// The item's icon on its colored square (REQ-UI-ITEM-ICON). A missing icon file
|
||||
// is not an error: the square stands alone then, and only an item with no square
|
||||
// either falls back to its id in text.
|
||||
const QPixmap icon =
|
||||
m_itemIcons->getSquarePixmap(entry.itemId, kSummaryIconSizePx);
|
||||
if (!icon.isNull())
|
||||
{
|
||||
QLabel* iconLabel = new QLabel(this);
|
||||
iconLabel->setPixmap(
|
||||
m_itemIcons->getPixmap(entry.itemId, kSummaryIconSizePx));
|
||||
iconLabel->setPixmap(icon);
|
||||
addAndShow(m_layout, iconLabel);
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user