From 04463f09548ace7597dc9631565a4c2ce01e7fc3 Mon Sep 17 00:00:00 2001 From: Jeff Patterson Date: Sun, 20 Sep 2026 11:57:21 -0500 Subject: [PATCH] Darken the hover ink on a light button face QETStyle::hoverColor() lightened the highlight color until it read at 3.5:1 against the Light role. On a dark face that is the way to go; on a light face lightening only fades the ink, so with a pale platform accent that QET keeps (macOS's green selection color, black selection text) the loop ran to white and every hovered line-art icon vanished. The ink now moves away from the face, darker on a light face, lighter on a dark one, and falls back to the button text color if twenty steps are not enough. The hover test gets a row with that accent on each palette, and a new test sweeps accents across hues and lightness on both palettes and requires the hover ink to read at 3:1 on the face. Fixes #962 --- sources/qetstyle.cpp | 15 ++++++++--- tests/qttest/tst_qeticons.cpp | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/sources/qetstyle.cpp b/sources/qetstyle.cpp index 9dc63ac13..d8a292240 100644 --- a/sources/qetstyle.cpp +++ b/sources/qetstyle.cpp @@ -67,20 +67,27 @@ bool QETStyle::isLineArt(const QImage &image) /** @brief QETStyle::hoverColor The palette's highlight color is the accent users already know from - selections. On a dark palette it is too dark to read on a hovered - button face, so it is lightened, a step at a time, until it reaches + selections, moved away from the hovered button face until it reaches 3:1 (WCAG 1.4.11) against the Light role: Fusion paints a hovered auto-raise button with a gradient that runs from Button up to about - that color, and the icon has to read on the lightest part of it. + that color, and the icon has to read on the lightest part of it. On a + dark face the accent is lightened, a step at a time; on a light face it + is darkened, which keeps a pale accent (macOS's green or yellow + selection color, which comes with black selection text) from being + pushed to white. Should twenty steps not get there, the button text + color serves, which reads on the face by construction. */ QColor QETStyle::hoverColor(const QPalette &palette) { const QColor face = palette.color(QPalette::Active, QPalette::Light); + const bool light_face = face.lightnessF() > 0.5; QColor ink = palette.color(QPalette::Active, QPalette::Highlight); // 3.5 rather than 3.0: the top of Fusion's hover gradient is a shade // lighter than the Light role, so the icon needs some headroom there. for (int step = 0; step < 20 && QET::Palette::contrastRatio(ink, face) < 3.5; ++step) - ink = ink.lighter(110); + ink = light_face ? ink.darker(110) : ink.lighter(110); + if (QET::Palette::contrastRatio(ink, face) < 3.5) + ink = palette.color(QPalette::Active, QPalette::ButtonText); return ink; } diff --git a/tests/qttest/tst_qeticons.cpp b/tests/qttest/tst_qeticons.cpp index 3393702fe..5d0d7f0a8 100644 --- a/tests/qttest/tst_qeticons.cpp +++ b/tests/qttest/tst_qeticons.cpp @@ -63,6 +63,8 @@ class tst_qeticons : public QObject void toolbarIconIsReadable(); void hoverChangesTheIcon_data(); void hoverChangesTheIcon(); + void hoverInkReadsWithAnyAccent_data(); + void hoverInkReadsWithAnyAccent(); void coloredIconKeepsItsColorsOnHover(); void menuIconReadsOnHighlight_data(); void menuIconReadsOnHighlight(); @@ -107,6 +109,17 @@ namespace { } const QStringList kSizes = {"16x16", "22x22", "32x32", "48x48", "128x128"}; + + /// palette with the given selection colors, as QET::Palette::withPlatformAccent leaves them. + QPalette withAccent(QPalette palette, const QColor &highlight, const QColor &highlighted_text) + { + for (QPalette::ColorGroup group : {QPalette::Active, QPalette::Inactive}) + { + palette.setColor(group, QPalette::Highlight, highlight); + palette.setColor(group, QPalette::HighlightedText, highlighted_text); + } + return palette; + } } void tst_qeticons::initTestCase() @@ -334,6 +347,12 @@ void tst_qeticons::hoverChangesTheIcon_data() // nothing in the frame, so the icon is the only hover signal there. QTest::newRow("light-checked") << "qet" << QET::Palette::fusionLight() << true; QTest::newRow("dark-checked") << "qet-dark" << QET::Palette::fusionDark() << true; + // A platform accent QET keeps because its selection text reads on + // it: macOS's green selection color comes with black text. Lightening + // it toward 3:1 against a white face can only end at white, which + // made every hovered line-art icon vanish on a light palette. + QTest::newRow("light-pale-accent") << "qet" << withAccent(QET::Palette::fusionLight(), QColor(198, 231, 188), Qt::black) << false; + QTest::newRow("dark-pale-accent") << "qet-dark" << withAccent(QET::Palette::fusionDark(), QColor(198, 231, 188), Qt::black) << false; } /** @@ -502,6 +521,36 @@ void tst_qeticons::configPageIconsComeAtPageSize() QVERIFY2(small.isEmpty(), qPrintable("page icons short of 128 pixels: " + small.join(", "))); } +/** + Whatever accent the platform hands QET, the hover ink must read on the + hovered button face of both palettes: a sweep over hues at every + lightness, including the pale and the near-black ones. +*/ +void tst_qeticons::hoverInkReadsWithAnyAccent_data() +{ + QTest::addColumn("palette"); + for (int lightness = 10; lightness <= 250; lightness += 40) + for (int hue = 0; hue < 360; hue += 60) + { + const QColor accent = QColor::fromHsl(hue, 200, lightness); + const QColor text = QET::Palette::contrastRatio(accent, Qt::white) >= 4.5 ? Qt::white : Qt::black; + QTest::newRow(qPrintable(QString("light-%1").arg(accent.name()))) + << withAccent(QET::Palette::fusionLight(), accent, text); + QTest::newRow(qPrintable(QString("dark-%1").arg(accent.name()))) + << withAccent(QET::Palette::fusionDark(), accent, text); + } +} + +void tst_qeticons::hoverInkReadsWithAnyAccent() +{ + QFETCH(QPalette, palette); + const QColor face = palette.color(QPalette::Active, QPalette::Light); + const QColor ink = QETStyle::hoverColor(palette); + const double contrast = QET::Palette::contrastRatio(ink, face); + QVERIFY2(contrast >= kIconRatio, + qPrintable(QString("hover ink %1 reads %2:1 on the face %3").arg(ink.name()).arg(contrast).arg(face.name()))); +} + int main(int argc, char **argv) { if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM"))