mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-11 04:23:29 -08:00
[Icons] Author icon emphasis colors as palette roles
The emphasis levels were computed from the accent at paint time, so a theme could not vary them: every theme got the same derived strong and soft tones, and a semantic hue was pushed toward the accent whether the author wanted that or not. Give each family three authored roles instead. Icon/IconStrong/IconSoft carry the neutral glyph ink and Positive/Negative gain the same Strong and Soft companions. Emphasis becomes a lookup inside the family, and a caller that names a variant pins that level at every emphasis. The paint path returns the authored color verbatim; only the filled treatments keep their saturation floor, because carrying a disc or a fill is a treatment decision. Shipped palettes author the full ladder for both schemes; the palette editor exposes the new rows and the generator writes them. System keeps deriving its neutral ink from the platform palette, since it ships no palette of its own.
This commit is contained in:
parent
115d16a74b
commit
03a4a33ba5
21 changed files with 551 additions and 277 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
#include "palette_generator.h"
|
#include "palette_generator.h"
|
||||||
|
|
||||||
#include "../theme_config.h"
|
#include "../theme_config.h"
|
||||||
|
#include "../theme_icon_ink.h"
|
||||||
|
|
||||||
#include <QColor>
|
#include <QColor>
|
||||||
#include <QMap>
|
#include <QMap>
|
||||||
|
|
@ -167,10 +168,19 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
|
||||||
// the lightened, desaturated companion used for button-gradient highlights.
|
// the lightened, desaturated companion used for button-gradient highlights.
|
||||||
cfg.appColors[AppColor::AccentStrong] = hl;
|
cfg.appColors[AppColor::AccentStrong] = hl;
|
||||||
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
|
cfg.appColors[AppColor::AccentSoft] = hsl(accent.lightness() + 60, qRound(accent.hslSaturation() * 70 / 100.0));
|
||||||
|
// Neutral icon ink follows the generated accent at the same three emphasis
|
||||||
|
// levels a hand-authored theme stores, so the preview matches what the icon
|
||||||
|
// engine renders before the palette is saved.
|
||||||
|
const QColor iconInk = ThemeIconInk::glyphInk(winText, bg_win, hl);
|
||||||
|
cfg.appColors[AppColor::Icon] = iconInk;
|
||||||
|
cfg.appColors[AppColor::IconStrong] = ThemeIconInk::forEmphasis(hl, bg_win, IconEmphasis::Primary);
|
||||||
|
cfg.appColors[AppColor::IconSoft] = ThemeIconInk::forEmphasis(iconInk, bg_win, IconEmphasis::Quiet);
|
||||||
// Semantic roles stay independent of the accent: a generated palette must
|
// Semantic roles stay independent of the accent: a generated palette must
|
||||||
// still paint add/ready green and remove/not-ready red.
|
// still paint add/ready green and remove/not-ready red.
|
||||||
cfg.appColors[AppColor::Positive] = defaultAppColor(AppColor::Positive, dark);
|
for (const AppColor::Role role : {AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft,
|
||||||
cfg.appColors[AppColor::Negative] = defaultAppColor(AppColor::Negative, dark);
|
AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}) {
|
||||||
|
cfg.appColors[role] = defaultAppColor(role, dark);
|
||||||
|
}
|
||||||
|
|
||||||
// BrightText
|
// BrightText
|
||||||
QColor bright;
|
QColor bright;
|
||||||
|
|
|
||||||
|
|
@ -65,10 +65,19 @@ static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
|
||||||
QT_TRANSLATE_NOOP("PaletteGridWidget", "Vivid primary accent (e.g. home-tab button gradient start)")},
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Vivid primary accent (e.g. home-tab button gradient start)")},
|
||||||
{AppColor::AccentSoft,
|
{AppColor::AccentSoft,
|
||||||
QT_TRANSLATE_NOOP("PaletteGridWidget", "Lightened, desaturated accent (e.g. home-tab button gradient end)")},
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Lightened, desaturated accent (e.g. home-tab button gradient end)")},
|
||||||
|
{AppColor::Icon, QT_TRANSLATE_NOOP("PaletteGridWidget", "Neutral icon ink at the default action weight")},
|
||||||
|
{AppColor::IconStrong,
|
||||||
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Neutral icon ink for the one action a surface exists for")},
|
||||||
|
{AppColor::IconSoft,
|
||||||
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Neutral icon ink for a glyph that labels rather than acts")},
|
||||||
{AppColor::Positive,
|
{AppColor::Positive,
|
||||||
QT_TRANSLATE_NOOP("PaletteGridWidget", "Positive state and additive actions (ready, add, increment)")},
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Positive state and additive actions (ready, add, increment)")},
|
||||||
|
{AppColor::PositiveStrong, QT_TRANSLATE_NOOP("PaletteGridWidget", "Positive state at primary emphasis")},
|
||||||
|
{AppColor::PositiveSoft, QT_TRANSLATE_NOOP("PaletteGridWidget", "Positive state at quiet emphasis")},
|
||||||
{AppColor::Negative,
|
{AppColor::Negative,
|
||||||
QT_TRANSLATE_NOOP("PaletteGridWidget", "Negative state and removal actions (not ready, remove, decrement)")},
|
QT_TRANSLATE_NOOP("PaletteGridWidget", "Negative state and removal actions (not ready, remove, decrement)")},
|
||||||
|
{AppColor::NegativeStrong, QT_TRANSLATE_NOOP("PaletteGridWidget", "Negative state at primary emphasis")},
|
||||||
|
{AppColor::NegativeSoft, QT_TRANSLATE_NOOP("PaletteGridWidget", "Negative state at quiet emphasis")},
|
||||||
};
|
};
|
||||||
|
|
||||||
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)
|
||||||
|
|
|
||||||
|
|
@ -458,12 +458,25 @@ QColor defaultAppColor(AppColor::Role role, bool dark)
|
||||||
switch (role) {
|
switch (role) {
|
||||||
case AppColor::AccentStrong:
|
case AppColor::AccentStrong:
|
||||||
case AppColor::AccentSoft:
|
case AppColor::AccentSoft:
|
||||||
// Derived from the palette highlight by ThemeManager::appColor().
|
// The neutral icon family is palette-derived like the accent, so a
|
||||||
|
// theme without a palette of its own (System) keeps following the
|
||||||
|
// platform instead of pinning one scheme's ink.
|
||||||
|
case AppColor::Icon:
|
||||||
|
case AppColor::IconStrong:
|
||||||
|
case AppColor::IconSoft:
|
||||||
return {};
|
return {};
|
||||||
case AppColor::Positive:
|
case AppColor::Positive:
|
||||||
return QColor(dark ? QStringLiteral("#6dd58c") : QStringLiteral("#1b7f3b"));
|
return QColor(dark ? QStringLiteral("#6dd58c") : QStringLiteral("#1b7f3b"));
|
||||||
|
case AppColor::PositiveStrong:
|
||||||
|
return QColor(dark ? QStringLiteral("#99e2ae") : QStringLiteral("#104a22"));
|
||||||
|
case AppColor::PositiveSoft:
|
||||||
|
return QColor(dark ? QStringLiteral("#5e8873") : QStringLiteral("#5d7f6f"));
|
||||||
case AppColor::Negative:
|
case AppColor::Negative:
|
||||||
return QColor(dark ? QStringLiteral("#ff8a80") : QStringLiteral("#b3261e"));
|
return QColor(dark ? QStringLiteral("#ff8a80") : QStringLiteral("#b3261e"));
|
||||||
|
case AppColor::NegativeStrong:
|
||||||
|
return QColor(dark ? QStringLiteral("#ffb3ac") : QStringLiteral("#7c1b15"));
|
||||||
|
case AppColor::NegativeSoft:
|
||||||
|
return QColor(dark ? QStringLiteral("#9b7678") : QStringLiteral("#9a6c6f"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,13 @@
|
||||||
// set. Stored in the same palette-<scheme>.toml under an [AppColors] section
|
// set. Stored in the same palette-<scheme>.toml under an [AppColors] section
|
||||||
// and editable from the palette editor, so theme authors can control colors
|
// and editable from the palette editor, so theme authors can control colors
|
||||||
// beyond what Qt's palette can express.
|
// beyond what Qt's palette can express.
|
||||||
|
//
|
||||||
|
// The icon families carry one role per emphasis level. Icon is the neutral
|
||||||
|
// glyph ink; IconStrong and IconSoft are its primary and quiet levels. Positive
|
||||||
|
// and Negative follow the same pattern, so an emphasis never has to be computed
|
||||||
|
// from a hue at paint time: the theme authors all three levels. Naming a variant
|
||||||
|
// explicitly (`IconStrong`) pins that level; naming the family (`Icon`) lets the
|
||||||
|
// caller's emphasis choose.
|
||||||
namespace AppColor
|
namespace AppColor
|
||||||
{
|
{
|
||||||
Q_NAMESPACE
|
Q_NAMESPACE
|
||||||
|
|
@ -20,8 +27,15 @@ enum Role
|
||||||
{
|
{
|
||||||
AccentStrong,
|
AccentStrong,
|
||||||
AccentSoft,
|
AccentSoft,
|
||||||
|
Icon,
|
||||||
|
IconStrong,
|
||||||
|
IconSoft,
|
||||||
Positive,
|
Positive,
|
||||||
|
PositiveStrong,
|
||||||
|
PositiveSoft,
|
||||||
Negative,
|
Negative,
|
||||||
|
NegativeStrong,
|
||||||
|
NegativeSoft,
|
||||||
};
|
};
|
||||||
Q_ENUM_NS(Role)
|
Q_ENUM_NS(Role)
|
||||||
} // namespace AppColor
|
} // namespace AppColor
|
||||||
|
|
@ -47,9 +61,13 @@ Q_ENUM_NS(Role)
|
||||||
* @brief Fallback for an application color role when the active theme does not
|
* @brief Fallback for an application color role when the active theme does not
|
||||||
* define one.
|
* define one.
|
||||||
*
|
*
|
||||||
* Accent roles return an invalid QColor because ThemeManager derives them from
|
* Accent roles and the neutral Icon family return an invalid QColor because
|
||||||
* the palette highlight. Semantic roles return an opaque default that suits the
|
* ThemeManager derives them from the palette: the accent from the highlight,
|
||||||
* scheme, so themes written before the role existed still render it.
|
* the icon ink from the neutral text color plus the highlight. Semantic roles
|
||||||
|
* return an opaque default that suits the scheme, so themes written before the
|
||||||
|
* role existed still render it. A theme with no palette of its own (the System
|
||||||
|
* theme) keeps deriving its icon ink from the platform palette instead of
|
||||||
|
* pinning one scheme's colors.
|
||||||
*/
|
*/
|
||||||
QColor defaultAppColor(AppColor::Role role, bool dark);
|
QColor defaultAppColor(AppColor::Role role, bool dark);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
#include "libcockatrice/utility/svg_recolor.h"
|
#include "libcockatrice/utility/svg_recolor.h"
|
||||||
#include "theme_icon_badge.h"
|
#include "theme_icon_badge.h"
|
||||||
#include "theme_icon_fill.h"
|
|
||||||
#include "theme_icon_ink.h"
|
#include "theme_icon_ink.h"
|
||||||
#include "theme_icon_sticker.h"
|
#include "theme_icon_sticker.h"
|
||||||
#include "theme_manager.h"
|
#include "theme_manager.h"
|
||||||
|
|
@ -318,6 +317,35 @@ private:
|
||||||
mutable bool hasResolvedAsset = false;
|
mutable bool hasResolvedAsset = false;
|
||||||
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)
|
||||||
|
|
@ -342,7 +370,6 @@ IconTint IconTint::quiet()
|
||||||
QColor IconTint::resolve(QIcon::Mode mode) const
|
QColor IconTint::resolve(QIcon::Mode mode) const
|
||||||
{
|
{
|
||||||
const QPalette &palette = qApp->palette();
|
const QPalette &palette = qApp->palette();
|
||||||
const QColor surface = palette.color(QPalette::Active, QPalette::Window);
|
|
||||||
const QColor accent = palette.color(QPalette::Active, QPalette::Highlight);
|
const QColor accent = palette.color(QPalette::Active, QPalette::Highlight);
|
||||||
|
|
||||||
if (source == Source::AppColor) {
|
if (source == Source::AppColor) {
|
||||||
|
|
@ -350,13 +377,11 @@ QColor IconTint::resolve(QIcon::Mode mode) const
|
||||||
if (mode == QIcon::Disabled) {
|
if (mode == QIcon::Disabled) {
|
||||||
return palette.color(QPalette::Disabled, QPalette::WindowText);
|
return palette.color(QPalette::Disabled, QPalette::WindowText);
|
||||||
}
|
}
|
||||||
// The three treatments paint the same tone: a line glyph, a badge disc
|
// The theme authors one color per emphasis level, so the paint path
|
||||||
// and a sticker fill must not disagree on how saturated the role is.
|
// uses the resolved role verbatim. The filled treatments apply their
|
||||||
// The fill treatments apply this floor themselves, so this only changes
|
// own saturation floor when a disc or a fill needs mass; a line glyph
|
||||||
// the line path, and only for the desaturated dark-scheme tones. The
|
// keeps the color the author picked.
|
||||||
// emphasis then decides how loud that tone is, without changing its hue.
|
const QColor color = themeManager->appColor(emphasisRole(appRole, emphasis));
|
||||||
const QColor color =
|
|
||||||
ThemeIconInk::forEmphasis(ThemeIconFill::saturated(themeManager->appColor(appRole)), surface, emphasis);
|
|
||||||
if (mode != QIcon::Selected) {
|
if (mode != QIcon::Selected) {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
@ -373,14 +398,10 @@ QColor IconTint::resolve(QIcon::Mode mode) const
|
||||||
const QPalette::ColorRole effective =
|
const QPalette::ColorRole effective =
|
||||||
mode == QIcon::Selected && paletteRole == QPalette::WindowText ? QPalette::HighlightedText : paletteRole;
|
mode == QIcon::Selected && paletteRole == QPalette::WindowText ? QPalette::HighlightedText : paletteRole;
|
||||||
const QColor color = palette.color(group, effective);
|
const QColor color = palette.color(group, effective);
|
||||||
// A neutral label role is inked as an icon, not as body text: it borrows a
|
// A neutral label role resolves to the theme's authored icon ink. Semantic
|
||||||
// little accent chroma and is held to a contrast floor. Semantic roles and
|
// roles and the selected-row swap keep the color the palette gave them.
|
||||||
// the selected-row swap keep the color the palette gave them.
|
|
||||||
if (group == QPalette::Active && (effective == QPalette::WindowText || effective == QPalette::Text)) {
|
if (group == QPalette::Active && (effective == QPalette::WindowText || effective == QPalette::Text)) {
|
||||||
// The primary action of a surface takes the accent itself; the other
|
return themeManager->appColor(emphasisRole(AppColor::Icon, emphasis));
|
||||||
// levels stay neutral. Emphasis then sets the contrast the glyph pays.
|
|
||||||
const QColor base = emphasis == IconEmphasis::Primary ? accent : ThemeIconInk::glyphInk(color, surface, accent);
|
|
||||||
return ThemeIconInk::forEmphasis(base, surface, emphasis);
|
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,18 +47,20 @@ enum class IconStyle
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Selects how much attention a glyph claims inside its surface.
|
* @brief Selects which authored level of a color family a glyph uses.
|
||||||
*
|
*
|
||||||
* Tint says which hue, style says how much shape, emphasis says how loud. The
|
* Tint says which family, style says how much shape, emphasis says how loud.
|
||||||
* three are independent: a primary action can be a line glyph in the theme
|
* The three are independent: a primary action can be a line glyph in the
|
||||||
* accent, and a quiet one can be a sticker in a dimmed tone.
|
* theme's primary ink, and a quiet one can be a sticker in that family's quiet
|
||||||
|
* level.
|
||||||
*
|
*
|
||||||
* Secondary is the default and the color every icon had before the axis
|
* Each family (neutral icon, positive, negative) stores one color per level, so
|
||||||
* existed, so existing call sites keep their look. Primary is for the one
|
* emphasis is a lookup rather than a computation: the palette editor's rows are
|
||||||
* action a surface exists for: a neutral primary glyph takes the accent, and a
|
* the whole source of truth. Secondary is the default and the color every icon
|
||||||
* tinted one is pushed to full contrast. Quiet is for glyphs that label rather
|
* had before the axis existed, so existing call sites keep their look. Primary
|
||||||
* than act, such as the magnifier inside a search field: legible, but never
|
* is for the one action a surface exists for. Quiet is for glyphs that label
|
||||||
* competing with the buttons next to it.
|
* rather than act, such as the magnifier inside a search field: legible, but
|
||||||
|
* never competing with the buttons next to it.
|
||||||
*/
|
*/
|
||||||
enum class IconEmphasis
|
enum class IconEmphasis
|
||||||
{
|
{
|
||||||
|
|
@ -93,19 +95,23 @@ public:
|
||||||
*
|
*
|
||||||
* A disabled application color resolves to the disabled foreground, so an
|
* A disabled application color resolves to the disabled foreground, so an
|
||||||
* unavailable control does not keep its semantic color. A selected palette
|
* unavailable control does not keep its semantic color. A selected palette
|
||||||
* color keeps the WindowText -> HighlightedText swap. Application colors go
|
* color keeps the WindowText -> HighlightedText swap.
|
||||||
* through the saturation floor shared by the filled treatments, so a line
|
*
|
||||||
* glyph, a badge disc and a sticker fill carry the same tone.
|
* 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.
|
||||||
*
|
*
|
||||||
* 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 borrows a little accent chroma
|
* raw palette color: ThemeIconInk::glyphInk derives it from the accent and
|
||||||
* and holds the glyph to a contrast floor, because a 2px glyph in body-text
|
* holds the glyph to a contrast floor, because a 2px glyph in body-text ink
|
||||||
* ink reads thinner than the text beside it. Semantic roles keep the hue the
|
* reads thinner than the text beside it. A theme that authors the neutral
|
||||||
* call site asked for.
|
* icon roles overrides that derivation. Semantic roles keep the hue the call
|
||||||
|
* site asked for.
|
||||||
*
|
*
|
||||||
* Emphasis is applied after the hue: Primary lifts a neutral glyph to the
|
* Emphasis selects the family's authored variant, so the palette editor's
|
||||||
* accent and pushes a tinted one to full contrast; Quiet blends either
|
* rows are the whole source of truth. A caller that names a variant
|
||||||
* toward the surface down to the non-text legibility floor.
|
* explicitly (a strong or soft role) pins that level at every emphasis.
|
||||||
*/
|
*/
|
||||||
QColor resolve(QIcon::Mode mode) const;
|
QColor resolve(QIcon::Mode mode) const;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "../../client/settings/cache_settings.h"
|
#include "../../client/settings/cache_settings.h"
|
||||||
#include "pixel_map_generator.h"
|
#include "pixel_map_generator.h"
|
||||||
#include "theme_config.h"
|
#include "theme_config.h"
|
||||||
|
#include "theme_icon_ink.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QChar>
|
#include <QChar>
|
||||||
|
|
@ -651,8 +652,33 @@ QColor ThemeManager::appColor(AppColor::Role role) const
|
||||||
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
|
qBound(0, qRound(accent.hslSaturation() * SOFT_SATURATION_PERCENT / 100.0), 255),
|
||||||
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
|
qBound(0, accent.lightness() + SOFT_LIGHTNESS_OFFSET, 255));
|
||||||
}
|
}
|
||||||
|
case AppColor::Icon:
|
||||||
|
case AppColor::IconStrong:
|
||||||
|
case AppColor::IconSoft: {
|
||||||
|
// Legacy fallback for a theme written before the icon roles
|
||||||
|
// existed, or one with no palette of its own (System). The three
|
||||||
|
// levels are the ink rule this series shipped, applied once here so
|
||||||
|
// authored roles never get recomputed at paint time.
|
||||||
|
const QPalette &palette = qApp->palette();
|
||||||
|
const QColor surface = palette.color(QPalette::Active, QPalette::Window);
|
||||||
|
const QColor ink =
|
||||||
|
ThemeIconInk::glyphInk(palette.color(QPalette::Active, QPalette::WindowText), surface, accent);
|
||||||
|
switch (role) {
|
||||||
|
case AppColor::IconStrong:
|
||||||
|
return ThemeIconInk::forEmphasis(accent, surface, IconEmphasis::Primary);
|
||||||
|
case AppColor::IconSoft:
|
||||||
|
return ThemeIconInk::forEmphasis(ink, surface, IconEmphasis::Quiet);
|
||||||
|
case AppColor::Icon:
|
||||||
|
default:
|
||||||
|
return ink;
|
||||||
|
}
|
||||||
|
}
|
||||||
case AppColor::Positive:
|
case AppColor::Positive:
|
||||||
|
case AppColor::PositiveStrong:
|
||||||
|
case AppColor::PositiveSoft:
|
||||||
case AppColor::Negative:
|
case AppColor::Negative:
|
||||||
|
case AppColor::NegativeStrong:
|
||||||
|
case AppColor::NegativeSoft:
|
||||||
// Semantic colors are independent of the accent: a missing role must
|
// Semantic colors are independent of the accent: a missing role must
|
||||||
// not silently paint "remove" in the theme's highlight color.
|
// not silently paint "remove" in the theme's highlight color.
|
||||||
return defaultAppColor(role, isDarkModeActive());
|
return defaultAppColor(role, isDarkModeActive());
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #4a82e6
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #3a6fd6
|
AccentStrong = #3a6fd6
|
||||||
AccentSoft = #8fb8f2
|
AccentSoft = #8fb8f2
|
||||||
|
Icon = #7b9fe4
|
||||||
|
IconStrong = #b1c7ef
|
||||||
|
IconSoft = #70809d
|
||||||
Positive = #6dd58c
|
Positive = #6dd58c
|
||||||
|
PositiveStrong = #99e2ae
|
||||||
|
PositiveSoft = #5e8873
|
||||||
Negative = #ff8a80
|
Negative = #ff8a80
|
||||||
|
NegativeStrong = #ffb3ac
|
||||||
|
NegativeSoft = #9b7678
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #2f6fd6
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #3a6fd6
|
AccentStrong = #3a6fd6
|
||||||
AccentSoft = #8fb8f2
|
AccentSoft = #8fb8f2
|
||||||
|
Icon = #2f59ad
|
||||||
|
IconStrong = #213e79
|
||||||
|
IconSoft = #687899
|
||||||
Positive = #1b7f3b
|
Positive = #1b7f3b
|
||||||
|
PositiveStrong = #104a22
|
||||||
|
PositiveSoft = #5d7f6f
|
||||||
Negative = #b3261e
|
Negative = #b3261e
|
||||||
|
NegativeStrong = #7c1b15
|
||||||
|
NegativeSoft = #9a6c6f
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,15 @@ Accent = #ff1e1e1e
|
||||||
|
|
||||||
|
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff139740
|
AccentStrong = #ff139740
|
||||||
AccentSoft = #ffc9fd62
|
AccentSoft = #ffc9fd62
|
||||||
Positive = #6dd58c
|
Icon = #5db87c
|
||||||
Negative = #ff8a80
|
IconStrong = #aad9b9
|
||||||
|
IconSoft = #6e8d79
|
||||||
|
Positive = #6dd58c
|
||||||
|
PositiveStrong = #99e2ae
|
||||||
|
PositiveSoft = #698e74
|
||||||
|
Negative = #ff8a80
|
||||||
|
NegativeStrong = #ffbfb9
|
||||||
|
NegativeSoft = #a37c78
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,15 @@ Accent = #fff0f0f0
|
||||||
|
|
||||||
|
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff139740
|
AccentStrong = #ff139740
|
||||||
AccentSoft = #ffc9fd62
|
AccentSoft = #ffc9fd62
|
||||||
Positive = #1b7f3b
|
Icon = #107e36
|
||||||
Negative = #b3261e
|
IconStrong = #0b5826
|
||||||
|
IconSoft = #6c937a
|
||||||
|
Positive = #1b7f3b
|
||||||
|
PositiveStrong = #135929
|
||||||
|
PositiveSoft = #70927b
|
||||||
|
Negative = #b3261e
|
||||||
|
NegativeStrong = #7d1b15
|
||||||
|
NegativeSoft = #b37d7b
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #c9995a
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #b7823b
|
AccentStrong = #b7823b
|
||||||
AccentSoft = #e4c58f
|
AccentSoft = #e4c58f
|
||||||
|
Icon = #bc8c4a
|
||||||
|
IconStrong = #d4b68c
|
||||||
|
IconSoft = #86745a
|
||||||
Positive = #6dd58c
|
Positive = #6dd58c
|
||||||
|
PositiveStrong = #99e2ae
|
||||||
|
PositiveSoft = #588061
|
||||||
Negative = #ff8a80
|
Negative = #ff8a80
|
||||||
|
NegativeStrong = #ffada6
|
||||||
|
NegativeSoft = #996b66
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #a5712f
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #b7823b
|
AccentStrong = #b7823b
|
||||||
AccentSoft = #e4c58f
|
AccentSoft = #e4c58f
|
||||||
|
Icon = #735125
|
||||||
|
IconStrong = #4e3719
|
||||||
|
IconSoft = #817059
|
||||||
Positive = #1b7f3b
|
Positive = #1b7f3b
|
||||||
|
PositiveStrong = #0f4520
|
||||||
|
PositiveSoft = #61795d
|
||||||
Negative = #b3261e
|
Negative = #b3261e
|
||||||
|
NegativeStrong = #741914
|
||||||
|
NegativeSoft = #9a665b
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #6a4df0
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #5b3ee0
|
AccentStrong = #5b3ee0
|
||||||
AccentSoft = #a28bf7
|
AccentSoft = #a28bf7
|
||||||
|
Icon = #927fea
|
||||||
|
IconStrong = #b7aaf1
|
||||||
|
IconSoft = #736b97
|
||||||
Positive = #6dd58c
|
Positive = #6dd58c
|
||||||
|
PositiveStrong = #99e2ae
|
||||||
|
PositiveSoft = #517a63
|
||||||
Negative = #ff8a80
|
Negative = #ff8a80
|
||||||
|
NegativeStrong = #ffada6
|
||||||
|
NegativeSoft = #926567
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #5a3ee2
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #5b3ee0
|
AccentStrong = #5b3ee0
|
||||||
AccentSoft = #a28bf7
|
AccentSoft = #a28bf7
|
||||||
|
Icon = #5439d0
|
||||||
|
IconStrong = #39278e
|
||||||
|
IconSoft = #7569a4
|
||||||
Positive = #1b7f3b
|
Positive = #1b7f3b
|
||||||
|
PositiveStrong = #0e431f
|
||||||
|
PositiveSoft = #5c776d
|
||||||
Negative = #b3261e
|
Negative = #b3261e
|
||||||
|
NegativeStrong = #711913
|
||||||
|
NegativeSoft = #95646d
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@
|
||||||
# overrides. Only the identity and semantic app colors are provided here, so
|
# overrides. Only the identity and semantic app colors are provided here, so
|
||||||
# buttons keep their classic greens while the actual palette stays native to
|
# buttons keep their classic greens while the actual palette stays native to
|
||||||
# the OS.
|
# the OS.
|
||||||
|
# Icon, IconStrong and IconSoft are deliberately absent: a theme with no
|
||||||
|
# palette of its own derives its icon ink from the platform palette.
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff148c3c
|
AccentStrong = #ff148c3c
|
||||||
AccentSoft = #ff78c850
|
AccentSoft = #ff78c850
|
||||||
Positive = #ff6dd58c
|
Positive = #ff6dd58c
|
||||||
Negative = #ffff8a80
|
PositiveStrong = #ff99e2ae
|
||||||
|
PositiveSoft = #ff5e8873
|
||||||
|
Negative = #ffff8a80
|
||||||
|
NegativeStrong = #ffffb3ac
|
||||||
|
NegativeSoft = #ff9b7678
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@
|
||||||
# overrides. Only the identity and semantic app colors are provided here, so
|
# overrides. Only the identity and semantic app colors are provided here, so
|
||||||
# buttons keep their classic greens while the actual palette stays native to
|
# buttons keep their classic greens while the actual palette stays native to
|
||||||
# the OS.
|
# the OS.
|
||||||
|
# Icon, IconStrong and IconSoft are deliberately absent: a theme with no
|
||||||
|
# palette of its own derives its icon ink from the platform palette.
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #ff148c3c
|
AccentStrong = #ff148c3c
|
||||||
AccentSoft = #ff78c850
|
AccentSoft = #ff78c850
|
||||||
Positive = #ff1b7f3b
|
Positive = #ff1b7f3b
|
||||||
Negative = #ffb3261e
|
PositiveStrong = #ff104a22
|
||||||
|
PositiveSoft = #ff5d7f6f
|
||||||
|
Negative = #ffb3261e
|
||||||
|
NegativeStrong = #ff7c1b15
|
||||||
|
NegativeSoft = #ff9a6c6f
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #8fa6c0
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #54687e
|
AccentStrong = #54687e
|
||||||
AccentSoft = #9db0c4
|
AccentSoft = #9db0c4
|
||||||
|
Icon = #8392a1
|
||||||
|
IconStrong = #acb6c0
|
||||||
|
IconSoft = #6d737a
|
||||||
Positive = #6dd58c
|
Positive = #6dd58c
|
||||||
|
PositiveStrong = #99e2ae
|
||||||
|
PositiveSoft = #517c61
|
||||||
Negative = #ff8a80
|
Negative = #ff8a80
|
||||||
|
NegativeStrong = #ffada6
|
||||||
|
NegativeSoft = #926865
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,13 @@ Accent = #54687e
|
||||||
[AppColors]
|
[AppColors]
|
||||||
AccentStrong = #54687e
|
AccentStrong = #54687e
|
||||||
AccentSoft = #9db0c4
|
AccentSoft = #9db0c4
|
||||||
|
Icon = #4a5b6f
|
||||||
|
IconStrong = #333e4c
|
||||||
|
IconSoft = #6e7580
|
||||||
Positive = #1b7f3b
|
Positive = #1b7f3b
|
||||||
|
PositiveStrong = #0f4620
|
||||||
|
PositiveSoft = #5b7b6a
|
||||||
Negative = #b3261e
|
Negative = #b3261e
|
||||||
|
NegativeStrong = #761a14
|
||||||
|
NegativeSoft = #96696b
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <tuple>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
@ -332,28 +333,26 @@ TEST_F(ThemeIconRenderTest, SelectedAppColorKeepsAContrastingTint)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Line glyphs share the filled treatments' saturation floor.
|
* @brief A line glyph renders the authored role verbatim.
|
||||||
*
|
*
|
||||||
* The dark-scheme semantic tones are the visible case, but `AccentSoft` is below
|
* The emphasis colors are authored data now, so the paint path must not
|
||||||
* the floor in every shipped light palette, so the resolve path is exercised
|
* rewrite them. A muted quiet role stays muted on a line glyph; a badge disc or
|
||||||
* without swapping the theme. A line glyph next to a badge or a sticker has to
|
* a sticker fill boosts its own fill, because carrying a whole shape is a
|
||||||
* carry the same tone; reverting the floor here would leave it pale beside them.
|
* treatment concern rather than a role concern.
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, LineTintAppliesTheFillSaturationFloor)
|
TEST_F(ThemeIconRenderTest, LineTintRendersTheAuthoredRoleVerbatim)
|
||||||
{
|
{
|
||||||
const QColor raw = themeManager->appColor(AppColor::AccentSoft);
|
const QColor raw = themeManager->appColor(AppColor::AccentSoft);
|
||||||
ASSERT_TRUE(raw.isValid());
|
ASSERT_TRUE(raw.isValid());
|
||||||
ASSERT_LT(raw.hsvSaturation(), 170);
|
ASSERT_LT(raw.hsvSaturation(), 170);
|
||||||
|
|
||||||
const QColor saturated = ThemeIconFill::saturated(raw);
|
EXPECT_EQ(IconTint(AppColor::AccentSoft).resolve(QIcon::Normal), raw);
|
||||||
EXPECT_NE(saturated, raw);
|
|
||||||
EXPECT_EQ(IconTint(AppColor::AccentSoft).resolve(QIcon::Normal), saturated);
|
|
||||||
|
|
||||||
const QIcon icon = loadThemedIcon(QStringLiteral("icons/arrow_right"), AppColor::AccentSoft);
|
const QIcon icon = loadThemedIcon(QStringLiteral("icons/arrow_right"), AppColor::AccentSoft);
|
||||||
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, saturated), 0);
|
EXPECT_GT(countOpaquePixels(pixmap, raw), 0);
|
||||||
EXPECT_EQ(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. */
|
||||||
|
|
@ -1131,6 +1130,7 @@ struct InkPalette
|
||||||
QColor window;
|
QColor window;
|
||||||
QColor button;
|
QColor button;
|
||||||
QColor accent;
|
QColor accent;
|
||||||
|
QMap<AppColor::Role, QColor> roles;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1146,12 +1146,30 @@ InkPalette shippedPalette(const QString &theme, bool dark)
|
||||||
.absoluteFilePath(QStringLiteral("cockatrice/themes/%1/palette-default-%2.toml")
|
.absoluteFilePath(QStringLiteral("cockatrice/themes/%1/palette-default-%2.toml")
|
||||||
.arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
.arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
||||||
const PaletteConfig config = PaletteConfig::fromFile(file);
|
const PaletteConfig config = PaletteConfig::fromFile(file);
|
||||||
if (!config.hasPalette()) {
|
// A theme may author only [AppColors] (System does), so appColors alone do
|
||||||
|
// not mean it ships a palette: the keys have to be present.
|
||||||
|
if (config.colors.isEmpty()) {
|
||||||
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::WindowText), palette.color(QPalette::Active, QPalette::Window),
|
||||||
palette.color(QPalette::Active, QPalette::Button), palette.color(QPalette::Active, QPalette::Highlight)};
|
palette.color(QPalette::Active, QPalette::Button), palette.color(QPalette::Active, QPalette::Highlight),
|
||||||
|
config.appColors};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The [AppColors] a shipped theme authors, even when it ships no palette.
|
||||||
|
*
|
||||||
|
* System deliberately has no palette of its own, but it still authors its
|
||||||
|
* semantic roles; the icon roles stay unset there so they follow the platform.
|
||||||
|
*/
|
||||||
|
QMap<AppColor::Role, QColor> shippedAppColors(const QString &theme, bool dark)
|
||||||
|
{
|
||||||
|
const QString file =
|
||||||
|
QDir(QStringLiteral(COCKATRICE_SOURCE_DIR))
|
||||||
|
.absoluteFilePath(QStringLiteral("cockatrice/themes/%1/palette-default-%2.toml")
|
||||||
|
.arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
||||||
|
return PaletteConfig::fromFile(file).appColors;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
@ -1365,107 +1383,224 @@ bool applyShippedPalette(const QString &theme, bool dark)
|
||||||
qApp->setPalette(config.apply(QPalette()));
|
qApp->setPalette(config.apply(QPalette()));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QColor surfaceColor()
|
|
||||||
{
|
|
||||||
return qApp->palette().color(QPalette::Active, QPalette::Window);
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The primary action of a surface takes the accent, not a darker gray.
|
* @brief Emphasis selects the authored variant of a family, not a computed color.
|
||||||
*
|
*
|
||||||
* A glyph hierarchy cannot be built out of "dark" and "darker": at any usable
|
* The theme stores one color per level, so the mapping is a lookup: a base role
|
||||||
* contrast a neutral ink is already near the palette's pole, so the top level
|
* follows the caller's emphasis, while naming a variant pins that level at every
|
||||||
* has to be a different color. The accent is the palette's own answer to "which
|
* emphasis. This is the contract that makes the palette editor's rows the whole
|
||||||
* color means action", so primary uses it instead of inventing a token.
|
* source of truth.
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, PrimaryEmphasisUsesTheAccent)
|
TEST_F(ThemeIconRenderTest, EmphasisSelectsTheAuthoredVariant)
|
||||||
{
|
{
|
||||||
ASSERT_TRUE(applyShippedPalette(QStringLiteral("Fabric"), false));
|
EXPECT_EQ(IconTint().resolve(QIcon::Normal), themeManager->appColor(AppColor::Icon));
|
||||||
|
EXPECT_EQ(IconTint::primary().resolve(QIcon::Normal), themeManager->appColor(AppColor::IconStrong));
|
||||||
|
EXPECT_EQ(IconTint::quiet().resolve(QIcon::Normal), themeManager->appColor(AppColor::IconSoft));
|
||||||
|
EXPECT_EQ(IconTint(QPalette::WindowText, IconEmphasis::Primary).resolve(QIcon::Normal),
|
||||||
|
themeManager->appColor(AppColor::IconStrong));
|
||||||
|
|
||||||
const QColor secondary = IconTint().resolve(QIcon::Normal);
|
struct Family
|
||||||
const QColor primary = IconTint::primary().resolve(QIcon::Normal);
|
{
|
||||||
|
AppColor::Role base;
|
||||||
|
AppColor::Role strong;
|
||||||
|
AppColor::Role soft;
|
||||||
|
};
|
||||||
|
|
||||||
EXPECT_NE(primary, secondary);
|
for (const Family &family : {Family{AppColor::Icon, AppColor::IconStrong, AppColor::IconSoft},
|
||||||
EXPECT_GT(primary.blue(), primary.red()) << "the primary glyph did not take the accent's blue";
|
Family{AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft},
|
||||||
EXPECT_GE(ThemeIconInk::contrastRatio(primary, surfaceColor()), ThemeIconInk::primaryContrastFloor());
|
Family{AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}}) {
|
||||||
EXPECT_GE(ThemeIconInk::contrastRatio(primary, surfaceColor()),
|
EXPECT_EQ(IconTint(family.base, IconEmphasis::Primary).resolve(QIcon::Normal),
|
||||||
ThemeIconInk::contrastRatio(secondary, surfaceColor()));
|
themeManager->appColor(family.strong));
|
||||||
|
EXPECT_EQ(IconTint(family.base).resolve(QIcon::Normal), themeManager->appColor(family.base));
|
||||||
|
EXPECT_EQ(IconTint(family.base, IconEmphasis::Quiet).resolve(QIcon::Normal),
|
||||||
|
themeManager->appColor(family.soft));
|
||||||
|
// Naming a variant is explicit: emphasis does not rewrite it.
|
||||||
|
EXPECT_EQ(IconTint(family.strong, IconEmphasis::Quiet).resolve(QIcon::Normal),
|
||||||
|
themeManager->appColor(family.strong));
|
||||||
|
EXPECT_EQ(IconTint(family.soft, IconEmphasis::Primary).resolve(QIcon::Normal),
|
||||||
|
themeManager->appColor(family.soft));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The primary level out-reads the default one on both schemes.
|
* @brief The authored primary level out-reads the default one on both schemes.
|
||||||
*
|
*
|
||||||
* The default ink borrows enough accent to sit off the palette's pole (5.4:1 on
|
* The three levels are authored against the window and button surfaces, so the
|
||||||
* light, 6.4:1 on dark), so the 7:1 primary floor is measurably above it in both
|
* ordering has to be visible on both, not just on the window the palette is
|
||||||
* schemes instead of being lost against a near-black or near-white default.
|
* usually previewed against.
|
||||||
* Hue alone would not keep the order; the contrast target does.
|
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, PrimaryOutReadsSecondaryOnBothSchemes)
|
TEST_F(ThemeIconRenderTest, PrimaryOutReadsSecondaryOnBothSchemes)
|
||||||
{
|
{
|
||||||
for (const bool dark : {false, true}) {
|
for (const bool dark : {false, true}) {
|
||||||
ASSERT_TRUE(applyShippedPalette(QStringLiteral("Fabric"), dark));
|
const InkPalette palette = shippedPalette(QStringLiteral("Fabric"), dark);
|
||||||
const QColor surface = surfaceColor();
|
ASSERT_TRUE(palette.text.isValid()) << (dark ? "dark" : "light");
|
||||||
const QColor secondary = IconTint().resolve(QIcon::Normal);
|
const QColor secondary = palette.roles.value(AppColor::Icon);
|
||||||
const QColor primary = IconTint::primary().resolve(QIcon::Normal);
|
const QColor primary = palette.roles.value(AppColor::IconStrong);
|
||||||
EXPECT_NE(primary, secondary) << (dark ? "dark" : "light");
|
ASSERT_TRUE(secondary.isValid() && primary.isValid()) << (dark ? "dark" : "light");
|
||||||
EXPECT_GT(ThemeIconInk::contrastRatio(primary, surface), ThemeIconInk::contrastRatio(secondary, surface))
|
|
||||||
<< (dark ? "dark" : "light");
|
for (const QColor &surface : {palette.window, palette.button}) {
|
||||||
EXPECT_GE(ThemeIconInk::contrastRatio(primary, surface), ThemeIconInk::primaryContrastFloor())
|
EXPECT_GT(ThemeIconInk::contrastRatio(primary, surface), ThemeIconInk::contrastRatio(secondary, surface))
|
||||||
<< (dark ? "dark" : "light");
|
<< (dark ? "dark" : "light");
|
||||||
|
EXPECT_GE(ThemeIconInk::contrastRatio(primary, surface), ThemeIconInk::primaryContrastFloor())
|
||||||
|
<< (dark ? "dark" : "light");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief A quiet glyph recedes toward its surface without dropping below 3:1. */
|
/** @brief A quiet glyph recedes toward its surface without dropping below 3:1. */
|
||||||
TEST_F(ThemeIconRenderTest, QuietEmphasisRecedesButStaysLegible)
|
TEST_F(ThemeIconRenderTest, QuietEmphasisRecedesButStaysLegible)
|
||||||
{
|
{
|
||||||
ASSERT_TRUE(applyShippedPalette(QStringLiteral("Fabric"), false));
|
for (const bool dark : {false, true}) {
|
||||||
|
const InkPalette palette = shippedPalette(QStringLiteral("Fabric"), dark);
|
||||||
|
ASSERT_TRUE(palette.text.isValid()) << (dark ? "dark" : "light");
|
||||||
|
const QColor secondary = palette.roles.value(AppColor::Icon);
|
||||||
|
const QColor quiet = palette.roles.value(AppColor::IconSoft);
|
||||||
|
ASSERT_TRUE(secondary.isValid() && quiet.isValid()) << (dark ? "dark" : "light");
|
||||||
|
|
||||||
const QColor secondary = IconTint().resolve(QIcon::Normal);
|
for (const QColor &surface : {palette.window, palette.button}) {
|
||||||
const QColor quiet = IconTint::quiet().resolve(QIcon::Normal);
|
EXPECT_LT(ThemeIconInk::contrastRatio(quiet, surface), ThemeIconInk::contrastRatio(secondary, surface))
|
||||||
|
<< "the quiet glyph does not recede";
|
||||||
EXPECT_LT(ThemeIconInk::contrastRatio(quiet, surfaceColor()),
|
EXPECT_GE(ThemeIconInk::contrastRatio(quiet, surface), ThemeIconInk::quietContrastFloor())
|
||||||
ThemeIconInk::contrastRatio(secondary, surfaceColor()))
|
<< (dark ? "dark" : "light");
|
||||||
<< "the quiet glyph does not recede";
|
}
|
||||||
EXPECT_GE(ThemeIconInk::contrastRatio(quiet, surfaceColor()), ThemeIconInk::quietContrastFloor());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Emphasis changes how loud a semantic color is, never which color it is. */
|
/** @brief Every authored emphasis level keeps a semantic family's hue. */
|
||||||
TEST_F(ThemeIconRenderTest, EmphasisKeepsASemanticHue)
|
TEST_F(ThemeIconRenderTest, EmphasisKeepsASemanticHue)
|
||||||
{
|
{
|
||||||
ASSERT_TRUE(applyShippedPalette(QStringLiteral("Fabric"), false));
|
const QString themesRoot =
|
||||||
|
QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(QStringLiteral("cockatrice/themes"));
|
||||||
|
const QStringList themes = QDir(themesRoot).entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
|
||||||
|
ASSERT_FALSE(themes.isEmpty());
|
||||||
|
|
||||||
for (const IconEmphasis emphasis : {IconEmphasis::Primary, IconEmphasis::Secondary, IconEmphasis::Quiet}) {
|
int checked = 0;
|
||||||
const QColor ink = IconTint(AppColor::Positive, emphasis).resolve(QIcon::Normal);
|
for (const QString &theme : themes) {
|
||||||
EXPECT_GT(ink.green(), ink.red()) << static_cast<int>(emphasis);
|
for (const bool dark : {false, true}) {
|
||||||
EXPECT_GT(ink.green(), ink.blue()) << static_cast<int>(emphasis);
|
const QMap<AppColor::Role, QColor> roles = shippedAppColors(theme, dark);
|
||||||
|
|
||||||
|
for (const AppColor::Role role : {AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft}) {
|
||||||
|
const QColor ink = roles.value(role);
|
||||||
|
if (!ink.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++checked;
|
||||||
|
EXPECT_GT(ink.green(), ink.red()) << theme.toStdString() << static_cast<int>(role);
|
||||||
|
EXPECT_GT(ink.green(), ink.blue()) << theme.toStdString() << static_cast<int>(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const AppColor::Role role : {AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}) {
|
||||||
|
const QColor ink = roles.value(role);
|
||||||
|
if (!ink.isValid()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++checked;
|
||||||
|
EXPECT_GT(ink.red(), ink.green()) << theme.toStdString() << static_cast<int>(role);
|
||||||
|
EXPECT_GT(ink.red(), ink.blue()) << theme.toStdString() << static_cast<int>(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
EXPECT_GT(checked, 0) << "no shipped theme authored a semantic emphasis level";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Every palette-shipping theme authors the whole emphasis ladder.
|
||||||
|
*
|
||||||
|
* The palette editor treats an unset role as "derive it", so a missing key is
|
||||||
|
* not a crash: it silently reintroduces the computed colors this change exists
|
||||||
|
* to remove. Treat a missing role as a test failure instead, and keep the same
|
||||||
|
* contrast contract the old computed levels had.
|
||||||
|
*/
|
||||||
|
TEST(IconInk, ShippedPalettesAuthorTheEmphasisLadder)
|
||||||
|
{
|
||||||
|
const QString themesRoot =
|
||||||
|
QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)).absoluteFilePath(QStringLiteral("cockatrice/themes"));
|
||||||
|
const QStringList themes = QDir(themesRoot).entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
|
||||||
|
ASSERT_FALSE(themes.isEmpty());
|
||||||
|
|
||||||
|
int checked = 0;
|
||||||
|
for (const QString &theme : themes) {
|
||||||
|
for (const bool dark : {false, true}) {
|
||||||
|
const InkPalette palette = shippedPalette(theme, dark);
|
||||||
|
if (!palette.text.isValid()) {
|
||||||
|
continue; // System ships no palette and derives its icon ink.
|
||||||
|
}
|
||||||
|
++checked;
|
||||||
|
|
||||||
|
for (const AppColor::Role role : {AppColor::Icon, AppColor::IconStrong, AppColor::IconSoft,
|
||||||
|
AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft,
|
||||||
|
AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}) {
|
||||||
|
ASSERT_TRUE(palette.roles.contains(role))
|
||||||
|
<< theme.toStdString() << (dark ? " dark" : " light") << static_cast<int>(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const QColor &surface : {palette.window, palette.button}) {
|
||||||
|
const double secondary = ThemeIconInk::contrastRatio(palette.roles.value(AppColor::Icon), surface);
|
||||||
|
const double primary = ThemeIconInk::contrastRatio(palette.roles.value(AppColor::IconStrong), surface);
|
||||||
|
const double quiet = ThemeIconInk::contrastRatio(palette.roles.value(AppColor::IconSoft), surface);
|
||||||
|
EXPECT_GE(secondary, ThemeIconInk::labelContrastFloor())
|
||||||
|
<< theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
EXPECT_GE(primary, ThemeIconInk::primaryContrastFloor())
|
||||||
|
<< theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
EXPECT_GT(primary, secondary) << theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
EXPECT_GE(quiet, ThemeIconInk::quietContrastFloor())
|
||||||
|
<< theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
EXPECT_LT(quiet, secondary) << theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
|
||||||
|
for (const auto &[base, strong, soft] :
|
||||||
|
{std::tuple{AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft},
|
||||||
|
std::tuple{AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}}) {
|
||||||
|
const double baseContrast = ThemeIconInk::contrastRatio(palette.roles.value(base), surface);
|
||||||
|
EXPECT_GE(ThemeIconInk::contrastRatio(palette.roles.value(soft), surface),
|
||||||
|
ThemeIconInk::quietContrastFloor())
|
||||||
|
<< theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
EXPECT_LT(ThemeIconInk::contrastRatio(palette.roles.value(soft), surface), baseContrast)
|
||||||
|
<< theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
EXPECT_GT(ThemeIconInk::contrastRatio(palette.roles.value(strong), surface), baseContrast)
|
||||||
|
<< theme.toStdString() << (dark ? " dark" : " light");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_GT(checked, 0) << "no shipped palette was loadable";
|
||||||
|
|
||||||
|
// The System theme is the deliberate exception: it authors its semantic
|
||||||
|
// levels but leaves the neutral family to the platform palette.
|
||||||
|
const QMap<AppColor::Role, QColor> system = shippedAppColors(QStringLiteral("System"), false);
|
||||||
|
EXPECT_TRUE(system.contains(AppColor::PositiveStrong));
|
||||||
|
EXPECT_TRUE(system.contains(AppColor::NegativeSoft));
|
||||||
|
EXPECT_FALSE(system.contains(AppColor::Icon));
|
||||||
|
EXPECT_FALSE(system.contains(AppColor::IconStrong));
|
||||||
|
EXPECT_FALSE(system.contains(AppColor::IconSoft));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Every emphasis keeps the selected-row swap to HighlightedText. */
|
/** @brief Every emphasis keeps the selected-row swap to HighlightedText. */
|
||||||
TEST_F(ThemeIconRenderTest, SelectedStillFallsBackToHighlightedText)
|
TEST_F(ThemeIconRenderTest, SelectedStillFallsBackToHighlightedText)
|
||||||
{
|
{
|
||||||
|
const QColor tone = IconTint(AppColor::AccentSoft).resolve(QIcon::Normal);
|
||||||
|
ASSERT_TRUE(tone.isValid());
|
||||||
|
|
||||||
QPalette palette = qApp->palette();
|
QPalette palette = qApp->palette();
|
||||||
const QColor selectedText(QStringLiteral("#222222"));
|
const QColor selectedText(QStringLiteral("#222222"));
|
||||||
palette.setColor(QPalette::Active, QPalette::Highlight, QColor(QStringLiteral("#000000")));
|
palette.setColor(QPalette::Active, QPalette::Highlight, tone);
|
||||||
palette.setColor(QPalette::Active, QPalette::HighlightedText, selectedText);
|
palette.setColor(QPalette::Active, QPalette::HighlightedText, selectedText);
|
||||||
qApp->setPalette(palette);
|
qApp->setPalette(palette);
|
||||||
|
|
||||||
// Black ink on a black highlight is illegible, so the selection wins for
|
// Tone on a highlight of the same color is illegible, so the selection wins
|
||||||
// both the default and the quiet level.
|
// for both the default and an explicitly tinted glyph.
|
||||||
EXPECT_EQ(IconTint().resolve(QIcon::Selected), selectedText);
|
EXPECT_EQ(IconTint().resolve(QIcon::Selected), selectedText);
|
||||||
EXPECT_EQ(IconTint::quiet().resolve(QIcon::Selected), selectedText);
|
EXPECT_EQ(IconTint(AppColor::AccentSoft).resolve(QIcon::Selected), selectedText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Writes the emphasis ladder sheets for every shipped palette theme.
|
* @brief Writes the authored emphasis ladder when asked; skipped otherwise.
|
||||||
*
|
*
|
||||||
* One sheet per theme and scheme: the neutral, positive and negative families at
|
* One sheet per scheme showing what a theme author actually stores: the neutral,
|
||||||
* their quiet, secondary and primary levels, each as a line glyph, a sticker and
|
* positive and negative families, each at its quiet, secondary and primary
|
||||||
* a badge, three glyphs per family, at 16 and 24 px, on the window and button
|
* level. The paint path is a lookup, so this is the visual proof that the
|
||||||
* surfaces. The tone is the one the emphasis rule resolves for that surface, so
|
* palette rows and the rendered glyphs agree.
|
||||||
* the sheet is what the engine paints rather than a hand-picked sample.
|
|
||||||
*/
|
*/
|
||||||
TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested)
|
TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested)
|
||||||
{
|
{
|
||||||
|
|
@ -1476,164 +1611,91 @@ TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested)
|
||||||
ASSERT_TRUE(QDir().mkpath(outDir));
|
ASSERT_TRUE(QDir().mkpath(outDir));
|
||||||
|
|
||||||
constexpr int MARGIN = 24;
|
constexpr int MARGIN = 24;
|
||||||
constexpr int LABEL_WIDTH = 230;
|
constexpr int HEADER_HEIGHT = 34;
|
||||||
constexpr int CELL_WIDTH = 166;
|
constexpr int ROW_HEIGHT = 38;
|
||||||
constexpr int ROW_HEIGHT = 58;
|
constexpr int ICON_SIZE = 20;
|
||||||
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 SIZES[] = {16, 24};
|
constexpr int WIDTH = 760;
|
||||||
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 Family
|
struct Family
|
||||||
{
|
{
|
||||||
const char *name;
|
const char *name;
|
||||||
const char *glyphs[3];
|
const char *glyph;
|
||||||
|
AppColor::Role quiet;
|
||||||
|
AppColor::Role secondary;
|
||||||
|
AppColor::Role primary;
|
||||||
|
};
|
||||||
|
struct Sample
|
||||||
|
{
|
||||||
|
const char *level;
|
||||||
AppColor::Role role;
|
AppColor::Role role;
|
||||||
bool neutral;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const QList<Family> families = {
|
const QList<Family> families = {
|
||||||
{"Neutral", {"settings", "info", "player"}, AppColor::Positive, true},
|
{"Neutral", "settings", AppColor::IconSoft, AppColor::Icon, AppColor::IconStrong},
|
||||||
{"Positive", {"increment", "ready_start", "arrow_up"}, AppColor::Positive, false},
|
{"Positive", "increment", AppColor::PositiveSoft, AppColor::Positive, AppColor::PositiveStrong},
|
||||||
{"Negative", {"decrement", "not_ready_start", "remove_row"}, AppColor::Negative, false},
|
{"Negative", "decrement", AppColor::NegativeSoft, AppColor::Negative, AppColor::NegativeStrong},
|
||||||
};
|
|
||||||
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) {
|
for (const bool dark : {false, true}) {
|
||||||
QSvgRenderer renderer(resolveSvgColorSentinel(svg, ink.name(QColor::HexRgb).toLatin1()));
|
const InkPalette palette = shippedPalette(QStringLiteral("Fabric"), dark);
|
||||||
QPixmap pixmap(size);
|
ASSERT_TRUE(palette.text.isValid());
|
||||||
pixmap.fill(Qt::transparent);
|
ASSERT_TRUE(applyShippedPalette(QStringLiteral("Fabric"), dark));
|
||||||
QPainter painter(&pixmap);
|
|
||||||
|
const int logicalHeight = MARGIN + 30 + families.size() * (HEADER_HEIGHT + 3 * ROW_HEIGHT) + MARGIN;
|
||||||
|
QImage image(QSize(qRound(WIDTH * DPR), qRound(logicalHeight * DPR)), QImage::Format_ARGB32_Premultiplied);
|
||||||
|
image.fill(palette.window);
|
||||||
|
QPainter painter(&image);
|
||||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
renderer.render(&painter);
|
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
return pixmap;
|
painter.scale(DPR, DPR);
|
||||||
};
|
|
||||||
|
|
||||||
const int panelWidth = LABEL_WIDTH + static_cast<int>(std::size(TREATMENTS)) * CELL_WIDTH + PANEL_PADDING;
|
qreal y = MARGIN;
|
||||||
const int panelHeight =
|
painter.setPen(palette.text);
|
||||||
PANEL_TITLE + COLUMN_HEADER + static_cast<int>(families.size()) * 3 * ROW_HEIGHT + PANEL_PADDING;
|
painter.setFont(QFont(QStringLiteral("Sans"), 10, QFont::DemiBold));
|
||||||
const int logicalWidth = 2 * MARGIN + panelWidth;
|
painter.drawText(QPointF(MARGIN, y), (dark ? QStringLiteral("Dark scheme") : QStringLiteral("Light scheme")) +
|
||||||
const int logicalHeight = MARGIN + 30 + 2 * (panelHeight + 24) + MARGIN;
|
QStringLiteral(" — authored emphasis ladder (Fabric)"));
|
||||||
|
y += 30;
|
||||||
for (const QString &theme : themes) {
|
|
||||||
for (const bool dark : {false, true}) {
|
|
||||||
const InkPalette palette = shippedPalette(theme, dark);
|
|
||||||
ASSERT_TRUE(palette.text.isValid()) << theme.toStdString();
|
|
||||||
|
|
||||||
// The emphasis rule reads the live palette, so apply the theme
|
|
||||||
// before resolving tones for the sheet.
|
|
||||||
ASSERT_TRUE(applyShippedPalette(theme, dark));
|
|
||||||
|
|
||||||
const auto toneFor = [&](const Family &family, int level) {
|
|
||||||
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]);
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
for (const Family &family : families) {
|
||||||
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, MARGIN),
|
painter.drawText(QPointF(MARGIN, y), QString::fromLatin1(family.name));
|
||||||
QStringLiteral("%1 — %2 scheme, emphasis across treatments")
|
y += HEADER_HEIGHT;
|
||||||
.arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
|
||||||
|
|
||||||
struct Surface
|
const QList<Sample> samples = {
|
||||||
{
|
{"quiet", family.quiet},
|
||||||
const char *name;
|
{"secondary", family.secondary},
|
||||||
QColor background;
|
{"primary", family.primary},
|
||||||
QColor text;
|
|
||||||
};
|
|
||||||
const QList<Surface> surfaces = {
|
|
||||||
{"Window surface", palette.window, palette.text},
|
|
||||||
{"Button surface", palette.button, palette.text},
|
|
||||||
};
|
};
|
||||||
|
for (const Sample &sample : samples) {
|
||||||
|
const QColor ink = palette.roles.value(sample.role);
|
||||||
|
ASSERT_TRUE(ink.isValid()) << family.name << " " << sample.level;
|
||||||
|
|
||||||
qreal panelTop = MARGIN + 30;
|
const QByteArray svg = readIconSvg(family.glyph);
|
||||||
for (const Surface &surface : surfaces) {
|
ASSERT_FALSE(svg.isEmpty()) << family.glyph;
|
||||||
painter.setPen(Qt::NoPen);
|
QSvgRenderer renderer(resolveSvgColorSentinel(svg, ink.name(QColor::HexRgb).toLatin1()));
|
||||||
painter.setBrush(surface.background);
|
QPixmap glyph(QSize(ICON_SIZE, ICON_SIZE));
|
||||||
painter.drawRoundedRect(QRectF(MARGIN, panelTop, panelWidth, panelHeight), 8, 8);
|
glyph.fill(Qt::transparent);
|
||||||
|
QPainter glyphPainter(&glyph);
|
||||||
|
glyphPainter.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
renderer.render(&glyphPainter);
|
||||||
|
glyphPainter.end();
|
||||||
|
painter.drawPixmap(QPointF(MARGIN + 8, y - ICON_SIZE + 4), glyph);
|
||||||
|
|
||||||
const qreal contentLeft = MARGIN + PANEL_PADDING;
|
painter.setPen(palette.text);
|
||||||
painter.setPen(surface.text);
|
painter.setFont(QFont(QStringLiteral("Sans"), 10));
|
||||||
painter.setFont(QFont(QStringLiteral("Sans"), 9, QFont::DemiBold));
|
painter.drawText(QPointF(MARGIN + ICON_SIZE + 24, y), QString::fromLatin1(sample.level));
|
||||||
painter.drawText(QPointF(contentLeft, panelTop + 18), QString::fromLatin1(surface.name));
|
painter.setPen(palette.text);
|
||||||
|
painter.setFont(QFont(QStringLiteral("Mono"), 9));
|
||||||
painter.setFont(QFont(QStringLiteral("Sans"), 8));
|
painter.drawText(QPointF(MARGIN + 200, y), ink.name(QColor::HexRgb));
|
||||||
for (std::size_t treatment = 0; treatment < std::size(TREATMENTS); ++treatment) {
|
y += ROW_HEIGHT;
|
||||||
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 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panelTop += panelHeight + 24;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
painter.end();
|
|
||||||
const QString path = QDir(outDir).absoluteFilePath(
|
|
||||||
QStringLiteral("emphasis-preview-%1-%2.png")
|
|
||||||
.arg(theme.toLower(), dark ? QStringLiteral("dark") : QStringLiteral("light")));
|
|
||||||
ASSERT_TRUE(image.save(path)) << path.toStdString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
painter.end();
|
||||||
|
const QString path = QDir(outDir).absoluteFilePath(dark ? QStringLiteral("emphasis-preview-dark.png")
|
||||||
|
: QStringLiteral("emphasis-preview-light.png"));
|
||||||
|
ASSERT_TRUE(image.save(path)) << path.toStdString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,11 +153,26 @@ TEST(ThemeAppColorTest, SemanticDefaultsFollowTheScheme)
|
||||||
{
|
{
|
||||||
EXPECT_EQ(defaultAppColor(AppColor::Positive, false), QColor(QStringLiteral("#1b7f3b")));
|
EXPECT_EQ(defaultAppColor(AppColor::Positive, false), QColor(QStringLiteral("#1b7f3b")));
|
||||||
EXPECT_EQ(defaultAppColor(AppColor::Positive, true), QColor(QStringLiteral("#6dd58c")));
|
EXPECT_EQ(defaultAppColor(AppColor::Positive, true), QColor(QStringLiteral("#6dd58c")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::PositiveStrong, false), QColor(QStringLiteral("#104a22")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::PositiveStrong, true), QColor(QStringLiteral("#99e2ae")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::PositiveSoft, false), QColor(QStringLiteral("#5d7f6f")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::PositiveSoft, true), QColor(QStringLiteral("#5e8873")));
|
||||||
EXPECT_EQ(defaultAppColor(AppColor::Negative, false), QColor(QStringLiteral("#b3261e")));
|
EXPECT_EQ(defaultAppColor(AppColor::Negative, false), QColor(QStringLiteral("#b3261e")));
|
||||||
EXPECT_EQ(defaultAppColor(AppColor::Negative, true), QColor(QStringLiteral("#ff8a80")));
|
EXPECT_EQ(defaultAppColor(AppColor::Negative, true), QColor(QStringLiteral("#ff8a80")));
|
||||||
// Accent roles are derived from the palette, so they have no static default.
|
EXPECT_EQ(defaultAppColor(AppColor::NegativeStrong, false), QColor(QStringLiteral("#7c1b15")));
|
||||||
EXPECT_FALSE(defaultAppColor(AppColor::AccentStrong, false).isValid());
|
EXPECT_EQ(defaultAppColor(AppColor::NegativeStrong, true), QColor(QStringLiteral("#ffb3ac")));
|
||||||
EXPECT_FALSE(defaultAppColor(AppColor::AccentSoft, true).isValid());
|
EXPECT_EQ(defaultAppColor(AppColor::NegativeSoft, false), QColor(QStringLiteral("#9a6c6f")));
|
||||||
|
EXPECT_EQ(defaultAppColor(AppColor::NegativeSoft, true), QColor(QStringLiteral("#9b7678")));
|
||||||
|
// Accent and neutral icon roles are derived from the palette, so they have
|
||||||
|
// no static default. A theme with no palette (System) keeps following the
|
||||||
|
// platform instead of pinning one scheme's ink.
|
||||||
|
for (const bool dark : {false, true}) {
|
||||||
|
EXPECT_FALSE(defaultAppColor(AppColor::AccentStrong, dark).isValid());
|
||||||
|
EXPECT_FALSE(defaultAppColor(AppColor::AccentSoft, dark).isValid());
|
||||||
|
EXPECT_FALSE(defaultAppColor(AppColor::Icon, dark).isValid());
|
||||||
|
EXPECT_FALSE(defaultAppColor(AppColor::IconStrong, dark).isValid());
|
||||||
|
EXPECT_FALSE(defaultAppColor(AppColor::IconSoft, dark).isValid());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
|
TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
|
||||||
|
|
@ -166,10 +181,20 @@ TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
|
||||||
ASSERT_TRUE(dir.isValid());
|
ASSERT_TRUE(dir.isValid());
|
||||||
|
|
||||||
PaletteConfig config;
|
PaletteConfig config;
|
||||||
const QColor positive(QStringLiteral("#1b7f3b"));
|
const QMap<AppColor::Role, QColor> roles = {
|
||||||
const QColor negative(QStringLiteral("#b3261e"));
|
{AppColor::Icon, QColor(QStringLiteral("#2f59ad"))},
|
||||||
config.appColors[AppColor::Positive] = positive;
|
{AppColor::IconStrong, QColor(QStringLiteral("#213e79"))},
|
||||||
config.appColors[AppColor::Negative] = negative;
|
{AppColor::IconSoft, QColor(QStringLiteral("#687899"))},
|
||||||
|
{AppColor::Positive, QColor(QStringLiteral("#1b7f3b"))},
|
||||||
|
{AppColor::PositiveStrong, QColor(QStringLiteral("#104a22"))},
|
||||||
|
{AppColor::PositiveSoft, QColor(QStringLiteral("#5d7f6f"))},
|
||||||
|
{AppColor::Negative, QColor(QStringLiteral("#b3261e"))},
|
||||||
|
{AppColor::NegativeStrong, QColor(QStringLiteral("#7c1b15"))},
|
||||||
|
{AppColor::NegativeSoft, QColor(QStringLiteral("#9a6c6f"))},
|
||||||
|
};
|
||||||
|
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
|
||||||
|
config.appColors[it.key()] = it.value();
|
||||||
|
}
|
||||||
|
|
||||||
const QString path = QDir(dir.path()).absoluteFilePath(QStringLiteral("palette-light.toml"));
|
const QString path = QDir(dir.path()).absoluteFilePath(QStringLiteral("palette-light.toml"));
|
||||||
QFile file(path);
|
QFile file(path);
|
||||||
|
|
@ -178,10 +203,10 @@ TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
const PaletteConfig parsed = PaletteConfig::fromFile(path);
|
const PaletteConfig parsed = PaletteConfig::fromFile(path);
|
||||||
ASSERT_TRUE(parsed.appColors.contains(AppColor::Positive));
|
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
|
||||||
ASSERT_TRUE(parsed.appColors.contains(AppColor::Negative));
|
ASSERT_TRUE(parsed.appColors.contains(it.key()));
|
||||||
EXPECT_EQ(parsed.appColors.value(AppColor::Positive), positive);
|
EXPECT_EQ(parsed.appColors.value(it.key()), it.value());
|
||||||
EXPECT_EQ(parsed.appColors.value(AppColor::Negative), negative);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and
|
// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue