mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-07 10:33:24 -08:00
[Icons] Render a badge/sticker sheet for every shipped theme
The PR showed one theme's badge and sticker sheets, which cannot show that the filled treatments read on every palette. Each shipped theme and scheme now gets its own sheet: both semantic families, three glyphs, as line glyphs, stickers and badges, at 16 and 24 px, on the window and button surfaces the contrast is measured against, with the disabled fallback under each family. The generator lives in its own test file so the preview work does not have to move the branches above it.
This commit is contained in:
parent
ceef749ac3
commit
43f146e9fc
2 changed files with 246 additions and 0 deletions
|
|
@ -17,6 +17,7 @@ cockatrice_add_test(
|
||||||
theme_migration_test.cpp
|
theme_migration_test.cpp
|
||||||
theme_icon_render_test.cpp
|
theme_icon_render_test.cpp
|
||||||
icon_tint_preview_test.cpp
|
icon_tint_preview_test.cpp
|
||||||
|
icon_treatment_preview_test.cpp
|
||||||
LIBS
|
LIBS
|
||||||
libcockatrice_settings
|
libcockatrice_settings
|
||||||
libcockatrice_network
|
libcockatrice_network
|
||||||
|
|
|
||||||
245
tests/theme_migration/icon_treatment_preview_test.cpp
Normal file
245
tests/theme_migration/icon_treatment_preview_test.cpp
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
#include "interface/theme_config.h"
|
||||||
|
#include "interface/theme_icon_badge.h"
|
||||||
|
#include "interface/theme_icon_fill.h"
|
||||||
|
#include "interface/theme_icon_sticker.h"
|
||||||
|
#include "libcockatrice/utility/svg_recolor.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QStringLiteral>
|
||||||
|
#include <QSvgRenderer>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief Creates the GUI application QPixmap needs, using the offscreen plugin.
|
||||||
|
*
|
||||||
|
* The theme-migration binary is headless; creating this lazily keeps the other
|
||||||
|
* test files free of a display dependency.
|
||||||
|
*/
|
||||||
|
void ensureGuiApplication()
|
||||||
|
{
|
||||||
|
static int argc = 1;
|
||||||
|
static char name[] = "icon_treatment_preview_test";
|
||||||
|
static char *argv[] = {name, nullptr};
|
||||||
|
// Leaked on purpose: a function-local static QApplication is destroyed before Qt's
|
||||||
|
// own library statics at exit, and the network module's destructors then run
|
||||||
|
// against a torn-down QObject layer (SEGV under AddressSanitizer).
|
||||||
|
static QApplication *app = new QApplication(argc, argv);
|
||||||
|
Q_UNUSED(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString resourcesDir()
|
||||||
|
{
|
||||||
|
return QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(QStringLiteral("cockatrice/resources"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Reads a sentinel SVG from the shipped icon set. */
|
||||||
|
QByteArray readIconSvg(const char *name)
|
||||||
|
{
|
||||||
|
QFile file(QDir(resourcesDir())
|
||||||
|
.absoluteFilePath(QStringLiteral("icons/") + QString::fromLatin1(name) + QStringLiteral(".svg")));
|
||||||
|
return file.open(QFile::ReadOnly) ? file.readAll() : QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Renders a pixmap at device size and tags it with the ratio. */
|
||||||
|
template <typename Render> QPixmap renderAtDpr(const QSize &logicalSize, qreal dpr, Render &&render)
|
||||||
|
{
|
||||||
|
QPixmap pixmap = render(logicalSize * dpr);
|
||||||
|
pixmap.setDevicePixelRatio(dpr);
|
||||||
|
return pixmap;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes one badge/sticker sheet per shipped palette theme.
|
||||||
|
*
|
||||||
|
* The treatments are only useful if they read on every palette, so each sheet
|
||||||
|
* is rendered from the palette the theme actually ships: both semantic
|
||||||
|
* families, three glyphs each, as line glyphs, stickers and badges, at the two
|
||||||
|
* sizes the UI uses, on the window and button surfaces the contrast is measured
|
||||||
|
* against. A disabled row closes each family: every treatment falls back to the
|
||||||
|
* line glyph there, so an unavailable control still reads as unavailable.
|
||||||
|
*/
|
||||||
|
TEST(IconTreatmentPreview, WritesOneSheetPerShippedTheme)
|
||||||
|
{
|
||||||
|
const QString outDir = qEnvironmentVariable("COCKATRICE_ICON_PREVIEW_DIR");
|
||||||
|
if (outDir.isEmpty()) {
|
||||||
|
GTEST_SKIP() << "set COCKATRICE_ICON_PREVIEW_DIR to write the preview PNGs";
|
||||||
|
}
|
||||||
|
ensureGuiApplication();
|
||||||
|
ASSERT_TRUE(QDir().mkpath(outDir));
|
||||||
|
|
||||||
|
constexpr int MARGIN = 24;
|
||||||
|
constexpr int LABEL_WIDTH = 190;
|
||||||
|
constexpr int SLOT = 36;
|
||||||
|
constexpr int TREATMENT_WIDTH = 120;
|
||||||
|
constexpr int ROW_HEIGHT = 56;
|
||||||
|
constexpr int PANEL_TITLE = 26;
|
||||||
|
constexpr int COLUMN_HEADER = 20;
|
||||||
|
constexpr int PANEL_PADDING = 12;
|
||||||
|
constexpr qreal DPR = 2.0;
|
||||||
|
constexpr int SIZES[] = {16, 24};
|
||||||
|
constexpr const char *TREATMENTS[] = {"line", "sticker", "badge"};
|
||||||
|
|
||||||
|
struct Family
|
||||||
|
{
|
||||||
|
const char *name;
|
||||||
|
AppColor::Role role;
|
||||||
|
const char *glyphs[3];
|
||||||
|
};
|
||||||
|
const QList<Family> families = {
|
||||||
|
{"Positive", AppColor::Positive, {"increment", "ready_start", "arrow_up"}},
|
||||||
|
{"Negative", AppColor::Negative, {"decrement", "not_ready_start", "remove_row"}},
|
||||||
|
};
|
||||||
|
const QStringList themes = {
|
||||||
|
QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"),
|
||||||
|
QStringLiteral("Plasma"), QStringLiteral("VelvetMarble"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto renderLine = [](const QByteArray &svg, const QColor &ink, const QSize &size) {
|
||||||
|
QSvgRenderer renderer(resolveSvgColorSentinel(svg, ink.name(QColor::HexRgb).toLatin1()));
|
||||||
|
QPixmap pixmap(size);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
QPainter painter(&pixmap);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
renderer.render(&painter);
|
||||||
|
return pixmap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const int treatmentCount = static_cast<int>(std::size(TREATMENTS));
|
||||||
|
const int panelWidth = LABEL_WIDTH + treatmentCount * TREATMENT_WIDTH + PANEL_PADDING;
|
||||||
|
const int panelHeight =
|
||||||
|
PANEL_TITLE + COLUMN_HEADER + static_cast<int>(families.size()) * 2 * ROW_HEIGHT + PANEL_PADDING;
|
||||||
|
const int logicalWidth = 2 * MARGIN + panelWidth;
|
||||||
|
const int logicalHeight = MARGIN + 30 + 2 * (panelHeight + 24) + MARGIN;
|
||||||
|
|
||||||
|
for (const QString &theme : themes) {
|
||||||
|
for (const bool dark : {false, true}) {
|
||||||
|
const QString file =
|
||||||
|
QDir(QStringLiteral(COCKATRICE_SOURCE_DIR))
|
||||||
|
.absoluteFilePath(QStringLiteral("cockatrice/themes/%1/palette-default-%2.toml")
|
||||||
|
.arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
||||||
|
const PaletteConfig config = PaletteConfig::fromFile(file);
|
||||||
|
ASSERT_TRUE(config.hasPalette()) << theme.toStdString();
|
||||||
|
|
||||||
|
const QPalette palette = config.apply(QPalette());
|
||||||
|
const QColor window = palette.color(QPalette::Active, QPalette::Window);
|
||||||
|
const QColor button = palette.color(QPalette::Active, QPalette::Button);
|
||||||
|
const QColor text = palette.color(QPalette::Active, QPalette::WindowText);
|
||||||
|
const QColor disabled = palette.color(QPalette::Disabled, QPalette::WindowText);
|
||||||
|
|
||||||
|
QImage image(QSize(qRound(logicalWidth * DPR), qRound(logicalHeight * DPR)),
|
||||||
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
|
image.fill(window);
|
||||||
|
QPainter painter(&image);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
painter.scale(DPR, DPR);
|
||||||
|
|
||||||
|
painter.setPen(text);
|
||||||
|
painter.setFont(QFont(QStringLiteral("Sans"), 10, QFont::DemiBold));
|
||||||
|
painter.drawText(QPointF(MARGIN, MARGIN),
|
||||||
|
QStringLiteral("%1 — %2 scheme, treatments across glyphs")
|
||||||
|
.arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
||||||
|
|
||||||
|
struct Surface
|
||||||
|
{
|
||||||
|
const char *name;
|
||||||
|
QColor background;
|
||||||
|
};
|
||||||
|
const QList<Surface> surfaces = {
|
||||||
|
{"Window surface", window},
|
||||||
|
{"Button surface", button},
|
||||||
|
};
|
||||||
|
|
||||||
|
qreal panelTop = MARGIN + 30;
|
||||||
|
for (const Surface &surface : surfaces) {
|
||||||
|
painter.setPen(Qt::NoPen);
|
||||||
|
painter.setBrush(surface.background);
|
||||||
|
painter.drawRoundedRect(QRectF(MARGIN, panelTop, panelWidth, panelHeight), 8, 8);
|
||||||
|
|
||||||
|
const qreal contentLeft = MARGIN + PANEL_PADDING;
|
||||||
|
painter.setPen(text);
|
||||||
|
painter.setFont(QFont(QStringLiteral("Sans"), 9, QFont::DemiBold));
|
||||||
|
painter.drawText(QPointF(contentLeft, panelTop + 18), QString::fromLatin1(surface.name));
|
||||||
|
|
||||||
|
painter.setFont(QFont(QStringLiteral("Sans"), 8));
|
||||||
|
for (int treatment = 0; treatment < treatmentCount; ++treatment) {
|
||||||
|
const qreal x = contentLeft + LABEL_WIDTH + static_cast<qreal>(treatment) * TREATMENT_WIDTH;
|
||||||
|
painter.drawText(QPointF(x, panelTop + PANEL_TITLE + 14),
|
||||||
|
QString::fromLatin1(TREATMENTS[treatment]) + QStringLiteral(" 16 / 24"));
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal y = panelTop + PANEL_TITLE + COLUMN_HEADER;
|
||||||
|
for (const Family &family : families) {
|
||||||
|
const QColor raw = config.appColors.value(family.role, defaultAppColor(family.role, dark));
|
||||||
|
ASSERT_TRUE(raw.isValid()) << theme.toStdString() << " " << family.name;
|
||||||
|
const QColor tone = ThemeIconFill::saturated(raw);
|
||||||
|
|
||||||
|
for (int state = 0; state < 2; ++state) {
|
||||||
|
const bool isDisabled = state == 1;
|
||||||
|
const QColor ink = isDisabled ? disabled : tone;
|
||||||
|
|
||||||
|
painter.setPen(text);
|
||||||
|
painter.setFont(QFont(QStringLiteral("Sans"), 8, QFont::DemiBold));
|
||||||
|
painter.drawText(QPointF(contentLeft, y + 14),
|
||||||
|
QStringLiteral("%1%2").arg(
|
||||||
|
QString::fromLatin1(family.name),
|
||||||
|
isDisabled ? QStringLiteral(" / disabled") : QString()));
|
||||||
|
painter.setFont(QFont(QStringLiteral("Mono"), 7));
|
||||||
|
painter.drawText(QPointF(contentLeft, y + 28), ink.name(QColor::HexRgb));
|
||||||
|
|
||||||
|
for (int treatment = 0; treatment < treatmentCount; ++treatment) {
|
||||||
|
const qreal cellX =
|
||||||
|
contentLeft + LABEL_WIDTH + static_cast<qreal>(treatment) * TREATMENT_WIDTH;
|
||||||
|
for (std::size_t sizeIndex = 0; sizeIndex < std::size(SIZES); ++sizeIndex) {
|
||||||
|
const int side = SIZES[sizeIndex];
|
||||||
|
const QSize size(side, side);
|
||||||
|
const qreal rowTop = y + (sizeIndex == 0 ? 4 : 28);
|
||||||
|
for (int glyphIndex = 0; glyphIndex < 3; ++glyphIndex) {
|
||||||
|
const QByteArray svg = readIconSvg(family.glyphs[glyphIndex]);
|
||||||
|
ASSERT_FALSE(svg.isEmpty()) << family.glyphs[glyphIndex];
|
||||||
|
const QPixmap pixmap = renderAtDpr(size, DPR, [&](const QSize &pixelSize) {
|
||||||
|
if (isDisabled || treatment == 0) {
|
||||||
|
return renderLine(svg, ink, pixelSize);
|
||||||
|
}
|
||||||
|
if (treatment == 1) {
|
||||||
|
return ThemeIconSticker::renderSticker(svg, ink, pixelSize);
|
||||||
|
}
|
||||||
|
return ThemeIconBadge::renderBadge(svg, ink, pixelSize);
|
||||||
|
});
|
||||||
|
const qreal x = cellX + 6 + static_cast<qreal>(glyphIndex) * SLOT +
|
||||||
|
(SLOT - side) / 2.0;
|
||||||
|
painter.drawPixmap(QPointF(x, rowTop + (SLOT - side) / 2.0), pixmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y += ROW_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panelTop += panelHeight + 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.end();
|
||||||
|
const QString path = QDir(outDir).absoluteFilePath(
|
||||||
|
QStringLiteral("treatment-preview-%1-%2.png")
|
||||||
|
.arg(theme.toLower(), dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
||||||
|
ASSERT_TRUE(image.save(path)) << path.toStdString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue