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
12class KWordWrapPrivate : public QSharedData
13{
14public:
15 QRect m_constrainingRect;
16 QList<int> m_breakPositions;
17 QList<int> m_lineWidths;
18 QRect m_boundingRect;
19 QString m_text;
20};
21
22KWordWrap::KWordWrap(const QRect &r)
23 : d(new KWordWrapPrivate)
24{
25 d->m_constrainingRect = r;
26}
27
28KWordWrap 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
145KWordWrap::~KWordWrap()
146{
147}
148
149KWordWrap::KWordWrap(const KWordWrap &other)
150 : d(other.d)
151{
152}
153
154KWordWrap &KWordWrap::operator=(const KWordWrap &other)
155{
156 d = other.d;
157 return *this;
158}
159
160QString 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
176QString 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
189static 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
196void 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
243void 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
249void 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
302QRect KWordWrap::boundingRect() const
303{
304 return d->m_boundingRect;
305}
306

source code of kguiaddons/src/text/kwordwrap.cpp