From 256767255689b850f2b0b0c862a85fd749175342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20Br=C3=BCbach?= <44814898+BruebachL@users.noreply.github.com> Date: Sun, 8 Nov 2026 20:59:41 +0100 Subject: [PATCH] [Icons] Add a Movement semantic color family A card changing position inside the library is neither a gain, a loss nor information, so it had no hue. Movement names it with teal and covers the three library placements (top, bottom, X from top). It follows the existing ladder: authored roles per theme, scheme defaults otherwise, an emphasis mapping, palette-editor rows and generator seeding. The defaults clear the 4.5:1 floor on the representative platform surfaces, keep strong > base > soft saturation, and keep green > blue > red so teal cannot be mistaken for the green gain family. --- .../palette_editor/palette_generator.cpp | 5 ++-- .../palette_editor/palette_grid_widget.cpp | 4 +++ cockatrice/src/interface/theme_config.cpp | 9 +++++++ cockatrice/src/interface/theme_config.h | 11 +++++--- cockatrice/src/interface/theme_icon.cpp | 4 +++ cockatrice/src/interface/theme_manager.cpp | 3 +++ .../icon_tint_preview_test.cpp | 1 + .../icon_treatment_preview_test.cpp | 1 + .../theme_icon_render_test.cpp | 27 +++++++++++++++++-- .../theme_migration/theme_migration_test.cpp | 6 +++++ 10 files changed, 63 insertions(+), 8 deletions(-) diff --git a/cockatrice/src/interface/palette_editor/palette_generator.cpp b/cockatrice/src/interface/palette_editor/palette_generator.cpp index 51630f998..94ad5492c 100644 --- a/cockatrice/src/interface/palette_editor/palette_generator.cpp +++ b/cockatrice/src/interface/palette_editor/palette_generator.cpp @@ -179,12 +179,13 @@ PaletteConfig fromAccent(const QColor &accent, int intensity, const QString &sch 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, remove/not-ready red, information blue, - // hidden information violet and marks amber. + // hidden information violet, marks amber and movement teal. for (const AppColor::Role role : {AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft, AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft, AppColor::Informational, AppColor::InformationalStrong, AppColor::InformationalSoft, AppColor::Hidden, AppColor::HiddenStrong, AppColor::HiddenSoft, AppColor::Marked, - AppColor::MarkedStrong, AppColor::MarkedSoft}) { + AppColor::MarkedStrong, AppColor::MarkedSoft, AppColor::Movement, AppColor::MovementStrong, + AppColor::MovementSoft}) { cfg.appColors[role] = defaultAppColor(role, dark); } diff --git a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp index 2252efa4b..e7d91d330 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -90,6 +90,10 @@ static const QMap APP_ROLE_DESCRIPTIONS = { {AppColor::Marked, QT_TRANSLATE_NOOP("PaletteGridWidget", "Marks left on a card (annotation, draw arrow)")}, {AppColor::MarkedStrong, QT_TRANSLATE_NOOP("PaletteGridWidget", "Marks on a card at primary emphasis")}, {AppColor::MarkedSoft, QT_TRANSLATE_NOOP("PaletteGridWidget", "Marks on a card at quiet emphasis")}, + {AppColor::Movement, + QT_TRANSLATE_NOOP("PaletteGridWidget", "Cards moving without a gain or a loss (library placements)")}, + {AppColor::MovementStrong, QT_TRANSLATE_NOOP("PaletteGridWidget", "Movement at primary emphasis")}, + {AppColor::MovementSoft, QT_TRANSLATE_NOOP("PaletteGridWidget", "Movement at quiet emphasis")}, }; PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent) diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index 8e7283efe..525a46905 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -509,6 +509,15 @@ QColor defaultAppColor(AppColor::Role role, bool dark) return QColor(dark ? QStringLiteral("#ffb020") : QStringLiteral("#8a4f02")); case AppColor::MarkedSoft: return QColor(dark ? QStringLiteral("#c2ab7d") : QStringLiteral("#7b6a4a")); + // Movement is a card changing position without a gain or a loss: the + // library placements (top, bottom, X from top). Teal keeps it apart + // from the green "into play" moves and the blue informational states. + case AppColor::Movement: + return QColor(dark ? QStringLiteral("#4cc2b5") : QStringLiteral("#0e7a72")); + case AppColor::MovementStrong: + return QColor(dark ? QStringLiteral("#2dd4bf") : QStringLiteral("#096b62")); + case AppColor::MovementSoft: + return QColor(dark ? QStringLiteral("#a3c1bc") : QStringLiteral("#4a6b67")); } return {}; diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 764074521..0848baeb8 100644 --- a/cockatrice/src/interface/theme_config.h +++ b/cockatrice/src/interface/theme_config.h @@ -16,10 +16,10 @@ // // 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, -// Negative, Informational, Hidden and Marked 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. +// Negative, Informational, Hidden, Marked and Movement 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 @@ -45,6 +45,9 @@ enum Role Marked, MarkedStrong, MarkedSoft, + Movement, + MovementStrong, + MovementSoft, }; Q_ENUM_NS(Role) } // namespace AppColor diff --git a/cockatrice/src/interface/theme_icon.cpp b/cockatrice/src/interface/theme_icon.cpp index e7dd66df1..3a71f149c 100644 --- a/cockatrice/src/interface/theme_icon.cpp +++ b/cockatrice/src/interface/theme_icon.cpp @@ -66,6 +66,10 @@ AppColor::Role emphasisRole(AppColor::Role role, IconEmphasis emphasis) return emphasis == IconEmphasis::Primary ? AppColor::MarkedStrong : emphasis == IconEmphasis::Quiet ? AppColor::MarkedSoft : AppColor::Marked; + case AppColor::Movement: + return emphasis == IconEmphasis::Primary ? AppColor::MovementStrong + : emphasis == IconEmphasis::Quiet ? AppColor::MovementSoft + : AppColor::Movement; default: return role; } diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index e06920796..3a2ecf9cf 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -692,6 +692,9 @@ QColor ThemeManager::appColor(AppColor::Role role) const case AppColor::Marked: case AppColor::MarkedStrong: case AppColor::MarkedSoft: + case AppColor::Movement: + case AppColor::MovementStrong: + case AppColor::MovementSoft: // 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()); diff --git a/tests/theme_migration/icon_tint_preview_test.cpp b/tests/theme_migration/icon_tint_preview_test.cpp index 6703d07a0..b5b57f7b2 100644 --- a/tests/theme_migration/icon_tint_preview_test.cpp +++ b/tests/theme_migration/icon_tint_preview_test.cpp @@ -105,6 +105,7 @@ TEST(IconTintPreview, WritesOneSheetPerShippedTheme) {"Informational", AppColor::Informational, {"arrow_right", "view", "arrow_to_top"}}, {"Hidden", AppColor::Hidden, {"view", "arrow_right", "swap"}}, {"Marked", AppColor::Marked, {"pencil", "share", "pin"}}, + {"Movement", AppColor::Movement, {"arrow_to_top", "arrow_to_bottom", "arrow_down"}}, }; const QStringList themes = { QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"), diff --git a/tests/theme_migration/icon_treatment_preview_test.cpp b/tests/theme_migration/icon_treatment_preview_test.cpp index 9992f74ce..04d5f85da 100644 --- a/tests/theme_migration/icon_treatment_preview_test.cpp +++ b/tests/theme_migration/icon_treatment_preview_test.cpp @@ -113,6 +113,7 @@ TEST(IconTreatmentPreview, WritesOneSheetPerShippedTheme) {"Informational", AppColor::Informational, {"arrow_right", "view", "arrow_to_top"}}, {"Hidden", AppColor::Hidden, {"view", "arrow_right", "swap"}}, {"Marked", AppColor::Marked, {"pencil", "share", "pin"}}, + {"Movement", AppColor::Movement, {"arrow_to_top", "arrow_to_bottom", "arrow_down"}}, }; const QStringList themes = { QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"), diff --git a/tests/theme_migration/theme_icon_render_test.cpp b/tests/theme_migration/theme_icon_render_test.cpp index 9297c566b..a56c723b4 100644 --- a/tests/theme_migration/theme_icon_render_test.cpp +++ b/tests/theme_migration/theme_icon_render_test.cpp @@ -1541,7 +1541,8 @@ TEST_F(ThemeIconRenderTest, EmphasisSelectsTheAuthoredVariant) Family{AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}, Family{AppColor::Informational, AppColor::InformationalStrong, AppColor::InformationalSoft}, Family{AppColor::Hidden, AppColor::HiddenStrong, AppColor::HiddenSoft}, - Family{AppColor::Marked, AppColor::MarkedStrong, AppColor::MarkedSoft}}) { + Family{AppColor::Marked, AppColor::MarkedStrong, AppColor::MarkedSoft}, + Family{AppColor::Movement, AppColor::MovementStrong, AppColor::MovementSoft}}) { EXPECT_EQ(IconTint(family.base, IconEmphasis::Primary).resolve(QIcon::Normal), themeManager->appColor(family.strong)); EXPECT_EQ(IconTint(family.base).resolve(QIcon::Normal), themeManager->appColor(family.base)); @@ -1675,6 +1676,17 @@ TEST_F(ThemeIconRenderTest, EmphasisKeepsASemanticHue) EXPECT_GT(ink.red(), ink.green()) << theme.toStdString() << static_cast(role); EXPECT_GT(ink.green(), ink.blue()) << theme.toStdString() << static_cast(role); } + + for (const AppColor::Role role : {AppColor::Movement, AppColor::MovementStrong, AppColor::MovementSoft}) { + // Movement is teal: green leads, then blue, then red. + const QColor ink = roles.value(role, defaultAppColor(role, dark)); + if (!ink.isValid()) { + continue; + } + ++checked; + EXPECT_GT(ink.green(), ink.blue()) << theme.toStdString() << static_cast(role); + EXPECT_GT(ink.blue(), ink.red()) << theme.toStdString() << static_cast(role); + } } } EXPECT_GT(checked, 0) << "no shipped theme authored a semantic emphasis level"; @@ -1863,7 +1875,8 @@ TEST(IconInk, DefaultSemanticFamilyMeetsTheLadderOnPlatformSurfaces) std::tuple{AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}, std::tuple{AppColor::Informational, AppColor::InformationalStrong, AppColor::InformationalSoft}, std::tuple{AppColor::Hidden, AppColor::HiddenStrong, AppColor::HiddenSoft}, - std::tuple{AppColor::Marked, AppColor::MarkedStrong, AppColor::MarkedSoft}}) { + std::tuple{AppColor::Marked, AppColor::MarkedStrong, AppColor::MarkedSoft}, + std::tuple{AppColor::Movement, AppColor::MovementStrong, AppColor::MovementSoft}}) { const QColor secondary = defaultAppColor(base, scheme.dark); const QColor primary = defaultAppColor(strong, scheme.dark); const QColor quiet = defaultAppColor(soft, scheme.dark); @@ -1957,6 +1970,11 @@ TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested) AppColor::InformationalStrong}, {"Hidden", {"view", "arrow_right", "swap"}, AppColor::HiddenSoft, AppColor::Hidden, AppColor::HiddenStrong}, {"Marked", {"pencil", "share", "pin"}, AppColor::MarkedSoft, AppColor::Marked, AppColor::MarkedStrong}, + {"Movement", + {"arrow_to_top", "arrow_to_bottom", "arrow_down"}, + AppColor::MovementSoft, + AppColor::Movement, + AppColor::MovementStrong}, }; const QStringList themes = { QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"), @@ -2157,6 +2175,11 @@ TEST_F(ThemeIconRenderTest, EmphasisTreatmentsPreviewWritesPngsWhenRequested) AppColor::InformationalStrong}, {"Hidden", {"view", "arrow_right", "swap"}, AppColor::HiddenSoft, AppColor::Hidden, AppColor::HiddenStrong}, {"Marked", {"pencil", "share", "pin"}, AppColor::MarkedSoft, AppColor::Marked, AppColor::MarkedStrong}, + {"Movement", + {"arrow_to_top", "arrow_to_bottom", "arrow_down"}, + AppColor::MovementSoft, + AppColor::Movement, + AppColor::MovementStrong}, }; const QStringList themes = { QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"), diff --git a/tests/theme_migration/theme_migration_test.cpp b/tests/theme_migration/theme_migration_test.cpp index 6a8d1a22e..329d13458 100644 --- a/tests/theme_migration/theme_migration_test.cpp +++ b/tests/theme_migration/theme_migration_test.cpp @@ -185,6 +185,12 @@ TEST(ThemeAppColorTest, SemanticDefaultsFollowTheScheme) EXPECT_EQ(defaultAppColor(AppColor::MarkedStrong, true), QColor(QStringLiteral("#ffb020"))); EXPECT_EQ(defaultAppColor(AppColor::MarkedSoft, false), QColor(QStringLiteral("#7b6a4a"))); EXPECT_EQ(defaultAppColor(AppColor::MarkedSoft, true), QColor(QStringLiteral("#c2ab7d"))); + EXPECT_EQ(defaultAppColor(AppColor::Movement, false), QColor(QStringLiteral("#0e7a72"))); + EXPECT_EQ(defaultAppColor(AppColor::Movement, true), QColor(QStringLiteral("#4cc2b5"))); + EXPECT_EQ(defaultAppColor(AppColor::MovementStrong, false), QColor(QStringLiteral("#096b62"))); + EXPECT_EQ(defaultAppColor(AppColor::MovementStrong, true), QColor(QStringLiteral("#2dd4bf"))); + EXPECT_EQ(defaultAppColor(AppColor::MovementSoft, false), QColor(QStringLiteral("#4a6b67"))); + EXPECT_EQ(defaultAppColor(AppColor::MovementSoft, true), QColor(QStringLiteral("#a3c1bc"))); // Accent and neutral icon roles have no static default: an older theme // written before the roles existed falls back to deriving them from its // palette rather than pinning one scheme's ink.