5353
5454#include < cmath>
5555#include < cstdint>
56+ #include < functional>
57+ #include < utility>
5658
5759namespace {
5860
@@ -76,6 +78,44 @@ constexpr int kThoughtBrowserPadLeft = 4;
7678constexpr int kUserBubblePadH = 14 ;
7779constexpr int kUserBubbleGap = 32 ; // space kept free to the left of a full-width user bubble
7880constexpr int kThoughtItalicRightSlack = 4 ;
81+
82+ // A word that cannot wrap (a path, a URL) must not set the bubble's minimum
83+ // width. A bubble whose minimum is wider than the transcript gets laid out by
84+ // QBoxLayout for its minimum width, not the width it has: the height comes out
85+ // short and the last lines are clipped. The word is clipped instead.
86+ void allowShrink (QWidget *w)
87+ {
88+ w->setMinimumWidth (1 );
89+ }
90+
91+ // The transcript layout asks a bubble's layout (QWidgetItem::heightForWidth),
92+ // not the bubble, for its height at the full transcript width; AlignRight then
93+ // narrows the bubble to its hint. Answer for the width the bubble really gets,
94+ // or lines that wrap only at that width are clipped.
95+ class UserBubbleLayout : public QVBoxLayout
96+ {
97+ public:
98+ UserBubbleLayout (QWidget *parent, std::function<int ()> width)
99+ : QVBoxLayout(parent)
100+ , m_width(std::move(width))
101+ {
102+ }
103+
104+ int heightForWidth (int w) const override { return QVBoxLayout::heightForWidth (qMin (w, m_width ())); }
105+
106+ // Runs on every text, font or visibility change of a child.
107+ void invalidate () override
108+ {
109+ contentWidth = -1 ;
110+ QVBoxLayout::invalidate ();
111+ }
112+
113+ // AcpMessageWidget::measureUserContent() (text shaping), -1 = stale.
114+ mutable int contentWidth = -1 ;
115+
116+ private:
117+ std::function<int ()> m_width;
118+ };
79119// Final-report action row: square icon buttons, glyph per ui-dna toolbar chrome.
80120constexpr int kResponseActionsH = 22 ;
81121constexpr int kResponseActionIcon = 14 ;
@@ -135,7 +175,11 @@ AcpMessageWidget::AcpMessageWidget(QString role, QWidget *parent)
135175 QString::fromLatin1 (kFrameStyleThought ) +
136176 QString::fromLatin1 (kFrameStyleSystem ));
137177
138- m_layout = new QVBoxLayout (this );
178+ if (m_role == QLatin1String (" user" )) {
179+ m_layout = new UserBubbleLayout (this , [this ]() { return userBubbleWidth (); });
180+ } else {
181+ m_layout = new QVBoxLayout (this );
182+ }
139183 if (m_role == QLatin1String (" thought" )) {
140184 m_layout->setContentsMargins (4 , 4 , 4 , 4 );
141185 } else if (m_role == QLatin1String (" user" )) {
@@ -300,6 +344,7 @@ void AcpMessageWidget::setContent(const QVector<AcpProtocol::AcpContentBlock> &c
300344 if (!pendingTextLabel) {
301345 pendingTextLabel = new QLabel (this );
302346 pendingTextLabel->setWordWrap (true );
347+ allowShrink (pendingTextLabel);
303348 pendingTextLabel->setTextInteractionFlags (Qt::TextSelectableByMouse);
304349 // Stamp the chat font now: this QLabel is created after the
305350 // view's initial setChatFont() pass, and being inside a
@@ -360,6 +405,7 @@ void AcpMessageWidget::setCollapsedSummary(const QString &summary, const QString
360405 m_summaryHeader->setCheckable (true );
361406 m_summaryHeader->setAutoRaise (true );
362407 m_summaryHeader->setToolButtonStyle (Qt::ToolButtonTextOnly);
408+ allowShrink (m_summaryHeader);
363409 m_summaryHeader->setStyleSheet (QStringLiteral (
364410 " QToolButton { border: none; padding: 0; margin: 0; text-align: left; }" ));
365411 m_summaryHeader->setCursor (Qt::PointingHandCursor);
@@ -369,6 +415,7 @@ void AcpMessageWidget::setCollapsedSummary(const QString &summary, const QString
369415 m_summaryOverview = new QLabel (this );
370416 m_summaryOverview->setTextFormat (Qt::PlainText);
371417 m_summaryOverview->setWordWrap (true );
418+ allowShrink (m_summaryOverview);
372419 m_summaryOverview->setTextInteractionFlags (Qt::TextSelectableByMouse);
373420 if (m_chatFontSet)
374421 m_summaryOverview->setFont (m_chatFont);
@@ -463,8 +510,28 @@ QSize AcpMessageWidget::sizeHint() const
463510 if (m_role != QLatin1String (" user" ) || !m_layout)
464511 return QFrame::sizeHint ();
465512
513+ const int w = userBubbleWidth ();
514+ const int h = m_layout->hasHeightForWidth () ? m_layout->totalHeightForWidth (w)
515+ : QFrame::sizeHint ().height ();
516+ return QSize (w, h);
517+ }
518+
519+ int AcpMessageWidget::userBubbleWidth () const
520+ {
466521 int marginL = 0 , marginT = 0 , marginR = 0 , marginB = 0 ;
467522 m_layout->getContentsMargins (&marginL, &marginT, &marginR, &marginB);
523+ const auto *layout = static_cast <const UserBubbleLayout *>(m_layout);
524+ if (layout->contentWidth < 0 )
525+ layout->contentWidth = measureUserContent ();
526+ // +2 border, +2 so a line that exactly fits is not wrapped by rounding.
527+ int w = layout->contentWidth + marginL + marginR + 4 ;
528+ if (const QWidget *host = parentWidget ())
529+ w = qMin (w, qMax (host->width () - kUserBubbleGap , 120 ));
530+ return w;
531+ }
532+
533+ int AcpMessageWidget::measureUserContent () const
534+ {
468535 int contentW = 0 ;
469536 if (m_summaryHeader)
470537 contentW = m_summaryHeader->sizeHint ().width ();
@@ -487,13 +554,7 @@ QSize AcpMessageWidget::sizeHint() const
487554 if (!ui.original .isNull () && !ui.label ->isHidden ())
488555 contentW = qMax (contentW, ui.original .width ());
489556 }
490- // +2 border, +2 so a line that exactly fits is not wrapped by rounding.
491- int w = contentW + marginL + marginR + 4 ;
492- if (const QWidget *host = parentWidget ())
493- w = qMin (w, qMax (host->width () - kUserBubbleGap , 120 ));
494- const int h = m_layout->hasHeightForWidth () ? m_layout->totalHeightForWidth (w)
495- : QFrame::sizeHint ().height ();
496- return QSize (w, h);
557+ return contentW;
497558}
498559
499560void AcpMessageWidget::refitBrowserHeight ()
0 commit comments