aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-26 15:50:33 +0200
committerDanilo M. <danix@danix.xyz>2026-08-26 15:50:33 +0200
commit5e769b073062a8913d2c4250d079aeb2d7d7b0e8 (patch)
tree4b2078bebb96ba2b2f6c0270aecb04dd2c1233f0
parentc3cfd28844c34aa7f7b390215f73d0b56534bdd0 (diff)
downloadqtmaildir-5e769b073062a8913d2c4250d079aeb2d7d7b0e8.tar.gz
qtmaildir-5e769b073062a8913d2c4250d079aeb2d7d7b0e8.zip
feat: reserve a card's avatar gutter
-rw-r--r--src/cardlayout.cpp10
-rw-r--r--src/cardlayout.h10
-rw-r--r--tests/test_cardlayout.cpp73
3 files changed, 91 insertions, 2 deletions
diff --git a/src/cardlayout.cpp b/src/cardlayout.cpp
index d32935b..a9bbe52 100644
--- a/src/cardlayout.cpp
+++ b/src/cardlayout.cpp
@@ -175,6 +175,16 @@ CardLayout CardLayout::compute(const Input &input, const QRect &rect,
const int indent = depth * kIndentStep;
out.contentLeft = textLeft + kPaddingX + indent;
+ // The avatar, square, in the gutter between the indent and the text.
+ // Sized from the card's HEIGHT rather than from a pixel constant, so it
+ // follows the desktop's font exactly as markSide() does.
+ const int avatarSide = qMax(0, rect.height() - kPaddingY * 2);
+ out.avatarRect = QRect(out.contentLeft, rect.top() + kPaddingY,
+ avatarSide, avatarSide);
+ // Everything after it starts past the squircle. This is what the item's
+ // cost is: a deep reply loses the gutter on top of its indent.
+ out.contentLeft = out.avatarRect.right() + 1 + kAvatarGap;
+
// One spine per level actually indented, each running the card's full
// height so an expansion reads as one continuous block.
for (int level = 0; level < depth; ++level) {
diff --git a/src/cardlayout.h b/src/cardlayout.h
index 92edd96..cd4eb76 100644
--- a/src/cardlayout.h
+++ b/src/cardlayout.h
@@ -142,6 +142,16 @@ struct CardLayout
/// without ever drawing two lines. Empty on a reply.
QRect accentRect;
+ /// The sender's avatar squircle, in its own gutter before the text.
+ ///
+ /// On EVERY row, thread and reply alike: a reply is where the sender
+ /// actually changes, so it is the row whose author is most worth seeing.
+ /// Square, and inset vertically so it does not touch the card's edges.
+ QRect avatarRect;
+
+ /// Space between the avatar and the text that follows it.
+ static constexpr int kAvatarGap = 8;
+
/// One full-height vertical line per depth level, outermost first.
QVector<QRect> spines;
diff --git a/tests/test_cardlayout.cpp b/tests/test_cardlayout.cpp
index f5f40ab..c81296f 100644
--- a/tests/test_cardlayout.cpp
+++ b/tests/test_cardlayout.cpp
@@ -45,6 +45,10 @@ private slots:
void theDateFitsWhenTheCardIsBold();
void theDateFollowsTheSystemLocale();
void aConfiguredDateFormatIsUsedAndReservedFor();
+ void everyRowCarriesAnAvatar();
+ void theAvatarPushesTheContentRight();
+ void theAvatarFollowsTheIndent();
+ void theAvatarIsSquareAndFitsTheCard();
};
namespace {
@@ -410,8 +414,14 @@ void TestCardLayout::marksDoNotCollideWithEachOtherOrTheExpander()
// The subject survives at a usable width rather than being squeezed to
// nothing by four marks: they are small and fixed, it is the elastic part.
- QVERIFY2(card.subjectRect.width() > 100,
- "four marks left the subject with almost no room on a 400px card");
+ // Relative rather than absolute: the avatar gutter shifts every text rect
+ // right, so a fixed pixel floor like 100 fails on a card that gained a
+ // gutter and would pass on one that had not. Comparing against another
+ // fixed element of the same card keeps the real invariant: the marks must
+ // not leave the subject narrower than the avatar gutter beside it.
+ QVERIFY2(card.subjectRect.width() > card.avatarRect.width(),
+ "four marks left the subject narrower than the avatar gutter on a "
+ "400px card");
}
void TestCardLayout::dateIsFlushRight()
@@ -580,5 +590,64 @@ void TestCardLayout::theDateFitsWhenTheCardIsBold()
.arg(boldWidth)));
}
+void TestCardLayout::everyRowCarriesAnAvatar()
+{
+ const QFont font;
+ const QRect rect(0, 0, 600, CardLayout::heightFor(font));
+
+ CardLayout::Input thread;
+ const CardLayout rootCard = CardLayout::compute(thread, rect, font);
+ QVERIFY(!rootCard.avatarRect.isEmpty());
+
+ // A reply gets one too: it is the row where the sender actually changes.
+ CardLayout::Input reply;
+ reply.isMessage = true;
+ reply.depth = 1;
+ const CardLayout replyCard = CardLayout::compute(reply, rect, font);
+ QVERIFY(!replyCard.avatarRect.isEmpty());
+}
+
+void TestCardLayout::theAvatarPushesTheContentRight()
+{
+ const QFont font;
+ const QRect rect(0, 0, 600, CardLayout::heightFor(font));
+ const CardLayout card = CardLayout::compute(CardLayout::Input(), rect, font);
+
+ // The text starts after the squircle, never on it.
+ QVERIFY(card.contentLeft >= card.avatarRect.right() + 1);
+}
+
+void TestCardLayout::theAvatarFollowsTheIndent()
+{
+ const QFont font;
+ const QRect rect(0, 0, 600, CardLayout::heightFor(font));
+
+ CardLayout::Input shallow;
+ shallow.isMessage = true;
+ shallow.depth = 1;
+ CardLayout::Input deep;
+ deep.isMessage = true;
+ deep.depth = 3;
+
+ const CardLayout shallowCard = CardLayout::compute(shallow, rect, font);
+ const CardLayout deepCard = CardLayout::compute(deep, rect, font);
+
+ // The squircle sits inside the card's own rect and moves with the nesting,
+ // which is the same reason contentLeft does. Asserting on the RECT here is
+ // safe precisely because it is CardLayout's own output, not a visualRect.
+ QVERIFY(deepCard.avatarRect.left() > shallowCard.avatarRect.left());
+}
+
+void TestCardLayout::theAvatarIsSquareAndFitsTheCard()
+{
+ const QFont font;
+ const QRect rect(0, 0, 600, CardLayout::heightFor(font));
+ const CardLayout card = CardLayout::compute(CardLayout::Input(), rect, font);
+
+ QCOMPARE(card.avatarRect.width(), card.avatarRect.height());
+ QVERIFY(card.avatarRect.top() >= rect.top());
+ QVERIFY(card.avatarRect.bottom() <= rect.bottom());
+}
+
QTEST_MAIN(TestCardLayout)
#include "test_cardlayout.moc"