[Icons] Add an Informational semantic color family

Face-down play and the exile zone are states that are neither a gain nor a
loss, and the pre-#7595 art painted them blue (arrow_right_blue). #7595 dropped
that hue, and the icon engine has had only Positive and Negative since, so both
states fall back to neutral ink.

Add the family with the same three-level ladder the other semantic roles use:
authored roles per theme, scheme defaults otherwise, an emphasis mapping, the
palette-editor rows and the generator seed. The defaults clear the 4.5:1 label
floor on both schemes and the ladder's saturation ordering; the existing
emphasis, hue and preview tests now cover the new family. Preview sheets that
name roles resolve an unauthored one through the same default the client
paints.
This commit is contained in:
Lukas Brübach 2026-11-08 20:07:46 +01:00
parent 2b3a04857f
commit 0aa0407f16
10 changed files with 93 additions and 23 deletions

View file

@ -178,9 +178,12 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch
cfg.appColors[AppColor::IconStrong] = ThemeIconInk::forEmphasis(iconInk, iconSurface, IconEmphasis::Primary);
cfg.appColors[AppColor::IconSoft] = ThemeIconInk::forEmphasis(iconInk, iconSurface, IconEmphasis::Quiet);
// Semantic roles stay independent of the accent: a generated palette must
// still paint add/ready green and remove/not-ready red.
for (const AppColor::Role role : {AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft,
AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}) {
// still paint add/ready green, remove/not-ready red, and the neutral
// information state blue.
for (const AppColor::Role role :
{AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft, AppColor::Negative,
AppColor::NegativeStrong, AppColor::NegativeSoft, AppColor::Informational, AppColor::InformationalStrong,
AppColor::InformationalSoft}) {
cfg.appColors[role] = defaultAppColor(role, dark);
}

View file

@ -78,6 +78,11 @@ static const QMap<AppColor::Role, const char *> APP_ROLE_DESCRIPTIONS = {
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")},
{AppColor::Informational,
QT_TRANSLATE_NOOP("PaletteGridWidget",
"Informational state that is neither a gain nor a loss (face-down, exile)")},
{AppColor::InformationalStrong, QT_TRANSLATE_NOOP("PaletteGridWidget", "Informational state at primary emphasis")},
{AppColor::InformationalSoft, QT_TRANSLATE_NOOP("PaletteGridWidget", "Informational state at quiet emphasis")},
};
PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent)

View file

@ -482,6 +482,15 @@ QColor defaultAppColor(AppColor::Role role, bool dark)
return QColor(dark ? QStringLiteral("#ff8378") : QStringLiteral("#da0b00"));
case AppColor::NegativeSoft:
return QColor(dark ? QStringLiteral("#c79e9b") : QStringLiteral("#935f5c"));
// Informational is the third semantic family: a state that is neither a
// gain nor a loss (a face-down play, the exile zone). It carries the
// same three-level ladder so its emphasis is authored, not computed.
case AppColor::Informational:
return QColor(dark ? QStringLiteral("#7aa9f2") : QStringLiteral("#1c64c4"));
case AppColor::InformationalStrong:
return QColor(dark ? QStringLiteral("#69aeff") : QStringLiteral("#0a4fb5"));
case AppColor::InformationalSoft:
return QColor(dark ? QStringLiteral("#9db2cd") : QStringLiteral("#526a8a"));
}
return {};

View file

@ -15,11 +15,11 @@
// 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.
// glyph ink; IconStrong and IconSoft are its primary and quiet levels. Positive,
// Negative and Informational 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
{
Q_NAMESPACE
@ -36,6 +36,9 @@ enum Role
Negative,
NegativeStrong,
NegativeSoft,
Informational,
InformationalStrong,
InformationalSoft,
};
Q_ENUM_NS(Role)
} // namespace AppColor

View file

@ -54,6 +54,10 @@ AppColor::Role emphasisRole(AppColor::Role role, IconEmphasis emphasis)
return emphasis == IconEmphasis::Primary ? AppColor::NegativeStrong
: emphasis == IconEmphasis::Quiet ? AppColor::NegativeSoft
: AppColor::Negative;
case AppColor::Informational:
return emphasis == IconEmphasis::Primary ? AppColor::InformationalStrong
: emphasis == IconEmphasis::Quiet ? AppColor::InformationalSoft
: AppColor::Informational;
default:
return role;
}

View file

@ -683,6 +683,9 @@ QColor ThemeManager::appColor(AppColor::Role role) const
case AppColor::Negative:
case AppColor::NegativeStrong:
case AppColor::NegativeSoft:
case AppColor::Informational:
case AppColor::InformationalStrong:
case AppColor::InformationalSoft:
// Semantic colors are independent of the accent: a missing role must
// not silently paint "remove" in the theme's highlight color.
return defaultAppColor(role, isDarkModeActive());