mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-12 04:50:01 -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
|
||||
{
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
|
|
@ -264,7 +307,7 @@ private:
|
|||
QByteArray variant;
|
||||
if (badge) {
|
||||
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) {
|
||||
variant = QByteArrayLiteral("S|") + tone.name(QColor::HexRgb).toLatin1();
|
||||
} else if (resolvedHasSentinel) {
|
||||
|
|
@ -282,7 +325,7 @@ private:
|
|||
if (badge) {
|
||||
// Geometry is computed from the device-pixel size, and the ratio is
|
||||
// attached below like every other path.
|
||||
pixmap = ThemeIconBadge::renderBadge(resolvedSvg, tone, pixelSize);
|
||||
pixmap = ThemeIconBadge::renderBadge(resolvedSvg, tone, pixelSize, activeScheme());
|
||||
} else if (sticker) {
|
||||
pixmap = ThemeIconSticker::renderSticker(resolvedSvg, tone, pixelSize);
|
||||
} else if (resolvedHasSentinel) {
|
||||
|
|
@ -318,34 +361,6 @@ private:
|
|||
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
|
||||
|
||||
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);
|
||||
}
|
||||
// The theme authors one color per emphasis level, so the paint path
|
||||
// uses the resolved role verbatim. The filled treatments apply their
|
||||
// own saturation floor when a disc or a fill needs mass; a line glyph
|
||||
// keeps the color the author picked.
|
||||
// uses the resolved role verbatim. The filled treatments only shape the
|
||||
// tone's value to give a disc or a fill depth; hue and saturation stay
|
||||
// the author's.
|
||||
const QColor color = themeManager->appColor(emphasisRole(appRole, emphasis));
|
||||
if (mode != QIcon::Selected) {
|
||||
return color;
|
||||
|
|
@ -428,7 +443,7 @@ QPixmap loadSentinelSvgPixmap(const QString &resolvedPath, IconTint tint, const
|
|||
if (canvas.isEmpty()) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ public:
|
|||
*
|
||||
* Application colors are returned verbatim: the theme author stores one
|
||||
* 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
|
||||
* carry; a line glyph keeps the author's color.
|
||||
* filled treatments only shape that tone's value to give a shape depth; the
|
||||
* hue and saturation stay the author's.
|
||||
*
|
||||
* Neutral label roles (WindowText, Text) resolve to icon ink rather than the
|
||||
* raw palette color: ThemeIconInk::glyphInk derives it from the accent and
|
||||
|
|
|
|||
|
|
@ -46,16 +46,16 @@ DiscFill discFill(const QColor &tone)
|
|||
|
||||
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
|
||||
// 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
|
||||
// 2.7:1 at the top, so the dark ink is the deliberate choice rather than a
|
||||
// contrast measurement that a palette tweak could flip. Deriving it from
|
||||
// the tone keeps themes in control.
|
||||
return ThemeIconFill::isDarkSchemeTone(tone) ? ThemeIconFill::saturated(tone).darker(GLYPH_DARKER)
|
||||
: QColor(Qt::white);
|
||||
// The lifted disc is light enough there that white sits near 2.7:1 at the
|
||||
// top, so the dark ink is the deliberate choice rather than a contrast
|
||||
// measurement that a palette tweak could flip. Deriving it from the tone
|
||||
// keeps themes in control, and the scheme is explicit because an authored
|
||||
// quiet level is a dark tone on the dark scheme too.
|
||||
return scheme == ThemeIconFill::Scheme::Dark ? tone.darker(GLYPH_DARKER) : QColor(Qt::white);
|
||||
}
|
||||
|
||||
QSize glyphSize(const QSize &pixelSize)
|
||||
|
|
@ -70,7 +70,7 @@ QSize glyphSize(const QSize &pixelSize)
|
|||
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()) {
|
||||
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
|
||||
// edge is the disc boundary, so the two meet without a seam.
|
||||
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;
|
||||
painter.drawEllipse(discRect.center(), ringRadius, ringRadius);
|
||||
|
||||
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);
|
||||
if (renderer.isValid()) {
|
||||
const QSize size = glyphSize(pixelSize);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
#ifndef THEME_ICON_BADGE_H
|
||||
#define THEME_ICON_BADGE_H
|
||||
|
||||
#include "theme_icon_fill.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QColor>
|
||||
#include <QPixmap>
|
||||
|
|
@ -34,13 +36,17 @@ namespace ThemeIconBadge
|
|||
/**
|
||||
* @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
|
||||
* 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
|
||||
* derived from the tone, so the theme keeps control of both ends.
|
||||
* the badge had before the filled treatments shared a fill. The scheme is passed
|
||||
* 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. */
|
||||
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 pixelSize Canvas size in device pixels; a non-square canvas centers
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
|
||||
#endif // THEME_ICON_BADGE_H
|
||||
|
|
|
|||
|
|
@ -1,61 +1,22 @@
|
|||
#include "theme_icon_fill.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
namespace
|
||||
{
|
||||
// Semantic tones are tuned for text. The dark-scheme ones are deliberately
|
||||
// desaturated, which reads washed out when the same color fills a whole shape,
|
||||
// so the fill gives its tone a saturation floor.
|
||||
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;
|
||||
// The bottom end of every filled treatment. The authored tone is the top, so a
|
||||
// single darkening keeps the emphasis level the palette author picked and still
|
||||
// gives the shape the lit-from-above depth the pre-#7595 art had.
|
||||
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 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)
|
||||
{
|
||||
if (isDarkSchemeTone(tone)) {
|
||||
return boost(tone, 0).darker(DARK_SCHEME_TOP_DARKER);
|
||||
}
|
||||
return boost(tone, MIN_VALUE);
|
||||
return tone;
|
||||
}
|
||||
|
||||
QColor bottomColor(const QColor &tone)
|
||||
{
|
||||
const int darker = isDarkSchemeTone(tone) ? DARK_SCHEME_BOTTOM_DARKER : GRADIENT_BOTTOM_DARKER;
|
||||
return boost(tone, 0).darker(darker);
|
||||
return tone.darker(GRADIENT_BOTTOM_DARKER);
|
||||
}
|
||||
} // namespace ThemeIconFill
|
||||
|
|
|
|||
|
|
@ -12,50 +12,41 @@
|
|||
* @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
|
||||
* semantic tones are tuned for text, not for filling a whole shape; the
|
||||
* dark-scheme tones in particular are desaturated and read washed out once a
|
||||
* 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.
|
||||
* 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
|
||||
* dark-scheme tone, so its gradient is pulled down. Filling a shape with a tone
|
||||
* that light is what reads pale.
|
||||
* The authored tone is the top of that gradient: the palette author stores one
|
||||
* color per emphasis level, so the treatment only adds depth by darkening the
|
||||
* bottom. Hue, saturation and value all stay the author's, so the rendered
|
||||
* ladder is the authored ladder on every scheme.
|
||||
*/
|
||||
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
|
||||
* line glyph next to it, and an achromatic tone stays gray.
|
||||
*
|
||||
* @param tone Glyph color the treatment is rendered with.
|
||||
* @return The tone, saturated enough to carry a whole shape.
|
||||
* The scheme is a property of the surface, not of the tone: an authored quiet
|
||||
* 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
|
||||
* palette and pass it explicitly.
|
||||
*/
|
||||
QColor saturated(const QColor &tone);
|
||||
|
||||
/**
|
||||
* @brief True when @p tone is a light tone, as the dark scheme's palette is.
|
||||
*
|
||||
* 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);
|
||||
enum class Scheme
|
||||
{
|
||||
Light,
|
||||
Dark,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Returns the top color of the fill gradient.
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @brief Returns the bottom color of the fill gradient.
|
||||
* @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);
|
||||
} // namespace ThemeIconFill
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ namespace ThemeIconSticker
|
|||
{
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue