mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-11 04:23:29 -08:00
[Icons] Keep the authored emphasis in filled treatments
The saturation floor was added while the emphasis levels were still computed: it rescued the pale dark-scheme tones once a whole shape carried them. With the levels authored, the floor overrides the author instead: a quiet level jumped to the secondary saturation as soon as a sticker or a badge painted it, so the ladder only existed for line glyphs. The authored tone is now the top of the fill gradient and the treatment only darkens the bottom for depth, so every treatment renders exactly the level the palette stores. The badge glyph ink no longer guesses the scheme from the tone's value: an authored quiet level is a dark color on the dark scheme too, so the scheme is passed in from the surface palette. The preview sheet renders every shipped palette theme, three glyphs per family, and each row prints the authored tone and the darker bottom.
This commit is contained in:
parent
03a4a33ba5
commit
2e4731d3a5
8 changed files with 346 additions and 171 deletions
|
|
@ -30,6 +30,49 @@
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @brief The role an emphasis level selects inside a base role's family.
|
||||||
|
*
|
||||||
|
* The theme authors all three levels, so emphasis is a lookup instead of a
|
||||||
|
* color computation. Naming a variant (`IconStrong`, `PositiveSoft`) is an
|
||||||
|
* explicit request and keeps that role at every emphasis; only the base roles
|
||||||
|
* take part in the mapping. Accent roles have no neutral middle member, so they
|
||||||
|
* are always used as named.
|
||||||
|
*/
|
||||||
|
AppColor::Role emphasisRole(AppColor::Role role, IconEmphasis emphasis)
|
||||||
|
{
|
||||||
|
switch (role) {
|
||||||
|
case AppColor::Icon:
|
||||||
|
return emphasis == IconEmphasis::Primary ? AppColor::IconStrong
|
||||||
|
: emphasis == IconEmphasis::Quiet ? AppColor::IconSoft
|
||||||
|
: AppColor::Icon;
|
||||||
|
case AppColor::Positive:
|
||||||
|
return emphasis == IconEmphasis::Primary ? AppColor::PositiveStrong
|
||||||
|
: emphasis == IconEmphasis::Quiet ? AppColor::PositiveSoft
|
||||||
|
: AppColor::Positive;
|
||||||
|
case AppColor::Negative:
|
||||||
|
return emphasis == IconEmphasis::Primary ? AppColor::NegativeStrong
|
||||||
|
: emphasis == IconEmphasis::Quiet ? AppColor::NegativeSoft
|
||||||
|
: AppColor::Negative;
|
||||||
|
default:
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The scheme the active application palette is painting for.
|
||||||
|
*
|
||||||
|
* The fill needs the surface scheme to choose the badge glyph ink, and it cannot
|
||||||
|
* read it from the tone: an authored quiet level is a dark color on the dark
|
||||||
|
* scheme too.
|
||||||
|
*/
|
||||||
|
ThemeIconFill::Scheme activeScheme()
|
||||||
|
{
|
||||||
|
const QPalette &palette = qApp->palette();
|
||||||
|
return palette.color(QPalette::Active, QPalette::Window).lightness() < 128 ? ThemeIconFill::Scheme::Dark
|
||||||
|
: ThemeIconFill::Scheme::Light;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reads an SVG through the `theme:` search path.
|
* @brief Reads an SVG through the `theme:` search path.
|
||||||
*/
|
*/
|
||||||
|
|
@ -264,7 +307,7 @@ private:
|
||||||
QByteArray variant;
|
QByteArray variant;
|
||||||
if (badge) {
|
if (badge) {
|
||||||
variant = QByteArrayLiteral("B|") + tone.name(QColor::HexRgb).toLatin1() + '|' +
|
variant = QByteArrayLiteral("B|") + tone.name(QColor::HexRgb).toLatin1() + '|' +
|
||||||
ThemeIconBadge::glyphColor(tone).name(QColor::HexRgb).toLatin1();
|
ThemeIconBadge::glyphColor(tone, activeScheme()).name(QColor::HexRgb).toLatin1();
|
||||||
} else if (sticker) {
|
} else if (sticker) {
|
||||||
variant = QByteArrayLiteral("S|") + tone.name(QColor::HexRgb).toLatin1();
|
variant = QByteArrayLiteral("S|") + tone.name(QColor::HexRgb).toLatin1();
|
||||||
} else if (resolvedHasSentinel) {
|
} else if (resolvedHasSentinel) {
|
||||||
|
|
@ -282,7 +325,7 @@ private:
|
||||||
if (badge) {
|
if (badge) {
|
||||||
// Geometry is computed from the device-pixel size, and the ratio is
|
// Geometry is computed from the device-pixel size, and the ratio is
|
||||||
// attached below like every other path.
|
// attached below like every other path.
|
||||||
pixmap = ThemeIconBadge::renderBadge(resolvedSvg, tone, pixelSize);
|
pixmap = ThemeIconBadge::renderBadge(resolvedSvg, tone, pixelSize, activeScheme());
|
||||||
} else if (sticker) {
|
} else if (sticker) {
|
||||||
pixmap = ThemeIconSticker::renderSticker(resolvedSvg, tone, pixelSize);
|
pixmap = ThemeIconSticker::renderSticker(resolvedSvg, tone, pixelSize);
|
||||||
} else if (resolvedHasSentinel) {
|
} else if (resolvedHasSentinel) {
|
||||||
|
|
@ -318,34 +361,6 @@ private:
|
||||||
mutable bool hasIntrinsicSize = false;
|
mutable bool hasIntrinsicSize = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief The role an emphasis level selects inside a base role's family.
|
|
||||||
*
|
|
||||||
* The theme authors all three levels, so emphasis is a lookup instead of a
|
|
||||||
* color computation. Naming a variant (`IconStrong`, `PositiveSoft`) is an
|
|
||||||
* explicit request and keeps that role at every emphasis; only the base roles
|
|
||||||
* take part in the mapping. Accent roles have no neutral middle member, so they
|
|
||||||
* are always used as named.
|
|
||||||
*/
|
|
||||||
AppColor::Role emphasisRole(AppColor::Role role, IconEmphasis emphasis)
|
|
||||||
{
|
|
||||||
switch (role) {
|
|
||||||
case AppColor::Icon:
|
|
||||||
return emphasis == IconEmphasis::Primary ? AppColor::IconStrong
|
|
||||||
: emphasis == IconEmphasis::Quiet ? AppColor::IconSoft
|
|
||||||
: AppColor::Icon;
|
|
||||||
case AppColor::Positive:
|
|
||||||
return emphasis == IconEmphasis::Primary ? AppColor::PositiveStrong
|
|
||||||
: emphasis == IconEmphasis::Quiet ? AppColor::PositiveSoft
|
|
||||||
: AppColor::Positive;
|
|
||||||
case AppColor::Negative:
|
|
||||||
return emphasis == IconEmphasis::Primary ? AppColor::NegativeStrong
|
|
||||||
: emphasis == IconEmphasis::Quiet ? AppColor::NegativeSoft
|
|
||||||
: AppColor::Negative;
|
|
||||||
default:
|
|
||||||
return role;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
IconTint::IconTint(QPalette::ColorRole role, IconEmphasis _emphasis) : paletteRole(role), emphasis(_emphasis)
|
IconTint::IconTint(QPalette::ColorRole role, IconEmphasis _emphasis) : paletteRole(role), emphasis(_emphasis)
|
||||||
|
|
@ -378,9 +393,9 @@ QColor IconTint::resolve(QIcon::Mode mode) const
|
||||||
return palette.color(QPalette::Disabled, QPalette::WindowText);
|
return palette.color(QPalette::Disabled, QPalette::WindowText);
|
||||||
}
|
}
|
||||||
// The theme authors one color per emphasis level, so the paint path
|
// The theme authors one color per emphasis level, so the paint path
|
||||||
// uses the resolved role verbatim. The filled treatments apply their
|
// uses the resolved role verbatim. The filled treatments only shape the
|
||||||
// own saturation floor when a disc or a fill needs mass; a line glyph
|
// tone's value to give a disc or a fill depth; hue and saturation stay
|
||||||
// keeps the color the author picked.
|
// the author's.
|
||||||
const QColor color = themeManager->appColor(emphasisRole(appRole, emphasis));
|
const QColor color = themeManager->appColor(emphasisRole(appRole, emphasis));
|
||||||
if (mode != QIcon::Selected) {
|
if (mode != QIcon::Selected) {
|
||||||
return color;
|
return color;
|
||||||
|
|
@ -428,7 +443,7 @@ QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, IconTint tint, const
|
||||||
if (canvas.isEmpty()) {
|
if (canvas.isEmpty()) {
|
||||||
canvas = QSvgRenderer(svg).defaultSize();
|
canvas = QSvgRenderer(svg).defaultSize();
|
||||||
}
|
}
|
||||||
return style == IconStyle::Badge ? ThemeIconBadge::renderBadge(svg, tone, canvas)
|
return style == IconStyle::Badge ? ThemeIconBadge::renderBadge(svg, tone, canvas, activeScheme())
|
||||||
: ThemeIconSticker::renderSticker(svg, tone, canvas);
|
: ThemeIconSticker::renderSticker(svg, tone, canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,8 @@ public:
|
||||||
*
|
*
|
||||||
* Application colors are returned verbatim: the theme author stores one
|
* Application colors are returned verbatim: the theme author stores one
|
||||||
* value per emphasis level, so the paint path does not rewrite it. The
|
* value per emphasis level, so the paint path does not rewrite it. The
|
||||||
* filled treatments apply their own saturation floor to a fill they have to
|
* filled treatments only shape that tone's value to give a shape depth; the
|
||||||
* carry; a line glyph keeps the author's color.
|
* hue and saturation stay the author's.
|
||||||
*
|
*
|
||||||
* Neutral label roles (WindowText, Text) resolve to icon ink rather than the
|
* Neutral label roles (WindowText, Text) resolve to icon ink rather than the
|
||||||
* raw palette color: ThemeIconInk::glyphInk derives it from the accent and
|
* raw palette color: ThemeIconInk::glyphInk derives it from the accent and
|
||||||
|
|
|
||||||
|
|
@ -46,16 +46,16 @@ DiscFill discFill(const QColor &tone)
|
||||||
|
|
||||||
namespace ThemeIconBadge
|
namespace ThemeIconBadge
|
||||||
{
|
{
|
||||||
QColor glyphColor(const QColor &tone)
|
QColor glyphColor(const QColor &tone, ThemeIconFill::Scheme scheme)
|
||||||
{
|
{
|
||||||
// The badge carries the ink the pre-sticker badge did: white on the light
|
// The badge carries the ink the pre-sticker badge did: white on the light
|
||||||
// scheme, and the tone darkened to an outline-dark ink on the dark scheme.
|
// scheme, and the tone darkened to an outline-dark ink on the dark scheme.
|
||||||
// The shared saturated disc is light enough there that white sits near
|
// The lifted disc is light enough there that white sits near 2.7:1 at the
|
||||||
// 2.7:1 at the top, so the dark ink is the deliberate choice rather than a
|
// top, so the dark ink is the deliberate choice rather than a contrast
|
||||||
// contrast measurement that a palette tweak could flip. Deriving it from
|
// measurement that a palette tweak could flip. Deriving it from the tone
|
||||||
// the tone keeps themes in control.
|
// keeps themes in control, and the scheme is explicit because an authored
|
||||||
return ThemeIconFill::isDarkSchemeTone(tone) ? ThemeIconFill::saturated(tone).darker(GLYPH_DARKER)
|
// quiet level is a dark tone on the dark scheme too.
|
||||||
: QColor(Qt::white);
|
return scheme == ThemeIconFill::Scheme::Dark ? tone.darker(GLYPH_DARKER) : QColor(Qt::white);
|
||||||
}
|
}
|
||||||
|
|
||||||
QSize glyphSize(const QSize &pixelSize)
|
QSize glyphSize(const QSize &pixelSize)
|
||||||
|
|
@ -70,7 +70,7 @@ QSize glyphSize(const QSize &pixelSize)
|
||||||
return QSize(glyphSide, glyphSide);
|
return QSize(glyphSide, glyphSide);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap renderBadge(const QByteArray &svg, const QColor &tone, const QSize &pixelSize)
|
QPixmap renderBadge(const QByteArray &svg, const QColor &tone, const QSize &pixelSize, ThemeIconFill::Scheme scheme)
|
||||||
{
|
{
|
||||||
if (svg.isEmpty() || !tone.isValid() || pixelSize.isEmpty()) {
|
if (svg.isEmpty() || !tone.isValid() || pixelSize.isEmpty()) {
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -100,12 +100,12 @@ QPixmap renderBadge(const QByteArray &svg, const QColor &tone, const QSize &pixe
|
||||||
// The ring closes the disc: its outer edge touches the canvas, its inner
|
// The ring closes the disc: its outer edge touches the canvas, its inner
|
||||||
// edge is the disc boundary, so the two meet without a seam.
|
// edge is the disc boundary, so the two meet without a seam.
|
||||||
painter.setBrush(Qt::NoBrush);
|
painter.setBrush(Qt::NoBrush);
|
||||||
painter.setPen(QPen(ThemeIconFill::saturated(tone).darker(RING_DARKER), ringWidth));
|
painter.setPen(QPen(tone.darker(RING_DARKER), ringWidth));
|
||||||
const qreal ringRadius = (side - ringWidth) / 2.0;
|
const qreal ringRadius = (side - ringWidth) / 2.0;
|
||||||
painter.drawEllipse(discRect.center(), ringRadius, ringRadius);
|
painter.drawEllipse(discRect.center(), ringRadius, ringRadius);
|
||||||
|
|
||||||
const QByteArray glyphSvg = resolveSvgStrokeWidth(
|
const QByteArray glyphSvg = resolveSvgStrokeWidth(
|
||||||
resolveSvgColorSentinel(svg, glyphColor(tone).name(QColor::HexRgb).toLatin1()), GLYPH_STROKE_WIDTH);
|
resolveSvgColorSentinel(svg, glyphColor(tone, scheme).name(QColor::HexRgb).toLatin1()), GLYPH_STROKE_WIDTH);
|
||||||
QSvgRenderer renderer(glyphSvg);
|
QSvgRenderer renderer(glyphSvg);
|
||||||
if (renderer.isValid()) {
|
if (renderer.isValid()) {
|
||||||
const QSize size = glyphSize(pixelSize);
|
const QSize size = glyphSize(pixelSize);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
#ifndef THEME_ICON_BADGE_H
|
#ifndef THEME_ICON_BADGE_H
|
||||||
#define THEME_ICON_BADGE_H
|
#define THEME_ICON_BADGE_H
|
||||||
|
|
||||||
|
#include "theme_icon_fill.h"
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
|
@ -34,13 +36,17 @@ namespace ThemeIconBadge
|
||||||
/**
|
/**
|
||||||
* @brief Glyph color for a badge disc.
|
* @brief Glyph color for a badge disc.
|
||||||
*
|
*
|
||||||
* White on the light scheme, the saturated tone darkened to an outline-dark ink
|
* White on the light scheme, the authored tone darkened to an outline-dark ink
|
||||||
* on the dark scheme. The dark-scheme tones fill the disc light enough that
|
* on the dark scheme. The dark-scheme tones fill the disc light enough that
|
||||||
* white would sit near 2.7:1 at its top, so that scheme keeps the darkened ink
|
* white would sit near 2.7:1 at its top, so that scheme keeps the darkened ink
|
||||||
* the badge had before the filled treatments shared a fill. The dark ink is
|
* the badge had before the filled treatments shared a fill. The scheme is passed
|
||||||
* derived from the tone, so the theme keeps control of both ends.
|
* in rather than inferred from the tone, because an authored quiet level is a
|
||||||
|
* dark color on the dark scheme as well.
|
||||||
|
*
|
||||||
|
* @param tone Disc color, already resolved for the icon mode.
|
||||||
|
* @param scheme Scheme the badge is painted for.
|
||||||
*/
|
*/
|
||||||
QColor glyphColor(const QColor &tone);
|
QColor glyphColor(const QColor &tone, ThemeIconFill::Scheme scheme);
|
||||||
|
|
||||||
/** @brief Size of the engraved glyph for a badge of @p pixelSize, in device pixels. */
|
/** @brief Size of the engraved glyph for a badge of @p pixelSize, in device pixels. */
|
||||||
QSize glyphSize(const QSize &pixelSize);
|
QSize glyphSize(const QSize &pixelSize);
|
||||||
|
|
@ -63,9 +69,10 @@ QSize glyphSize(const QSize &pixelSize);
|
||||||
* @param tone Disc color, already resolved for the icon mode.
|
* @param tone Disc color, already resolved for the icon mode.
|
||||||
* @param pixelSize Canvas size in device pixels; a non-square canvas centers
|
* @param pixelSize Canvas size in device pixels; a non-square canvas centers
|
||||||
* the badge and leaves the remaining area transparent.
|
* the badge and leaves the remaining area transparent.
|
||||||
|
* @param scheme Scheme the badge is painted for; selects the glyph ink.
|
||||||
* @return The composited badge, or a null pixmap for empty or invalid input.
|
* @return The composited badge, or a null pixmap for empty or invalid input.
|
||||||
*/
|
*/
|
||||||
QPixmap renderBadge(const QByteArray &svg, const QColor &tone, const QSize &pixelSize);
|
QPixmap renderBadge(const QByteArray &svg, const QColor &tone, const QSize &pixelSize, ThemeIconFill::Scheme scheme);
|
||||||
} // namespace ThemeIconBadge
|
} // namespace ThemeIconBadge
|
||||||
|
|
||||||
#endif // THEME_ICON_BADGE_H
|
#endif // THEME_ICON_BADGE_H
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,22 @@
|
||||||
#include "theme_icon_fill.h"
|
#include "theme_icon_fill.h"
|
||||||
|
|
||||||
#include <QtGlobal>
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// Semantic tones are tuned for text. The dark-scheme ones are deliberately
|
// The bottom end of every filled treatment. The authored tone is the top, so a
|
||||||
// desaturated, which reads washed out when the same color fills a whole shape,
|
// single darkening keeps the emphasis level the palette author picked and still
|
||||||
// so the fill gives its tone a saturation floor.
|
// gives the shape the lit-from-above depth the pre-#7595 art had.
|
||||||
constexpr int MIN_SATURATION = 170;
|
|
||||||
// A tone this light belongs to the dark scheme; filling a glyph with it at full
|
|
||||||
// value is what looks pale, so its gradient is pulled down instead of up.
|
|
||||||
constexpr int LIGHT_TONE_VALUE = 200;
|
|
||||||
constexpr int MIN_VALUE = 200;
|
|
||||||
constexpr int GRADIENT_BOTTOM_DARKER = 140;
|
constexpr int GRADIENT_BOTTOM_DARKER = 140;
|
||||||
constexpr int DARK_SCHEME_TOP_DARKER = 120;
|
|
||||||
constexpr int DARK_SCHEME_BOTTOM_DARKER = 170;
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
/** @brief Raises a tone's saturation, and optionally its value, for a filled shape. */
|
|
||||||
QColor boost(const QColor &color, int minValue)
|
|
||||||
{
|
|
||||||
if (color.hue() < 0 || (color.hsvSaturation() >= MIN_SATURATION && color.value() >= minValue)) {
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
QColor result = color;
|
|
||||||
result.setHsv(color.hue(), qMax(color.hsvSaturation(), MIN_SATURATION), qMax(color.value(), minValue));
|
|
||||||
// Return RGB so callers comparing or caching the tone do not see a spec
|
|
||||||
// change for an unchanged color.
|
|
||||||
return result.toRgb();
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace ThemeIconFill
|
namespace ThemeIconFill
|
||||||
{
|
{
|
||||||
QColor saturated(const QColor &tone)
|
|
||||||
{
|
|
||||||
return boost(tone, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isDarkSchemeTone(const QColor &tone)
|
|
||||||
{
|
|
||||||
return tone.value() >= LIGHT_TONE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
QColor topColor(const QColor &tone)
|
QColor topColor(const QColor &tone)
|
||||||
{
|
{
|
||||||
if (isDarkSchemeTone(tone)) {
|
return tone;
|
||||||
return boost(tone, 0).darker(DARK_SCHEME_TOP_DARKER);
|
|
||||||
}
|
|
||||||
return boost(tone, MIN_VALUE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor bottomColor(const QColor &tone)
|
QColor bottomColor(const QColor &tone)
|
||||||
{
|
{
|
||||||
const int darker = isDarkSchemeTone(tone) ? DARK_SCHEME_BOTTOM_DARKER : GRADIENT_BOTTOM_DARKER;
|
return tone.darker(GRADIENT_BOTTOM_DARKER);
|
||||||
return boost(tone, 0).darker(darker);
|
|
||||||
}
|
}
|
||||||
} // namespace ThemeIconFill
|
} // namespace ThemeIconFill
|
||||||
|
|
|
||||||
|
|
@ -12,50 +12,41 @@
|
||||||
* @brief Picks the two colors of a filled treatment's vertical gradient.
|
* @brief Picks the two colors of a filled treatment's vertical gradient.
|
||||||
*
|
*
|
||||||
* The pre-#7595 icons were filled with a lighter top over a darker bottom. The
|
* The pre-#7595 icons were filled with a lighter top over a darker bottom. The
|
||||||
* semantic tones are tuned for text, not for filling a whole shape; the
|
* badge and the sticker share one fill rule so they sit next to each other with
|
||||||
* dark-scheme tones in particular are desaturated and read washed out once a
|
* the same green or red.
|
||||||
* disc or a glyph carries them. The badge and the sticker share one fill rule so
|
|
||||||
* they sit next to each other with the same green or red.
|
|
||||||
*
|
*
|
||||||
* The rule needs no scheme switch: a tone whose value is already high is a
|
* The authored tone is the top of that gradient: the palette author stores one
|
||||||
* dark-scheme tone, so its gradient is pulled down. Filling a shape with a tone
|
* color per emphasis level, so the treatment only adds depth by darkening the
|
||||||
* that light is what reads pale.
|
* bottom. Hue, saturation and value all stay the author's, so the rendered
|
||||||
|
* ladder is the authored ladder on every scheme.
|
||||||
*/
|
*/
|
||||||
namespace ThemeIconFill
|
namespace ThemeIconFill
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @brief Returns the tone with a saturation floor, for a filled shape.
|
* @brief Which scheme a treatment is painted for.
|
||||||
*
|
*
|
||||||
* The hue and value are left alone, so a light-scheme tone stays as dark as the
|
* The scheme is a property of the surface, not of the tone: an authored quiet
|
||||||
* line glyph next to it, and an achromatic tone stays gray.
|
* level is deliberately darker than its secondary level on the dark scheme too,
|
||||||
*
|
* so it cannot be inferred from the tone's value. Callers know the surface
|
||||||
* @param tone Glyph color the treatment is rendered with.
|
* palette and pass it explicitly.
|
||||||
* @return The tone, saturated enough to carry a whole shape.
|
|
||||||
*/
|
*/
|
||||||
QColor saturated(const QColor &tone);
|
enum class Scheme
|
||||||
|
{
|
||||||
/**
|
Light,
|
||||||
* @brief True when @p tone is a light tone, as the dark scheme's palette is.
|
Dark,
|
||||||
*
|
};
|
||||||
* The fill branches on this: a tone this light is darkened for a filled shape,
|
|
||||||
* and the badge keeps its darkened ink on it instead of the light scheme's
|
|
||||||
* white glyph.
|
|
||||||
*
|
|
||||||
* @param tone Glyph color the treatment is rendered with.
|
|
||||||
*/
|
|
||||||
bool isDarkSchemeTone(const QColor &tone);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the top color of the fill gradient.
|
* @brief Returns the top color of the fill gradient.
|
||||||
* @param tone Glyph color the treatment is rendered with.
|
* @param tone Glyph color the treatment is rendered with.
|
||||||
* @return The lighter end of the gradient.
|
* @return The authored tone, unchanged.
|
||||||
*/
|
*/
|
||||||
QColor topColor(const QColor &tone);
|
QColor topColor(const QColor &tone);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Returns the bottom color of the fill gradient.
|
* @brief Returns the bottom color of the fill gradient.
|
||||||
* @param tone Glyph color the treatment is rendered with.
|
* @param tone Glyph color the treatment is rendered with.
|
||||||
* @return The darker end of the gradient.
|
* @return The authored tone, darkened for depth.
|
||||||
*/
|
*/
|
||||||
QColor bottomColor(const QColor &tone);
|
QColor bottomColor(const QColor &tone);
|
||||||
} // namespace ThemeIconFill
|
} // namespace ThemeIconFill
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ namespace ThemeIconSticker
|
||||||
{
|
{
|
||||||
QColor outlineColor(const QColor &tone)
|
QColor outlineColor(const QColor &tone)
|
||||||
{
|
{
|
||||||
return ThemeIconFill::saturated(tone).darker(OUTLINE_DARKER);
|
return tone.darker(OUTLINE_DARKER);
|
||||||
}
|
}
|
||||||
|
|
||||||
QPixmap renderSticker(const QByteArray &svg, const QColor &tone, const QSize &pixelSize)
|
QPixmap renderSticker(const QByteArray &svg, const QColor &tone, const QSize &pixelSize)
|
||||||
|
|
|
||||||
|
|
@ -336,9 +336,8 @@ TEST_F(ThemeIconRenderTest, SelectedAppColorKeepsAContrastingTint)
|
||||||
* @brief A line glyph renders the authored role verbatim.
|
* @brief A line glyph renders the authored role verbatim.
|
||||||
*
|
*
|
||||||
* The emphasis colors are authored data now, so the paint path must not
|
* The emphasis colors are authored data now, so the paint path must not
|
||||||
* rewrite them. A muted quiet role stays muted on a line glyph; a badge disc or
|
* rewrite them. A muted quiet role stays muted on a line glyph, and the badge
|
||||||
* a sticker fill boosts its own fill, because carrying a whole shape is a
|
* and sticker only shape the value of that tone to give the shape depth.
|
||||||
* treatment concern rather than a role concern.
|
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, LineTintRendersTheAuthoredRoleVerbatim)
|
TEST_F(ThemeIconRenderTest, LineTintRendersTheAuthoredRoleVerbatim)
|
||||||
{
|
{
|
||||||
|
|
@ -352,7 +351,6 @@ TEST_F(ThemeIconRenderTest, LineTintRendersTheAuthoredRoleVerbatim)
|
||||||
const QPixmap pixmap = icon.pixmap(QSize(24, 24));
|
const QPixmap pixmap = icon.pixmap(QSize(24, 24));
|
||||||
ASSERT_FALSE(pixmap.isNull());
|
ASSERT_FALSE(pixmap.isNull());
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, raw), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, raw), 0);
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, ThemeIconFill::saturated(raw)), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief A badge is a disc with a gradient fill, a dark ring and a contrast glyph. */
|
/** @brief A badge is a disc with a gradient fill, a dark ring and a contrast glyph. */
|
||||||
|
|
@ -371,8 +369,8 @@ TEST_F(ThemeIconRenderTest, BadgeHasDiscGradientAndRing)
|
||||||
EXPECT_EQ(image.pixelColor(0, 0).alpha(), 0);
|
EXPECT_EQ(image.pixelColor(0, 0).alpha(), 0);
|
||||||
EXPECT_GT(image.pixelColor(32, 32).alpha(), 200);
|
EXPECT_GT(image.pixelColor(32, 32).alpha(), 200);
|
||||||
|
|
||||||
// The ring is the saturated tone darkened, and it must actually be painted.
|
// The ring is the authored tone darkened, and it must actually be painted.
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconFill::saturated(tone).darker(300)), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, tone.darker(300)), 0);
|
||||||
|
|
||||||
// The fill runs from a lighter top to a darker bottom. Sample left of the
|
// The fill runs from a lighter top to a darker bottom. Sample left of the
|
||||||
// check, which now reaches almost to the disc edge.
|
// check, which now reaches almost to the disc edge.
|
||||||
|
|
@ -384,40 +382,45 @@ TEST_F(ThemeIconRenderTest, BadgeHasDiscGradientAndRing)
|
||||||
EXPECT_GT(top.lightness(), bottom.lightness());
|
EXPECT_GT(top.lightness(), bottom.lightness());
|
||||||
|
|
||||||
// The glyph uses the contrast color chosen for this tone.
|
// The glyph uses the contrast color chosen for this tone.
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconBadge::glyphColor(tone)), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconBadge::glyphColor(tone, ThemeIconFill::Scheme::Light)), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The dark-scheme disc carries the same saturated fill as the sticker.
|
* @brief The dark-scheme disc keeps the authored tone and darkens it.
|
||||||
*
|
*
|
||||||
* The dark tones are tuned for text and read as pale mint or salmon once they
|
* The authored tone is the top of the gradient, so the level the palette author
|
||||||
* fill a disc, so the badge saturates its fill. The dark scheme keeps the
|
* picked survives the treatment; the shape only adds depth by darkening the
|
||||||
* darkened ink the badge had before the fill moved to the sticker ramp.
|
* bottom. It must not re-saturate the tone: the authored saturation is the theme
|
||||||
|
* author's choice. The dark scheme keeps the darkened ink the badge had before
|
||||||
|
* the fill moved to the sticker ramp.
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, BadgeDarkSchemeDiscUsesTheSaturatedFill)
|
TEST_F(ThemeIconRenderTest, BadgeDarkSchemeKeepsTheAuthoredTone)
|
||||||
{
|
{
|
||||||
const QColor tone = defaultAppColor(AppColor::Positive, true);
|
const QColor tone = defaultAppColor(AppColor::Positive, true);
|
||||||
ASSERT_TRUE(tone.isValid());
|
ASSERT_TRUE(tone.isValid());
|
||||||
ASSERT_GE(tone.value(), 200);
|
ASSERT_GE(tone.value(), 200);
|
||||||
|
|
||||||
const QPixmap pixmap = ThemeIconBadge::renderBadge(readIconSvg("ready_start"), tone, QSize(64, 64));
|
const QPixmap pixmap =
|
||||||
|
ThemeIconBadge::renderBadge(readIconSvg("ready_start"), tone, QSize(64, 64), ThemeIconFill::Scheme::Dark);
|
||||||
ASSERT_FALSE(pixmap.isNull());
|
ASSERT_FALSE(pixmap.isNull());
|
||||||
const QImage image = pixmap.toImage();
|
const QImage image = pixmap.toImage();
|
||||||
|
|
||||||
// The disc neither keeps the pale raw tone nor the old unsaturating bottom.
|
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, tone), 0);
|
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, tone.darker(104)), 0);
|
|
||||||
const QColor top = image.pixelColor(10, 16);
|
const QColor top = image.pixelColor(10, 16);
|
||||||
const QColor bottom = image.pixelColor(10, 48);
|
const QColor bottom = image.pixelColor(10, 48);
|
||||||
ASSERT_GT(top.alpha(), 200);
|
ASSERT_GT(top.alpha(), 200);
|
||||||
ASSERT_GT(bottom.alpha(), 200);
|
ASSERT_GT(bottom.alpha(), 200);
|
||||||
// Antialiasing costs a point or two of saturation at the sample points.
|
// The top is a darkening of the authored tone, never a lift, and it keeps
|
||||||
EXPECT_GE(top.hsvSaturation(), 160);
|
// the authored saturation; the sample sits a little down the gradient.
|
||||||
EXPECT_GE(bottom.hsvSaturation(), 160);
|
EXPECT_LE(top.value(), tone.value());
|
||||||
|
EXPECT_GT(top.value(), bottom.value());
|
||||||
|
EXPECT_LT(bottom.value(), tone.value());
|
||||||
EXPECT_GT(top.lightness(), bottom.lightness());
|
EXPECT_GT(top.lightness(), bottom.lightness());
|
||||||
|
// Antialiasing costs a point or two of saturation at the sample points.
|
||||||
|
EXPECT_NEAR(top.hsvSaturation(), tone.hsvSaturation(), 24);
|
||||||
|
EXPECT_NEAR(bottom.hsvSaturation(), tone.hsvSaturation(), 24);
|
||||||
|
|
||||||
// The dark scheme keeps the darkened ink, and it is actually painted.
|
// The dark scheme keeps the darkened ink, and it is actually painted.
|
||||||
const QColor ink = ThemeIconBadge::glyphColor(tone);
|
const QColor ink = ThemeIconBadge::glyphColor(tone, ThemeIconFill::Scheme::Dark);
|
||||||
ASSERT_NE(ink, QColor(Qt::white));
|
ASSERT_NE(ink, QColor(Qt::white));
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, ink), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, ink), 0);
|
||||||
}
|
}
|
||||||
|
|
@ -426,8 +429,8 @@ TEST_F(ThemeIconRenderTest, BadgeDarkSchemeDiscUsesTheSaturatedFill)
|
||||||
* @brief The badge ink follows the scheme.
|
* @brief The badge ink follows the scheme.
|
||||||
*
|
*
|
||||||
* Light-scheme tones keep the white glyph of the old art; dark-scheme tones
|
* Light-scheme tones keep the white glyph of the old art; dark-scheme tones
|
||||||
* keep the darkened ink, because the shared saturated disc is light enough that
|
* keep the darkened ink, because the lifted disc is light enough that white
|
||||||
* white would sit near 2.7:1 at its top. Pinning both cases stops the glyph from
|
* would sit near 2.7:1 at its top. Pinning both cases stops the glyph from
|
||||||
* silently flipping back to white when the fill or the palette changes.
|
* silently flipping back to white when the fill or the palette changes.
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, BadgeInkFollowsTheScheme)
|
TEST_F(ThemeIconRenderTest, BadgeInkFollowsTheScheme)
|
||||||
|
|
@ -444,7 +447,8 @@ TEST_F(ThemeIconRenderTest, BadgeInkFollowsTheScheme)
|
||||||
for (const auto &testCase : cases) {
|
for (const auto &testCase : cases) {
|
||||||
const QColor tone = defaultAppColor(testCase.role, testCase.dark);
|
const QColor tone = defaultAppColor(testCase.role, testCase.dark);
|
||||||
ASSERT_TRUE(tone.isValid());
|
ASSERT_TRUE(tone.isValid());
|
||||||
const QColor ink = ThemeIconBadge::glyphColor(tone);
|
const QColor ink = ThemeIconBadge::glyphColor(tone, testCase.dark ? ThemeIconFill::Scheme::Dark
|
||||||
|
: ThemeIconFill::Scheme::Light);
|
||||||
if (testCase.dark) {
|
if (testCase.dark) {
|
||||||
EXPECT_NE(ink, QColor(Qt::white));
|
EXPECT_NE(ink, QColor(Qt::white));
|
||||||
EXPECT_LT(ink.value(), 100);
|
EXPECT_LT(ink.value(), 100);
|
||||||
|
|
@ -487,7 +491,7 @@ TEST_F(ThemeIconRenderTest, BadgeStaysLegibleAtPlayerListSize)
|
||||||
EXPECT_GT(image.pixelColor(7, 7).alpha(), 200);
|
EXPECT_GT(image.pixelColor(7, 7).alpha(), 200);
|
||||||
// A 1px ring on a 15px canvas antialiases into the disc and has no exact
|
// A 1px ring on a 15px canvas antialiases into the disc and has no exact
|
||||||
// color, so the glyph is what has to stay legible.
|
// color, so the glyph is what has to stay legible.
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconBadge::glyphColor(tone)), 8);
|
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconBadge::glyphColor(tone, ThemeIconFill::Scheme::Light)), 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief The pixmap API's badge path agrees with the engine's. */
|
/** @brief The pixmap API's badge path agrees with the engine's. */
|
||||||
|
|
@ -498,8 +502,8 @@ TEST_F(ThemeIconRenderTest, SentinelSvgBadgeMatchesTheEngine)
|
||||||
QSize(48, 48), IconStyle::Badge);
|
QSize(48, 48), IconStyle::Badge);
|
||||||
ASSERT_FALSE(pixmap.isNull());
|
ASSERT_FALSE(pixmap.isNull());
|
||||||
EXPECT_EQ(pixmap.size(), QSize(48, 48));
|
EXPECT_EQ(pixmap.size(), QSize(48, 48));
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconFill::saturated(tone).darker(300)), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, tone.darker(300)), 0);
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconBadge::glyphColor(tone)), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, ThemeIconBadge::glyphColor(tone, ThemeIconFill::Scheme::Light)), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -576,7 +580,7 @@ TEST_F(ThemeIconRenderTest, BadgeOnAuthoredArtUsesTheAssetPath)
|
||||||
// The authored color survives; no disc tone and no ring are painted.
|
// The authored color survives; no disc tone and no ring are painted.
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#336699"))), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, QColor(QStringLiteral("#336699"))), 0);
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, tone), 0);
|
EXPECT_EQ(countOpaquePixels(pixmap, tone), 0);
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, ThemeIconFill::saturated(tone).darker(300)), 0);
|
EXPECT_EQ(countOpaquePixels(pixmap, tone.darker(300)), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Disabled keeps the shape language but drops back to the line glyph. */
|
/** @brief Disabled keeps the shape language but drops back to the line glyph. */
|
||||||
|
|
@ -594,7 +598,7 @@ TEST_F(ThemeIconRenderTest, DisabledBadgeFallsBackToLineGlyph)
|
||||||
|
|
||||||
// A filled disc at full mass would not read as unavailable, so Disabled
|
// A filled disc at full mass would not read as unavailable, so Disabled
|
||||||
// drops the badge: no ring, no disc, just the disabled line glyph.
|
// drops the badge: no ring, no disc, just the disabled line glyph.
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, ThemeIconFill::saturated(tone).darker(300)), 0);
|
EXPECT_EQ(countOpaquePixels(pixmap, tone.darker(300)), 0);
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, disabledText.darker(300)), 0);
|
EXPECT_EQ(countOpaquePixels(pixmap, disabledText.darker(300)), 0);
|
||||||
EXPECT_EQ(pixmap.toImage().pixelColor(0, 0).alpha(), 0);
|
EXPECT_EQ(pixmap.toImage().pixelColor(0, 0).alpha(), 0);
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, disabledText), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, disabledText), 0);
|
||||||
|
|
@ -631,13 +635,13 @@ TEST_F(ThemeIconRenderTest, StickerPaintsToneFillWithDarkOutline)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The dark-scheme fill is the branch the treatment exists for.
|
* @brief The dark-scheme fill keeps the authored tone and darkens it.
|
||||||
*
|
*
|
||||||
* That tone is light and deliberately desaturated, so a plain gradient paints a
|
* The authored tone is the top of the gradient, so the level the palette author
|
||||||
* pale mint shape. The fill has to raise its saturation and pull its value down
|
* picked survives the treatment. The treatment only adds depth by darkening the
|
||||||
* instead of up.
|
* bottom, without re-saturating the tone.
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, StickerDarkSchemeFillIsSaturatedAndDarkerThanTheTone)
|
TEST_F(ThemeIconRenderTest, StickerDarkSchemeFillKeepsTheAuthoredTone)
|
||||||
{
|
{
|
||||||
const QColor tone = defaultAppColor(AppColor::Positive, true);
|
const QColor tone = defaultAppColor(AppColor::Positive, true);
|
||||||
ASSERT_TRUE(tone.isValid());
|
ASSERT_TRUE(tone.isValid());
|
||||||
|
|
@ -651,10 +655,14 @@ TEST_F(ThemeIconRenderTest, StickerDarkSchemeFillIsSaturatedAndDarkerThanTheTone
|
||||||
const QColor bottom = image.pixelColor(32, 50);
|
const QColor bottom = image.pixelColor(32, 50);
|
||||||
ASSERT_GT(top.alpha(), 200);
|
ASSERT_GT(top.alpha(), 200);
|
||||||
ASSERT_GT(bottom.alpha(), 200);
|
ASSERT_GT(bottom.alpha(), 200);
|
||||||
EXPECT_LT(top.value(), tone.value());
|
// The top is a darkening of the authored tone, never a lift, and it keeps
|
||||||
|
// the authored saturation; the sample sits a little down the gradient.
|
||||||
|
EXPECT_LE(top.value(), tone.value());
|
||||||
|
EXPECT_GT(top.value(), bottom.value());
|
||||||
EXPECT_LT(bottom.value(), tone.value());
|
EXPECT_LT(bottom.value(), tone.value());
|
||||||
EXPECT_GT(top.lightness(), bottom.lightness());
|
EXPECT_GT(top.lightness(), bottom.lightness());
|
||||||
EXPECT_GE(top.hsvSaturation(), 170);
|
EXPECT_NEAR(top.hsvSaturation(), tone.hsvSaturation(), 24);
|
||||||
|
EXPECT_NEAR(bottom.hsvSaturation(), tone.hsvSaturation(), 24);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief The sticker is rendered at the device-pixel size, not upscaled later. */
|
/** @brief The sticker is rendered at the device-pixel size, not upscaled later. */
|
||||||
|
|
@ -749,7 +757,7 @@ TEST_F(ThemeIconRenderTest, LineStyleStillRendersGlyphAlone)
|
||||||
loadThemedIcon(QStringLiteral("icons/ready_start"), AppColor::Positive).pixmap(QSize(64, 64));
|
loadThemedIcon(QStringLiteral("icons/ready_start"), AppColor::Positive).pixmap(QSize(64, 64));
|
||||||
ASSERT_FALSE(pixmap.isNull());
|
ASSERT_FALSE(pixmap.isNull());
|
||||||
|
|
||||||
EXPECT_EQ(countOpaquePixels(pixmap, ThemeIconFill::saturated(tone).darker(300)), 0);
|
EXPECT_EQ(countOpaquePixels(pixmap, tone.darker(300)), 0);
|
||||||
EXPECT_GT(countOpaquePixels(pixmap, tone), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, tone), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -938,8 +946,8 @@ TEST(IconStickerPreview, WritesPngsWhenRequested)
|
||||||
const QByteArray svg = readSvg(glyph.name);
|
const QByteArray svg = readSvg(glyph.name);
|
||||||
ASSERT_FALSE(svg.isEmpty()) << glyph.name;
|
ASSERT_FALSE(svg.isEmpty()) << glyph.name;
|
||||||
// The preview must show the line glyph the engine paints, and the
|
// The preview must show the line glyph the engine paints, and the
|
||||||
// engine saturates AppColor tones before drawing them.
|
// engine renders an authored tone verbatim.
|
||||||
const QColor tone = ThemeIconFill::saturated(defaultAppColor(glyph.role, dark));
|
const QColor tone = defaultAppColor(glyph.role, dark);
|
||||||
|
|
||||||
for (int row = 0; row < 2; ++row) {
|
for (int row = 0; row < 2; ++row) {
|
||||||
const bool sticker = row == 1;
|
const bool sticker = row == 1;
|
||||||
|
|
@ -972,8 +980,10 @@ TEST(IconStickerPreview, WritesPngsWhenRequested)
|
||||||
ASSERT_FALSE(svg.isEmpty()) << badgeContext[i].name;
|
ASSERT_FALSE(svg.isEmpty()) << badgeContext[i].name;
|
||||||
const QColor tone = defaultAppColor(badgeContext[i].role, dark);
|
const QColor tone = defaultAppColor(badgeContext[i].role, dark);
|
||||||
const qreal x = MARGIN + LABEL_WIDTH + static_cast<qreal>(i) * CELL_WIDTH;
|
const qreal x = MARGIN + LABEL_WIDTH + static_cast<qreal>(i) * CELL_WIDTH;
|
||||||
painter.drawPixmap(QPointF(x, y + 8),
|
painter.drawPixmap(
|
||||||
ThemeIconBadge::renderBadge(svg, tone, QSize(CONTEXT_SIZE, CONTEXT_SIZE)));
|
QPointF(x, y + 8),
|
||||||
|
ThemeIconBadge::renderBadge(svg, tone, QSize(CONTEXT_SIZE, CONTEXT_SIZE),
|
||||||
|
dark ? ThemeIconFill::Scheme::Dark : ThemeIconFill::Scheme::Light));
|
||||||
painter.drawText(QPointF(x, y + 8 + CONTEXT_SIZE + 12), QString::fromLatin1(badgeContext[i].name));
|
painter.drawText(QPointF(x, y + 8 + CONTEXT_SIZE + 12), QString::fromLatin1(badgeContext[i].name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -984,7 +994,7 @@ TEST(IconStickerPreview, WritesPngsWhenRequested)
|
||||||
for (std::size_t i = 0; i < std::size(badgeContext); ++i) {
|
for (std::size_t i = 0; i < std::size(badgeContext); ++i) {
|
||||||
const QByteArray svg = readSvg(badgeContext[i].name);
|
const QByteArray svg = readSvg(badgeContext[i].name);
|
||||||
ASSERT_FALSE(svg.isEmpty()) << badgeContext[i].name;
|
ASSERT_FALSE(svg.isEmpty()) << badgeContext[i].name;
|
||||||
const QColor tone = ThemeIconFill::saturated(defaultAppColor(badgeContext[i].role, dark));
|
const QColor tone = defaultAppColor(badgeContext[i].role, dark);
|
||||||
const qreal x = MARGIN + LABEL_WIDTH + static_cast<qreal>(i) * CELL_WIDTH * 1.5;
|
const qreal x = MARGIN + LABEL_WIDTH + static_cast<qreal>(i) * CELL_WIDTH * 1.5;
|
||||||
for (int j = 0; j < 3; ++j) {
|
for (int j = 0; j < 3; ++j) {
|
||||||
const QSize side(CONTEXT_SIZE, CONTEXT_SIZE);
|
const QSize side(CONTEXT_SIZE, CONTEXT_SIZE);
|
||||||
|
|
@ -992,7 +1002,8 @@ TEST(IconStickerPreview, WritesPngsWhenRequested)
|
||||||
if (j == 0) {
|
if (j == 0) {
|
||||||
pixmap = renderLine(svg, tone, side);
|
pixmap = renderLine(svg, tone, side);
|
||||||
} else if (j == 1) {
|
} else if (j == 1) {
|
||||||
pixmap = ThemeIconBadge::renderBadge(svg, tone, side);
|
pixmap = ThemeIconBadge::renderBadge(
|
||||||
|
svg, tone, side, dark ? ThemeIconFill::Scheme::Dark : ThemeIconFill::Scheme::Light);
|
||||||
} else {
|
} else {
|
||||||
pixmap = ThemeIconSticker::renderSticker(svg, tone, side);
|
pixmap = ThemeIconSticker::renderSticker(svg, tone, side);
|
||||||
}
|
}
|
||||||
|
|
@ -1090,8 +1101,8 @@ TEST(IconBadgePreview, WritesPngsWhenRequested)
|
||||||
const QByteArray svg = readSvg(glyph.name);
|
const QByteArray svg = readSvg(glyph.name);
|
||||||
ASSERT_FALSE(svg.isEmpty()) << glyph.name;
|
ASSERT_FALSE(svg.isEmpty()) << glyph.name;
|
||||||
// The preview must show the line glyph the engine paints, and the
|
// The preview must show the line glyph the engine paints, and the
|
||||||
// engine saturates AppColor tones before drawing them.
|
// engine renders an authored tone verbatim.
|
||||||
const QColor tone = ThemeIconFill::saturated(defaultAppColor(glyph.role, dark));
|
const QColor tone = defaultAppColor(glyph.role, dark);
|
||||||
|
|
||||||
for (int row = 0; row < 2; ++row) {
|
for (int row = 0; row < 2; ++row) {
|
||||||
const bool badge = row == 1;
|
const bool badge = row == 1;
|
||||||
|
|
@ -1101,7 +1112,9 @@ TEST(IconBadgePreview, WritesPngsWhenRequested)
|
||||||
QString::fromLatin1(glyph.name));
|
QString::fromLatin1(glyph.name));
|
||||||
for (std::size_t i = 0; i < std::size(SIZES); ++i) {
|
for (std::size_t i = 0; i < std::size(SIZES); ++i) {
|
||||||
const int side = SIZES[i];
|
const int side = SIZES[i];
|
||||||
const QPixmap pixmap = badge ? ThemeIconBadge::renderBadge(svg, tone, QSize(side, side))
|
const QPixmap pixmap = badge ? ThemeIconBadge::renderBadge(svg, tone, QSize(side, side),
|
||||||
|
dark ? ThemeIconFill::Scheme::Dark
|
||||||
|
: ThemeIconFill::Scheme::Light)
|
||||||
: renderLine(svg, tone, QSize(side, side));
|
: renderLine(svg, tone, QSize(side, side));
|
||||||
const qreal x = MARGIN + LABEL_WIDTH + static_cast<qreal>(i) * CELL_WIDTH;
|
const qreal x = MARGIN + LABEL_WIDTH + static_cast<qreal>(i) * CELL_WIDTH;
|
||||||
// Align all sizes on a common baseline so the weight
|
// Align all sizes on a common baseline so the weight
|
||||||
|
|
@ -1127,6 +1140,7 @@ namespace
|
||||||
struct InkPalette
|
struct InkPalette
|
||||||
{
|
{
|
||||||
QColor text;
|
QColor text;
|
||||||
|
QColor buttonText;
|
||||||
QColor window;
|
QColor window;
|
||||||
QColor button;
|
QColor button;
|
||||||
QColor accent;
|
QColor accent;
|
||||||
|
|
@ -1152,9 +1166,10 @@ InkPalette shippedPalette(const QString &theme, bool dark)
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const QPalette palette = config.apply(QPalette());
|
const QPalette palette = config.apply(QPalette());
|
||||||
return {palette.color(QPalette::Active, QPalette::WindowText), palette.color(QPalette::Active, QPalette::Window),
|
return {
|
||||||
palette.color(QPalette::Active, QPalette::Button), palette.color(QPalette::Active, QPalette::Highlight),
|
palette.color(QPalette::Active, QPalette::WindowText), palette.color(QPalette::Active, QPalette::ButtonText),
|
||||||
config.appColors};
|
palette.color(QPalette::Active, QPalette::Window), palette.color(QPalette::Active, QPalette::Button),
|
||||||
|
palette.color(QPalette::Active, QPalette::Highlight), config.appColors};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1699,3 +1714,189 @@ TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested)
|
||||||
ASSERT_TRUE(image.save(path)) << path.toStdString();
|
ASSERT_TRUE(image.save(path)) << path.toStdString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Writes the authored emphasis sheets for every shipped palette theme.
|
||||||
|
*
|
||||||
|
* The ladder is only useful if it survives the treatment a call site chooses, so
|
||||||
|
* each sheet renders the same three authored levels as line glyphs, as sticker
|
||||||
|
* fills and as badges, at the two sizes the UI uses, on the two surfaces the
|
||||||
|
* contrast targets are measured against. Each family carries three glyphs, so
|
||||||
|
* the sheet also proves the level, not just one lucky shape.
|
||||||
|
*
|
||||||
|
* The authored tone is the top of every filled gradient, so the sheet prints that
|
||||||
|
* tone and the darker bottom the shape actually carries.
|
||||||
|
*/
|
||||||
|
TEST_F(ThemeIconRenderTest, EmphasisTreatmentsPreviewWritesPngsWhenRequested)
|
||||||
|
{
|
||||||
|
const QString outDir = qEnvironmentVariable("COCKATRICE_ICON_PREVIEW_DIR");
|
||||||
|
if (outDir.isEmpty()) {
|
||||||
|
GTEST_SKIP() << "set COCKATRICE_ICON_PREVIEW_DIR to write the preview PNGs";
|
||||||
|
}
|
||||||
|
ASSERT_TRUE(QDir().mkpath(outDir));
|
||||||
|
|
||||||
|
constexpr int MARGIN = 24;
|
||||||
|
constexpr int LABEL_WIDTH = 230;
|
||||||
|
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 int SIZES[] = {16, 24};
|
||||||
|
constexpr const char *TREATMENTS[] = {"line", "sticker", "badge"};
|
||||||
|
constexpr const char *LEVEL_NAMES[] = {"Quiet", "Secondary", "Primary"};
|
||||||
|
|
||||||
|
struct Family
|
||||||
|
{
|
||||||
|
const char *name;
|
||||||
|
const char *glyphs[3];
|
||||||
|
AppColor::Role quiet;
|
||||||
|
AppColor::Role secondary;
|
||||||
|
AppColor::Role primary;
|
||||||
|
};
|
||||||
|
const QList<Family> families = {
|
||||||
|
{"Neutral", {"settings", "info", "player"}, AppColor::IconSoft, AppColor::Icon, AppColor::IconStrong},
|
||||||
|
{"Positive",
|
||||||
|
{"increment", "ready_start", "arrow_up"},
|
||||||
|
AppColor::PositiveSoft,
|
||||||
|
AppColor::Positive,
|
||||||
|
AppColor::PositiveStrong},
|
||||||
|
{"Negative",
|
||||||
|
{"decrement", "not_ready_start", "remove_row"},
|
||||||
|
AppColor::NegativeSoft,
|
||||||
|
AppColor::Negative,
|
||||||
|
AppColor::NegativeStrong},
|
||||||
|
};
|
||||||
|
const QStringList themes = {
|
||||||
|
QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"),
|
||||||
|
QStringLiteral("Plasma"), QStringLiteral("VelvetMarble"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto roleAt = [](const Family &family, int level) {
|
||||||
|
return level == 0 ? family.quiet : level == 1 ? family.secondary : family.primary;
|
||||||
|
};
|
||||||
|
const auto schemeFor = [](bool dark) { return dark ? ThemeIconFill::Scheme::Dark : ThemeIconFill::Scheme::Light; };
|
||||||
|
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}) {
|
||||||
|
const InkPalette palette = shippedPalette(theme, dark);
|
||||||
|
ASSERT_TRUE(palette.text.isValid()) << theme.toStdString();
|
||||||
|
|
||||||
|
QImage image(QSize(qRound(logicalWidth * DPR), qRound(logicalHeight * DPR)),
|
||||||
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
|
image.fill(palette.window);
|
||||||
|
QPainter painter(&image);
|
||||||
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
painter.scale(DPR, DPR);
|
||||||
|
|
||||||
|
painter.setPen(palette.text);
|
||||||
|
painter.setFont(QFont(QStringLiteral("Sans"), 10, QFont::DemiBold));
|
||||||
|
painter.drawText(QPointF(MARGIN, MARGIN),
|
||||||
|
QStringLiteral("%1 — %2 scheme, authored emphasis across treatments")
|
||||||
|
.arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
||||||
|
|
||||||
|
struct Surface
|
||||||
|
{
|
||||||
|
const char *name;
|
||||||
|
QColor background;
|
||||||
|
QColor text;
|
||||||
|
};
|
||||||
|
const QList<Surface> surfaces = {
|
||||||
|
{"Window surface", palette.window, palette.text},
|
||||||
|
{"Button surface", palette.button, palette.buttonText},
|
||||||
|
};
|
||||||
|
|
||||||
|
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(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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal y = panelTop + PANEL_TITLE + COLUMN_HEADER;
|
||||||
|
for (const Family &family : families) {
|
||||||
|
for (int level = 0; level < 3; ++level) {
|
||||||
|
const AppColor::Role role = roleAt(family, level);
|
||||||
|
const QColor ink = palette.roles.value(role);
|
||||||
|
ASSERT_TRUE(ink.isValid())
|
||||||
|
<< theme.toStdString() << " " << family.name << " " << LEVEL_NAMES[level];
|
||||||
|
const QColor bottom = ThemeIconFill::bottomColor(ink);
|
||||||
|
|
||||||
|
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));
|
||||||
|
if (bottom != ink) {
|
||||||
|
painter.drawText(QPointF(contentLeft + 82, y + 28),
|
||||||
|
QStringLiteral("bottom ") + bottom.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, schemeFor(dark));
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panelTop += panelHeight + 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
painter.end();
|
||||||
|
const QString path = QDir(outDir).absoluteFilePath(
|
||||||
|
QStringLiteral("emphasis-treatments-%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