[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.