[Icons] Render the emphasis sheet for every shipped theme

The preview only rendered one theme, so a reviewer could not see that the
emphasis levels behave the same way across Fabric, Fusion, Leather,
Plasma and VelvetMarble. It now writes one sheet per theme and scheme:
the neutral, positive and negative families at all three levels, three
glyphs per family, as line, sticker and badge at 16 and 24 px, on the
window and button surfaces.
This commit is contained in:
Lukas Brübach 2026-11-07 01:45:56 +01:00
parent 9ae7c6678c
commit dd5048e32c

View file

@ -1459,11 +1459,13 @@ TEST_F(ThemeIconRenderTest, SelectedStillFallsBackToHighlightedText)
} }
/** /**
* @brief Writes the emphasis ladder sheet when asked; skipped otherwise. * @brief Writes the emphasis ladder sheets for every shipped palette theme.
* *
* One strip per scheme: a mock toolbar with body text, a quiet labelling glyph, * One sheet per theme and scheme: the neutral, positive and negative families at
* secondary actions, the primary action, and the primary action on an * their quiet, secondary and primary levels, each as a line glyph, a sticker and
* accent-filled button. The point is the hierarchy at a glance, not the glyph. * a badge, three glyphs per family, at 16 and 24 px, on the window and button
* surfaces. The tone is the one the emphasis rule resolves for that surface, so
* the sheet is what the engine paints rather than a hand-picked sample.
*/ */
TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested) TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested)
{ {
@ -1474,80 +1476,164 @@ TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested)
ASSERT_TRUE(QDir().mkpath(outDir)); ASSERT_TRUE(QDir().mkpath(outDir));
constexpr int MARGIN = 24; constexpr int MARGIN = 24;
constexpr int ROW_HEIGHT = 44; constexpr int LABEL_WIDTH = 230;
constexpr int ICON_SIZE = 20; constexpr int CELL_WIDTH = 166;
constexpr int ROW_HEIGHT = 58;
constexpr int PANEL_TITLE = 26;
constexpr int COLUMN_HEADER = 20;
constexpr int PANEL_PADDING = 12;
constexpr int GLYPH_SLOT = 32;
constexpr qreal DPR = 2.0; constexpr qreal DPR = 2.0;
constexpr int WIDTH = 760; constexpr int SIZES[] = {16, 24};
constexpr const char *TREATMENTS[] = {"line", "sticker", "badge"};
constexpr const char *LEVEL_NAMES[] = {"Quiet", "Secondary", "Primary"};
constexpr IconEmphasis LEVELS[] = {IconEmphasis::Quiet, IconEmphasis::Secondary, IconEmphasis::Primary};
struct Sample struct Family
{ {
const char *glyph; const char *name;
const char *label; const char *glyphs[3];
IconTint tint; AppColor::Role role;
bool neutral;
};
const QList<Family> families = {
{"Neutral", {"settings", "info", "player"}, AppColor::Positive, true},
{"Positive", {"increment", "ready_start", "arrow_up"}, AppColor::Positive, false},
{"Negative", {"decrement", "not_ready_start", "remove_row"}, AppColor::Negative, false},
};
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 panelWidth = LABEL_WIDTH + static_cast<int>(std::size(TREATMENTS)) * CELL_WIDTH + PANEL_PADDING;
const int panelHeight =
PANEL_TITLE + COLUMN_HEADER + static_cast<int>(families.size()) * 3 * 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}) { for (const bool dark : {false, true}) {
const InkPalette palette = shippedPalette(QStringLiteral("Fabric"), dark); const InkPalette palette = shippedPalette(theme, dark);
ASSERT_TRUE(palette.text.isValid()); ASSERT_TRUE(palette.text.isValid()) << theme.toStdString();
ASSERT_TRUE(applyShippedPalette(QStringLiteral("Fabric"), dark));
const QList<Sample> samples = { // The emphasis rule reads the live palette, so apply the theme
{nullptr, "body text for comparison", {}}, // before resolving tones for the sheet.
{"search", "quiet hint", IconTint::quiet()}, ASSERT_TRUE(applyShippedPalette(theme, dark));
{"settings", "secondary", IconTint()},
{"arrow_right", "primary", IconTint::primary()}, const auto toneFor = [&](const Family &family, int level) {
{"increment", "primary, positive", IconTint(AppColor::Positive, IconEmphasis::Primary)}, QColor base;
if (family.neutral) {
base = level == 2 ? palette.accent
: ThemeIconInk::glyphInk(palette.text, palette.window, palette.accent);
} else {
base = ThemeIconFill::saturated(defaultAppColor(family.role, dark));
}
return ThemeIconInk::forEmphasis(base, palette.window, LEVELS[level]);
}; };
const int logicalHeight = MARGIN + 28 + static_cast<int>(samples.size()) * ROW_HEIGHT + ROW_HEIGHT + MARGIN; QImage image(QSize(qRound(logicalWidth * DPR), qRound(logicalHeight * DPR)),
QImage image(QSize(qRound(WIDTH * DPR), qRound(logicalHeight * DPR)), QImage::Format_ARGB32_Premultiplied); QImage::Format_ARGB32_Premultiplied);
image.fill(palette.window); image.fill(palette.window);
QPainter painter(&image); QPainter painter(&image);
painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true); painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
painter.scale(DPR, DPR); painter.scale(DPR, DPR);
qreal y = MARGIN;
painter.setPen(palette.text); painter.setPen(palette.text);
painter.setFont(QFont(QStringLiteral("Sans"), 10, QFont::DemiBold)); painter.setFont(QFont(QStringLiteral("Sans"), 10, QFont::DemiBold));
painter.drawText(QPointF(MARGIN, y), (dark ? QStringLiteral("Dark scheme") : QStringLiteral("Light scheme")) + painter.drawText(QPointF(MARGIN, MARGIN),
QStringLiteral(" — emphasis ladder")); QStringLiteral("%1 — %2 scheme, emphasis across treatments")
y += 28; .arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
for (const Sample &sample : samples) { struct Surface
if (sample.glyph != nullptr) { {
const QByteArray svg = readIconSvg(sample.glyph); const char *name;
ASSERT_FALSE(svg.isEmpty()) << sample.glyph; QColor background;
const QColor ink = sample.tint.resolve(QIcon::Normal); QColor text;
QSvgRenderer renderer(resolveSvgColorSentinel(svg, ink.name(QColor::HexRgb).toLatin1())); };
QPixmap glyph(QSize(ICON_SIZE, ICON_SIZE)); const QList<Surface> surfaces = {
glyph.fill(Qt::transparent); {"Window surface", palette.window, palette.text},
QPainter glyphPainter(&glyph); {"Button surface", palette.button, palette.text},
glyphPainter.setRenderHint(QPainter::Antialiasing, true); };
renderer.render(&glyphPainter);
glyphPainter.end(); qreal panelTop = MARGIN + 30;
painter.drawPixmap(QPointF(MARGIN, y - ICON_SIZE + 4), glyph); 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(surface.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 (std::size_t treatment = 0; treatment < std::size(TREATMENTS); ++treatment) {
const qreal x = contentLeft + LABEL_WIDTH + static_cast<qreal>(treatment) * CELL_WIDTH;
painter.drawText(QPointF(x, panelTop + PANEL_TITLE + 14),
QString::fromLatin1(TREATMENTS[treatment]) + QStringLiteral(" 16 / 24"));
} }
painter.setPen(palette.text); qreal y = panelTop + PANEL_TITLE + COLUMN_HEADER;
painter.setFont(QFont(QStringLiteral("Sans"), 10)); for (const Family &family : families) {
painter.drawText(QPointF(MARGIN + ICON_SIZE + 16, y), QString::fromLatin1(sample.label)); for (int level = 0; level < 3; ++level) {
const QColor ink = toneFor(family, level);
ASSERT_TRUE(ink.isValid())
<< theme.toStdString() << " " << family.name << " " << LEVEL_NAMES[level];
painter.setPen(surface.text);
painter.setFont(QFont(QStringLiteral("Sans"), 8, QFont::DemiBold));
painter.drawText(QPointF(contentLeft, y + 14),
QStringLiteral("%1 / %2").arg(QString::fromLatin1(family.name),
QString::fromLatin1(LEVEL_NAMES[level])));
painter.setFont(QFont(QStringLiteral("Mono"), 7));
painter.drawText(QPointF(contentLeft, y + 28), ink.name(QColor::HexRgb));
for (std::size_t treatment = 0; treatment < std::size(TREATMENTS); ++treatment) {
const qreal cellX = contentLeft + LABEL_WIDTH + static_cast<qreal>(treatment) * CELL_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 (std::size_t glyphIndex = 0; glyphIndex < std::size(family.glyphs); ++glyphIndex) {
const QByteArray svg = readIconSvg(family.glyphs[glyphIndex]);
ASSERT_FALSE(svg.isEmpty()) << family.glyphs[glyphIndex];
QPixmap pixmap;
if (treatment == 0) {
pixmap = renderLine(svg, ink, size);
} else if (treatment == 1) {
pixmap = ThemeIconSticker::renderSticker(svg, ink, size);
} else {
pixmap = ThemeIconBadge::renderBadge(svg, ink, size);
}
const qreal x = cellX + 6 + static_cast<qreal>(glyphIndex) * GLYPH_SLOT +
(GLYPH_SLOT - side) / 2.0;
painter.drawPixmap(QPointF(x, rowTop + (GLYPH_SLOT - side) / 2.0), pixmap);
}
}
}
y += ROW_HEIGHT; y += ROW_HEIGHT;
} }
}
panelTop += panelHeight + 24;
}
// The primary action on an accent-filled button: the container carries
// the emphasis, and the glyph has to switch to the on-accent ink.
const QRectF button(MARGIN, y - 28, 220, 34);
painter.setBrush(palette.accent);
painter.setPen(Qt::NoPen);
painter.drawRoundedRect(button, 6, 6);
painter.setPen(palette.text);
painter.setFont(QFont(QStringLiteral("Sans"), 10));
painter.drawText(QPointF(MARGIN + 12, y - 6), QStringLiteral("primary button: accent fill + on-accent glyph"));
painter.end(); painter.end();
const QString path = QDir(outDir).absoluteFilePath(
const QString path = QDir(outDir).absoluteFilePath(dark ? QStringLiteral("emphasis-preview-dark.png") QStringLiteral("emphasis-preview-%1-%2.png")
: QStringLiteral("emphasis-preview-light.png")); .arg(theme.toLower(), dark ? QStringLiteral("dark") : QStringLiteral("light")));
ASSERT_TRUE(image.save(path)) << path.toStdString(); ASSERT_TRUE(image.save(path)) << path.toStdString();
} }
}
} }