aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/carddelegate.cpp33
-rw-r--r--src/carddelegate.h28
-rw-r--r--tests/test_carddelegate.cpp41
3 files changed, 102 insertions, 0 deletions
diff --git a/src/carddelegate.cpp b/src/carddelegate.cpp
index 6541932..89c3ed0 100644
--- a/src/carddelegate.cpp
+++ b/src/carddelegate.cpp
@@ -18,6 +18,8 @@
#include "carddelegate.h"
+#include "avatar.h"
+#include "businesssenders.h"
#include "cardlayout.h"
#include "marks.h"
#include "tagchip.h"
@@ -160,6 +162,21 @@ QSize CardDelegate::sizeHint(const QStyleOptionViewItem &option,
return QSize(option.rect.width(), CardLayout::heightFor(option.font));
}
+QPixmap CardDelegate::avatarFor(const QString &senderAddress,
+ const QString &senderName,
+ const QString &accountAddress,
+ const QString &accountLabel,
+ bool isBusinessSender, int side,
+ const QFont &font)
+{
+ const bool haveSender = !senderAddress.trimmed().isEmpty();
+ const QString seed = haveSender ? senderAddress : accountAddress;
+ const QString initials =
+ Avatar::initialsFor(senderName, senderAddress, accountLabel);
+ const Avatar::Fill fill = Avatar::fillFor(senderName, isBusinessSender);
+ return Avatar::pixmapFor(seed, initials, fill, side, font);
+}
+
void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
@@ -232,6 +249,22 @@ void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
painter->fillRect(spine, spineColour);
}
+ // The sender's squircle, in the layout's reserved gutter. Drawn before the
+ // text so a wide avatar can never overprint the sender line.
+ if (!card.avatarRect.isEmpty()) {
+ const QString senderAddress =
+ index.data(ThreadListModel::SenderAddressRole).toString();
+ const QString senderName =
+ index.data(ThreadListModel::SenderNameRole).toString();
+ painter->drawPixmap(
+ card.avatarRect,
+ avatarFor(senderAddress, senderName, m_accountAddress,
+ m_accountLabel,
+ BusinessSenders::contains(m_businessSenders,
+ senderAddress),
+ card.avatarRect.width(), option.font));
+ }
+
// Selection outranks the model's foreground, and the order matters: a read
// card carries a dimmed colour blended against the UNSELECTED background,
// so over the highlight it lands grey-on-highlight and close to unreadable.
diff --git a/src/carddelegate.h b/src/carddelegate.h
index 5a0830c..18c735f 100644
--- a/src/carddelegate.h
+++ b/src/carddelegate.h
@@ -18,6 +18,7 @@
#pragma once
+#include "businesssenders.h"
#include "tagchip.h"
/// Paints a whole card: three lines, all of it, including the tag chips.
@@ -109,4 +110,31 @@ public:
/// survived exactly that kind of test.
static QSize chipSize(const QFontMetrics &metrics, const QString &text,
bool own);
+
+ /// The squircle for one row, resolved from what the model supplies.
+ ///
+ /// Falls back to the ACCOUNT when the row has no sender address, so every
+ /// card carries an avatar rather than a hole: the seed becomes the
+ /// account's own address and the letters come from its label.
+ static QPixmap avatarFor(const QString &senderAddress,
+ const QString &senderName,
+ const QString &accountAddress,
+ const QString &accountLabel,
+ bool isBusinessSender, int side,
+ const QFont &font);
+
+ void setAccountAddress(const QString &address)
+ {
+ m_accountAddress = address;
+ }
+ void setAccountLabel(const QString &label) { m_accountLabel = label; }
+ void setBusinessSenders(const BusinessSenders::List &list)
+ {
+ m_businessSenders = list;
+ }
+
+private:
+ QString m_accountAddress;
+ QString m_accountLabel;
+ BusinessSenders::List m_businessSenders;
};
diff --git a/tests/test_carddelegate.cpp b/tests/test_carddelegate.cpp
index 6ec43b8..8b53309 100644
--- a/tests/test_carddelegate.cpp
+++ b/tests/test_carddelegate.cpp
@@ -38,6 +38,8 @@ private slots:
void aSiblingChipsPaddingShrinksWithItsFont();
void theFadeEndsAtSixtyPercentOfTheCard();
void aReplyFadeStartsAtItsOwnSpine();
+ void theDelegateAsksForAScaledSquircle();
+ void aRowWithNoSenderFallsBackToTheAccount();
};
namespace {
@@ -284,5 +286,44 @@ void TestCardDelegate::aReplyFadeStartsAtItsOwnSpine()
QVERIFY(reply.width() < CardDelegate::fadeRectFor(card, QRect()).width());
}
+void TestCardDelegate::theDelegateAsksForAScaledSquircle()
+{
+ // Asserted through the function the PRODUCTION path calls, not through
+ // Avatar::pixmapFor() directly: a test pointed at the function being
+ // called into proves what that function does and nothing about whether the
+ // delegate asks it for the right thing. CLAUDE.md records a mutation that
+ // survived exactly that mistake.
+ const QRect card(0, 0, 500, 60);
+ const QFont font;
+ const CardLayout layout =
+ CardLayout::compute(CardLayout::Input(), card, font);
+
+ const QPixmap pixmap = CardDelegate::avatarFor(
+ QStringLiteral("john@example.org"), QStringLiteral("John Doe"),
+ QStringLiteral("me@example.org"), QStringLiteral("Work"), false,
+ layout.avatarRect.width(), font);
+
+ QCOMPARE(pixmap.size(),
+ QSize(layout.avatarRect.width(), layout.avatarRect.width()));
+}
+
+void TestCardDelegate::aRowWithNoSenderFallsBackToTheAccount()
+{
+ const QFont font;
+ // No sender address at all: the squircle is still drawn, seeded from the
+ // account, so a card never shows a hole.
+ const QPixmap fallback = CardDelegate::avatarFor(
+ QString(), QString(), QStringLiteral("me@example.org"),
+ QStringLiteral("Work"), false, 44, font);
+ QVERIFY(!fallback.isNull());
+
+ // And it is the ACCOUNT's identity, not an arbitrary one: seeding from the
+ // same account twice agrees.
+ const QPixmap again = CardDelegate::avatarFor(
+ QString(), QString(), QStringLiteral("me@example.org"),
+ QStringLiteral("Work"), false, 44, font);
+ QCOMPARE(fallback.toImage(), again.toImage());
+}
+
QTEST_MAIN(TestCardDelegate)
#include "test_carddelegate.moc"