Show building_block icon in header stock, expand button and build costs

This commit is contained in:
2026-07-23 21:13:37 +02:00
parent 0bba7686e6
commit 79b79ab7c3
9 changed files with 313 additions and 18 deletions

View File

@@ -3,12 +3,17 @@
#include <string>
#include <QByteArray>
#include <QColor>
#include <QFile>
#include <QFontMetrics>
#include <QGridLayout>
#include <QIcon>
#include <QPainter>
#include <QPaintEvent>
#include <QPalette>
#include <QPixmap>
#include <QPushButton>
#include <QRect>
#include <QRegularExpression>
#include <QSignalMapper>
#include <QSize>
@@ -21,6 +26,7 @@
#include "DisplayName.h"
#include "EventManager.h"
#include "ExitBuilderModeRequestedEvent.h"
#include "ItemIconCache.h"
#include "Simulation.h"
namespace
@@ -68,15 +74,123 @@ namespace
icon.addPixmap(renderChip(greyed.toUtf8()), QIcon::Disabled);
return icon;
}
// Normal and grey-background chip pixmaps for a "<id>.svg" file, using the same
// recolor rule as loadBuildingIcon. Empty pixmaps if the file cannot be read.
struct ChipPixmaps { QPixmap normal; QPixmap grey; };
ChipPixmaps loadChipPixmaps(const QString& path)
{
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) { return {}; }
const QByteArray svg = file.readAll();
ChipPixmaps result;
result.normal = renderChip(svg);
QString greyed = QString::fromUtf8(svg);
static const QRegularExpression fillPattern(QStringLiteral("fill=\"#[0-9a-fA-F]{6}\""));
const QRegularExpressionMatch match = fillPattern.match(greyed);
if (match.hasMatch())
{
greyed.replace(match.capturedStart(), match.capturedLength(),
QStringLiteral("fill=\"#5f636e\""));
}
result.grey = renderChip(greyed.toUtf8());
return result;
}
// A build button that paints its own face — chip icon at the left, the building
// name above the cost, and the building_block item icon after the cost number in
// place of the "Blocks" word (REQ-UI-BUILD-COST, REQ-UI-BUILD-ICON). Custom paint
// (rather than the native icon+text) is needed because a QPushButton holds only
// one icon; this stays adaptive to the button width and greys itself when the
// button is disabled/unaffordable (REQ-UI-BUILD-DISABLED).
class BuildButton : public QPushButton
{
public:
BuildButton(const ChipPixmaps& chip, const QString& name,
const QString& costText, const QPixmap& blockIcon, QWidget* parent)
: QPushButton(parent)
, m_chip(chip)
, m_name(name)
, m_costText(costText)
, m_blockIcon(blockIcon)
{
}
protected:
void paintEvent(QPaintEvent* event) override
{
QPushButton::paintEvent(event); // frame, checked/hover state
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
const bool on = isEnabled();
const QRect area = rect().adjusted(6, 4, -6, -4);
const int chipSize = kIconSize.width();
const QPixmap& chip = on ? m_chip.normal : m_chip.grey;
if (!chip.isNull())
{
painter.drawPixmap(
QRect(area.x(), area.y() + (area.height() - chipSize) / 2,
chipSize, chipSize), chip);
}
const QRect textArea(area.x() + chipSize + 6, area.y(),
area.width() - chipSize - 6, area.height());
const QFontMetrics metrics(font());
const int lineHeight = metrics.height();
painter.setFont(font());
painter.setPen(palette().color(on ? QPalette::Active : QPalette::Disabled,
QPalette::ButtonText));
// Name fills everything above the bottom cost line (word-wrapped).
painter.drawText(
QRect(textArea.x(), textArea.y(),
textArea.width(), textArea.height() - lineHeight),
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, m_name);
// Cost line: "<n>" then the block icon (or "<n> Blocks" when no icon).
const int costY = textArea.bottom() - lineHeight + 1;
if (m_blockIcon.isNull())
{
painter.drawText(
QRect(textArea.x(), costY, textArea.width(), lineHeight),
Qt::AlignLeft | Qt::AlignVCenter,
QObject::tr("%1 Blocks").arg(m_costText));
return;
}
const int costWidth = metrics.horizontalAdvance(m_costText);
painter.drawText(QRect(textArea.x(), costY, costWidth, lineHeight),
Qt::AlignLeft | Qt::AlignVCenter, m_costText);
const qreal iconDpr = m_blockIcon.devicePixelRatio();
const int iconW = static_cast<int>(m_blockIcon.width() / iconDpr);
const int iconH = static_cast<int>(m_blockIcon.height() / iconDpr);
if (!on) { painter.setOpacity(0.45); }
painter.drawPixmap(
QRect(textArea.x() + costWidth + 4, costY + (lineHeight - iconH) / 2,
iconW, iconH), m_blockIcon);
}
private:
ChipPixmaps m_chip;
QString m_name;
QString m_costText;
QPixmap m_blockIcon;
};
}
BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
const std::string& iconDir, QWidget* parent)
const std::string& iconDir,
const std::string& itemsIconDir, QWidget* parent)
: QWidget(parent)
, m_sim(sim)
, m_config(config)
, m_iconDir(iconDir)
, m_itemIcons(std::make_unique<ItemIconCache>(
QString::fromStdString(itemsIconDir)))
{
QGridLayout* layout = new QGridLayout(this);
layout->setSpacing(4);
@@ -87,6 +201,13 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
int row = 0;
const int kCols = 3;
// Block icon shown in each button's cost line (REQ-UI-BUILD-COST); null when no
// building_block icon exists, in which case buttons fall back to text costs.
const bool hasBlockIcon = m_itemIcons->hasIcon("building_block");
const QPixmap blockIcon = hasBlockIcon
? m_itemIcons->getPixmap("building_block", QFontMetrics(font()).height())
: QPixmap();
for (const BuildingDef& def : config->buildings.buildings)
{
if (!def.playerPlaceable)
@@ -107,16 +228,28 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
const QString name = (def.type == BuildingType::TunnelEntry)
? tr("Tunnel")
: QString::fromStdString(toDisplayName(def.id));
const QString label = name + "\n" + tr("%1 Building Blocks").arg(def.cost);
QPushButton* btn = new QPushButton(label, this);
btn->setCheckable(true);
btn->setFixedHeight(48);
// Icon file name matches the building id (REQ-UI-BUILD-GRID); Tunnel Entry's
// "tunnel_entry.svg" serves the shared Tunnel button.
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
+ QString::fromStdString(def.id) + ".svg";
btn->setIcon(loadBuildingIcon(iconPath));
btn->setIconSize(kIconSize);
QPushButton* btn = nullptr;
if (hasBlockIcon)
{
// Custom-painted button showing the cost with the block icon in place of
// the "Blocks" word (REQ-UI-BUILD-COST).
btn = new BuildButton(loadChipPixmaps(iconPath), name,
QString::number(def.cost), blockIcon, this);
}
else
{
// Fallback: native chip icon + text cost when no block icon exists.
btn = new QPushButton(name + "\n" + tr("%1 Blocks").arg(def.cost), this);
btn->setIcon(loadBuildingIcon(iconPath));
btn->setIconSize(kIconSize);
}
btn->setCheckable(true);
btn->setFixedHeight(48);
if (def.tooltip)
{
btn->setToolTip(QString::fromStdString(*def.tooltip));