[network/ruqola] src/widgets: Give the Normal message layout a deliberate vertical rhythm
Till Adam <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit b6d31c3086187ce97131d6b56aa0e8d8a52b16cc by Till Adam.
Committed on 28/07/2026 at 19:07.
Pushed by tilladam into branch 'master'.
Give the Normal message layout a deliberate vertical rhythm
## What
Give the **Normal** message-list layout a deliberate, consistent vertical rhythm. Before this, the spacing between messages was an incidental byproduct of a few unrelated constants, so a change of speaker got no more empty space above it than a grouped continuation from the same author — blocks didn't read as blocks and the list looked unharmonious.
## How
- Introduce a small vertical spacing scale in `MessageDelegateUtils`: `senderBlockSpacing()` above a message block when the author changes, and the smaller `groupedMessageSpacing()` above a consecutive message from the same author — both larger than the name-to-text gap. This gives a clear proximity ladder: **speaker-change > grouped > name-to-text**.
- Place that empty space *above* each block and shift the whole author line (name, its rect, avatar) down by the same amount as the text, reusing the existing top-band mechanism, so the name stays exactly one line above its own text.
- Shrink the per-row bottom breather (5 → 2) so the top-of-block spacing and the bottom breather don't stack into an oversized gap.
- Apply the author-line shift only on the text branch: attachment/no-text rows already derive their baseline from the shifted origin, so shifting again would double-count and drop the author line below its own attachment.
Scoped to the Normal layout for now; the scale lives in `MessageDelegateUtils` so the Cozy and Compact layouts can adopt it later.
## Test
Builds clean. Verified visually in the running app across single-line, multi-line, grouped-same-author, attachment/no-text, and date-separator rows.
M +10 -0 src/widgets/delegateutils/messagedelegateutils.cpp
M +6 -0 src/widgets/delegateutils/messagedelegateutils.h
M +23 -11 src/widgets/room/delegate/messagelistlayout/messagelistnormallayout.cpp
https://invent.kde.org/network/ruqola/-/commit/b6d31c3086187ce97131d6b56aa0e8d8a52b16cc
diff --git a/src/widgets/delegateutils/messagedelegateutils.cpp b/src/widgets/delegateutils/messagedelegateutils.cpp
index e851f35127..2bceded1b3 100644
--- a/src/widgets/delegateutils/messagedelegateutils.cpp
+++ b/src/widgets/delegateutils/messagedelegateutils.cpp
@@ -159,6 +159,16 @@ qreal MessageDelegateUtils::basicMargin()
return 8;
}
+int MessageDelegateUtils::senderBlockSpacing()
+{
+ return 10;
+}
+
+int MessageDelegateUtils::groupedMessageSpacing()
+{
+ return 1;
+}
+
QSize MessageDelegateUtils::timeStampSize(const QString &timeStampText, const QStyleOptionViewItem &option)
{
// This gives incorrect results (too small bounding rect), no idea why!
diff --git a/src/widgets/delegateutils/messagedelegateutils.h b/src/widgets/delegateutils/messagedelegateutils.h
index 3c6deb0a41..b92cb702e8 100644
--- a/src/widgets/delegateutils/messagedelegateutils.h
+++ b/src/widgets/delegateutils/messagedelegateutils.h
@@ -52,6 +52,12 @@ void setClipboardSelection(TextSelection *selection);
[[nodiscard]] QSizeF dprAwareSize(const QPixmap &pixmap);
[[nodiscard]] qreal basicMargin();
+// Vertical spacing scale for the message list. Empty space is placed above each message block:
+// senderBlockSpacing() when the author changes (so a new speaker reads as a new block) and the
+// smaller groupedMessageSpacing() for a consecutive message from the same author. Both are
+// deliberately larger than the name-to-text gap, so proximity groups each author with their text.
+[[nodiscard]] int senderBlockSpacing();
+[[nodiscard]] int groupedMessageSpacing();
[[nodiscard]] QSize timeStampSize(const QString &timeStampText, const QStyleOptionViewItem &option);
[[nodiscard]] QSize textSizeHint(QTextDocument *doc, qreal *pBaseLine);
[[nodiscard]] bool showIgnoreMessages(const QModelIndex &index);
diff --git a/src/widgets/room/delegate/messagelistlayout/messagelistnormallayout.cpp b/src/widgets/room/delegate/messagelistlayout/messagelistnormallayout.cpp
index 5ba55b729d..34c620d492 100644
--- a/src/widgets/room/delegate/messagelistlayout/messagelistnormallayout.cpp
+++ b/src/widgets/room/delegate/messagelistlayout/messagelistnormallayout.cpp
@@ -49,6 +49,12 @@ MessageListLayoutBase::Layout MessageListNormalLayout::doLayout(const QStyleOpti
QRect usableRect = option.rect;
const bool displayLastSeenMessage = index.data(MessagesModel::DisplayLastSeenMessage).toBool();
const bool dateDiffersFromPrevious = index.data(MessagesModel::DateDiffersFromPrevious).toBool();
+ // Empty space above the message block that sets the vertical rhythm: a full gap when the
+ // author changes (so a new speaker reads as a new block) and a small one for a grouped
+ // consecutive message from the same author. Deliberately larger than the name-to-text gap
+ // (textVMargin) further down, so proximity groups each author with their own text.
+ const int blockTopSpacing = layout.sameSenderAsPreviousMessage ? MessageDelegateUtils::groupedMessageSpacing() : MessageDelegateUtils::senderBlockSpacing();
+
// A date header and a standalone unread-messages line each occupy a band at the top of
// the row. Reserve it here; the author line is shifted down by the same amount below.
int topBandHeight = 0;
@@ -60,7 +66,7 @@ MessageListLayoutBase::Layout MessageListNormalLayout::doLayout(const QStyleOpti
// top of the next message.
layout.displayLastSeenMessageY = usableRect.top() + topBandHeight / 2;
}
- usableRect.setTop(usableRect.top() + topBandHeight);
+ usableRect.setTop(usableRect.top() + topBandHeight + blockTopSpacing);
layout.usableRect = usableRect; // Just for the top, for now. The left will move later on.
usableRect.setTop(usableRect.top() + senderAscent); // FIXME position.
@@ -165,15 +171,18 @@ MessageListLayoutBase::Layout MessageListNormalLayout::doLayout(const QStyleOpti
// Align top of sender rect so it matches the baseline of the richtext
layout.senderRect =
QRectF(senderX, layout.baseLine - senderAscent, senderTextSize.width(), (layout.sameSenderAsPreviousMessage ? 0 : senderTextSize.height()));
- if (topBandHeight > 0) {
- // A date header (drawn by drawDate) or a standalone unread-messages line occupies the
- // row's top band, and usableRect already pushed the message text down by that band's
- // height. Shift the whole author line — the name baseline, its rect, and therefore the
- // avatar — down by the same height, so such a row is exactly a regular new-sender row
- // plus the top band. (Replaces an "- 4" fudge that moved only the baseline, leaving the
- // author line looser above its text than a normal new-sender row.)
- layout.baseLine += topBandHeight;
- layout.senderRect.moveTop(layout.senderRect.top() + topBandHeight);
+ // usableRect already pushed the message text down by the top band (a date header drawn by
+ // drawDate, or a standalone unread-messages line) plus the block-top spacing. Shift the whole
+ // author line — the name baseline, its rect, and therefore the avatar — down by the same
+ // amount, so the name stays exactly one line above its own text regardless of that offset.
+ // Only the text branch needs this: it derived baseLine from option.rect.top() (unshifted).
+ // The empty-text branch (attachment/blocks/urls only) already derived baseLine from
+ // usableRect.top(), which includes both offsets, so shifting again would double-count and
+ // drop the author line below its own attachment.
+ const int authorLineShift = topBandHeight + blockTopSpacing;
+ if (textSize.isValid() && authorLineShift > 0) {
+ layout.baseLine += authorLineShift;
+ layout.senderRect.moveTop(layout.senderRect.top() + authorLineShift);
}
// Align top of avatar with top of sender rect
const double senderRectY{layout.senderRect.y()};
@@ -342,7 +351,10 @@ QSize MessageListNormalLayout::sizeHint(const QStyleOptionViewItem &option, cons
// Note: option.rect in this method is huge (as big as the viewport)
const MessageListLayoutBase::Layout layout = doLayout(option, index);
- int additionalHeight = 5;
+ // Most inter-message separation now lives in the block-top spacing (added in doLayout to the
+ // top of the next block), so only a small breather is needed under each row to keep the two
+ // gaps from stacking into an oversized space between messages.
+ int additionalHeight = 2;
// A little bit of margin below the very last item, it just looks better
if (index.row() == index.model()->rowCount() - 1) {
additionalHeight += 10; // Add more space as cozy mode