diff --git a/cockatrice/src/interface/palette_editor/palette_generator.cpp b/cockatrice/src/interface/palette_editor/palette_generator.cpp index b64858b68..51630f998 100644 --- a/cockatrice/src/interface/palette_editor/palette_generator.cpp +++ b/cockatrice/src/interface/palette_editor/palette_generator.cpp @@ -178,12 +178,13 @@ 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, remove/not-ready red, and the neutral - // information state blue. + // still paint add/ready green, remove/not-ready red, information blue, + // hidden information violet and marks amber. for (const AppColor::Role role : {AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft, AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft, AppColor::Informational, AppColor::InformationalStrong, - AppColor::InformationalSoft}) { + AppColor::InformationalSoft, AppColor::Hidden, AppColor::HiddenStrong, AppColor::HiddenSoft, AppColor::Marked, + AppColor::MarkedStrong, AppColor::MarkedSoft}) { 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 51c1f8fbb..2252efa4b 100644 --- a/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp +++ b/cockatrice/src/interface/palette_editor/palette_grid_widget.cpp @@ -83,6 +83,13 @@ static const QMap APP_ROLE_DESCRIPTIONS = { "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")}, + {AppColor::Hidden, + QT_TRANSLATE_NOOP("PaletteGridWidget", "Hidden information (face-down play, peek at the card face)")}, + {AppColor::HiddenStrong, QT_TRANSLATE_NOOP("PaletteGridWidget", "Hidden information at primary emphasis")}, + {AppColor::HiddenSoft, QT_TRANSLATE_NOOP("PaletteGridWidget", "Hidden information at quiet emphasis")}, + {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")}, }; PaletteGridWidget::PaletteGridWidget(QWidget *parent) : QWidget(parent) diff --git a/cockatrice/src/interface/theme_config.cpp b/cockatrice/src/interface/theme_config.cpp index d170e4fd1..8e7283efe 100644 --- a/cockatrice/src/interface/theme_config.cpp +++ b/cockatrice/src/interface/theme_config.cpp @@ -491,6 +491,24 @@ QColor defaultAppColor(AppColor::Role role, bool dark) return QColor(dark ? QStringLiteral("#69aeff") : QStringLiteral("#0a4fb5")); case AppColor::InformationalSoft: return QColor(dark ? QStringLiteral("#9db2cd") : QStringLiteral("#526a8a")); + // Hidden is the game's hidden-information cluster: a face-down play and + // peeking at a card face. Violet keeps it apart from the blue + // Informational family, which carries public information (reveal). + case AppColor::Hidden: + return QColor(dark ? QStringLiteral("#c6a5fa") : QStringLiteral("#6d28d9")); + case AppColor::HiddenStrong: + return QColor(dark ? QStringLiteral("#c99bff") : QStringLiteral("#4c0f9e")); + case AppColor::HiddenSoft: + return QColor(dark ? QStringLiteral("#b3a6c9") : QStringLiteral("#6a5a8c")); + // Marked is the marks a player leaves on a card: an annotation or a + // draw arrow. Amber names a persistent, removable mark rather than a + // gain or a loss, so annotation and arrow finally share a colour. + case AppColor::Marked: + return QColor(dark ? QStringLiteral("#e2a63a") : QStringLiteral("#9a5b05")); + case AppColor::MarkedStrong: + return QColor(dark ? QStringLiteral("#ffb020") : QStringLiteral("#8a4f02")); + case AppColor::MarkedSoft: + return QColor(dark ? QStringLiteral("#c2ab7d") : QStringLiteral("#7b6a4a")); } return {}; diff --git a/cockatrice/src/interface/theme_config.h b/cockatrice/src/interface/theme_config.h index 666bdcd44..764074521 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 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. +// 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. namespace AppColor { Q_NAMESPACE @@ -39,6 +39,12 @@ enum Role Informational, InformationalStrong, InformationalSoft, + Hidden, + HiddenStrong, + HiddenSoft, + Marked, + MarkedStrong, + MarkedSoft, }; Q_ENUM_NS(Role) } // namespace AppColor diff --git a/cockatrice/src/interface/theme_icon.cpp b/cockatrice/src/interface/theme_icon.cpp index eeaae5a97..e7dd66df1 100644 --- a/cockatrice/src/interface/theme_icon.cpp +++ b/cockatrice/src/interface/theme_icon.cpp @@ -58,6 +58,14 @@ AppColor::Role emphasisRole(AppColor::Role role, IconEmphasis emphasis) return emphasis == IconEmphasis::Primary ? AppColor::InformationalStrong : emphasis == IconEmphasis::Quiet ? AppColor::InformationalSoft : AppColor::Informational; + case AppColor::Hidden: + return emphasis == IconEmphasis::Primary ? AppColor::HiddenStrong + : emphasis == IconEmphasis::Quiet ? AppColor::HiddenSoft + : AppColor::Hidden; + case AppColor::Marked: + return emphasis == IconEmphasis::Primary ? AppColor::MarkedStrong + : emphasis == IconEmphasis::Quiet ? AppColor::MarkedSoft + : AppColor::Marked; default: return role; } diff --git a/cockatrice/src/interface/theme_manager.cpp b/cockatrice/src/interface/theme_manager.cpp index d0fa816dd..e06920796 100644 --- a/cockatrice/src/interface/theme_manager.cpp +++ b/cockatrice/src/interface/theme_manager.cpp @@ -686,6 +686,12 @@ QColor ThemeManager::appColor(AppColor::Role role) const case AppColor::Informational: case AppColor::InformationalStrong: case AppColor::InformationalSoft: + case AppColor::Hidden: + case AppColor::HiddenStrong: + case AppColor::HiddenSoft: + case AppColor::Marked: + case AppColor::MarkedStrong: + case AppColor::MarkedSoft: // 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 7cc6ca153..6703d07a0 100644 --- a/tests/theme_migration/icon_tint_preview_test.cpp +++ b/tests/theme_migration/icon_tint_preview_test.cpp @@ -103,6 +103,8 @@ TEST(IconTintPreview, WritesOneSheetPerShippedTheme) {"Positive", AppColor::Positive, {"increment", "ready_start", "arrow_up"}}, {"Negative", AppColor::Negative, {"decrement", "not_ready_start", "remove_row"}}, {"Informational", AppColor::Informational, {"arrow_right", "view", "arrow_to_top"}}, + {"Hidden", AppColor::Hidden, {"view", "arrow_right", "swap"}}, + {"Marked", AppColor::Marked, {"pencil", "share", "pin"}}, }; 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 ec2f087a9..9992f74ce 100644 --- a/tests/theme_migration/icon_treatment_preview_test.cpp +++ b/tests/theme_migration/icon_treatment_preview_test.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -110,6 +111,8 @@ TEST(IconTreatmentPreview, WritesOneSheetPerShippedTheme) {"Positive", AppColor::Positive, {"arrow_right", "increment", "arrow_left"}}, {"Negative", AppColor::Negative, {"arrow_down", "decrement", "remove_row"}}, {"Informational", AppColor::Informational, {"arrow_right", "view", "arrow_to_top"}}, + {"Hidden", AppColor::Hidden, {"view", "arrow_right", "swap"}}, + {"Marked", AppColor::Marked, {"pencil", "share", "pin"}}, }; const QStringList themes = { QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"), @@ -251,3 +254,312 @@ TEST(IconTreatmentPreview, WritesOneSheetPerShippedTheme) } } } + +namespace +{ +/** @brief One action-grid cell, with the role the panel hands the icon engine. */ +struct PreviewAction +{ + QString label; + const char *glyph; + AppColor::Role role; +}; + +/** @brief One titled section of a hover panel page. */ +struct PreviewSection +{ + QString title; + QList actions; +}; + +/** @brief One hover panel page, as the client builds it. */ +struct PreviewPage +{ + QString title; + QList sections; +}; + +/** @brief Renders a sentinel SVG in one ink at the requested pixel size. */ +QPixmap renderInkGlyph(const QByteArray &svg, const QColor &ink, const QSize &size) +{ + QSvgRenderer renderer(resolveSvgColorSentinel(svg, ink.name(QColor::HexRgb).toLatin1())); + QPixmap pixmap(size); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + renderer.render(&painter); + return pixmap; +} +} // namespace + +/** + * @brief Writes the hover panel's action grids the way the client paints them. + * + * The treatment sheets show glyphs in isolation; this renders the real layout — + * section headers and two-column cells with the glyph above its label — so the + * emphasis split (quiet tabs, primary grid) and the semantic map can be judged + * as the set the player actually sees. The roles are the ones the panel passes + * to the icon engine, already resolved to the emphasis level it requests, and a + * role the theme does not author resolves through the same scheme default the + * client paints. + */ +TEST(IconTreatmentPreview, HoverPanelGridWritesPngsWhenRequested) +{ + const QString outDir = qEnvironmentVariable("COCKATRICE_ICON_PREVIEW_DIR"); + if (outDir.isEmpty()) { + GTEST_SKIP() << "set COCKATRICE_ICON_PREVIEW_DIR to write the preview PNGs"; + } + ensureGuiApplication(); + ASSERT_TRUE(QDir().mkpath(outDir)); + + constexpr int MARGIN = 24; + constexpr int COLUMNS = 2; + constexpr int CELL_WIDTH = 146; + constexpr int CELL_HEIGHT = 62; + constexpr int SECTION_HEIGHT = 22; + constexpr int PANEL_PADDING = 10; + constexpr int PANEL_TITLE = 24; + constexpr int GLYPH = 22; + constexpr qreal DPR = 2.0; + + // The two pages a selected card shows, with every conditional action + // present so the whole map is visible at once. + const QList pages = { + PreviewPage{.title = QStringLiteral("Main — on the battlefield"), + .sections = + { + PreviewSection{.title = QStringLiteral("Battlefield"), + .actions = + { + PreviewAction{.label = QStringLiteral("Tap / Untap"), + .glyph = "sync", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Skip untapping"), + .glyph = "lock", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Turn Over"), + .glyph = "swap", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Peek at card face"), + .glyph = "view", + .role = AppColor::HiddenStrong}, + PreviewAction{.label = QStringLiteral("Clone"), + .glyph = "increment", + .role = AppColor::PositiveStrong}, + PreviewAction{.label = QStringLiteral("Attach to card..."), + .glyph = "pin", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Unattach"), + .glyph = "arrow_undo", + .role = AppColor::NegativeStrong}, + PreviewAction{.label = QStringLiteral("Draw arrow..."), + .glyph = "share", + .role = AppColor::MarkedStrong}, + PreviewAction{.label = QStringLiteral("Set annotation..."), + .glyph = "pencil", + .role = AppColor::MarkedStrong}, + PreviewAction{.label = QStringLiteral("Reduce life"), + .glyph = "arrow_down", + .role = AppColor::NegativeStrong}, + }}, + PreviewSection{.title = QStringLiteral("Select"), + .actions = + { + PreviewAction{.label = QStringLiteral("Select All"), + .glyph = "search", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Select Row"), + .glyph = "filter", + .role = AppColor::IconStrong}, + }}, + }}, + PreviewPage{.title = QStringLiteral("Main — in a zone"), + .sections = + { + PreviewSection{.title = QStringLiteral("Zone"), + .actions = + { + PreviewAction{.label = QStringLiteral("Play"), + .glyph = "arrow_right", + .role = AppColor::PositiveStrong}, + PreviewAction{.label = QStringLiteral("Play Face Down"), + .glyph = "arrow_right", + .role = AppColor::HiddenStrong}, + PreviewAction{.label = QStringLiteral("Clone"), + .glyph = "increment", + .role = AppColor::PositiveStrong}, + PreviewAction{.label = QStringLiteral("Attach to card..."), + .glyph = "pin", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Draw arrow..."), + .glyph = "share", + .role = AppColor::MarkedStrong}, + }}, + PreviewSection{.title = QStringLiteral("Select"), + .actions = + { + PreviewAction{.label = QStringLiteral("Select All"), + .glyph = "search", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Select Column"), + .glyph = "filter", + .role = AppColor::IconStrong}, + }}, + }}, + PreviewPage{.title = QStringLiteral("Move — card in a zone"), + .sections = + { + PreviewSection{.title = QStringLiteral("Move to"), + .actions = + { + PreviewAction{.label = QStringLiteral("Top of library"), + .glyph = "arrow_to_top", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("X from top..."), + .glyph = "search", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Bottom of library"), + .glyph = "arrow_to_bottom", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Table"), + .glyph = "player", + .role = AppColor::IconStrong}, + PreviewAction{.label = QStringLiteral("Hand"), + .glyph = "arrow_left", + .role = AppColor::PositiveStrong}, + PreviewAction{.label = QStringLiteral("Graveyard"), + .glyph = "arrow_down", + .role = AppColor::NegativeStrong}, + PreviewAction{.label = QStringLiteral("Exile"), + .glyph = "arrow_right", + .role = AppColor::InformationalStrong}, + }}, + PreviewSection{.title = QStringLiteral("Reveal to..."), + .actions = + { + PreviewAction{.label = QStringLiteral("All players"), + .glyph = "share", + .role = AppColor::InformationalStrong}, + PreviewAction{.label = QStringLiteral("Alice"), + .glyph = "player", + .role = AppColor::InformationalStrong}, + PreviewAction{.label = QStringLiteral("Bob"), + .glyph = "player", + .role = AppColor::InformationalStrong}, + }}, + }}, + }; + + const auto rowsIn = [](const PreviewSection §ion) { return (section.actions.size() + COLUMNS - 1) / COLUMNS; }; + const auto panelHeightFor = [&](const PreviewPage &page) { + int height = PANEL_TITLE + PANEL_PADDING; + for (const PreviewSection §ion : page.sections) { + height += SECTION_HEIGHT + rowsIn(section) * CELL_HEIGHT; + } + return height + PANEL_PADDING; + }; + + const int panelWidth = COLUMNS * CELL_WIDTH + 2 * PANEL_PADDING; + const int pageCount = static_cast(pages.size()); + int tallestPanel = 0; + for (const PreviewPage &page : pages) { + tallestPanel = qMax(tallestPanel, panelHeightFor(page)); + } + const int logicalWidth = 2 * MARGIN + pageCount * panelWidth + (pageCount - 1) * MARGIN; + const int logicalHeight = MARGIN + 30 + tallestPanel + MARGIN; + + const QString theme = QStringLiteral("Fabric"); + for (const bool dark : {false, true}) { + const QString file = + QDir(QStringLiteral(COCKATRICE_SOURCE_DIR)) + .absoluteFilePath(QStringLiteral("cockatrice/themes/%1/palette-default-%2.toml") + .arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light"))); + const PaletteConfig config = PaletteConfig::fromFile(file); + ASSERT_TRUE(config.hasPalette()) << theme.toStdString(); + + const QPalette palette = config.apply(QPalette()); + const QColor window = palette.color(QPalette::Active, QPalette::Window); + const QColor text = palette.color(QPalette::Active, QPalette::WindowText); + // The panel's cells sit on the popup surface; painting them on the + // button color is what a QToolButton shows when it is not hovered. + const QColor cell = palette.color(QPalette::Active, QPalette::Button); + + QImage image(QSize(qRound(logicalWidth * DPR), qRound(logicalHeight * DPR)), + QImage::Format_ARGB32_Premultiplied); + image.fill(window); + QPainter painter(&image); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + painter.scale(DPR, DPR); + + painter.setPen(text); + painter.setFont(QFont(QStringLiteral("Sans"), 10, QFont::DemiBold)); + painter.drawText(QPointF(MARGIN, MARGIN), + QStringLiteral("%1 — %2 scheme, hover panel action grids") + .arg(theme, dark ? QStringLiteral("dark") : QStringLiteral("light"))); + + const auto toneFor = [&](AppColor::Role role) { + return config.appColors.value(role, defaultAppColor(role, dark)); + }; + + for (int pageIndex = 0; pageIndex < pageCount; ++pageIndex) { + const PreviewPage &page = pages[pageIndex]; + const qreal panelLeft = MARGIN + static_cast(pageIndex) * (panelWidth + MARGIN); + const qreal panelTop = MARGIN + 30; + const int panelHeight = panelHeightFor(page); + + painter.setPen(Qt::NoPen); + painter.setBrush(cell.darker(dark ? 115 : 103)); + painter.drawRoundedRect(QRectF(panelLeft, panelTop, panelWidth, panelHeight), 8, 8); + + painter.setPen(text); + painter.setFont(QFont(QStringLiteral("Sans"), 9, QFont::DemiBold)); + painter.drawText(QPointF(panelLeft + PANEL_PADDING, panelTop + 17), page.title); + + qreal y = panelTop + PANEL_TITLE; + for (const PreviewSection §ion : page.sections) { + painter.setPen(text); + painter.setFont(QFont(QStringLiteral("Sans"), 8, QFont::DemiBold)); + painter.drawText(QPointF(panelLeft + PANEL_PADDING, y + 15), section.title); + y += SECTION_HEIGHT; + + const int rows = rowsIn(section); + for (int row = 0; row < rows; ++row) { + for (int column = 0; column < COLUMNS; ++column) { + const int index = row * COLUMNS + column; + if (index >= section.actions.size()) { + break; + } + const PreviewAction &action = section.actions.at(index); + const QRectF cellRect(panelLeft + PANEL_PADDING + column * CELL_WIDTH, y, CELL_WIDTH - 4, + CELL_HEIGHT - 4); + + painter.setPen(Qt::NoPen); + painter.setBrush(cell); + painter.drawRoundedRect(cellRect, 5, 5); + + const QByteArray svg = readIconSvg(action.glyph); + ASSERT_FALSE(svg.isEmpty()) << action.glyph; + const QPixmap pixmap = renderAtDpr(QSize(GLYPH, GLYPH), DPR, [&](const QSize &pixelSize) { + return renderInkGlyph(svg, toneFor(action.role), pixelSize); + }); + painter.drawPixmap(QPointF(cellRect.center().x() - GLYPH / 2.0, cellRect.top() + 7), pixmap); + + painter.setPen(text); + painter.setFont(QFont(QStringLiteral("Sans"), 7)); + const QFontMetrics metrics(painter.font()); + const QString label = metrics.elidedText(action.label, Qt::ElideRight, CELL_WIDTH - 12); + painter.drawText(QRectF(cellRect.left(), cellRect.bottom() - 20, cellRect.width(), 16), + Qt::AlignHCenter | Qt::AlignVCenter, label); + } + y += CELL_HEIGHT; + } + } + } + + painter.end(); + const QString path = QDir(outDir).absoluteFilePath( + QStringLiteral("hover-panel-map-%1.png").arg(dark ? QStringLiteral("dark") : QStringLiteral("light"))); + ASSERT_TRUE(image.save(path)) << path.toStdString(); + } +} diff --git a/tests/theme_migration/theme_icon_render_test.cpp b/tests/theme_migration/theme_icon_render_test.cpp index cac4c5a7f..9297c566b 100644 --- a/tests/theme_migration/theme_icon_render_test.cpp +++ b/tests/theme_migration/theme_icon_render_test.cpp @@ -1539,7 +1539,9 @@ TEST_F(ThemeIconRenderTest, EmphasisSelectsTheAuthoredVariant) {Family{AppColor::Icon, AppColor::IconStrong, AppColor::IconSoft}, Family{AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft}, Family{AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}, - Family{AppColor::Informational, AppColor::InformationalStrong, AppColor::InformationalSoft}}) { + Family{AppColor::Informational, AppColor::InformationalStrong, AppColor::InformationalSoft}, + Family{AppColor::Hidden, AppColor::HiddenStrong, AppColor::HiddenSoft}, + Family{AppColor::Marked, AppColor::MarkedStrong, AppColor::MarkedSoft}}) { 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)); @@ -1651,6 +1653,28 @@ TEST_F(ThemeIconRenderTest, EmphasisKeepsASemanticHue) EXPECT_GT(ink.blue(), ink.red()) << theme.toStdString() << static_cast(role); EXPECT_GT(ink.blue(), ink.green()) << theme.toStdString() << static_cast(role); } + + for (const AppColor::Role role : {AppColor::Hidden, AppColor::HiddenStrong, AppColor::HiddenSoft}) { + // Hidden is violet: blue leads, then red, then green. + const QColor ink = roles.value(role, defaultAppColor(role, dark)); + if (!ink.isValid()) { + continue; + } + ++checked; + EXPECT_GT(ink.blue(), ink.red()) << theme.toStdString() << static_cast(role); + EXPECT_GT(ink.red(), ink.green()) << theme.toStdString() << static_cast(role); + } + + for (const AppColor::Role role : {AppColor::Marked, AppColor::MarkedStrong, AppColor::MarkedSoft}) { + // Marked is amber: red leads, then green, then blue. + const QColor ink = roles.value(role, defaultAppColor(role, dark)); + if (!ink.isValid()) { + continue; + } + ++checked; + EXPECT_GT(ink.red(), ink.green()) << theme.toStdString() << static_cast(role); + EXPECT_GT(ink.green(), ink.blue()) << theme.toStdString() << static_cast(role); + } } } EXPECT_GT(checked, 0) << "no shipped theme authored a semantic emphasis level"; @@ -1837,7 +1861,9 @@ TEST(IconInk, DefaultSemanticFamilyMeetsTheLadderOnPlatformSurfaces) for (const auto &[base, strong, soft] : {std::tuple{AppColor::Positive, AppColor::PositiveStrong, AppColor::PositiveSoft}, std::tuple{AppColor::Negative, AppColor::NegativeStrong, AppColor::NegativeSoft}, - std::tuple{AppColor::Informational, AppColor::InformationalStrong, AppColor::InformationalSoft}}) { + std::tuple{AppColor::Informational, AppColor::InformationalStrong, AppColor::InformationalSoft}, + std::tuple{AppColor::Hidden, AppColor::HiddenStrong, AppColor::HiddenSoft}, + std::tuple{AppColor::Marked, AppColor::MarkedStrong, AppColor::MarkedSoft}}) { const QColor secondary = defaultAppColor(base, scheme.dark); const QColor primary = defaultAppColor(strong, scheme.dark); const QColor quiet = defaultAppColor(soft, scheme.dark); @@ -1929,6 +1955,8 @@ TEST_F(ThemeIconRenderTest, EmphasisPreviewWritesPngsWhenRequested) AppColor::InformationalSoft, AppColor::Informational, AppColor::InformationalStrong}, + {"Hidden", {"view", "arrow_right", "swap"}, AppColor::HiddenSoft, AppColor::Hidden, AppColor::HiddenStrong}, + {"Marked", {"pencil", "share", "pin"}, AppColor::MarkedSoft, AppColor::Marked, AppColor::MarkedStrong}, }; const QStringList themes = { QStringLiteral("Fabric"), QStringLiteral("Fusion"), QStringLiteral("Leather"), @@ -2127,6 +2155,8 @@ TEST_F(ThemeIconRenderTest, EmphasisTreatmentsPreviewWritesPngsWhenRequested) AppColor::InformationalSoft, AppColor::Informational, AppColor::InformationalStrong}, + {"Hidden", {"view", "arrow_right", "swap"}, AppColor::HiddenSoft, AppColor::Hidden, AppColor::HiddenStrong}, + {"Marked", {"pencil", "share", "pin"}, AppColor::MarkedSoft, AppColor::Marked, AppColor::MarkedStrong}, }; 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 c19089545..6a8d1a22e 100644 --- a/tests/theme_migration/theme_migration_test.cpp +++ b/tests/theme_migration/theme_migration_test.cpp @@ -173,6 +173,18 @@ TEST(ThemeAppColorTest, SemanticDefaultsFollowTheScheme) EXPECT_EQ(defaultAppColor(AppColor::InformationalStrong, true), QColor(QStringLiteral("#69aeff"))); EXPECT_EQ(defaultAppColor(AppColor::InformationalSoft, false), QColor(QStringLiteral("#526a8a"))); EXPECT_EQ(defaultAppColor(AppColor::InformationalSoft, true), QColor(QStringLiteral("#9db2cd"))); + EXPECT_EQ(defaultAppColor(AppColor::Hidden, false), QColor(QStringLiteral("#6d28d9"))); + EXPECT_EQ(defaultAppColor(AppColor::Hidden, true), QColor(QStringLiteral("#c6a5fa"))); + EXPECT_EQ(defaultAppColor(AppColor::HiddenStrong, false), QColor(QStringLiteral("#4c0f9e"))); + EXPECT_EQ(defaultAppColor(AppColor::HiddenStrong, true), QColor(QStringLiteral("#c99bff"))); + EXPECT_EQ(defaultAppColor(AppColor::HiddenSoft, false), QColor(QStringLiteral("#6a5a8c"))); + EXPECT_EQ(defaultAppColor(AppColor::HiddenSoft, true), QColor(QStringLiteral("#b3a6c9"))); + EXPECT_EQ(defaultAppColor(AppColor::Marked, false), QColor(QStringLiteral("#9a5b05"))); + EXPECT_EQ(defaultAppColor(AppColor::Marked, true), QColor(QStringLiteral("#e2a63a"))); + EXPECT_EQ(defaultAppColor(AppColor::MarkedStrong, false), QColor(QStringLiteral("#8a4f02"))); + 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"))); // 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.