File indexing completed on 2024-04-14 14:21:01

0001 /*  This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 2001 David Faure <faure@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "kwordwrap.h"
0008 
0009 #include <QPainter>
0010 #include <QVector>
0011 
0012 class KWordWrapPrivate : public QSharedData
0013 {
0014 public:
0015     QRect m_constrainingRect;
0016     QVector<int> m_breakPositions;
0017     QVector<int> m_lineWidths;
0018     QRect m_boundingRect;
0019     QString m_text;
0020 };
0021 
0022 KWordWrap::KWordWrap(const QRect &r)
0023     : d(new KWordWrapPrivate)
0024 {
0025     d->m_constrainingRect = r;
0026 }
0027 
0028 KWordWrap KWordWrap::formatText(QFontMetrics &fm, const QRect &r, int /*flags*/, const QString &str, int len)
0029 {
0030     KWordWrap kw(r);
0031     // The wordwrap algorithm
0032     // The variable names and the global shape of the algorithm are inspired
0033     // from QTextFormatterBreakWords::format().
0034     // qDebug() << "KWordWrap::formatText " << str << " r=" << r.x() << "," << r.y() << " " << r.width() << "x" << r.height();
0035     int height = fm.height();
0036     if (len == -1) {
0037         kw.d->m_text = str;
0038     } else {
0039         kw.d->m_text = str.left(len);
0040     }
0041     if (len == -1) {
0042         len = str.length();
0043     }
0044     int lastBreak = -1;
0045     int lineWidth = 0;
0046     int x = 0;
0047     int y = 0;
0048     int w = r.width();
0049     int textwidth = 0;
0050     bool isBreakable = false;
0051     bool wasBreakable = false; // value of isBreakable for last char (i-1)
0052     bool isParens = false; // true if one of ({[
0053     bool wasParens = false; // value of isParens for last char (i-1)
0054     QString inputString = str;
0055 
0056     for (int i = 0; i < len; ++i) {
0057         const QChar c = inputString.at(i);
0058         const int ww = fm.horizontalAdvance(c);
0059 
0060         isParens = (c == QLatin1Char('(') //
0061                     || c == QLatin1Char('[') //
0062                     || c == QLatin1Char('{'));
0063         // isBreakable is true when we can break _after_ this character.
0064         isBreakable = (c.isSpace() || c.isPunct() || c.isSymbol()) & !isParens;
0065 
0066         // Special case for '(', '[' and '{': we want to break before them
0067         if (!isBreakable && i < len - 1) {
0068             const QChar nextc = inputString.at(i + 1); // look at next char
0069             isBreakable = (nextc == QLatin1Char('(') //
0070                            || nextc == QLatin1Char('[') //
0071                            || nextc == QLatin1Char('{'));
0072         }
0073         // Special case for '/': after normal chars it's breakable (e.g. inside a path),
0074         // but after another breakable char it's not (e.g. "mounted at /foo")
0075         // Same thing after a parenthesis (e.g. "dfaure [/fool]")
0076         if (c == QLatin1Char('/') && (wasBreakable || wasParens)) {
0077             isBreakable = false;
0078         }
0079 
0080         /*qDebug() << "c='" << QString(c) << "' i=" << i << "/" << len
0081                   << " x=" << x << " ww=" << ww << " w=" << w
0082                   << " lastBreak=" << lastBreak << " isBreakable=" << isBreakable << endl;*/
0083         int breakAt = -1;
0084         if (x + ww > w && lastBreak != -1) { // time to break and we know where
0085             breakAt = lastBreak;
0086         }
0087         if (x + ww > w - 4 && lastBreak == -1) { // time to break but found nowhere [-> break here]
0088             breakAt = i;
0089         }
0090         if (i == len - 2 && x + ww + fm.horizontalAdvance(inputString.at(i + 1)) > w) { // don't leave the last char alone
0091             breakAt = lastBreak == -1 ? i - 1 : lastBreak;
0092         }
0093         if (c == QLatin1Char('\n')) { // Forced break here
0094             if (breakAt == -1 && lastBreak != -1) { // only break if not already breaking
0095                 breakAt = i - 1;
0096                 lastBreak = -1;
0097             }
0098             // remove the line feed from the string
0099             kw.d->m_text.remove(i, 1);
0100             inputString.remove(i, 1);
0101             len--;
0102         }
0103         if (breakAt != -1) {
0104             // qDebug() << "KWordWrap::formatText breaking after " << breakAt;
0105             kw.d->m_breakPositions.append(breakAt);
0106             int thisLineWidth = lastBreak == -1 ? x + ww : lineWidth;
0107             kw.d->m_lineWidths.append(thisLineWidth);
0108             textwidth = qMax(textwidth, thisLineWidth);
0109             x = 0;
0110             y += height;
0111             wasBreakable = true;
0112             wasParens = false;
0113             if (lastBreak != -1) {
0114                 // Breakable char was found, restart from there
0115                 i = lastBreak;
0116                 lastBreak = -1;
0117                 continue;
0118             }
0119         } else if (isBreakable) {
0120             lastBreak = i;
0121             lineWidth = x + ww;
0122         }
0123         x += ww;
0124         wasBreakable = isBreakable;
0125         wasParens = isParens;
0126     }
0127     textwidth = qMax(textwidth, x);
0128     kw.d->m_lineWidths.append(x);
0129     y += height;
0130     // qDebug() << "KWordWrap::formatText boundingRect:" << r.x() << "," << r.y() << " " << textwidth << "x" << y;
0131     if (r.height() >= 0 && y > r.height()) {
0132         textwidth = r.width();
0133     }
0134     int realY = y;
0135     if (r.height() >= 0) {
0136         while (realY > r.height()) {
0137             realY -= height;
0138         }
0139         realY = qMax(realY, 0);
0140     }
0141     kw.d->m_boundingRect.setRect(0, 0, textwidth, realY);
0142     return kw;
0143 }
0144 
0145 KWordWrap::~KWordWrap()
0146 {
0147 }
0148 
0149 KWordWrap::KWordWrap(const KWordWrap &other)
0150     : d(other.d)
0151 {
0152 }
0153 
0154 KWordWrap &KWordWrap::operator=(const KWordWrap &other)
0155 {
0156     d = other.d;
0157     return *this;
0158 }
0159 
0160 QString KWordWrap::wrappedString() const
0161 {
0162     const QStringView strView(d->m_text);
0163     // We use the calculated break positions to insert '\n' into the string
0164     QString ws;
0165     int start = 0;
0166     for (int i = 0; i < d->m_breakPositions.count(); ++i) {
0167         int end = d->m_breakPositions.at(i);
0168         ws += strView.mid(start, end - start + 1);
0169         ws += QLatin1Char('\n');
0170         start = end + 1;
0171     }
0172     ws += strView.mid(start);
0173     return ws;
0174 }
0175 
0176 QString KWordWrap::truncatedString(bool dots) const
0177 {
0178     if (d->m_breakPositions.isEmpty()) {
0179         return d->m_text;
0180     }
0181 
0182     QString ts = d->m_text.left(d->m_breakPositions.first() + 1);
0183     if (dots) {
0184         ts += QLatin1String("...");
0185     }
0186     return ts;
0187 }
0188 
0189 static QColor mixColors(double p1, QColor c1, QColor c2)
0190 {
0191     return QColor(int(c1.red() * p1 + c2.red() * (1.0 - p1)), //
0192                   int(c1.green() * p1 + c2.green() * (1.0 - p1)), //
0193                   int(c1.blue() * p1 + c2.blue() * (1.0 - p1)));
0194 }
0195 
0196 void KWordWrap::drawFadeoutText(QPainter *p, int x, int y, int maxW, const QString &t)
0197 {
0198     QFontMetrics fm = p->fontMetrics();
0199     QColor bgColor = p->background().color();
0200     QColor textColor = p->pen().color();
0201 
0202     if ((fm.boundingRect(t).width() > maxW) && (t.length() > 1)) {
0203         int tl = 0;
0204         int w = 0;
0205         while (tl < t.length()) {
0206             w += fm.horizontalAdvance(t.at(tl));
0207             if (w >= maxW) {
0208                 break;
0209             }
0210             tl++;
0211         }
0212 
0213         int n = qMin(tl, 3);
0214         if (t.isRightToLeft()) {
0215             x += maxW; // start from the right side for RTL string
0216             if (tl > 3) {
0217                 x -= fm.horizontalAdvance(t.left(tl - 3));
0218                 p->drawText(x, y, t.left(tl - 3));
0219             }
0220             for (int i = 0; i < n; i++) {
0221                 p->setPen(mixColors(0.70 - i * 0.25, textColor, bgColor));
0222                 QString s(t.at(tl - n + i));
0223                 x -= fm.horizontalAdvance(s);
0224                 p->drawText(x, y, s);
0225             }
0226         } else {
0227             if (tl > 3) {
0228                 p->drawText(x, y, t.left(tl - 3));
0229                 x += fm.horizontalAdvance(t.left(tl - 3));
0230             }
0231             for (int i = 0; i < n; i++) {
0232                 p->setPen(mixColors(0.70 - i * 0.25, textColor, bgColor));
0233                 QString s(t.at(tl - n + i));
0234                 p->drawText(x, y, s);
0235                 x += fm.horizontalAdvance(s);
0236             }
0237         }
0238     } else {
0239         p->drawText(x, y, t);
0240     }
0241 }
0242 
0243 void KWordWrap::drawTruncateText(QPainter *p, int x, int y, int maxW, const QString &t)
0244 {
0245     QString tmpText = p->fontMetrics().elidedText(t, Qt::ElideRight, maxW);
0246     p->drawText(x, y, tmpText);
0247 }
0248 
0249 void KWordWrap::drawText(QPainter *painter, int textX, int textY, int flags) const
0250 {
0251     // qDebug() << "KWordWrap::drawText text=" << wrappedString() << " x=" << textX << " y=" << textY;
0252     // We use the calculated break positions to draw the text line by line using QPainter
0253     int start = 0;
0254     int y = 0;
0255     QFontMetrics fm = painter->fontMetrics();
0256     int height = fm.height(); // line height
0257     int ascent = fm.ascent();
0258     int maxwidth = d->m_boundingRect.width();
0259     int i;
0260     int lwidth = 0;
0261     int end = 0;
0262     for (i = 0; i < d->m_breakPositions.count(); ++i) {
0263         // if this is the last line, leave the loop
0264         if (d->m_constrainingRect.height() >= 0 //
0265             && ((y + 2 * height) > d->m_constrainingRect.height())) {
0266             break;
0267         }
0268         end = d->m_breakPositions.at(i);
0269         lwidth = d->m_lineWidths.at(i);
0270         int x = textX;
0271         if (flags & Qt::AlignHCenter) {
0272             x += (maxwidth - lwidth) / 2;
0273         } else if (flags & Qt::AlignRight) {
0274             x += maxwidth - lwidth;
0275         }
0276         painter->drawText(x, textY + y + ascent, d->m_text.mid(start, end - start + 1));
0277         y += height;
0278         start = end + 1;
0279     }
0280 
0281     // Draw the last line
0282     lwidth = d->m_lineWidths.last();
0283     int x = textX;
0284     if (flags & Qt::AlignHCenter) {
0285         x += (maxwidth - lwidth) / 2;
0286     } else if (flags & Qt::AlignRight) {
0287         x += maxwidth - lwidth;
0288     }
0289     if ((d->m_constrainingRect.height() < 0) || ((y + height) <= d->m_constrainingRect.height())) {
0290         if (i == d->m_breakPositions.count()) {
0291             painter->drawText(x, textY + y + ascent, d->m_text.mid(start));
0292         } else if (flags & FadeOut) {
0293             drawFadeoutText(painter, textX, textY + y + ascent, d->m_constrainingRect.width(), d->m_text.mid(start));
0294         } else if (flags & Truncate) {
0295             drawTruncateText(painter, textX, textY + y + ascent, d->m_constrainingRect.width(), d->m_text.mid(start));
0296         } else {
0297             painter->drawText(x, textY + y + ascent, d->m_text.mid(start));
0298         }
0299     }
0300 }
0301 
0302 QRect KWordWrap::boundingRect() const
0303 {
0304     return d->m_boundingRect;
0305 }