[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:
Lukas Brübach 2026-11-07 00:12:45 +01:00
parent 115d16a74b
commit 03a4a33ba5
21 changed files with 551 additions and 277 deletions

View file

@ -153,11 +153,26 @@ TEST(ThemeAppColorTest, SemanticDefaultsFollowTheScheme)
{
EXPECT_EQ(defaultAppColor(AppColor::Positive, false), QColor(QStringLiteral("#1b7f3b")));
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, true), QColor(QStringLiteral("#ff8a80")));
// Accent roles are derived from the palette, so they have no static default.
EXPECT_FALSE(defaultAppColor(AppColor::AccentStrong, false).isValid());
EXPECT_FALSE(defaultAppColor(AppColor::AccentSoft, true).isValid());
EXPECT_EQ(defaultAppColor(AppColor::NegativeStrong, false), QColor(QStringLiteral("#7c1b15")));
EXPECT_EQ(defaultAppColor(AppColor::NegativeStrong, true), QColor(QStringLiteral("#ffb3ac")));
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)
@ -166,10 +181,20 @@ TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
ASSERT_TRUE(dir.isValid());
PaletteConfig config;
const QColor positive(QStringLiteral("#1b7f3b"));
const QColor negative(QStringLiteral("#b3261e"));
config.appColors[AppColor::Positive] = positive;
config.appColors[AppColor::Negative] = negative;
const QMap<AppColor::Role, QColor> roles = {
{AppColor::Icon, QColor(QStringLiteral("#2f59ad"))},
{AppColor::IconStrong, QColor(QStringLiteral("#213e79"))},
{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"));
QFile file(path);
@ -178,10 +203,10 @@ TEST(ThemeAppColorTest, PaletteConfigRoundTripsSemanticRoles)
file.close();
const PaletteConfig parsed = PaletteConfig::fromFile(path);
ASSERT_TRUE(parsed.appColors.contains(AppColor::Positive));
ASSERT_TRUE(parsed.appColors.contains(AppColor::Negative));
EXPECT_EQ(parsed.appColors.value(AppColor::Positive), positive);
EXPECT_EQ(parsed.appColors.value(AppColor::Negative), negative);
for (auto it = roles.cbegin(); it != roles.cend(); ++it) {
ASSERT_TRUE(parsed.appColors.contains(it.key()));
EXPECT_EQ(parsed.appColors.value(it.key()), it.value());
}
}
// Every test brings its own main(): GTest is not guaranteed to be found on the build machines, and