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 | |