From ef4d48c56c39f19efc751488cc2df6ee5af24a7f Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Wed, 26 Aug 2026 15:58:40 +0200 Subject: feat: fade the account colour across a card --- src/carddelegate.cpp | 31 +++++++++++++++++++++++++++++++ src/carddelegate.h | 15 +++++++++++++++ tests/test_carddelegate.cpp | 25 +++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/src/carddelegate.cpp b/src/carddelegate.cpp index 9a98c69..6541932 100644 --- a/src/carddelegate.cpp +++ b/src/carddelegate.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,18 @@ QColor CardDelegate::mutedChipColour(const QColor &chipColour) return QColor::fromHslF(h, s * kSaturationScale, l, a); } +QRect CardDelegate::fadeRectFor(const QRect &card, const QRect &innermostSpine) +{ + // The EXCLUSIVE right edge, then a rect built from it: QRect::right() is + // inclusive, which is the trap CardLayout already documents. + const int end = card.left() + int(card.width() * kFadeFraction); + const int start = innermostSpine.isEmpty() ? card.left() + : innermostSpine.left(); + if (end <= start) + return QRect(); + return QRect(start, card.top(), end - start, card.height()); +} + QColor CardDelegate::accentLineColour(const QColor &accountColour) { if (!accountColour.isValid()) @@ -178,6 +191,24 @@ void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, root.data(ThreadListModel::AccountColourRole).value(); const QColor lineColour = accentLineColour(accountColour); + // The account's fade. Under everything but the chrome, so the selection + // highlight and the doomed-row tint still cover it: a selected row reading + // mostly as selection is expected, not a fault. + const QRect fade = + fadeRectFor(option.rect, + card.spines.isEmpty() ? QRect() : card.spines.last()); + if (!fade.isEmpty() && accountColour.isValid()) { + QColor from = lineColour; + // A reply's wash is weaker than its root's, so an expanded thread + // reads as one block with the root leading it. + from.setAlphaF(card.accentRect.isEmpty() ? 0.14 : 0.30); + QLinearGradient gradient(fade.topLeft(), fade.topRight()); + gradient.setColorAt(0.0, from); + from.setAlphaF(0.0); + gradient.setColorAt(1.0, from); + painter->fillRect(fade, gradient); + } + // The accent bar, thread cards only. Drawn after the chrome so the // selection highlight cannot cover it: which account a card belongs to // must stay readable on the row the user is looking at. diff --git a/src/carddelegate.h b/src/carddelegate.h index 1846359..5a0830c 100644 --- a/src/carddelegate.h +++ b/src/carddelegate.h @@ -77,6 +77,21 @@ public: /// Falls back to threadLineColour() for a thread with no account tag. static QColor accentLineColour(const QColor &accountColour); + /// Where the account's fade runs, given the card and the row's innermost + /// spine (an empty rect for a thread root, which has none). + /// + /// A root's fade starts at the card's left edge; a reply's starts at its + /// own spine, which IS its coloured left border, so the wash steps right + /// with the nesting. Both end at 60% of the card's width, so a deeper + /// reply's wash is shorter as well as further right. + /// + /// Static and rect-in, rect-out so the geometry is assertable without a + /// painter, for the same reason CardLayout is. + static QRect fadeRectFor(const QRect &card, const QRect &innermostSpine); + + /// How far across the card the account's colour reaches. + static constexpr qreal kFadeFraction = 0.60; + /// A tag chip's colour, drained for the SIBLING tier (item 111). /// /// Saturation only: hue stays so the tag is still recognisable, and diff --git a/tests/test_carddelegate.cpp b/tests/test_carddelegate.cpp index a14671d..6ec43b8 100644 --- a/tests/test_carddelegate.cpp +++ b/tests/test_carddelegate.cpp @@ -36,6 +36,8 @@ private slots: void aSiblingChipIsMutedButStaysLegibleAndRecognisable(); void aSiblingChipFontIsSmallerThanItsOwnTier(); void aSiblingChipsPaddingShrinksWithItsFont(); + void theFadeEndsAtSixtyPercentOfTheCard(); + void aReplyFadeStartsAtItsOwnSpine(); }; namespace { @@ -259,5 +261,28 @@ void TestCardDelegate::aSiblingChipsPaddingShrinksWithItsFont() "on the chip's rounded end"); } +void TestCardDelegate::theFadeEndsAtSixtyPercentOfTheCard() +{ + const QRect card(0, 0, 500, 60); + const QRect root = CardDelegate::fadeRectFor(card, QRect()); + QCOMPARE(root.left(), card.left()); + QCOMPARE(root.width(), 300); +} + +void TestCardDelegate::aReplyFadeStartsAtItsOwnSpine() +{ + const QRect card(0, 0, 500, 60); + // The innermost spine of a nested reply, which is its own coloured border. + const QRect spine(80, 0, 2, 60); + const QRect reply = CardDelegate::fadeRectFor(card, spine); + + // It hangs off the spine, not off the card's edge. + QCOMPARE(reply.left(), spine.left()); + // And still ends at 60% of the CARD, so a deeper reply's wash is shorter + // as well as further right. + QCOMPARE(reply.right(), CardDelegate::fadeRectFor(card, QRect()).right()); + QVERIFY(reply.width() < CardDelegate::fadeRectFor(card, QRect()).width()); +} + QTEST_MAIN(TestCardDelegate) #include "test_carddelegate.moc" -- cgit v1.2.3