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

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