Show building_block icon in header stock, expand button, build costs
Header stock reads "Stock: <n>" + block icon; the expand button and each build button show the block icon in place of the "Blocks" word (REQ-UI-BLOCKS-ICON, REQ-UI-EXPAND-BUTTON, REQ-UI-BUILD-COST). New IconCaption::renderCaptionWithIcon composes "text + trailing icon" into one pixmap (Qt buttons/labels can't place an icon after text). The header label uses it as a pixmap; the expand button as its QIcon (Normal + greyed Disabled). Build buttons need both the building chip and the block icon, so a custom-painted BuildButton draws the whole face, adaptive to width and greying when unaffordable. All paths fall back to text when no building_block icon exists. Item icon dir threaded through MainWindow to HeaderBar and BuildButtonGrid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7N59FsLA5e2kuVdqe4Uhc
This commit is contained in:
@@ -3,12 +3,17 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
#include <QColor>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QFontMetrics>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QIcon>
|
#include <QIcon>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QPalette>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QRect>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSignalMapper>
|
#include <QSignalMapper>
|
||||||
#include <QSize>
|
#include <QSize>
|
||||||
@@ -21,6 +26,7 @@
|
|||||||
#include "DisplayName.h"
|
#include "DisplayName.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
#include "ExitBuilderModeRequestedEvent.h"
|
#include "ExitBuilderModeRequestedEvent.h"
|
||||||
|
#include "ItemIconCache.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -68,15 +74,123 @@ namespace
|
|||||||
icon.addPixmap(renderChip(greyed.toUtf8()), QIcon::Disabled);
|
icon.addPixmap(renderChip(greyed.toUtf8()), QIcon::Disabled);
|
||||||
return icon;
|
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,
|
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)
|
: QWidget(parent)
|
||||||
, m_sim(sim)
|
, m_sim(sim)
|
||||||
, m_config(config)
|
, m_config(config)
|
||||||
, m_iconDir(iconDir)
|
, m_iconDir(iconDir)
|
||||||
|
, m_itemIcons(std::make_unique<ItemIconCache>(
|
||||||
|
QString::fromStdString(itemsIconDir)))
|
||||||
{
|
{
|
||||||
QGridLayout* layout = new QGridLayout(this);
|
QGridLayout* layout = new QGridLayout(this);
|
||||||
layout->setSpacing(4);
|
layout->setSpacing(4);
|
||||||
@@ -87,6 +201,13 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
|||||||
int row = 0;
|
int row = 0;
|
||||||
const int kCols = 3;
|
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)
|
for (const BuildingDef& def : config->buildings.buildings)
|
||||||
{
|
{
|
||||||
if (!def.playerPlaceable)
|
if (!def.playerPlaceable)
|
||||||
@@ -107,16 +228,28 @@ BuildButtonGrid::BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
|||||||
const QString name = (def.type == BuildingType::TunnelEntry)
|
const QString name = (def.type == BuildingType::TunnelEntry)
|
||||||
? tr("Tunnel")
|
? tr("Tunnel")
|
||||||
: QString::fromStdString(toDisplayName(def.id));
|
: 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
|
// Icon file name matches the building id (REQ-UI-BUILD-GRID); Tunnel Entry's
|
||||||
// "tunnel_entry.svg" serves the shared Tunnel button.
|
// "tunnel_entry.svg" serves the shared Tunnel button.
|
||||||
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
|
const QString iconPath = QString::fromStdString(m_iconDir) + "/"
|
||||||
+ QString::fromStdString(def.id) + ".svg";
|
+ 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)
|
if (def.tooltip)
|
||||||
{
|
{
|
||||||
btn->setToolTip(QString::fromStdString(*def.tooltip));
|
btn->setToolTip(QString::fromStdString(*def.tooltip));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
|
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
class Simulation;
|
class Simulation;
|
||||||
|
class ItemIconCache;
|
||||||
|
|
||||||
class BuildButtonGrid : public QWidget,
|
class BuildButtonGrid : public QWidget,
|
||||||
public CombinedEventHandler<BuilderModeExitedEvent,
|
public CombinedEventHandler<BuilderModeExitedEvent,
|
||||||
@@ -30,9 +32,12 @@ class BuildButtonGrid : public QWidget,
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
// iconDir is the directory holding the per-building "<id>.svg" chip icons
|
// iconDir is the directory holding the per-building "<id>.svg" chip icons
|
||||||
// (REQ-UI-BUILD-GRID); read from disk at runtime, like the config files.
|
// (REQ-UI-BUILD-GRID); itemsIconDir holds the per-item icons and supplies the
|
||||||
|
// building_block icon shown in each button's cost (REQ-UI-BUILD-COST). Both are
|
||||||
|
// read from disk at runtime, like the config files.
|
||||||
BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
BuildButtonGrid(Simulation* sim, const GameConfig* config,
|
||||||
const std::string& iconDir, QWidget* parent = nullptr);
|
const std::string& iconDir, const std::string& itemsIconDir,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
~BuildButtonGrid() override;
|
~BuildButtonGrid() override;
|
||||||
|
|
||||||
void clearActiveButton();
|
void clearActiveButton();
|
||||||
@@ -60,6 +65,7 @@ private:
|
|||||||
Simulation* m_sim;
|
Simulation* m_sim;
|
||||||
const GameConfig* m_config;
|
const GameConfig* m_config;
|
||||||
std::string m_iconDir;
|
std::string m_iconDir;
|
||||||
|
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||||
std::vector<BuildingType> m_types;
|
std::vector<BuildingType> m_types;
|
||||||
std::vector<QPushButton*> m_buttons;
|
std::vector<QPushButton*> m_buttons;
|
||||||
std::map<BuildingType, int> m_costs;
|
std::map<BuildingType, int> m_costs;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,5 +37,6 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeSelectionDialog.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/RecipeTooltip.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemIconCache.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/IconCaption.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,34 +3,51 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <QFontMetrics>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QIcon>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
|
#include <QPalette>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
#include <QSignalMapper>
|
#include <QSignalMapper>
|
||||||
|
#include <QSize>
|
||||||
|
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "CommandRequestedEvent.h"
|
#include "CommandRequestedEvent.h"
|
||||||
#include "EventManager.h"
|
#include "EventManager.h"
|
||||||
|
#include "IconCaption.h"
|
||||||
|
#include "ItemIconCache.h"
|
||||||
#include "SpeedChangeRequestedEvent.h"
|
#include "SpeedChangeRequestedEvent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Item id of the building blocks resource, whose icon stands in for the "Blocks"
|
||||||
|
// word in the header stock and expand button (REQ-UI-BLOCKS-ICON).
|
||||||
|
const char* const kBlockItemId = "building_block";
|
||||||
|
}
|
||||||
|
|
||||||
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
const double HeaderBar::kSpeeds[] = { 0.0, 0.5, 1.0, 2.0, 10.0 };
|
||||||
const int HeaderBar::kSpeedCount = 5;
|
const int HeaderBar::kSpeedCount = 5;
|
||||||
|
|
||||||
HeaderBar::HeaderBar(const GameConfig* config, QWidget* parent)
|
HeaderBar::HeaderBar(const GameConfig* config, const std::string& itemsIconDir,
|
||||||
|
QWidget* parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
|
, m_itemIcons(std::make_unique<ItemIconCache>(
|
||||||
|
QString::fromStdString(itemsIconDir)))
|
||||||
{
|
{
|
||||||
QHBoxLayout* layout = new QHBoxLayout(this);
|
QHBoxLayout* layout = new QHBoxLayout(this);
|
||||||
layout->setContentsMargins(8, 4, 8, 4);
|
layout->setContentsMargins(8, 4, 8, 4);
|
||||||
layout->setSpacing(8);
|
layout->setSpacing(8);
|
||||||
|
|
||||||
m_timeLabel = new QLabel("00:00", this);
|
m_timeLabel = new QLabel("00:00", this);
|
||||||
m_blocksLabel = new QLabel(tr("Building Blocks: 0"), this);
|
m_blocksLabel = new QLabel(this);
|
||||||
if (config->world.buildingBlocksTooltip)
|
if (config->world.buildingBlocksTooltip)
|
||||||
{
|
{
|
||||||
m_blocksLabel->setToolTip(
|
m_blocksLabel->setToolTip(
|
||||||
QString::fromStdString(*config->world.buildingBlocksTooltip));
|
QString::fromStdString(*config->world.buildingBlocksTooltip));
|
||||||
}
|
}
|
||||||
|
updateBlocksLabel();
|
||||||
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
|
m_artifactsLabel = new QLabel(tr("Artifacts: 0/?"), this);
|
||||||
if (config->world.artifactTooltip)
|
if (config->world.artifactTooltip)
|
||||||
{
|
{
|
||||||
@@ -91,7 +108,7 @@ void HeaderBar::handleEvent(std::shared_ptr<const TickAdvancedEvent> event)
|
|||||||
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
void HeaderBar::handleEvent(std::shared_ptr<const BuildingBlocksChangedEvent> event)
|
||||||
{
|
{
|
||||||
m_blocks = event->blocks;
|
m_blocks = event->blocks;
|
||||||
m_blocksLabel->setText(tr("Building Blocks: %1").arg(event->blocks));
|
updateBlocksLabel();
|
||||||
updateExpandButton();
|
updateExpandButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +118,58 @@ void HeaderBar::handleEvent(std::shared_ptr<const ExpansionCostChangedEvent> eve
|
|||||||
updateExpandButton();
|
updateExpandButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QPixmap HeaderBar::blockIcon() const
|
||||||
|
{
|
||||||
|
if (!m_itemIcons->hasIcon(kBlockItemId)) { return QPixmap(); }
|
||||||
|
// Sized to the header text height so it sits inline with the caption.
|
||||||
|
const int sizePx = QFontMetrics(font()).height();
|
||||||
|
return m_itemIcons->getPixmap(kBlockItemId, sizePx);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HeaderBar::updateBlocksLabel()
|
||||||
|
{
|
||||||
|
const QPixmap icon = blockIcon();
|
||||||
|
if (icon.isNull())
|
||||||
|
{
|
||||||
|
// Fallback text form when no building_block icon exists (REQ-UI-BLOCKS-ICON).
|
||||||
|
m_blocksLabel->setText(tr("Stock: %1 Blocks").arg(m_blocks));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_blocksLabel->setPixmap(renderCaptionWithIcon(
|
||||||
|
tr("Stock: %1").arg(m_blocks), icon, font(),
|
||||||
|
m_blocksLabel->palette().color(QPalette::WindowText)));
|
||||||
|
}
|
||||||
|
|
||||||
void HeaderBar::updateExpandButton()
|
void HeaderBar::updateExpandButton()
|
||||||
{
|
{
|
||||||
m_expandButton->setText(tr("Expand: %1 Building Blocks").arg(m_expansionCost));
|
|
||||||
m_expandButton->setEnabled(m_blocks >= m_expansionCost);
|
m_expandButton->setEnabled(m_blocks >= m_expansionCost);
|
||||||
|
|
||||||
|
const QPixmap icon = blockIcon();
|
||||||
|
if (icon.isNull())
|
||||||
|
{
|
||||||
|
// Fallback text form when no building_block icon exists (REQ-UI-EXPAND-BUTTON).
|
||||||
|
m_expandButton->setIcon(QIcon());
|
||||||
|
m_expandButton->setText(tr("Expand: %1 Blocks").arg(m_expansionCost));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString text = tr("Expand: %1").arg(m_expansionCost);
|
||||||
|
const QPalette& pal = m_expandButton->palette();
|
||||||
|
const QPixmap normal = renderCaptionWithIcon(
|
||||||
|
text, icon, m_expandButton->font(), pal.color(QPalette::ButtonText));
|
||||||
|
const QPixmap greyed = renderCaptionWithIcon(
|
||||||
|
text, icon, m_expandButton->font(),
|
||||||
|
pal.color(QPalette::Disabled, QPalette::ButtonText));
|
||||||
|
|
||||||
|
QIcon buttonIcon;
|
||||||
|
buttonIcon.addPixmap(normal, QIcon::Normal);
|
||||||
|
buttonIcon.addPixmap(greyed, QIcon::Disabled);
|
||||||
|
m_expandButton->setText(QString());
|
||||||
|
m_expandButton->setIcon(buttonIcon);
|
||||||
|
const qreal dpr = normal.devicePixelRatio();
|
||||||
|
m_expandButton->setIconSize(QSize(
|
||||||
|
static_cast<int>(normal.width() / dpr),
|
||||||
|
static_cast<int>(normal.height() / dpr)));
|
||||||
}
|
}
|
||||||
|
|
||||||
void HeaderBar::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
void HeaderBar::handleEvent(std::shared_ptr<const GameSpeedChangedEvent> event)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPixmap>
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
#include "ArtifactCountChangedEvent.h"
|
#include "ArtifactCountChangedEvent.h"
|
||||||
@@ -16,6 +19,7 @@
|
|||||||
|
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
class ItemIconCache;
|
||||||
|
|
||||||
class HeaderBar : public QWidget,
|
class HeaderBar : public QWidget,
|
||||||
public CombinedEventHandler<TickAdvancedEvent,
|
public CombinedEventHandler<TickAdvancedEvent,
|
||||||
@@ -28,7 +32,11 @@ class HeaderBar : public QWidget,
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit HeaderBar(const GameConfig* config, QWidget* parent = nullptr);
|
// itemsIconDir holds the per-item icon SVGs (REQ-UI-ITEM-ICON); used to show the
|
||||||
|
// building_block icon in the stock display and expand button (REQ-UI-BLOCKS-ICON,
|
||||||
|
// REQ-UI-EXPAND-BUTTON).
|
||||||
|
HeaderBar(const GameConfig* config, const std::string& itemsIconDir,
|
||||||
|
QWidget* parent = nullptr);
|
||||||
~HeaderBar() override;
|
~HeaderBar() override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
@@ -46,6 +54,15 @@ private:
|
|||||||
// expansion cost and building block stock (REQ-UI-EXPAND-BUTTON).
|
// expansion cost and building block stock (REQ-UI-EXPAND-BUTTON).
|
||||||
void updateExpandButton();
|
void updateExpandButton();
|
||||||
|
|
||||||
|
// Refreshes the building blocks stock display from m_blocks: `Stock: <n>` with
|
||||||
|
// the building_block icon after it, or the `Stock: <n> Blocks` text fallback when
|
||||||
|
// no icon file exists (REQ-UI-BLOCKS-ICON).
|
||||||
|
void updateBlocksLabel();
|
||||||
|
|
||||||
|
// The building_block icon at the header's text height, or a null pixmap when no
|
||||||
|
// icon file exists. Loaded once via m_itemIcons on first use.
|
||||||
|
QPixmap blockIcon() const;
|
||||||
|
|
||||||
QLabel* m_timeLabel;
|
QLabel* m_timeLabel;
|
||||||
QLabel* m_blocksLabel;
|
QLabel* m_blocksLabel;
|
||||||
QLabel* m_artifactsLabel;
|
QLabel* m_artifactsLabel;
|
||||||
@@ -53,6 +70,8 @@ private:
|
|||||||
QPushButton* m_expandButton;
|
QPushButton* m_expandButton;
|
||||||
std::vector<QPushButton*> m_speedButtons;
|
std::vector<QPushButton*> m_speedButtons;
|
||||||
|
|
||||||
|
std::unique_ptr<ItemIconCache> m_itemIcons;
|
||||||
|
|
||||||
int m_blocks = 0;
|
int m_blocks = 0;
|
||||||
int m_expansionCost = 0;
|
int m_expansionCost = 0;
|
||||||
|
|
||||||
|
|||||||
48
src/ui/IconCaption.cpp
Normal file
48
src/ui/IconCaption.cpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#include "IconCaption.h"
|
||||||
|
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QRect>
|
||||||
|
|
||||||
|
QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
||||||
|
const QFont& font, const QColor& textColor)
|
||||||
|
{
|
||||||
|
const QFontMetrics metrics(font);
|
||||||
|
const int textWidth = metrics.horizontalAdvance(text);
|
||||||
|
const int gap = icon.isNull() ? 0 : 6;
|
||||||
|
|
||||||
|
// The icon is drawn at the caption's device-independent pixel size; the source
|
||||||
|
// pixmap may be larger (rasterized at a target size / device pixel ratio), so
|
||||||
|
// divide by its own dpr to get its logical size.
|
||||||
|
const qreal iconDpr = icon.isNull() ? 1.0 : icon.devicePixelRatio();
|
||||||
|
const int iconW = icon.isNull() ? 0
|
||||||
|
: static_cast<int>(icon.width() / iconDpr);
|
||||||
|
const int iconH = icon.isNull() ? 0
|
||||||
|
: static_cast<int>(icon.height() / iconDpr);
|
||||||
|
|
||||||
|
const int width = textWidth + gap + iconW;
|
||||||
|
const int height = qMax(metrics.height(), iconH);
|
||||||
|
|
||||||
|
const qreal dpr = qApp ? qApp->devicePixelRatio() : 1.0;
|
||||||
|
QPixmap pixmap(static_cast<int>(width * dpr), static_cast<int>(height * dpr));
|
||||||
|
pixmap.setDevicePixelRatio(dpr);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
painter.setFont(font);
|
||||||
|
painter.setPen(textColor);
|
||||||
|
painter.drawText(QRect(0, 0, textWidth, height),
|
||||||
|
Qt::AlignLeft | Qt::AlignVCenter, text);
|
||||||
|
|
||||||
|
if (!icon.isNull())
|
||||||
|
{
|
||||||
|
const int iconX = textWidth + gap;
|
||||||
|
const int iconY = (height - iconH) / 2;
|
||||||
|
painter.drawPixmap(QRect(iconX, iconY, iconW, iconH), icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pixmap;
|
||||||
|
}
|
||||||
16
src/ui/IconCaption.h
Normal file
16
src/ui/IconCaption.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
// Renders `text` followed by `icon` to its right, vertically centered, onto a
|
||||||
|
// transparent pixmap sized to fit both (REQ-UI-BLOCKS-ICON, REQ-UI-BUILD-COST,
|
||||||
|
// REQ-UI-EXPAND-BUTTON). Used where a caption must show an inline item icon in
|
||||||
|
// place of a trailing word — something QPushButton/QLabel cannot do natively.
|
||||||
|
//
|
||||||
|
// `textColor` and `font` come from the target widget so the caption stays
|
||||||
|
// theme-correct; the result is devicePixelRatio-aware so text and icon stay crisp.
|
||||||
|
QPixmap renderCaptionWithIcon(const QString& text, const QPixmap& icon,
|
||||||
|
const QFont& font, const QColor& textColor);
|
||||||
@@ -42,7 +42,12 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
setWindowTitle(tr("Dota Factory"));
|
setWindowTitle(tr("Dota Factory"));
|
||||||
resize(1280, 768);
|
resize(1280, 768);
|
||||||
|
|
||||||
m_headerBar = new HeaderBar(&sim->getConfig(), this);
|
// Item icons live alongside the config (a sibling of the config dir), read from
|
||||||
|
// disk at runtime like the building icons and visuals.toml (REQ-UI-ITEM-ICON).
|
||||||
|
const std::string itemsIconDir = QDir::cleanPath(
|
||||||
|
QString::fromStdString(m_configDir) + "/../icons/items").toStdString();
|
||||||
|
|
||||||
|
m_headerBar = new HeaderBar(&sim->getConfig(), itemsIconDir, this);
|
||||||
|
|
||||||
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
m_gameWorldView = new GameWorldView(sim, &sim->getConfig(), &m_visuals, m_configDir,
|
||||||
m_replay.get(), this);
|
m_replay.get(), this);
|
||||||
@@ -58,7 +63,7 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
|
|||||||
QString::fromStdString(m_configDir) + "/../icons/buildings").toStdString();
|
QString::fromStdString(m_configDir) + "/../icons/buildings").toStdString();
|
||||||
|
|
||||||
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||||
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), iconDir, m_sidePanel);
|
m_buildButtonGrid = new BuildButtonGrid(sim, &sim->getConfig(), iconDir, itemsIconDir, m_sidePanel);
|
||||||
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
|
m_blueprintPanel = new BlueprintPanel(sim, &sim->getConfig(), m_sidePanel);
|
||||||
|
|
||||||
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
sideLayout->addWidget(m_selectedBuildingPanel, 1);
|
||||||
|
|||||||
Reference in New Issue
Block a user