1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2007 Mirko Stocker <me@misto.ch>
4 SPDX-FileCopyrightText: 2003-2005 Hamish Rodda <rodda@kde.org>
5 SPDX-FileCopyrightText: 2001 Christoph Cullmann <cullmann@kde.org>
6 SPDX-FileCopyrightText: 2001 Joseph Wenninger <jowenn@kde.org>
7 SPDX-FileCopyrightText: 1999 Jochen Wilhelmy <digisnap@cs.tu-berlin.de>
8 SPDX-FileCopyrightText: 2013 Andrey Matveyakin <a.matveyakin@gmail.com>
9
10 SPDX-License-Identifier: LGPL-2.0-only
11*/
12
13#include "katerenderer.h"
14
15#include "inlinenotedata.h"
16#include "katebuffer.h"
17#include "katedocument.h"
18#include "kateextendedattribute.h"
19#include "katehighlight.h"
20#include "katerenderrange.h"
21#include "katetextlayout.h"
22#include "kateview.h"
23
24#include "ktexteditor/attribute.h"
25#include "ktexteditor/inlinenote.h"
26#include "ktexteditor/inlinenoteprovider.h"
27
28#include "katepartdebug.h"
29
30#include <QBrush>
31#include <QPaintEngine>
32#include <QPainter>
33#include <QPainterPath>
34#include <QRegularExpression>
35#include <QStack>
36#include <QtMath> // qCeil
37
38static const QChar tabChar(QLatin1Char('\t'));
39static const QChar spaceChar(QLatin1Char(' '));
40static const QChar nbSpaceChar(0xa0); // non-breaking space
41
42KateRenderer::KateRenderer(KTextEditor::DocumentPrivate *doc, Kate::TextFolding &folding, KTextEditor::ViewPrivate *view)
43 : m_doc(doc)
44 , m_folding(folding)
45 , m_view(view)
46 , m_tabWidth(m_doc->config()->tabWidth())
47 , m_indentWidth(m_doc->config()->indentationWidth())
48 , m_caretStyle(KTextEditor::caretStyles::Line)
49 , m_drawCaret(true)
50 , m_showSelections(true)
51 , m_showTabs(true)
52 , m_showSpaces(KateDocumentConfig::Trailing)
53 , m_showNonPrintableSpaces(false)
54 , m_printerFriendly(false)
55 , m_config(new KateRendererConfig(this))
56 , m_font(m_config->baseFont())
57 , m_fontMetrics(m_font)
58{
59 updateAttributes();
60
61 // initialize with a sane font height
62 updateFontHeight();
63
64 // make the proper calculation for markerSize
65 updateMarkerSize();
66}
67
68void KateRenderer::updateAttributes()
69{
70 m_attributes = m_doc->highlight()->attributes(schema: config()->schema());
71}
72
73const KTextEditor::Attribute::Ptr &KateRenderer::attribute(uint pos) const
74{
75 if (pos < (uint)m_attributes.count()) {
76 return m_attributes[pos];
77 }
78
79 return m_attributes[0];
80}
81
82KTextEditor::Attribute::Ptr KateRenderer::specificAttribute(int context) const
83{
84 if (context >= 0 && context < m_attributes.count()) {
85 return m_attributes[context];
86 }
87
88 return m_attributes[0];
89}
90
91void KateRenderer::setDrawCaret(bool drawCaret)
92{
93 m_drawCaret = drawCaret;
94}
95
96void KateRenderer::setCaretStyle(KTextEditor::caretStyles style)
97{
98 m_caretStyle = style;
99}
100
101void KateRenderer::setShowTabs(bool showTabs)
102{
103 m_showTabs = showTabs;
104}
105
106void KateRenderer::setShowSpaces(KateDocumentConfig::WhitespaceRendering showSpaces)
107{
108 m_showSpaces = showSpaces;
109}
110
111void KateRenderer::setShowNonPrintableSpaces(const bool on)
112{
113 m_showNonPrintableSpaces = on;
114}
115
116void KateRenderer::setTabWidth(int tabWidth)
117{
118 m_tabWidth = tabWidth;
119}
120
121bool KateRenderer::showIndentLines() const
122{
123 return m_config->showIndentationLines();
124}
125
126void KateRenderer::setShowIndentLines(bool showIndentLines)
127{
128 // invalidate our "active indent line" cached stuff
129 m_currentBracketRange = KTextEditor::Range::invalid();
130 m_currentBracketX = -1;
131
132 m_config->setShowIndentationLines(showIndentLines);
133}
134
135void KateRenderer::setIndentWidth(int indentWidth)
136{
137 m_indentWidth = indentWidth;
138}
139
140void KateRenderer::setShowSelections(bool showSelections)
141{
142 m_showSelections = showSelections;
143}
144
145void KateRenderer::addToFontSize(qreal step) const
146{
147 // ensure we don't run into corner cases in Qt, bug 500766
148 QFont f(config()->baseFont());
149 if (auto newS = f.pointSizeF() + step; newS >= 4 && newS <= 2048) {
150 f.setPointSizeF(newS);
151 config()->setFont(f);
152 }
153}
154
155void KateRenderer::resetFontSizes() const
156{
157 QFont f(KateRendererConfig::global()->baseFont());
158 config()->setFont(f);
159}
160
161bool KateRenderer::isPrinterFriendly() const
162{
163 return m_printerFriendly;
164}
165
166void KateRenderer::setPrinterFriendly(bool printerFriendly)
167{
168 m_printerFriendly = printerFriendly;
169 setShowTabs(false);
170 setShowSpaces(KateDocumentConfig::None);
171 setShowSelections(false);
172 setDrawCaret(false);
173}
174
175void KateRenderer::paintTextLineBackground(QPainter &paint, KateLineLayout *layout, int currentViewLine, int xStart, int xEnd)
176{
177 if (isPrinterFriendly()) {
178 return;
179 }
180
181 // Normal background color
182 QColor backgroundColor(config()->backgroundColor());
183
184 // paint the current line background if we're on the current line
185 QColor currentLineColor = config()->highlightedLineColor();
186
187 // Check for mark background
188 int markRed = 0;
189 int markGreen = 0;
190 int markBlue = 0;
191 int markCount = 0;
192
193 // Retrieve marks for this line
194 uint mrk = m_doc->mark(line: layout->line());
195 if (mrk) {
196 for (uint bit = 0; bit < 32; bit++) {
197 KTextEditor::Document::MarkTypes markType = (KTextEditor::Document::MarkTypes)(1U << bit);
198 if (mrk & markType) {
199 QColor markColor = config()->lineMarkerColor(type: markType);
200
201 if (markColor.isValid()) {
202 markCount++;
203 markRed += markColor.red();
204 markGreen += markColor.green();
205 markBlue += markColor.blue();
206 }
207 }
208 } // for
209 } // Marks
210
211 if (markCount) {
212 markRed /= markCount;
213 markGreen /= markCount;
214 markBlue /= markCount;
215 backgroundColor.setRgb(r: int((backgroundColor.red() * 0.9) + (markRed * 0.1)),
216 g: int((backgroundColor.green() * 0.9) + (markGreen * 0.1)),
217 b: int((backgroundColor.blue() * 0.9) + (markBlue * 0.1)),
218 a: backgroundColor.alpha());
219 }
220
221 // Draw line background
222 paint.fillRect(x: 0, y: 0, w: xEnd - xStart, h: lineHeight() * layout->viewLineCount(), b: backgroundColor);
223
224 // paint the current line background if we're on the current line
225 const bool currentLineHasSelection = m_view && m_view->selection() && m_view->selectionRange().overlapsLine(line: layout->line());
226 if (currentViewLine != -1 && !currentLineHasSelection) {
227 if (markCount) {
228 markRed /= markCount;
229 markGreen /= markCount;
230 markBlue /= markCount;
231 currentLineColor.setRgb(r: int((currentLineColor.red() * 0.9) + (markRed * 0.1)),
232 g: int((currentLineColor.green() * 0.9) + (markGreen * 0.1)),
233 b: int((currentLineColor.blue() * 0.9) + (markBlue * 0.1)),
234 a: currentLineColor.alpha());
235 }
236
237 paint.fillRect(x: 0, y: lineHeight() * currentViewLine, w: xEnd - xStart, h: lineHeight(), b: currentLineColor);
238 }
239}
240
241void KateRenderer::paintTabstop(QPainter &paint, qreal x, qreal y) const
242{
243 QPen penBackup(paint.pen());
244 QPen pen(config()->tabMarkerColor());
245 pen.setWidthF(qMax(a: 1.0, b: spaceWidth() / 10.0));
246 paint.setPen(pen);
247
248 int dist = spaceWidth() * 0.3;
249 QPoint points[8];
250 points[0] = QPoint(x - dist, y - dist);
251 points[1] = QPoint(x, y);
252 points[2] = QPoint(x, y);
253 points[3] = QPoint(x - dist, y + dist);
254 x += spaceWidth() / 3.0;
255 points[4] = QPoint(x - dist, y - dist);
256 points[5] = QPoint(x, y);
257 points[6] = QPoint(x, y);
258 points[7] = QPoint(x - dist, y + dist);
259 paint.drawLines(pointPairs: points, lineCount: 4);
260 paint.setPen(penBackup);
261}
262
263void KateRenderer::paintSpaces(QPainter &paint, const QPointF *points, const int count) const
264{
265 if (count == 0) {
266 return;
267 }
268 QPen penBackup(paint.pen());
269 QPen pen(config()->tabMarkerColor());
270
271 pen.setWidthF(m_markerSize);
272 pen.setCapStyle(Qt::RoundCap);
273 paint.setPen(pen);
274 paint.setRenderHint(hint: QPainter::Antialiasing, on: true);
275 paint.drawPoints(points, pointCount: count);
276 paint.setPen(penBackup);
277 paint.setRenderHint(hint: QPainter::Antialiasing, on: false);
278}
279
280void KateRenderer::paintNonBreakSpace(QPainter &paint, qreal x, qreal y) const
281{
282 QPen penBackup(paint.pen());
283 QPen pen(config()->tabMarkerColor());
284 pen.setWidthF(qMax(a: 1.0, b: spaceWidth() / 10.0));
285 paint.setPen(pen);
286
287 const int height = fontHeight();
288 const int width = spaceWidth();
289
290 QPoint points[6];
291 points[0] = QPoint(x + width / 10, y + height / 4);
292 points[1] = QPoint(x + width / 10, y + height / 3);
293 points[2] = QPoint(x + width / 10, y + height / 3);
294 points[3] = QPoint(x + width - width / 10, y + height / 3);
295 points[4] = QPoint(x + width - width / 10, y + height / 3);
296 points[5] = QPoint(x + width - width / 10, y + height / 4);
297 paint.drawLines(pointPairs: points, lineCount: 3);
298 paint.setPen(penBackup);
299}
300
301void KateRenderer::paintNonPrintableSpaces(QPainter &paint, qreal x, qreal y, const QChar &chr)
302{
303 paint.save();
304 QPen pen(config()->spellingMistakeLineColor());
305 pen.setWidthF(qMax(a: 1.0, b: spaceWidth() * 0.1));
306 paint.setPen(pen);
307
308 const int height = fontHeight();
309 const int width = m_fontMetrics.boundingRect(chr).width();
310 const int offset = spaceWidth() * 0.1;
311
312 QPoint points[8];
313 points[0] = QPoint(x - offset, y + offset);
314 points[1] = QPoint(x + width + offset, y + offset);
315 points[2] = QPoint(x + width + offset, y + offset);
316 points[3] = QPoint(x + width + offset, y - height - offset);
317 points[4] = QPoint(x + width + offset, y - height - offset);
318 points[5] = QPoint(x - offset, y - height - offset);
319 points[6] = QPoint(x - offset, y - height - offset);
320 points[7] = QPoint(x - offset, y + offset);
321 paint.drawLines(pointPairs: points, lineCount: 4);
322 paint.restore();
323}
324
325/**
326 * Helper function that checks if our cursor is at a bracket
327 * and calculates X position for opening/closing brackets. We
328 * then use this data to color the indentation line differently.
329 * @p view is current view
330 * @p range is the current range from @ref paintTextLine
331 * @p spaceWidth width of space char
332 * @p c is the position of cursor
333 * @p bracketXPos will be X position of close bracket or -1 if not found
334 */
335static KTextEditor::Range cursorAtBracket(KTextEditor::ViewPrivate *view, const KateLineLayout *range, int spaceWidth, KTextEditor::Cursor c, int &bracketXPos)
336{
337 bracketXPos = -1;
338 if (range->line() != c.line()) {
339 return KTextEditor::Range::invalid();
340 }
341
342 auto *doc = view->doc();
343 // Avoid work if we are below tabwidth
344 if (c.column() < doc->config()->tabWidth()) {
345 return KTextEditor::Range::invalid();
346 }
347
348 // We match these brackets only
349 static constexpr QChar brackets[] = {QLatin1Char('{'), QLatin1Char('}'), QLatin1Char('('), QLatin1Char(')')};
350 // look for character in front
351 QChar right = doc->characterAt(position: c);
352 auto it = std::find(first: std::begin(arr: brackets), last: std::end(arr: brackets), val: right);
353
354 KTextEditor::Range ret = KTextEditor::Range::invalid();
355 bool found = false;
356 if (it != std::end(arr: brackets)) {
357 found = true;
358 } else {
359 // look at previous character
360 QChar left = doc->characterAt(position: {c.line(), c.column() - 1});
361 it = std::find(first: std::begin(arr: brackets), last: std::end(arr: brackets), val: left);
362 if (it != std::end(arr: brackets)) {
363 found = true;
364 }
365 }
366
367 // We have a bracket
368 if (found) {
369 ret = doc->findMatchingBracket(start: c, maxLines: 500);
370 if (!ret.isValid()) {
371 return ret;
372 }
373 bracketXPos = (ret.end().column() * spaceWidth) + 1;
374 }
375
376 return ret;
377}
378
379void KateRenderer::paintIndentMarker(QPainter &paint, uint x, int line)
380{
381 const QPen penBackup(paint.pen());
382 static const QList<qreal> dashPattern = QList<qreal>() << 1 << 1;
383 QPen myPen;
384
385 const bool onBracket = m_currentBracketX == (int)x;
386 if (onBracket && m_currentBracketRange.containsLine(line)) {
387 QColor c = view()->theme().textColor(style: KSyntaxHighlighting::Theme::Normal);
388 c.setAlphaF(0.7);
389 myPen.setColor(c);
390 } else {
391 myPen.setColor(config()->indentationLineColor());
392 myPen.setDashPattern(dashPattern);
393 }
394
395 paint.setPen(myPen);
396
397 QPainter::RenderHints renderHints = paint.renderHints();
398 paint.setRenderHints(hints: renderHints, on: false);
399
400 paint.drawLine(x1: x + 2, y1: 0, x2: x + 2, y2: lineHeight());
401
402 paint.setRenderHints(hints: renderHints, on: true);
403
404 paint.setPen(penBackup);
405}
406
407static bool rangeLessThanForRenderer(const Kate::TextRange *a, const Kate::TextRange *b)
408{
409 // compare Z-Depth first
410 // smaller Z-Depths should win!
411 if (a->zDepth() != b->zDepth()) {
412 return a->zDepth() > b->zDepth();
413 }
414
415 if (a->end().toCursor() != b->end().toCursor()) {
416 return a->end().toCursor() > b->end().toCursor();
417 }
418
419 return a->start().toCursor() < b->start().toCursor();
420}
421
422QList<QTextLayout::FormatRange> KateRenderer::decorationsForLine(const Kate::TextLine &textLine, int line, bool skipSelections) const
423{
424 // limit number of attributes we can highlight in reasonable time
425 const int limitOfRanges = 1024;
426 auto rangesWithAttributes = m_doc->buffer().rangesForLine(line, view: m_printerFriendly ? nullptr : m_view, rangesWithAttributeOnly: true);
427 if (rangesWithAttributes.size() > limitOfRanges) {
428 rangesWithAttributes.clear();
429 }
430
431 // Don't compute the highlighting if there isn't going to be any highlighting
432 const auto &al = textLine.attributesList();
433 if (al.empty() && rangesWithAttributes.empty() && !m_view->selection()) {
434 return QList<QTextLayout::FormatRange>();
435 }
436
437 // Add the inbuilt highlighting to the list, limit with limitOfRanges
438 RenderRangeVector renderRanges;
439 if (!al.empty()) {
440 auto &currentRange = renderRanges.pushNewRange();
441 for (int i = 0; i < std::min<int>(a: al.size(), b: limitOfRanges); ++i) {
442 if (al[i].length > 0 && al[i].attributeValue > 0) {
443 currentRange.addRange(range: KTextEditor::Range(KTextEditor::Cursor(line, al[i].offset), al[i].length), attribute: specificAttribute(context: al[i].attributeValue));
444 }
445 }
446 }
447
448 // check for dynamic hl stuff
449 const QSet<Kate::TextRange *> *rangesMouseIn = m_view ? m_view->rangesMouseIn() : nullptr;
450 const QSet<Kate::TextRange *> *rangesCaretIn = m_view ? m_view->rangesCaretIn() : nullptr;
451 bool anyDynamicHlsActive = m_view && (!rangesMouseIn->empty() || !rangesCaretIn->empty());
452
453 // sort all ranges, we want that the most specific ranges win during rendering, multiple equal ranges are kind of random, still better than old smart
454 // rangs behavior ;)
455 std::sort(first: rangesWithAttributes.begin(), last: rangesWithAttributes.end(), comp: rangeLessThanForRenderer);
456
457 renderRanges.reserve(size: rangesWithAttributes.size());
458 // loop over all ranges
459 for (int i = 0; i < rangesWithAttributes.size(); ++i) {
460 // real range
461 Kate::TextRange *kateRange = rangesWithAttributes[i];
462
463 // calculate attribute, default: normal attribute
464 KTextEditor::Attribute::Ptr attribute = kateRange->attribute();
465 if (anyDynamicHlsActive) {
466 // check mouse in
467 if (KTextEditor::Attribute::Ptr attributeMouseIn = attribute->dynamicAttribute(type: KTextEditor::Attribute::ActivateMouseIn)) {
468 if (rangesMouseIn->contains(value: kateRange)) {
469 attribute = attributeMouseIn;
470 }
471 }
472
473 // check caret in
474 if (KTextEditor::Attribute::Ptr attributeCaretIn = attribute->dynamicAttribute(type: KTextEditor::Attribute::ActivateCaretIn)) {
475 if (rangesCaretIn->contains(value: kateRange)) {
476 attribute = attributeCaretIn;
477 }
478 }
479 }
480
481 // span range
482 renderRanges.pushNewRange().addRange(range: *kateRange, attribute: std::move(attribute));
483 }
484
485 // Add selection highlighting if we're creating the selection decorations
486 if (!skipSelections && ((m_view && showSelections() && m_view->selection()) || (m_view && m_view->blockSelection()))) {
487 auto &currentRange = renderRanges.pushNewRange();
488
489 // Set up the selection background attribute TODO: move this elsewhere, eg. into the config?
490 static KTextEditor::Attribute::Ptr backgroundAttribute;
491 if (!backgroundAttribute) {
492 backgroundAttribute = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute());
493 }
494
495 backgroundAttribute->setBackground(config()->selectionColor());
496 backgroundAttribute->setForeground(attribute(pos: KSyntaxHighlighting::Theme::TextStyle::Normal)->selectedForeground().color());
497
498 // Create a range for the current selection
499 if (m_view->blockSelection() && m_view->selectionRange().overlapsLine(line)) {
500 currentRange.addRange(range: m_doc->rangeOnLine(range: m_view->selectionRange(), line), attribute: backgroundAttribute);
501 } else {
502 currentRange.addRange(range: m_view->selectionRange(), attribute: backgroundAttribute);
503 }
504 }
505
506 // no render ranges, nothing to do, else we loop below endless!
507 if (renderRanges.isEmpty()) {
508 return QList<QTextLayout::FormatRange>();
509 }
510
511 // Calculate the range which we need to iterate in order to get the highlighting for just this line
512 KTextEditor::Cursor currentPosition = KTextEditor::Cursor(line, 0);
513 const KTextEditor::Cursor endPosition = KTextEditor::Cursor(line + 1, 0);
514
515 // Background formats have lower priority so they get overriden by selection
516 const KTextEditor::Range selectionRange = m_view->selectionRange();
517
518 // Main iterative loop. This walks through each set of highlighting ranges, and stops each
519 // time the highlighting changes. It then creates the corresponding QTextLayout::FormatRanges.
520 QList<QTextLayout::FormatRange> newHighlight;
521 while (currentPosition < endPosition) {
522 renderRanges.advanceTo(pos: currentPosition);
523
524 if (!renderRanges.hasAttribute()) {
525 // No attribute, don't need to create a FormatRange for this text range
526 currentPosition = renderRanges.nextBoundary();
527 continue;
528 }
529
530 KTextEditor::Cursor nextPosition = renderRanges.nextBoundary();
531
532 // Create the format range and populate with the correct start, length and format info
533 QTextLayout::FormatRange fr;
534 fr.start = currentPosition.column();
535
536 if (nextPosition < endPosition || endPosition.line() <= line) {
537 fr.length = nextPosition.column() - currentPosition.column();
538
539 } else {
540 // before we did here +1 to force background drawing at the end of the line when it's warranted
541 // we now skip this, we don't draw e.g. full line backgrounds
542 fr.length = textLine.length() - currentPosition.column();
543 }
544
545 KTextEditor::Attribute::Ptr a = renderRanges.generateAttribute();
546 if (a) {
547 fr.format = *a;
548 if (!skipSelections && selectionRange.contains(cursor: currentPosition)) {
549 if (!m_view->blockSelection()) {
550 assignSelectionBrushesFromAttribute(target&: fr, attribute: *a);
551 } else if (m_doc->rangeOnLine(range: selectionRange, line).containsColumn(column: currentPosition.column())) {
552 assignSelectionBrushesFromAttribute(target&: fr, attribute: *a);
553 }
554 }
555 }
556
557 newHighlight.append(t: fr);
558
559 currentPosition = nextPosition;
560 }
561
562 // ensure bold & italic fonts work, even if the main font is e.g. light or something like that
563 for (auto &formatRange : newHighlight) {
564 if (formatRange.format.fontWeight() == QFont::Bold || formatRange.format.fontItalic()) {
565 formatRange.format.setFontStyleName(QString());
566 }
567 }
568
569 return newHighlight;
570}
571
572void KateRenderer::assignSelectionBrushesFromAttribute(QTextLayout::FormatRange &target, const KTextEditor::Attribute &attribute)
573{
574 if (attribute.hasProperty(propertyId: SelectedForeground)) {
575 target.format.setForeground(attribute.selectedForeground());
576 }
577 if (attribute.hasProperty(propertyId: SelectedBackground)) {
578 target.format.setBackground(attribute.selectedBackground());
579 }
580}
581
582void KateRenderer::paintTextBackground(QPainter &paint, KateLineLayout *layout, const QList<QTextLayout::FormatRange> &selRanges, int xStart) const
583{
584 const bool rtl = layout->isRightToLeft();
585
586 for (const auto &sel : selRanges) {
587 const int s = sel.start;
588 const int e = sel.start + sel.length;
589 QBrush br = sel.format.background();
590
591 if (br == Qt::NoBrush) {
592 // skip if no brush to fill with
593 continue;
594 }
595
596 const int startViewLine = layout->viewLineForColumn(column: s);
597 const int endViewLine = layout->viewLineForColumn(column: e);
598 if (startViewLine == endViewLine) {
599 KateTextLayout l = layout->viewLine(viewLine: startViewLine);
600 // subtract xStart so that the selection is shown where it belongs
601 // we don't do it in the else case because this only matters when dynWrap==false
602 // and when dynWrap==false, we always have 1 QTextLine per layout
603 const int startX = cursorToX(range: l, col: s) - xStart;
604 const int endX = cursorToX(range: l, col: e) - xStart;
605 const int y = startViewLine * lineHeight();
606 QRect r(startX, y, (endX - startX), lineHeight());
607 paint.fillRect(r, br);
608 } else {
609 QPainterPath p;
610 for (int l = startViewLine; l <= endViewLine; ++l) {
611 auto kateLayout = layout->viewLine(viewLine: l);
612 int sx = 0;
613 int width = rtl ? kateLayout.lineLayout().width() : kateLayout.lineLayout().naturalTextWidth();
614
615 if (l == startViewLine) {
616 if (rtl) {
617 // For rtl, Rect starts at 0 and ends at selection start
618 sx = 0;
619 width = kateLayout.lineLayout().cursorToX(cursorPos: s);
620 } else {
621 sx = kateLayout.lineLayout().cursorToX(cursorPos: s);
622 }
623 } else if (l == endViewLine) {
624 if (rtl) {
625 // Drawing will start at selection end, and end at the view border
626 sx = kateLayout.lineLayout().cursorToX(cursorPos: e);
627 } else {
628 width = kateLayout.lineLayout().cursorToX(cursorPos: e);
629 }
630 }
631
632 const int y = l * lineHeight();
633 QRect r(sx, y, width - sx, lineHeight());
634 p.addRect(rect: r);
635 }
636 paint.fillPath(path: p, brush: br);
637 }
638 }
639}
640
641void KateRenderer::paintTextLine(QPainter &paint,
642 KateLineLayout *range,
643 int xStart,
644 int xEnd,
645 const QRectF &textClipRect,
646 const KTextEditor::Cursor *cursor,
647 PaintTextLineFlags flags)
648{
649 Q_ASSERT(range->isValid());
650
651 // qCDebug(LOG_KTE)<<"KateRenderer::paintTextLine";
652
653 // font data
654 const QFontMetricsF &fm = m_fontMetrics;
655 const Kate::TextLine textLine = doc()->kateTextLine(i: range->line());
656
657 int currentViewLine = -1;
658 if (cursor && cursor->line() == range->line()) {
659 currentViewLine = range->viewLineForColumn(column: cursor->column());
660 }
661
662 paintTextLineBackground(paint, layout: range, currentViewLine, xStart, xEnd);
663
664 // Draws the dashed underline at the start of a folded block of text.
665 if (!(flags & SkipDrawFirstInvisibleLineUnderlined) && range->startsInvisibleBlock(folding&: folding())) {
666 QPen pen(config()->foldingColor());
667 pen.setCosmetic(true);
668 pen.setStyle(Qt::DashLine);
669 pen.setDashOffset(xStart);
670 pen.setWidth(2);
671 paint.setPen(pen);
672 paint.drawLine(x1: 0, y1: (lineHeight() * range->viewLineCount()) - 2, x2: xEnd - xStart, y2: (lineHeight() * range->viewLineCount()) - 2);
673 }
674
675 if (range->layout().lineCount() > 0) {
676 bool drawSelection =
677 m_view && m_view->selection() && showSelections() && m_view->selectionRange().overlapsLine(line: range->line()) && !flags.testFlag(flag: SkipDrawLineSelection);
678 // Draw selection in block selection mode. We need 2 kinds of selections that QTextLayout::draw can't render:
679 // - past-end-of-line selection and
680 // - 0-column-wide selection (used to indicate where text will be typed)
681 if (drawSelection && m_view->blockSelection()) {
682 int selectionStartColumn = m_doc->fromVirtualColumn(line: range->line(), column: m_doc->toVirtualColumn(m_view->selectionRange().start()));
683 int selectionEndColumn = m_doc->fromVirtualColumn(line: range->line(), column: m_doc->toVirtualColumn(m_view->selectionRange().end()));
684 QBrush selectionBrush = config()->selectionColor();
685 if (selectionStartColumn != selectionEndColumn) {
686 KateTextLayout lastLine = range->viewLine(viewLine: range->viewLineCount() - 1);
687 if (selectionEndColumn > lastLine.startCol()) {
688 int selectionStartX = (selectionStartColumn > lastLine.startCol()) ? cursorToX(range: lastLine, col: selectionStartColumn, returnPastLine: true) : 0;
689 int selectionEndX = cursorToX(range: lastLine, col: selectionEndColumn, returnPastLine: true);
690 paint.fillRect(QRect(selectionStartX - xStart, (int)lastLine.lineLayout().y(), selectionEndX - selectionStartX, lineHeight()),
691 selectionBrush);
692 }
693 } else {
694 const int selectStickWidth = 2;
695 KateTextLayout selectionLine = range->viewLine(viewLine: range->viewLineForColumn(column: selectionStartColumn));
696 int selectionX = cursorToX(range: selectionLine, col: selectionStartColumn, returnPastLine: true);
697 paint.fillRect(QRect(selectionX - xStart, (int)selectionLine.lineLayout().y(), selectStickWidth, lineHeight()), selectionBrush);
698 }
699 }
700
701 if (textLine.length() > 0) {
702 // We may have changed the pen, be absolutely sure it gets set back to
703 // normal foreground color before drawing text for text that does not
704 // set the pen color
705 paint.setPen(attribute(pos: KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color());
706
707 // Draw text background
708 const auto decos = decorationsForLine(textLine, line: range->line(), /*skipSelections=*/!drawSelection);
709 paintTextBackground(paint, layout: range, selRanges: decos, xStart);
710
711 // Draw the text :)
712 range->layout().draw(p: &paint, pos: QPoint(-xStart, 0), selections: QList<QTextLayout::FormatRange>{}, clip: textClipRect);
713 }
714
715 // Check if we are at a bracket and color the indentation
716 // line differently
717 const bool indentLinesEnabled = showIndentLines();
718 if (cursor && indentLinesEnabled) {
719 auto cur = *cursor;
720 cur.setColumn(cur.column() - 1);
721 if (!m_currentBracketRange.boundaryAtCursor(cursor: *cursor) && m_currentBracketRange.end() != cur && m_currentBracketRange.start() != cur) {
722 m_currentBracketRange = cursorAtBracket(view: view(), range, spaceWidth: spaceWidth(), c: *cursor, bracketXPos&: m_currentBracketX);
723 }
724 }
725
726 // Loop each individual line for additional text decoration etc.
727 for (int i = 0; i < range->viewLineCount(); ++i) {
728 KateTextLayout line = range->viewLine(viewLine: i);
729
730 // Draw indent lines
731 if (!m_printerFriendly && (indentLinesEnabled && i == 0)) {
732 const qreal w = spaceWidth();
733 const int lastIndentColumn = textLine.indentDepth(tabWidth: m_tabWidth);
734 for (int x = m_indentWidth; x < lastIndentColumn; x += m_indentWidth) {
735 auto xPos = x * w + 1 - xStart;
736 if (xPos >= 0) {
737 paintIndentMarker(paint, x: xPos, line: range->line());
738 }
739 }
740 }
741
742 // draw an open box to mark non-breaking spaces
743 const QString &text = textLine.text();
744 int y = lineHeight() * i + m_fontAscent - fm.strikeOutPos();
745 int nbSpaceIndex = text.indexOf(ch: nbSpaceChar, from: line.lineLayout().xToCursor(x: xStart));
746
747 while (nbSpaceIndex != -1 && nbSpaceIndex < line.endCol()) {
748 int x = line.lineLayout().cursorToX(cursorPos: nbSpaceIndex);
749 if (x > xEnd) {
750 break;
751 }
752 paintNonBreakSpace(paint, x: x - xStart, y);
753 nbSpaceIndex = text.indexOf(ch: nbSpaceChar, from: nbSpaceIndex + 1);
754 }
755
756 // draw tab stop indicators
757 if (showTabs()) {
758 int tabIndex = text.indexOf(ch: tabChar, from: line.lineLayout().xToCursor(x: xStart));
759 while (tabIndex != -1 && tabIndex < line.endCol()) {
760 int x = line.lineLayout().cursorToX(cursorPos: tabIndex);
761 if (x > xEnd) {
762 break;
763 }
764 paintTabstop(paint, x: x - xStart + spaceWidth() / 2.0, y);
765 tabIndex = text.indexOf(ch: tabChar, from: tabIndex + 1);
766 }
767 }
768
769 // draw trailing spaces
770 if (showSpaces() != KateDocumentConfig::None) {
771 int spaceIndex = line.endCol() - 1;
772 const int trailingPos = showSpaces() == KateDocumentConfig::All ? 0 : qMax(a: textLine.lastChar(), b: 0);
773
774 if (spaceIndex >= trailingPos) {
775 QVarLengthArray<int, 32> spacePositions;
776 // Adjust to visible contents
777 const auto dir = range->layout().textOption().textDirection();
778 const bool isRTL = dir == Qt::RightToLeft && m_view->dynWordWrap();
779 int start = isRTL ? xEnd : xStart;
780 int end = isRTL ? xStart : xEnd;
781
782 spaceIndex = std::min(a: line.lineLayout().xToCursor(x: end), b: spaceIndex);
783 int visibleStart = line.lineLayout().xToCursor(x: start);
784
785 for (; spaceIndex >= line.startCol(); --spaceIndex) {
786 if (!text.at(i: spaceIndex).isSpace()) {
787 if (showSpaces() == KateDocumentConfig::Trailing) {
788 break;
789 } else {
790 continue;
791 }
792 }
793 if (text.at(i: spaceIndex) != QLatin1Char('\t') || !showTabs()) {
794 spacePositions << spaceIndex;
795 }
796
797 if (spaceIndex < visibleStart) {
798 break;
799 }
800 }
801
802 QPointF prev;
803 QVarLengthArray<QPointF, 32> spacePoints;
804 const auto spaceWidth = this->spaceWidth();
805 // reverse because we want to look at the spaces at the beginning of line first
806 for (auto rit = spacePositions.rbegin(); rit != spacePositions.rend(); ++rit) {
807 const int spaceIdx = *rit;
808 qreal x = line.lineLayout().cursorToX(cursorPos: spaceIdx) - xStart;
809 int dir = 1; // 1 == ltr, -1 == rtl
810 if (range->layout().textOption().alignment() == Qt::AlignRight) {
811 dir = -1;
812 if (spaceIdx > 0) {
813 QChar c = text.at(i: spaceIdx - 1);
814 // line is LTR aligned, but is the char ltr or rtl?
815 if (!isLineRightToLeft(str: QStringView(&c, 1))) {
816 dir = 1;
817 }
818 }
819 } else {
820 if (spaceIdx > 0) {
821 // line is LTR aligned, but is the char ltr or rtl?
822 QChar c = text.at(i: spaceIdx - 1);
823 if (isLineRightToLeft(str: QStringView(&c, 1))) {
824 dir = -1;
825 }
826 }
827 }
828
829 x += dir * (spaceWidth / 2.0);
830
831 const QPointF currentPoint(x, y);
832 if (!prev.isNull() && currentPoint == prev) {
833 break;
834 }
835 spacePoints << currentPoint;
836 prev = QPointF(x, y);
837 }
838 if (!spacePoints.isEmpty()) {
839 paintSpaces(paint, points: spacePoints.constData(), count: spacePoints.size());
840 }
841 }
842 }
843
844 if (showNonPrintableSpaces()) {
845 const int y = lineHeight() * i + m_fontAscent;
846
847 static const QRegularExpression nonPrintableSpacesRegExp(
848 QStringLiteral("[\\x{0000}-\\x{0008}\\x{000A}-\\x{001F}\\x{2000}-\\x{200F}\\x{2028}-\\x{202F}\\x{205F}-\\x{2064}\\x{206A}-\\x{206F}]"));
849 QRegularExpressionMatchIterator i = nonPrintableSpacesRegExp.globalMatch(subject: text, offset: line.lineLayout().xToCursor(x: xStart));
850
851 while (i.hasNext()) {
852 const int charIndex = i.next().capturedStart();
853
854 const int x = line.lineLayout().cursorToX(cursorPos: charIndex);
855 if (x > xEnd) {
856 break;
857 }
858
859 paintNonPrintableSpaces(paint, x: x - xStart, y, chr: text[charIndex]);
860 }
861 }
862
863 // draw word-wrap-honor-indent filling
864 if ((i > 0) && range->shiftX && (range->shiftX > xStart)) {
865 // fill background first with selection if we had selection from the previous line
866 if (drawSelection && !m_view->blockSelection() && m_view->selectionRange().start() < line.start()
867 && m_view->selectionRange().end() >= line.start()) {
868 paint.fillRect(x: 0, y: lineHeight() * i, w: range->shiftX - xStart, h: lineHeight(), b: QBrush(config()->selectionColor()));
869 }
870
871 // paint the normal filling for the word wrap markers
872 paint.fillRect(x: 0, y: lineHeight() * i, w: range->shiftX - xStart, h: lineHeight(), b: QBrush(config()->wordWrapMarkerColor(), Qt::Dense4Pattern));
873 }
874 }
875
876 // Draw carets
877 if (m_view && cursor && drawCaret()) {
878 const auto &secCursors = view()->secondaryCursors();
879 // Find carets on this line
880 auto mIt = std::lower_bound(first: secCursors.begin(), last: secCursors.end(), val: range->line(), comp: [](const KTextEditor::ViewPrivate::SecondaryCursor &l, int line) {
881 return l.pos->line() < line;
882 });
883 bool skipPrimary = false;
884 if (mIt != secCursors.end() && mIt->cursor().line() == range->line()) {
885 KTextEditor::Cursor last = KTextEditor::Cursor::invalid();
886 auto primaryCursor = *cursor;
887 for (; mIt != secCursors.end(); ++mIt) {
888 auto cursor = mIt->cursor();
889 skipPrimary = skipPrimary || cursor == primaryCursor;
890 if (cursor == last) {
891 continue;
892 }
893 last = cursor;
894 if (cursor.line() == range->line()) {
895 paintCaret(cursor, range, paint, xStart, xEnd);
896 } else {
897 break;
898 }
899 }
900 }
901 if (!skipPrimary) {
902 paintCaret(cursor: *cursor, range, paint, xStart, xEnd);
903 }
904 }
905 }
906
907 // show word wrap marker if desirable
908 if ((!isPrinterFriendly()) && config()->wordWrapMarker()) {
909 const QPainter::RenderHints backupRenderHints = paint.renderHints();
910 paint.setPen(config()->wordWrapMarkerColor());
911 int _x = qreal(m_doc->config()->wordWrapAt()) * fm.horizontalAdvance(QLatin1Char('x')) - xStart;
912 paint.drawLine(x1: _x, y1: 0, x2: _x, y2: lineHeight());
913 paint.setRenderHints(hints: backupRenderHints);
914 }
915
916 // Draw inline notes
917 if (!isPrinterFriendly()) {
918 const auto inlineNotes = m_view->inlineNotes(line: range->line());
919 for (const auto &inlineNoteData : inlineNotes) {
920 KTextEditor::InlineNote inlineNote(inlineNoteData);
921 const int column = inlineNote.position().column();
922 const int viewLine = range->viewLineForColumn(column);
923 // We only consider a line "rtl" if dynamic wrap is enabled. If it is disabled, our
924 // text is always on the left side of the view
925 const auto dir = range->layout().textOption().textDirection();
926 const bool isRTL = dir == Qt::RightToLeft && m_view->dynWordWrap();
927
928 // Determine the position where to paint the note.
929 // We start by getting the x coordinate of cursor placed to the column.
930 // If the text is ltr or rtl + dyn wrap, get the X from column
931 qreal x;
932 if (dir == Qt::LeftToRight || (dir == Qt::RightToLeft && m_view->dynWordWrap())) {
933 x = range->viewLine(viewLine).lineLayout().cursorToX(cursorPos: column) - xStart;
934 } else /* rtl + dynWordWrap == false */ {
935 // if text is rtl and dynamic wrap is false, the x offsets are in the opposite
936 // direction i.e., [0] == biggest offset, [1] = next
937 x = range->viewLine(viewLine).lineLayout().cursorToX(cursorPos: textLine.length() - column) - xStart;
938 }
939 int textLength = textLine.length();
940 if (column == 0 || column < textLength) {
941 // If the note is inside text or at the beginning, then there is a hole in the text where the
942 // note should be painted and the cursor gets placed at the right side of it. So we have to
943 // subtract the width of the note to get to left side of the hole.
944 x -= inlineNote.width();
945 } else {
946 // If the note is outside the text, then the X coordinate is located at the end of the line.
947 // Add appropriate amount of spaces to reach the required column.
948 const auto spaceToAdd = spaceWidth() * (column - textLength);
949 x += isRTL ? -spaceToAdd : spaceToAdd;
950 }
951
952 qreal y = lineHeight() * viewLine;
953
954 // Paint the note
955 paint.save();
956 paint.translate(dx: x, dy: y);
957 inlineNote.provider()->paintInlineNote(note: inlineNote, painter&: paint, direction: isRTL ? Qt::RightToLeft : Qt::LeftToRight);
958 paint.restore();
959 }
960 }
961}
962
963static void drawCursor(const QTextLayout &layout, QPainter *p, const QPointF &pos, int cursorPosition, int width, const int height)
964{
965 cursorPosition = qBound(min: 0, val: cursorPosition, max: layout.text().length());
966 const QTextLine l = layout.lineForTextPosition(pos: cursorPosition);
967 Q_ASSERT(l.isValid());
968 if (!l.isValid()) {
969 return;
970 }
971 const QPainter::CompositionMode origCompositionMode = p->compositionMode();
972 if (p->paintEngine()->hasFeature(feature: QPaintEngine::RasterOpModes)) {
973 p->setCompositionMode(QPainter::RasterOp_NotDestination);
974 }
975
976 const QPointF position = pos + layout.position();
977 const qreal x = position.x() + l.cursorToX(cursorPos: cursorPosition);
978 const qreal y = l.lineNumber() * height;
979 p->fillRect(QRectF(x, y, (qreal)width, (qreal)height), p->pen().brush());
980 p->setCompositionMode(origCompositionMode);
981}
982
983void KateRenderer::paintCaret(KTextEditor::Cursor cursor, KateLineLayout *range, QPainter &paint, int xStart, int xEnd)
984{
985 if (range->includesCursor(realCursor: cursor)) {
986 int caretWidth;
987 int lineWidth = 2;
988 QColor color;
989 const int lineLength = range->layout().text().length();
990 QTextLine line = range->layout().lineForTextPosition(pos: qMin(a: cursor.column(), b: lineLength));
991
992 // Determine the caret's style
993 KTextEditor::caretStyles style = caretStyle();
994
995 // Make the caret the desired width
996 if (style == KTextEditor::caretStyles::Line) {
997 caretWidth = lineWidth;
998 } else if (line.isValid() && cursor.column() < lineLength) {
999 caretWidth = int(line.cursorToX(cursorPos: cursor.column() + 1) - line.cursorToX(cursorPos: cursor.column()));
1000 if (caretWidth < 0) {
1001 caretWidth = -caretWidth;
1002 }
1003 } else {
1004 caretWidth = spaceWidth();
1005 }
1006
1007 // Determine the color
1008 if (m_caretOverrideColor.isValid()) {
1009 // Could actually use the real highlighting system for this...
1010 // would be slower, but more accurate for corner cases
1011 color = m_caretOverrideColor;
1012 } else {
1013 // search for the FormatRange that includes the cursor
1014 const auto formatRanges = range->layout().formats();
1015 for (const QTextLayout::FormatRange &r : formatRanges) {
1016 if ((r.start <= cursor.column()) && ((r.start + r.length) > cursor.column())) {
1017 // check for Qt::NoBrush, as the returned color is black() and no invalid QColor
1018 QBrush foregroundBrush = r.format.foreground();
1019 if (foregroundBrush != Qt::NoBrush) {
1020 color = r.format.foreground().color();
1021 }
1022 break;
1023 }
1024 }
1025 // still no color found, fall back to default style
1026 if (!color.isValid()) {
1027 color = attribute(pos: KSyntaxHighlighting::Theme::TextStyle::Normal)->foreground().color();
1028 }
1029 }
1030
1031 paint.save();
1032 switch (style) {
1033 case KTextEditor::caretStyles::Line:
1034 paint.setPen(QPen(color, caretWidth));
1035 break;
1036 case KTextEditor::caretStyles::Block:
1037 // use a gray caret so it's possible to see the character
1038 color.setAlpha(128);
1039 paint.setPen(QPen(color, caretWidth));
1040 break;
1041 case KTextEditor::caretStyles::Underline:
1042 break;
1043 case KTextEditor::caretStyles::Half:
1044 color.setAlpha(128);
1045 paint.setPen(QPen(color, caretWidth));
1046 break;
1047 }
1048
1049 if (cursor.column() <= lineLength) {
1050 // Ensure correct cursor placement for RTL text
1051 if (range->layout().textOption().textDirection() == Qt::RightToLeft) {
1052 xStart += caretWidth;
1053 }
1054 qreal width = 0;
1055 if (cursor.column() < lineLength) {
1056 const auto inlineNotes = m_view->inlineNotes(line: range->line());
1057 for (const auto &inlineNoteData : inlineNotes) {
1058 KTextEditor::InlineNote inlineNote(inlineNoteData);
1059 if (inlineNote.position().column() == cursor.column()) {
1060 width = inlineNote.width() + (caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0);
1061 }
1062 }
1063 }
1064 drawCursor(layout: range->layout(), p: &paint, pos: QPoint(-xStart - width, 0), cursorPosition: cursor.column(), width: caretWidth, height: lineHeight());
1065 } else {
1066 // Off the end of the line... must be block mode. Draw the caret ourselves.
1067 const KateTextLayout &lastLine = range->viewLine(viewLine: range->viewLineCount() - 1);
1068 int x = cursorToX(range: lastLine, pos: KTextEditor::Cursor(range->line(), cursor.column()), returnPastLine: true);
1069 if ((x >= xStart) && (x <= xEnd)) {
1070 paint.fillRect(x: x - xStart, y: (int)lastLine.lineLayout().y(), w: caretWidth, h: lineHeight(), b: color);
1071 }
1072 }
1073
1074 paint.restore();
1075 }
1076}
1077
1078uint KateRenderer::fontHeight() const
1079{
1080 return m_fontHeight;
1081}
1082
1083uint KateRenderer::documentHeight() const
1084{
1085 return m_doc->lines() * lineHeight();
1086}
1087
1088int KateRenderer::lineHeight() const
1089{
1090 return fontHeight();
1091}
1092
1093bool KateRenderer::getSelectionBounds(int line, int lineLength, int &start, int &end) const
1094{
1095 bool hasSel = false;
1096
1097 if (m_view->selection() && !m_view->blockSelection()) {
1098 if (m_view->lineIsSelection(line)) {
1099 start = m_view->selectionRange().start().column();
1100 end = m_view->selectionRange().end().column();
1101 hasSel = true;
1102 } else if (line == m_view->selectionRange().start().line()) {
1103 start = m_view->selectionRange().start().column();
1104 end = lineLength;
1105 hasSel = true;
1106 } else if (m_view->selectionRange().containsLine(line)) {
1107 start = 0;
1108 end = lineLength;
1109 hasSel = true;
1110 } else if (line == m_view->selectionRange().end().line()) {
1111 start = 0;
1112 end = m_view->selectionRange().end().column();
1113 hasSel = true;
1114 }
1115 } else if (m_view->lineHasSelected(line)) {
1116 start = m_view->selectionRange().start().column();
1117 end = m_view->selectionRange().end().column();
1118 hasSel = true;
1119 }
1120
1121 if (start > end) {
1122 int temp = end;
1123 end = start;
1124 start = temp;
1125 }
1126
1127 return hasSel;
1128}
1129
1130void KateRenderer::updateConfig()
1131{
1132 // update the attribute list pointer
1133 updateAttributes();
1134
1135 // update font height, do this before we update the view!
1136 updateFontHeight();
1137
1138 // trigger view update, if any!
1139 if (m_view) {
1140 m_view->updateRendererConfig();
1141 }
1142}
1143
1144bool KateRenderer::hasCustomLineHeight() const
1145{
1146 return !qFuzzyCompare(p1: config()->lineHeightMultiplier(), p2: 1.0);
1147}
1148
1149void KateRenderer::updateFontHeight()
1150{
1151 // cache font + metrics
1152 m_font = config()->baseFont();
1153 m_fontMetrics = QFontMetricsF(m_font);
1154
1155 // ensure minimal height of one pixel to not fall in the div by 0 trap somewhere
1156 //
1157 // use a line spacing that matches the code in qt to layout/paint text
1158 //
1159 // see bug 403868
1160 // https://github.com/qt/qtbase/blob/5.12/src/gui/text/qtextlayout.cpp (line 2270 at the moment) where the text height is set as:
1161 //
1162 // qreal height = maxY + fontHeight - minY;
1163 //
1164 // with fontHeight:
1165 //
1166 // qreal fontHeight = font.ascent() + font.descent();
1167 m_fontHeight = qMax(a: 1, b: qCeil(v: m_fontMetrics.ascent() + m_fontMetrics.descent()));
1168 m_fontAscent = m_fontMetrics.ascent();
1169
1170 if (hasCustomLineHeight()) {
1171 const auto oldFontHeight = m_fontHeight;
1172 const qreal newFontHeight = qreal(m_fontHeight) * config()->lineHeightMultiplier();
1173 m_fontHeight = newFontHeight;
1174
1175 qreal diff = std::abs(x: oldFontHeight - newFontHeight);
1176 m_fontAscent += (diff / 2);
1177 }
1178}
1179
1180void KateRenderer::updateMarkerSize()
1181{
1182 // marker size will be calculated over the value defined
1183 // on dialog
1184
1185 m_markerSize = spaceWidth() / (3.5 - (m_doc->config()->markerSize() * 0.5));
1186}
1187
1188qreal KateRenderer::spaceWidth() const
1189{
1190 return m_fontMetrics.horizontalAdvance(spaceChar);
1191}
1192
1193void KateRenderer::layoutLine(Kate::TextLine textLine, KateLineLayout *lineLayout, int maxwidth, bool cacheLayout, bool skipSelections) const
1194{
1195 // if maxwidth == -1 we have no wrap
1196
1197 QTextLayout &l = lineLayout->modifiableLayout();
1198 l.setText(textLine.text());
1199 l.setFont(m_font);
1200 l.setCacheEnabled(cacheLayout);
1201
1202 // Initial setup of the QTextLayout.
1203
1204 // Tab width
1205 QTextOption opt;
1206 opt.setFlags(QTextOption::IncludeTrailingSpaces);
1207 opt.setTabStopDistance(m_tabWidth * m_fontMetrics.horizontalAdvance(spaceChar));
1208 if (m_view && m_view->config()->dynWrapAnywhere()) {
1209 opt.setWrapMode(QTextOption::WrapAnywhere);
1210 } else {
1211 opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
1212 }
1213
1214 // Find the first strong character in the string.
1215 // If it is an RTL character, set the base layout direction of the string to RTL.
1216 //
1217 // See https://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3).
1218 // Qt's text renderer ("scribe") version 4.2 assumes a "higher-level protocol"
1219 // (such as KatePart) will specify the paragraph level, so it does not apply P2 & P3
1220 // by itself. If this ever change in Qt, the next code block could be removed.
1221 // -----
1222 // Only force RTL direction if dynWordWrap is on. Otherwise the view has infinite width
1223 // and the lines will never be forced RTL no matter what direction we set. The layout
1224 // can't force a line to the right if it doesn't know where the "right" is
1225 if (isLineRightToLeft(str: textLine.text()) || (view()->dynWordWrap() && view()->forceRTLDirection())) {
1226 opt.setAlignment(Qt::AlignRight);
1227 opt.setTextDirection(Qt::RightToLeft);
1228 // Must turn off this flag otherwise cursor placement
1229 // is totally broken.
1230 if (view()->config()->dynWordWrap()) {
1231 auto flags = opt.flags();
1232 flags.setFlag(flag: QTextOption::IncludeTrailingSpaces, on: false);
1233 opt.setFlags(flags);
1234 }
1235 } else {
1236 opt.setAlignment(Qt::AlignLeft);
1237 opt.setTextDirection(Qt::LeftToRight);
1238 }
1239
1240 l.setTextOption(opt);
1241
1242 // Syntax highlighting, inbuilt and arbitrary
1243 QList<QTextLayout::FormatRange> decorations = decorationsForLine(textLine, line: lineLayout->line(), skipSelections);
1244 // clear background, that is draw separately
1245 for (auto &decoration : decorations) {
1246 decoration.format.clearBackground();
1247 }
1248
1249 int firstLineOffset = 0;
1250
1251 if (!isPrinterFriendly() && m_view) {
1252 const auto inlineNotes = m_view->inlineNotes(line: lineLayout->line());
1253 for (const KateInlineNoteData &noteData : inlineNotes) {
1254 const KTextEditor::InlineNote inlineNote(noteData);
1255 const int column = inlineNote.position().column();
1256 int width = inlineNote.width();
1257
1258 // Make space for every inline note.
1259 // If it is on column 0 (at the beginning of the line), we must offset the first line.
1260 // If it is inside the text, we use absolute letter spacing to create space for it between the two letters.
1261 // If it is outside of the text, we don't have to make space for it.
1262 if (column == 0) {
1263 firstLineOffset = width;
1264 } else if (column < l.text().length()) {
1265 QTextCharFormat text_char_format;
1266 const qreal caretWidth = caretStyle() == KTextEditor::caretStyles::Line ? 2.0 : 0.0;
1267 text_char_format.setFontLetterSpacing(width + caretWidth);
1268 text_char_format.setFontLetterSpacingType(QFont::AbsoluteSpacing);
1269 decorations.append(t: QTextLayout::FormatRange{.start = column - 1, .length = 1, .format = text_char_format});
1270 }
1271 }
1272 }
1273 l.setFormats(decorations);
1274
1275 // Begin layouting
1276 l.beginLayout();
1277
1278 int height = 0;
1279 int shiftX = 0;
1280
1281 bool needShiftX = (maxwidth != -1) && m_view && (m_view->config()->dynWordWrapAlignIndent() > 0);
1282
1283 while (true) {
1284 QTextLine line = l.createLine();
1285 if (!line.isValid()) {
1286 break;
1287 }
1288
1289 if (maxwidth > 0) {
1290 line.setLineWidth(maxwidth);
1291 } else {
1292 line.setLineWidth(INT_MAX);
1293 }
1294
1295 // we include the leading, this must match the ::updateFontHeight code!
1296 line.setLeadingIncluded(true);
1297
1298 line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height - line.ascent() + m_fontAscent));
1299
1300 if (needShiftX && line.width() > 0) {
1301 needShiftX = false;
1302 // Determine x offset for subsequent-lines-of-paragraph indenting
1303 int pos = textLine.nextNonSpaceChar(pos: 0);
1304
1305 if (pos > 0) {
1306 shiftX = (int)line.cursorToX(cursorPos: pos);
1307 }
1308
1309 // check for too deep shift value and limit if necessary
1310 if (m_view && shiftX > ((double)maxwidth / 100 * m_view->config()->dynWordWrapAlignIndent())) {
1311 shiftX = 0;
1312 }
1313
1314 // if shiftX > 0, the maxwidth has to adapted
1315 maxwidth -= shiftX;
1316
1317 lineLayout->shiftX = shiftX;
1318 }
1319
1320 height += lineHeight();
1321 }
1322
1323 // will end layout and trigger that we mark the layout as changed
1324 lineLayout->endLayout();
1325}
1326
1327// 1) QString::isRightToLeft() sux
1328// 2) QString::isRightToLeft() is marked as internal (WTF?)
1329// 3) QString::isRightToLeft() does not seem to work on my setup
1330// 4) isStringRightToLeft() should behave much better than QString::isRightToLeft() therefore:
1331// 5) isStringRightToLeft() kicks ass
1332bool KateRenderer::isLineRightToLeft(QStringView str)
1333{
1334 // borrowed from QString::updateProperties()
1335 for (auto c : str) {
1336 switch (c.direction()) {
1337 case QChar::DirL:
1338 case QChar::DirLRO:
1339 case QChar::DirLRE:
1340 return false;
1341
1342 case QChar::DirR:
1343 case QChar::DirAL:
1344 case QChar::DirRLO:
1345 case QChar::DirRLE:
1346 return true;
1347
1348 default:
1349 break;
1350 }
1351 }
1352
1353 return false;
1354#if 0
1355 // or should we use the direction of the widget?
1356 QWidget *display = qobject_cast<QWidget *>(view()->parent());
1357 if (!display) {
1358 return false;
1359 }
1360 return display->layoutDirection() == Qt::RightToLeft;
1361#endif
1362}
1363
1364qreal KateRenderer::cursorToX(const KateTextLayout &range, int col, bool returnPastLine) const
1365{
1366 return cursorToX(range, pos: KTextEditor::Cursor(range.line(), col), returnPastLine);
1367}
1368
1369qreal KateRenderer::cursorToX(const KateTextLayout &range, const KTextEditor::Cursor pos, bool returnPastLine) const
1370{
1371 Q_ASSERT(range.isValid());
1372
1373 qreal x = 0;
1374 if (range.lineLayout().width() > 0) {
1375 x = range.lineLayout().cursorToX(cursorPos: pos.column());
1376 }
1377
1378 if (const int over = pos.column() - range.endCol(); returnPastLine && over > 0) {
1379 x += over * spaceWidth();
1380 }
1381
1382 return x;
1383}
1384
1385KTextEditor::Cursor KateRenderer::xToCursor(const KateTextLayout &range, int x, bool returnPastLine) const
1386{
1387 Q_ASSERT(range.isValid());
1388 KTextEditor::Cursor ret(range.line(), range.lineLayout().xToCursor(x));
1389
1390 // Do not wrap to the next line. (bug #423253)
1391 if (range.wrap() && ret.column() >= range.endCol() && range.length() > 0) {
1392 ret.setColumn(range.endCol() - 1);
1393 }
1394 // TODO wrong for RTL lines?
1395 if (returnPastLine && range.endCol(indicateEOL: true) == -1 && x > range.width() + range.xOffset()) {
1396 ret.setColumn(ret.column() + round(x: (x - (range.width() + range.xOffset())) / spaceWidth()));
1397 }
1398
1399 return ret;
1400}
1401
1402void KateRenderer::setCaretOverrideColor(const QColor &color)
1403{
1404 m_caretOverrideColor = color;
1405}
1406
1407void KateRenderer::paintSelection(QPaintDevice *d, int startLine, int xStart, int endLine, int xEnd, int viewWidth, qreal scale)
1408{
1409 if (!d || scale < 0.0) {
1410 return;
1411 }
1412
1413 const int lineHeight = std::max(a: 1, b: this->lineHeight());
1414 QPainter paint(d);
1415 paint.scale(sx: scale, sy: scale);
1416
1417 // clip out non selected parts of start / end line
1418 {
1419 QRect mainRect(0, 0, d->width(), d->height());
1420 QRegion main(mainRect);
1421 // start line
1422 QRect startRect(0, 0, xStart, lineHeight);
1423 QRegion startRegion(startRect);
1424 // end line
1425 QRect endRect(mainRect.bottomLeft().x() + xEnd, mainRect.bottomRight().y() - lineHeight, mainRect.width() - xEnd, lineHeight);
1426 QRegion drawRegion = main.subtracted(r: startRegion).subtracted(r: QRegion(endRect));
1427 paint.setClipRegion(drawRegion);
1428 }
1429
1430 for (int line = startLine; line <= endLine; ++line) {
1431 // get real line, skip if invalid!
1432 if (line < 0 || line >= doc()->lines()) {
1433 continue;
1434 }
1435
1436 // compute layout WITHOUT cache to not poison it + render it
1437 KateLineLayout lineLayout;
1438 lineLayout.setLine(folding&: folding(), line, virtualLine: -1);
1439 layoutLine(textLine: doc()->kateTextLine(i: line), lineLayout: &lineLayout, maxwidth: viewWidth, cacheLayout: false /* no layout cache */, /*skipSelections=*/true);
1440 KateRenderer::PaintTextLineFlags flags;
1441 flags.setFlag(flag: KateRenderer::SkipDrawFirstInvisibleLineUnderlined);
1442 flags.setFlag(flag: KateRenderer::SkipDrawLineSelection);
1443 paintTextLine(paint, range: &lineLayout, xStart: 0, xEnd: 0, textClipRect: QRectF{}, cursor: nullptr, flags);
1444
1445 // translate for next line
1446 paint.translate(dx: 0, dy: lineHeight * lineLayout.viewLineCount());
1447 }
1448}
1449

source code of ktexteditor/src/render/katerenderer.cpp