TerminalPainter.cpp 24 KB
Newer Older
1
/*
2
3
4
5
6
    SPDX-FileCopyrightText: 2020-2020 Gustavo Carneiro <gcarneiroa@hotmail.com>
    SPDX-FileCopyrightText: 2007-2008 Robert Knight <robertknight@gmail.com>
    SPDX-FileCopyrightText: 1997, 1998 Lars Doelle <lars.doelle@on-line.de>

    SPDX-License-Identifier: GPL-2.0-or-later
7
8
9
10
11
12
13
*/

// Own
#include "TerminalPainter.hpp"

// Konsole
#include "TerminalDisplay.h"
14
#include "TerminalScrollBar.hpp"
15
#include "ExtendedCharTable.h"
16
#include "../characters/LineBlockCharacters.h"
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

// Qt
#include <QRect>
#include <QColor>
#include <QRegion>
#include <QPainter>
#include <QString>
#include <QVector>
#include <QChar>
#include <QMatrix>
#include <QTransform>
#include <QTimer>
#include <QPen>
#include <QDebug>

Gustavo Carneiro's avatar
Gustavo Carneiro committed
32
33
// we use this to force QPainter to display text in LTR mode
// more information can be found in: https://unicode.org/reports/tr9/
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const QChar LTR_OVERRIDE_CHAR(0x202D);

namespace Konsole
{

    TerminalPainter::TerminalPainter(TerminalDisplay *display)
        : _display(display)
    {}

    static inline bool isLineCharString(const QString &string)
    {
        if (string.length() == 0) {
            return false;
        }
        return LineBlockCharacters::canDraw(string.at(0).unicode());
    }

    static int baseCodePoint(const Character &ch)
    {
        if (ch.rendition & RE_EXTENDED_CHAR) {
            ushort extendedCharLength = 0;
            const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(ch.character, extendedCharLength);
            return chars[0];
        } else {
            return ch.character;
        }
    }

62
    void TerminalPainter::drawContents(QPainter &paint, const QRect &rect, bool printerFriendly)
63
64
65
66
67
68
    {
        const int numberOfColumns = _display->_usedColumns;
        QVector<uint> univec;
        univec.reserve(numberOfColumns);
        for (int y = rect.y(); y <= rect.bottom(); y++) {
            int x = rect.x();
69
            if ((_display->_image[_display->loc(rect.x(), y)].character == 0u) && (x != 0)) {
70
71
72
73
74
75
76
77
78
79
80
81
                x--; // Search for start of multi-column character
            }
            for (; x <= rect.right(); x++) {
                int len = 1;
                int p = 0;

                // reset our buffer to the number of columns
                int bufferSize = numberOfColumns;
                univec.resize(bufferSize);
                uint *disstrU = univec.data();

                // is this a single character or a sequence of characters ?
82
                if ((_display->_image[_display->loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) {
83
84
                    // sequence of characters
                    ushort extendedCharLength = 0;
85
                    const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(_display->_image[_display->loc(x, y)].character, extendedCharLength);
86
87
88
89
90
91
92
93
94
95
96
                    if (chars != nullptr) {
                        Q_ASSERT(extendedCharLength > 1);
                        bufferSize += extendedCharLength - 1;
                        univec.resize(bufferSize);
                        disstrU = univec.data();
                        for (int index = 0; index < extendedCharLength; index++) {
                            Q_ASSERT(p < bufferSize);
                            disstrU[p++] = chars[index];
                        }
                    }
                } else {
97
                    const uint c = _display->_image[_display->loc(x, y)].character;
98
99
100
101
102
103
                    if (c != 0u) {
                        Q_ASSERT(p < bufferSize);
                        disstrU[p++] = c;
                    }
                }

104
105
106
107
108
109
                const bool lineDraw = LineBlockCharacters::canDraw(_display->_image[_display->loc(x, y)].character);
                const bool doubleWidth = (_display->_image[qMin(_display->loc(x, y) + 1, _display->_imageSize - 1)].character == 0);
                const CharacterColor currentForeground = _display->_image[_display->loc(x, y)].foregroundColor;
                const CharacterColor currentBackground = _display->_image[_display->loc(x, y)].backgroundColor;
                const RenditionFlags currentRendition = _display->_image[_display->loc(x, y)].rendition;
                const QChar::Script currentScript = QChar::script(baseCodePoint(_display->_image[_display->loc(x, y)]));
110
111
112

                const auto isInsideDrawArea = [&](int column) { return column <= rect.right(); };
                const auto hasSameColors = [&](int column) {
113
114
                    return _display->_image[_display->loc(column, y)].foregroundColor == currentForeground
                        && _display->_image[_display->loc(column, y)].backgroundColor == currentBackground;
115
116
                };
                const auto hasSameRendition = [&](int column) {
117
                    return (_display->_image[_display->loc(column, y)].rendition & ~RE_EXTENDED_CHAR)
118
119
120
                        == (currentRendition & ~RE_EXTENDED_CHAR);
                };
                const auto hasSameWidth = [&](int column) {
121
                    const int characterLoc = qMin(_display->loc(column, y) + 1, _display->_imageSize - 1);
122
123
124
                    return (_display->_image[characterLoc].character == 0) == doubleWidth;
                };
                const auto hasSameLineDrawStatus = [&](int column) {
125
                    return LineBlockCharacters::canDraw(_display->_image[_display->loc(column, y)].character)
126
127
128
                        == lineDraw;
                };
                const auto isSameScript = [&](int column) {
129
                    const QChar::Script script = QChar::script(baseCodePoint(_display->_image[_display->loc(column, y)]));
130
131
132
133
134
135
136
                    if (currentScript == QChar::Script_Common || script == QChar::Script_Common
                        || currentScript == QChar::Script_Inherited || script == QChar::Script_Inherited) {
                        return true;
                    }
                    return currentScript == script;
                };
                const auto canBeGrouped = [&](int column) {
137
138
                    return _display->_image[_display->loc(column, y)].character <= 0x7e
                            || (_display->_image[_display->loc(column, y)].rendition & RE_EXTENDED_CHAR)
139
140
141
142
143
144
145
146
                            || (_display->_bidiEnabled && !doubleWidth);
                };

                if (canBeGrouped(x)) {
                    while (isInsideDrawArea(x + len) && hasSameColors(x + len)
                            && hasSameRendition(x + len) && hasSameWidth(x + len)
                            && hasSameLineDrawStatus(x + len) && isSameScript(x + len)
                            && canBeGrouped(x + len)) {
147
148
                        const uint c = _display->_image[_display->loc(x + len, y)].character;
                        if ((_display->_image[_display->loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) {
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
                            // sequence of characters
                            ushort extendedCharLength = 0;
                            const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength);
                            if (chars != nullptr) {
                                Q_ASSERT(extendedCharLength > 1);
                                bufferSize += extendedCharLength - 1;
                                univec.resize(bufferSize);
                                disstrU = univec.data();
                                for (int index = 0; index < extendedCharLength; index++) {
                                    Q_ASSERT(p < bufferSize);
                                    disstrU[p++] = chars[index];
                                }
                            }
                        } else {
                            // single character
                            if (c != 0u) {
                                Q_ASSERT(p < bufferSize);
                                disstrU[p++] = c;
                            }
                        }

                        if (doubleWidth) {
                            len++;
                        }
                        len++;
                    }
                } else {
                    // Group spaces following any non-wide character with the character. This allows for
                    // rendering ambiguous characters with wide glyphs without clipping them.
                    while (!doubleWidth && isInsideDrawArea(x + len)
179
                            && _display->_image[_display->loc(x + len, y)].character == ' ' && hasSameColors(x + len)
180
181
182
183
184
                            && hasSameRendition(x + len)) {
                        // disstrU intentionally not modified - trailing spaces are meaningless
                        len++;
                    }
                }
185
                if ((x + len < _display->_usedColumns) && (_display->_image[_display->loc(x + len, y)].character == 0u)) {
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
                    len++; // Adjust for trailing part of multi-column character
                }

                const bool save__fixedFont = _display->_fixedFont;
                if (lineDraw) {
                    _display->_fixedFont = false;
                }
                if (doubleWidth) {
                    _display->_fixedFont = false;
                }
                univec.resize(p);

                QMatrix textScale;

                if (y < _display->_lineProperties.size()) {
                    if ((_display->_lineProperties[y] & LINE_DOUBLEWIDTH) != 0) {
                        textScale.scale(2, 1);
                    }

                    if ((_display->_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) {
                        textScale.scale(1, 2);
                    }
                }

Kurt Hindenburg's avatar
Kurt Hindenburg committed
210
                // Apply text scaling matrix
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
                paint.setWorldTransform(QTransform(textScale), true);

                // Calculate the area in which the text will be drawn
                QRect textArea = QRect(_display->contentRect().left() + _display->contentsRect().left() + _display->fontWidth() * x,
                                        _display->contentRect().top() + _display->contentsRect().top() + _display->fontHeight() * y,
                                        _display->fontWidth() * len,
                                        _display->fontHeight());

                //move the calculated area to take account of scaling applied to the painter.
                //the position of the area from the origin (0,0) is scaled
                //by the opposite of whatever
                //transformation has been applied to the painter.  this ensures that
                //painting does actually start from textArea.topLeft()
                //(instead of textArea.topLeft() * painter-scale)
                textArea.moveTopLeft(textScale.inverted().map(textArea.topLeft()));

                QString unistr = QString::fromUcs4(univec.data(), univec.length());

                // paint text fragment
230
                if (printerFriendly) {
231
232
233
                    drawPrinterFriendlyTextFragment(paint,
                                                    textArea,
                                                    unistr,
234
                                                    &_display->_image[_display->loc(x, y)]);    
235
236
237
238
                } else {
                    drawTextFragment(paint,
                                    textArea,
                                    unistr,
239
                                    &_display->_image[_display->loc(x, y)]);
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
                }

                _display->_fixedFont = save__fixedFont;

                paint.setWorldTransform(QTransform(textScale.inverted()), true);

                if (y < _display->_lineProperties.size() - 1) {

                    if ((_display->_lineProperties[y] & LINE_DOUBLEHEIGHT) != 0) {
                        y++;
                    }
                }

                x += len - 1;
            }
        }
    }
    
    void TerminalPainter::drawCurrentResultRect(QPainter &painter)
    {
        if (_display->_screenWindow->currentResultLine() == -1) {
            return;
        }
        
        _display->_searchResultRect.setRect(0, _display->contentRect().top() + (_display->_screenWindow->currentResultLine() - _display->_screenWindow->currentLine()) * _display->fontHeight(),
            _display->columns() * _display->fontWidth(), _display->fontHeight());
        painter.fillRect(_display->_searchResultRect, QColor(0, 0, 255, 80));
    }
    
    void TerminalPainter::highlightScrolledLines(QPainter& painter) 
    {
        if (!_display->_highlightScrolledLinesControl.enabled) {
            return;
        }

        QColor color = QColor(_display->_colorTable[Color4Index]);
        color.setAlpha(_display->_highlightScrolledLinesControl.timer->isActive() ? 255 : 150);
        painter.fillRect(_display->_highlightScrolledLinesControl.rect, color);
    }
    
    QRegion TerminalPainter::highlightScrolledLinesRegion(bool nothingChanged) 
    {
        QRegion dirtyRegion;
283
        const int highlightLeftPosition = _display->scrollBar()->scrollBarPosition() == Enum::ScrollBarLeft ? _display->scrollBar()->width() : 0;
284
285
286

        int start = 0;
        int nb_lines = abs(_display->_screenWindow->scrollCount());
287
        if (nb_lines > 0 && _display->scrollBar()->maximum() > 0) {
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
            QRect new_highlight;
            bool addToCurrentHighlight = _display->_highlightScrolledLinesControl.timer->isActive() &&
                                     (_display->_screenWindow->scrollCount() * _display->_highlightScrolledLinesControl.previousScrollCount > 0);
            if (addToCurrentHighlight) {
                if (_display->_screenWindow->scrollCount() > 0) {
                    start = -1 * (_display->_highlightScrolledLinesControl.previousScrollCount + _display->screenWindow()->scrollCount()) + _display->screenWindow()->windowLines();
                } else {
                    start = -1 * _display->_highlightScrolledLinesControl.previousScrollCount;
                }
                _display->_highlightScrolledLinesControl.previousScrollCount += _display->_screenWindow->scrollCount();
            } else {
                start = _display->_screenWindow->scrollCount() > 0 ? _display->_screenWindow->windowLines() - nb_lines : 0;
                _display->_highlightScrolledLinesControl.previousScrollCount = _display->_screenWindow->scrollCount();
            }

            new_highlight.setRect(highlightLeftPosition, _display->contentRect().top() + start * _display->fontHeight(), _display->HIGHLIGHT_SCROLLED_LINES_WIDTH, nb_lines * _display->fontHeight());
            new_highlight.setTop(std::max(new_highlight.top(), _display->contentRect().top()));
            new_highlight.setBottom(std::min(new_highlight.bottom(), _display->contentRect().bottom()));
            if (!new_highlight.isValid()) {
                new_highlight = QRect(0, 0, 0, 0);
            }

            if (addToCurrentHighlight) {
                _display->_highlightScrolledLinesControl.rect |= new_highlight;
            } else {
                dirtyRegion |= _display->_highlightScrolledLinesControl.rect;
                _display->_highlightScrolledLinesControl.rect = new_highlight;
            }

            _display->_highlightScrolledLinesControl.timer->start();
        } else if (!nothingChanged || _display->_highlightScrolledLinesControl.needToClear) {
            dirtyRegion = _display->_highlightScrolledLinesControl.rect;
            _display->_highlightScrolledLinesControl.rect.setRect(0, 0, 0, 0);
            _display->_highlightScrolledLinesControl.needToClear = false;
        }

        return dirtyRegion;
    }
    
    void TerminalPainter::drawTextFragment(QPainter &painter, const QRect &rect, const QString &text,
                                const Character *style) 
    {
        // setup painter
        const QColor foregroundColor = style->foregroundColor.color(_display->_colorTable);
        const QColor backgroundColor = style->backgroundColor.color(_display->_colorTable);

        if (backgroundColor != _display->_colorTable[DEFAULT_BACK_COLOR]) {
            drawBackground(painter, rect, backgroundColor, false);
        }

        QColor characterColor;
        if ((style->rendition & RE_CURSOR) != 0) {
            drawCursor(painter, rect, foregroundColor, backgroundColor, characterColor);
        }

        // draw text
        drawCharacters(painter, rect, text, style, characterColor);
    }
    
    void TerminalPainter::drawPrinterFriendlyTextFragment(QPainter &painter, const QRect &rect, const QString &text,
                                                const Character *style) 
    {
        Character print_style = *style;
        print_style.foregroundColor = CharacterColor(COLOR_SPACE_RGB, 0x00000000);
        print_style.backgroundColor = CharacterColor(COLOR_SPACE_RGB, 0xFFFFFFFF);

        drawCharacters(painter, rect, text, &print_style, QColor());
    }
    
    void TerminalPainter::drawBackground(QPainter &painter, const QRect &rect, const QColor &backgroundColor,
                                bool useOpacitySetting) 
    {
        if (useOpacitySetting && !_display->_wallpaper->isNull() &&
                _display->_wallpaper->draw(painter, rect, _display->_opacity)) {
        } else if (qAlpha(_display->_blendColor) < 0xff && useOpacitySetting) {
#if defined(Q_OS_MACOS)
            // TODO: On MacOS, using CompositionMode doesn't work. Altering the
            //       transparency in the color scheme alters the brightness.
            painter.fillRect(rect, backgroundColor);
#else
            QColor color(backgroundColor);
            color.setAlpha(qAlpha(_display->_blendColor));

            const QPainter::CompositionMode originalMode = painter.compositionMode();
            painter.setCompositionMode(QPainter::CompositionMode_Source);
            painter.fillRect(rect, color);
            painter.setCompositionMode(originalMode);
#endif
        } else {
            painter.fillRect(rect, backgroundColor);
        }
    }
    
    void TerminalPainter::drawCursor(QPainter &painter, const QRect &rect, const QColor &foregroundColor,
                            const QColor &backgroundColor, QColor &characterColor) 
    {
        if (_display->_cursorBlinking) {
            return;
        }

        QRectF cursorRect = rect.adjusted(0, 1, 0, 0);

        QColor cursorColor = _display->_cursorColor.isValid() ? _display->_cursorColor : foregroundColor;
        QPen pen(cursorColor);
        // TODO: the relative pen width to draw the cursor is a bit hacky
        // and set to 1/12 of the font width. Visually it seems to work at
        // all scales but there must be better ways to do it
        const qreal width = qMax(_display->fontWidth() / 12.0, 1.0);
        const qreal halfWidth = width / 2.0;
        pen.setWidthF(width);
        painter.setPen(pen);

        if (_display->_cursorShape == Enum::BlockCursor) {
            painter.drawRect(cursorRect.adjusted(halfWidth, halfWidth, -halfWidth, -halfWidth));

            if (_display->hasFocus()) {
                painter.fillRect(cursorRect, cursorColor);

                characterColor = _display->_cursorTextColor.isValid() ? _display->_cursorTextColor : backgroundColor;
            }
        } else if (_display->_cursorShape == Enum::UnderlineCursor) {
            QLineF line(cursorRect.left() + halfWidth,
                        cursorRect.bottom() - halfWidth,
                        cursorRect.right() - halfWidth,
                        cursorRect.bottom() - halfWidth);
            painter.drawLine(line);

        } else if (_display->_cursorShape == Enum::IBeamCursor) {
            QLineF line(cursorRect.left() + halfWidth,
                        cursorRect.top() + halfWidth,
                        cursorRect.left() + halfWidth,
                        cursorRect.bottom() - halfWidth);
            painter.drawLine(line);
        }
    }
    
    void TerminalPainter::drawCharacters(QPainter &painter, const QRect &rect, const QString &text,
                                const Character *style, const QColor &characterColor) 
    {
        if (_display->_textBlinking && ((style->rendition & RE_BLINK) != 0)) {
            return;
        }

        if ((style->rendition & RE_CONCEAL) != 0) {
            return;
        }

        static constexpr int MaxFontWeight = 99;
        
        const int normalWeight = _display->font().weight();

        const int boldWeight = qMin(normalWeight + 26, MaxFontWeight);

        const auto isBold = [boldWeight](const QFont &font) { return font.weight() >= boldWeight; };

        const bool useBold = (((style->rendition & RE_BOLD) != 0) && _display->_boldIntense);
        const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || _display->font().underline();
445
        const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || _display->font().italic();
446
447
448
449
450
451
452
453
454
455
        const bool useStrikeOut = ((style->rendition & RE_STRIKEOUT) != 0) || _display->font().strikeOut();
        const bool useOverline = ((style->rendition & RE_OVERLINE) != 0) || _display->font().overline();

        QFont currentFont = painter.font();

        if (isBold(currentFont) != useBold
                || currentFont.underline() != useUnderline
                || currentFont.italic() != useItalic
                || currentFont.strikeOut() != useStrikeOut
                || currentFont.overline() != useOverline) {
456

457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
            currentFont.setWeight(useBold ? boldWeight : normalWeight);
            currentFont.setUnderline(useUnderline);
            currentFont.setItalic(useItalic);
            currentFont.setStrikeOut(useStrikeOut);
            currentFont.setOverline(useOverline);
            painter.setFont(currentFont);
        }

        // setup pen
        const QColor foregroundColor = style->foregroundColor.color(_display->_colorTable);
        const QColor color = characterColor.isValid() ? characterColor : foregroundColor;
        QPen pen = painter.pen();
        if (pen.color() != color) {
            pen.setColor(color);
            painter.setPen(color);
        }

        const bool origClipping = painter.hasClipping();
        const auto origClipRegion = painter.clipRegion();
        painter.setClipRect(rect);
        // draw text
        if (isLineCharString(text) && !_display->_useFontLineCharacters) {
            drawLineCharString(painter, rect.x(), rect.y(), text, style);
        } else {
            painter.setLayoutDirection(Qt::LeftToRight);

            if (_display->_bidiEnabled) {
                painter.drawText(rect.x(), rect.y() + _display->_fontAscent + _display->lineSpacing(), text);
            } else {
                painter.drawText(rect.x(), rect.y() + _display->_fontAscent + _display->lineSpacing(), LTR_OVERRIDE_CHAR + text);
            }
        }
        painter.setClipRegion(origClipRegion);
        painter.setClipping(origClipping);
    }
492

493
    void TerminalPainter::drawLineCharString(QPainter &painter, int x, int y, const QString &str,
494
                                    const Character *attributes)
495
496
497
498
499
500
501
502
503
504
505
    {
        painter.setRenderHint(QPainter::Antialiasing, _display->_antialiasText);

        const bool useBoldPen = (attributes->rendition & RE_BOLD) != 0 && _display->_boldIntense;

        QRect cellRect = {x, y, _display->fontWidth(), _display->fontHeight()};
        for (int i = 0; i < str.length(); i++) {
            LineBlockCharacters::draw(painter, cellRect.translated(i * _display->fontWidth(), 0), str[i], useBoldPen);
        }
        painter.setRenderHint(QPainter::Antialiasing, false);
    }
506
507

    void TerminalPainter::drawInputMethodPreeditString(QPainter &painter, const QRect &rect)
508
    {
509
        if (_display->_inputMethodData.preeditString.isEmpty() || !_display->isCursorOnDisplay()) {
510
511
            return;
        }
512

513
        const QPoint cursorPos = _display->cursorPosition();
514
515
516
517

        QColor characterColor;
        const QColor background = _display->_colorTable[DEFAULT_BACK_COLOR];
        const QColor foreground = _display->_colorTable[DEFAULT_FORE_COLOR];
518
        const Character *style = &_display->_image[_display->loc(cursorPos.x(), cursorPos.y())];
519
520
521
522
523
524
525
526
527

        drawBackground(painter, rect, background, true);
        drawCursor(painter, rect, foreground, background, characterColor);
        drawCharacters(painter, rect, _display->_inputMethodData.preeditString, style, characterColor);

        _display->_inputMethodData.previousPreeditRect = rect;
    }

}