| 1 | /* This file is part of the KDE libraries |
| 2 | SPDX-FileCopyrightText: 2001 David Faure <faure@kde.org> |
| 3 | |
| 4 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 5 | */ |
| 6 | |
| 7 | #include "kwordwrap.h" |
| 8 | |
| 9 | #include <QList> |
| 10 | #include <QPainter> |
| 11 | |
| 12 | class KWordWrapPrivate : public QSharedData |
| 13 | { |
| 14 | public: |
| 15 | QRect m_constrainingRect; |
| 16 | QList<int> m_breakPositions; |
| 17 | QList<int> m_lineWidths; |
| 18 | QRect m_boundingRect; |
| 19 | QString m_text; |
| 20 | }; |
| 21 | |
| 22 | KWordWrap::KWordWrap(const QRect &r) |
| 23 | : d(new KWordWrapPrivate) |
| 24 | { |
| 25 | d->m_constrainingRect = r; |
| 26 | } |
| 27 | |
| 28 | KWordWrap KWordWrap::formatText(QFontMetrics &fm, const QRect &r, int /*flags*/, const QString &str, int len) |
| 29 | { |
| 30 | KWordWrap kw(r); |
| 31 | // The wordwrap algorithm |
| 32 | // The variable names and the global shape of the algorithm are inspired |
| 33 | // from QTextFormatterBreakWords::format(). |
| 34 | // qDebug() << "KWordWrap::formatText " << str << " r=" << r.x() << "," << r.y() << " " << r.width() << "x" << r.height(); |
| 35 | int height = fm.height(); |
| 36 | if (len == -1) { |
| 37 | kw.d->m_text = str; |
| 38 | } else { |
| 39 | kw.d->m_text = str.left(n: len); |
| 40 | } |
| 41 | if (len == -1) { |
| 42 | len = str.length(); |
| 43 | } |
| 44 | int lastBreak = -1; |
| 45 | int lineWidth = 0; |
| 46 | int x = 0; |
| 47 | int y = 0; |
| 48 | int w = r.width(); |
| 49 | int textwidth = 0; |
| 50 | bool isBreakable = false; |
| 51 | bool wasBreakable = false; // value of isBreakable for last char (i-1) |
| 52 | bool isParens = false; // true if one of ({[ |
| 53 | bool wasParens = false; // value of isParens for last char (i-1) |
| 54 | QString inputString = str; |
| 55 | |
| 56 | for (int i = 0; i < len; ++i) { |
| 57 | const QChar c = inputString.at(i); |
| 58 | const int ww = fm.horizontalAdvance(c); |
| 59 | |
| 60 | isParens = (c == QLatin1Char('(') // |
| 61 | || c == QLatin1Char('[') // |
| 62 | || c == QLatin1Char('{')); |
| 63 | // isBreakable is true when we can break _after_ this character. |
| 64 | isBreakable = (c.isSpace() || c.isPunct() || c.isSymbol()) & !isParens; |
| 65 | |
| 66 | // Special case for '(', '[' and '{': we want to break before them |
| 67 | if (!isBreakable && i < len - 1) { |
| 68 | const QChar nextc = inputString.at(i: i + 1); // look at next char |
| 69 | isBreakable = (nextc == QLatin1Char('(') // |
| 70 | || nextc == QLatin1Char('[') // |
| 71 | || nextc == QLatin1Char('{')); |
| 72 | } |
| 73 | // Special case for '/': after normal chars it's breakable (e.g. inside a path), |
| 74 | // but after another breakable char it's not (e.g. "mounted at /foo") |
| 75 | // Same thing after a parenthesis (e.g. "dfaure [/fool]") |
| 76 | if (c == QLatin1Char('/') && (wasBreakable || wasParens)) { |
| 77 | isBreakable = false; |
| 78 | } |
| 79 | |
| 80 | /*qDebug() << "c='" << QString(c) << "' i=" << i << "/" << len |
| 81 | << " x=" << x << " ww=" << ww << " w=" << w |
| 82 | << " lastBreak=" << lastBreak << " isBreakable=" << isBreakable << endl;*/ |
| 83 | int breakAt = -1; |
| 84 | if (x + ww > w && lastBreak != -1) { // time to break and we know where |
| 85 | breakAt = lastBreak; |
| 86 | } |
| 87 | if (x + ww > w - 4 && lastBreak == -1) { // time to break but found nowhere [-> break here] |
| 88 | breakAt = i; |
| 89 | } |
| 90 | if (i == len - 2 && x + ww + fm.horizontalAdvance(inputString.at(i: i + 1)) > w) { // don't leave the last char alone |
| 91 | breakAt = lastBreak == -1 ? i - 1 : lastBreak; |
| 92 | } |
| 93 | if (c == QLatin1Char('\n')) { // Forced break here |
| 94 | if (breakAt == -1 && lastBreak != -1) { // only break if not already breaking |
| 95 | breakAt = i - 1; |
| 96 | lastBreak = -1; |
| 97 | } |
| 98 | // remove the line feed from the string |
| 99 | kw.d->m_text.remove(i, len: 1); |
| 100 | inputString.remove(i, len: 1); |
| 101 | len--; |
| 102 | } |
| 103 | if (breakAt != -1) { |
| 104 | // qDebug() << "KWordWrap::formatText breaking after " << breakAt; |
| 105 | kw.d->m_breakPositions.append(t: breakAt); |
| 106 | int thisLineWidth = lastBreak == -1 ? x + ww : lineWidth; |
| 107 | kw.d->m_lineWidths.append(t: thisLineWidth); |
| 108 | textwidth = qMax(a: textwidth, b: thisLineWidth); |
| 109 | x = 0; |
| 110 | y += height; |
| 111 | wasBreakable = true; |
| 112 | wasParens = false; |
| 113 | if (lastBreak != -1) { |
| 114 | // Breakable char was found, restart from there |
| 115 | i = lastBreak; |
| 116 | lastBreak = -1; |
| 117 | continue; |
| 118 | } |
| 119 | } else if (isBreakable) { |
| 120 | lastBreak = i; |
| 121 | lineWidth = x + ww; |
| 122 | } |
| 123 | x += ww; |
| 124 | wasBreakable = isBreakable; |
| 125 | wasParens = isParens; |
| 126 | } |
| 127 | textwidth = qMax(a: textwidth, b: x); |
| 128 | kw.d->m_lineWidths.append(t: x); |
| 129 | y += height; |
| 130 | // qDebug() << "KWordWrap::formatText boundingRect:" << r.x() << "," << r.y() << " " << textwidth << "x" << y; |
| 131 | if (r.height() >= 0 && y > r.height()) { |
| 132 | textwidth = r.width(); |
| 133 | } |
| 134 | int realY = y; |
| 135 | if (r.height() >= 0) { |
| 136 | while (realY > r.height()) { |
| 137 | realY -= height; |
| 138 | } |
| 139 | realY = qMax(a: realY, b: 0); |
| 140 | } |
| 141 | kw.d->m_boundingRect.setRect(ax: 0, ay: 0, aw: textwidth, ah: realY); |
| 142 | return kw; |
| 143 | } |
| 144 | |
| 145 | KWordWrap::~KWordWrap() |
| 146 | { |
| 147 | } |
| 148 | |
| 149 | KWordWrap::KWordWrap(const KWordWrap &other) |
| 150 | : d(other.d) |
| 151 | { |
| 152 | } |
| 153 | |
| 154 | KWordWrap &KWordWrap::operator=(const KWordWrap &other) |
| 155 | { |
| 156 | d = other.d; |
| 157 | return *this; |
| 158 | } |
| 159 | |
| 160 | QString KWordWrap::wrappedString() const |
| 161 | { |
| 162 | const QStringView strView(d->m_text); |
| 163 | // We use the calculated break positions to insert '\n' into the string |
| 164 | QString ws; |
| 165 | int start = 0; |
| 166 | for (int i = 0; i < d->m_breakPositions.count(); ++i) { |
| 167 | int end = d->m_breakPositions.at(i); |
| 168 | ws += strView.mid(pos: start, n: end - start + 1); |
| 169 | ws += QLatin1Char('\n'); |
| 170 | start = end + 1; |
| 171 | } |
| 172 | ws += strView.mid(pos: start); |
| 173 | return ws; |
| 174 | } |
| 175 | |
| 176 | QString KWordWrap::truncatedString(bool dots) const |
| 177 | { |
| 178 | if (d->m_breakPositions.isEmpty()) { |
| 179 | return d->m_text; |
| 180 | } |
| 181 | |
| 182 | QString ts = d->m_text.left(n: d->m_breakPositions.first() + 1); |
| 183 | if (dots) { |
| 184 | ts += QLatin1String("..." ); |
| 185 | } |
| 186 | return ts; |
| 187 | } |
| 188 | |
| 189 | static QColor mixColors(double p1, QColor c1, QColor c2) |
| 190 | { |
| 191 | return QColor(int(c1.red() * p1 + c2.red() * (1.0 - p1)), // |
| 192 | int(c1.green() * p1 + c2.green() * (1.0 - p1)), // |
| 193 | int(c1.blue() * p1 + c2.blue() * (1.0 - p1))); |
| 194 | } |
| 195 | |
| 196 | void KWordWrap::drawFadeoutText(QPainter *p, int x, int y, int maxW, const QString &t) |
| 197 | { |
| 198 | QFontMetrics fm = p->fontMetrics(); |
| 199 | QColor bgColor = p->background().color(); |
| 200 | QColor textColor = p->pen().color(); |
| 201 | |
| 202 | if ((fm.boundingRect(text: t).width() > maxW) && (t.length() > 1)) { |
| 203 | int tl = 0; |
| 204 | int w = 0; |
| 205 | while (tl < t.length()) { |
| 206 | w += fm.horizontalAdvance(t.at(i: tl)); |
| 207 | if (w >= maxW) { |
| 208 | break; |
| 209 | } |
| 210 | tl++; |
| 211 | } |
| 212 | |
| 213 | int n = qMin(a: tl, b: 3); |
| 214 | if (t.isRightToLeft()) { |
| 215 | x += maxW; // start from the right side for RTL string |
| 216 | if (tl > 3) { |
| 217 | x -= fm.horizontalAdvance(t.left(n: tl - 3)); |
| 218 | p->drawText(x, y, s: t.left(n: tl - 3)); |
| 219 | } |
| 220 | for (int i = 0; i < n; i++) { |
| 221 | p->setPen(mixColors(p1: 0.70 - i * 0.25, c1: textColor, c2: bgColor)); |
| 222 | QString s(t.at(i: tl - n + i)); |
| 223 | x -= fm.horizontalAdvance(s); |
| 224 | p->drawText(x, y, s); |
| 225 | } |
| 226 | } else { |
| 227 | if (tl > 3) { |
| 228 | p->drawText(x, y, s: t.left(n: tl - 3)); |
| 229 | x += fm.horizontalAdvance(t.left(n: tl - 3)); |
| 230 | } |
| 231 | for (int i = 0; i < n; i++) { |
| 232 | p->setPen(mixColors(p1: 0.70 - i * 0.25, c1: textColor, c2: bgColor)); |
| 233 | QString s(t.at(i: tl - n + i)); |
| 234 | p->drawText(x, y, s); |
| 235 | x += fm.horizontalAdvance(s); |
| 236 | } |
| 237 | } |
| 238 | } else { |
| 239 | p->drawText(x, y, s: t); |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | void KWordWrap::drawTruncateText(QPainter *p, int x, int y, int maxW, const QString &t) |
| 244 | { |
| 245 | QString tmpText = p->fontMetrics().elidedText(text: t, mode: Qt::ElideRight, width: maxW); |
| 246 | p->drawText(x, y, s: tmpText); |
| 247 | } |
| 248 | |
| 249 | void KWordWrap::drawText(QPainter *painter, int textX, int textY, int flags) const |
| 250 | { |
| 251 | // qDebug() << "KWordWrap::drawText text=" << wrappedString() << " x=" << textX << " y=" << textY; |
| 252 | // We use the calculated break positions to draw the text line by line using QPainter |
| 253 | int start = 0; |
| 254 | int y = 0; |
| 255 | QFontMetrics fm = painter->fontMetrics(); |
| 256 | int height = fm.height(); // line height |
| 257 | int ascent = fm.ascent(); |
| 258 | int maxwidth = d->m_boundingRect.width(); |
| 259 | int i; |
| 260 | int lwidth = 0; |
| 261 | int end = 0; |
| 262 | for (i = 0; i < d->m_breakPositions.count(); ++i) { |
| 263 | // if this is the last line, leave the loop |
| 264 | if (d->m_constrainingRect.height() >= 0 // |
| 265 | && ((y + 2 * height) > d->m_constrainingRect.height())) { |
| 266 | break; |
| 267 | } |
| 268 | end = d->m_breakPositions.at(i); |
| 269 | lwidth = d->m_lineWidths.at(i); |
| 270 | int x = textX; |
| 271 | if (flags & Qt::AlignHCenter) { |
| 272 | x += (maxwidth - lwidth) / 2; |
| 273 | } else if (flags & Qt::AlignRight) { |
| 274 | x += maxwidth - lwidth; |
| 275 | } |
| 276 | painter->drawText(x, y: textY + y + ascent, s: d->m_text.mid(position: start, n: end - start + 1)); |
| 277 | y += height; |
| 278 | start = end + 1; |
| 279 | } |
| 280 | |
| 281 | // Draw the last line |
| 282 | lwidth = d->m_lineWidths.last(); |
| 283 | int x = textX; |
| 284 | if (flags & Qt::AlignHCenter) { |
| 285 | x += (maxwidth - lwidth) / 2; |
| 286 | } else if (flags & Qt::AlignRight) { |
| 287 | x += maxwidth - lwidth; |
| 288 | } |
| 289 | if ((d->m_constrainingRect.height() < 0) || ((y + height) <= d->m_constrainingRect.height())) { |
| 290 | if (i == d->m_breakPositions.count()) { |
| 291 | painter->drawText(x, y: textY + y + ascent, s: d->m_text.mid(position: start)); |
| 292 | } else if (flags & FadeOut) { |
| 293 | drawFadeoutText(p: painter, x: textX, y: textY + y + ascent, maxW: d->m_constrainingRect.width(), t: d->m_text.mid(position: start)); |
| 294 | } else if (flags & Truncate) { |
| 295 | drawTruncateText(p: painter, x: textX, y: textY + y + ascent, maxW: d->m_constrainingRect.width(), t: d->m_text.mid(position: start)); |
| 296 | } else { |
| 297 | painter->drawText(x, y: textY + y + ascent, s: d->m_text.mid(position: start)); |
| 298 | } |
| 299 | } |
| 300 | } |
| 301 | |
| 302 | QRect KWordWrap::boundingRect() const |
| 303 | { |
| 304 | return d->m_boundingRect; |
| 305 | } |
| 306 | |