You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

610 lines
25 KiB

/*
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
*/
// Own
#include "TerminalPainter.h"
// Konsole
#include "../characters/ExtendedCharTable.h"
#include "../characters/LineBlockCharacters.h"
#include "TerminalColor.h"
#include "TerminalFonts.h"
#include "TerminalScrollBar.h"
// Qt
#include <QChar>
#include <QColor>
#include <QDebug>
#include <QMatrix>
#include <QPainter>
#include <QPen>
#include <QRect>
#include <QRegion>
#include <QString>
#include <QTransform>
#include <QtMath>
#include <optional>
// we use this to force QPainter to display text in LTR mode
// more information can be found in: https://unicode.org/reports/tr9/
const QChar LTR_OVERRIDE_CHAR(0x202D);
namespace Konsole
{
TerminalPainter::TerminalPainter(QObject *parent)
: QObject(parent)
{
}
static inline bool isLineCharString(const QString &string)
{
if (string.length() == 0) {
return false;
}
return LineBlockCharacters::canDraw(string.at(0).unicode());
}
void TerminalPainter::drawContents(Character *image,
QPainter &paint,
const QRect &rect,
bool printerFriendly,
int imageSize,
bool bidiEnabled,
QVector<LineProperty> lineProperties)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
QVector<uint> univec;
univec.reserve(display->usedColumns());
for (int y = rect.y(); y <= rect.bottom(); y++) {
int x = rect.x();
bool doubleHeightLinePair = false;
// Search for start of multi-column character
if ((image[display->loc(rect.x(), y)].character == 0u) && (x != 0)) {
x--;
}
for (; x <= rect.right(); x++) {
int len = 1;
// is this a single character or a sequence of characters ?
if ((image[display->loc(x, y)].rendition & RE_EXTENDED_CHAR) != 0) {
// sequence of characters
ushort extendedCharLength = 0;
const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(image[display->loc(x, y)].character, extendedCharLength);
if (chars != nullptr) {
Q_ASSERT(extendedCharLength > 1);
for (int index = 0; index < extendedCharLength; index++) {
univec << chars[index];
}
}
} else {
const uint c = image[display->loc(x, y)].character;
if (c != 0u) {
univec << c;
}
}
// TODO: Move all those lambdas to Character, so it's easy to test.
const bool doubleWidth = (image[qMin(display->loc(x, y) + 1, imageSize - 1)].character == 0);
const auto isInsideDrawArea = [&](int column) {
return column <= rect.right();
};
const auto hasSameWidth = [&](int column) {
const int characterLoc = qMin(display->loc(column, y) + 1, imageSize - 1);
return (image[characterLoc].character == 0) == doubleWidth;
};
const Character &char_value = image[display->loc(x, y)];
if (char_value.canBeGrouped(bidiEnabled, doubleWidth)) {
while (isInsideDrawArea(x + len)) {
Character next_char = image[display->loc(x + len, y)];
if (!char_value.hasSameColors(next_char) || !char_value.hasSameRendition(next_char) || !hasSameWidth(x + len)
|| !char_value.hasSameLineDrawStatus(next_char) || !char_value.isSameScript(next_char)
|| !next_char.canBeGrouped(bidiEnabled, doubleWidth)) {
break;
}
const uint c = image[display->loc(x + len, y)].character;
if ((image[display->loc(x + len, y)].rendition & RE_EXTENDED_CHAR) != 0) {
// sequence of characters
ushort extendedCharLength = 0;
const uint *chars = ExtendedCharTable::instance.lookupExtendedChar(c, extendedCharLength);
if (chars != nullptr) {
Q_ASSERT(extendedCharLength > 1);
for (int index = 0; index < extendedCharLength; index++) {
univec << chars[index];
}
}
} else {
// single character
if (c != 0u) {
univec << 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)) {
const Character next_char = image[display->loc(x + len, y)];
if (next_char.character == ' ' && char_value.hasSameColors(next_char) && char_value.hasSameRendition(next_char)) {
// univec intentionally not modified - trailing spaces are meaningless
len++;
}
}
}
// Adjust for trailing part of multi-column character
if ((x + len < display->usedColumns()) && (image[display->loc(x + len, y)].character == 0u)) {
len++;
}
QMatrix textScale;
bool doubleHeight = false;
bool doubleWidthLine = false;
if (y < lineProperties.size()) {
if ((lineProperties[y] & LINE_DOUBLEWIDTH) != 0) {
textScale.scale(2, 1);
doubleWidthLine = true;
}
doubleHeight = lineProperties[y] & (LINE_DOUBLEHEIGHT_TOP | LINE_DOUBLEHEIGHT_BOTTOM);
if (doubleHeight) {
textScale.scale(1, 2);
}
}
if (y < lineProperties.size() - 1) {
if (((lineProperties[y] & LINE_DOUBLEHEIGHT_TOP) != 0) && ((lineProperties[y + 1] & LINE_DOUBLEHEIGHT_BOTTOM) != 0)) {
doubleHeightLinePair = true;
}
}
// Apply text scaling matrix
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->terminalFont()->fontWidth() * x * (doubleWidthLine ? 2 : 1),
display->contentRect().top() + display->contentsRect().top() + display->terminalFont()->fontHeight() * y,
display->terminalFont()->fontWidth() * len,
doubleHeight && !doubleHeightLinePair ? display->terminalFont()->fontHeight() / 2 : display->terminalFont()->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());
univec.clear();
// paint text fragment
if (printerFriendly) {
drawPrinterFriendlyTextFragment(paint, textArea, unistr, &image[display->loc(x, y)], y < lineProperties.size() ? lineProperties[y] : 0);
} else {
drawTextFragment(paint,
textArea,
unistr,
&image[display->loc(x, y)],
display->terminalColor()->colorTable(),
y < lineProperties.size() ? lineProperties[y] : 0);
}
paint.setWorldTransform(QTransform(textScale.inverted()), true);
x += len - 1;
}
if (doubleHeightLinePair) {
y++;
}
}
}
void TerminalPainter::drawCurrentResultRect(QPainter &painter, QRect searchResultRect)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
if (display->screenWindow()->currentResultLine() == -1) {
return;
}
searchResultRect.setRect(0,
display->contentRect().top()
+ (display->screenWindow()->currentResultLine() - display->screenWindow()->currentLine())
* display->terminalFont()->fontHeight(),
display->columns() * display->terminalFont()->fontWidth(),
display->terminalFont()->fontHeight());
painter.fillRect(searchResultRect, QColor(0, 0, 255, 80));
}
void TerminalPainter::highlightScrolledLines(QPainter &painter, bool isTimerActive, QRect rect)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
QColor color = QColor(display->terminalColor()->colorTable()[Color4Index]);
color.setAlpha(isTimerActive ? 255 : 150);
painter.fillRect(rect, color);
}
QRegion TerminalPainter::highlightScrolledLinesRegion(bool nothingChanged, TerminalScrollBar *scrollBar)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
QRegion dirtyRegion;
const int highlightLeftPosition = display->scrollBar()->scrollBarPosition() == Enum::ScrollBarLeft ? display->scrollBar()->width() : 0;
int start = 0;
int nb_lines = abs(display->screenWindow()->scrollCount());
if (nb_lines > 0 && display->scrollBar()->maximum() > 0) {
QRect new_highlight;
bool addToCurrentHighlight = scrollBar->highlightScrolledLines().isTimerActive()
&& (display->screenWindow()->scrollCount() * scrollBar->highlightScrolledLines().getPreviousScrollCount() > 0);
if (addToCurrentHighlight) {
const int oldScrollCount = scrollBar->highlightScrolledLines().getPreviousScrollCount();
if (display->screenWindow()->scrollCount() > 0) {
start = -1 * (oldScrollCount + display->screenWindow()->scrollCount()) + display->screenWindow()->windowLines();
} else {
start = -1 * oldScrollCount;
}
scrollBar->highlightScrolledLines().setPreviousScrollCount(oldScrollCount + display->screenWindow()->scrollCount());
} else {
start = display->screenWindow()->scrollCount() > 0 ? display->screenWindow()->windowLines() - nb_lines : 0;
scrollBar->highlightScrolledLines().setPreviousScrollCount(display->screenWindow()->scrollCount());
}
new_highlight.setRect(highlightLeftPosition,
display->contentRect().top() + start * display->terminalFont()->fontHeight(),
scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH,
nb_lines * display->terminalFont()->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) {
scrollBar->highlightScrolledLines().rect() |= new_highlight;
} else {
dirtyRegion |= scrollBar->highlightScrolledLines().rect();
scrollBar->highlightScrolledLines().rect() = new_highlight;
}
dirtyRegion |= new_highlight;
scrollBar->highlightScrolledLines().startTimer();
} else if (!nothingChanged || scrollBar->highlightScrolledLines().isNeedToClear()) {
dirtyRegion = scrollBar->highlightScrolledLines().rect();
scrollBar->highlightScrolledLines().rect().setRect(0, 0, 0, 0);
scrollBar->highlightScrolledLines().setNeedToClear(false);
}
return dirtyRegion;
}
QColor alphaBlend(const QColor &foreground, const QColor &background)
{
const auto foregroundAlpha = foreground.alphaF();
const auto inverseForegroundAlpha = 1.0 - foregroundAlpha;
const auto backgroundAlpha = background.alphaF();
if (foregroundAlpha == 0.0) {
return background;
}
if (backgroundAlpha == 1.0) {
return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseForegroundAlpha * background.red()),
(foregroundAlpha * foreground.green()) + (inverseForegroundAlpha * background.green()),
(foregroundAlpha * foreground.blue()) + (inverseForegroundAlpha * background.blue()),
0xff);
} else {
const auto inverseBackgroundAlpha = (backgroundAlpha * inverseForegroundAlpha);
const auto finalAlpha = foregroundAlpha + inverseBackgroundAlpha;
Q_ASSERT(finalAlpha != 0.0);
return QColor::fromRgb((foregroundAlpha * foreground.red()) + (inverseBackgroundAlpha * background.red()),
(foregroundAlpha * foreground.green()) + (inverseBackgroundAlpha * background.green()),
(foregroundAlpha * foreground.blue()) + (inverseBackgroundAlpha * background.blue()),
finalAlpha);
}
}
qreal wcag20AdjustColorPart(qreal v)
{
return v <= 0.03928 ? v / 12.92 : qPow((v + 0.055) / 1.055, 2.4);
}
qreal wcag20RelativeLuminosity(const QColor &of)
{
auto r = of.redF(), g = of.greenF(), b = of.blueF();
const auto a = wcag20AdjustColorPart;
auto r2 = a(r), g2 = a(g), b2 = a(b);
return r2 * 0.2126 + g2 * 0.7152 + b2 * 0.0722;
}
qreal wcag20Contrast(const QColor &c1, const QColor &c2)
{
const auto l1 = wcag20RelativeLuminosity(c1) + 0.05, l2 = wcag20RelativeLuminosity(c2) + 0.05;
return (l1 > l2) ? l1 / l2 : l2 / l1;
}
std::optional<QColor> calculateBackgroundColor(const Character *style, const QColor *colorTable)
{
auto c1 = style->backgroundColor.color(colorTable);
if (!(style->rendition & RE_BLEND_SELECTION_COLORS)) {
return c1;
}
const auto initialBG = c1;
c1.setAlphaF(0.8);
const auto blend1 = alphaBlend(c1, colorTable[DEFAULT_FORE_COLOR]), blend2 = alphaBlend(c1, colorTable[DEFAULT_BACK_COLOR]);
const auto fg = style->foregroundColor.color(colorTable);
const auto contrast1 = wcag20Contrast(fg, blend1), contrast2 = wcag20Contrast(fg, blend2);
const auto contrastBG1 = wcag20Contrast(blend1, initialBG), contrastBG2 = wcag20Contrast(blend2, initialBG);
const auto fgFactor = 5.5; // if text contrast is too low against our calculated bg, then we flip to reverse
const auto bgFactor = 1.6; // if bg contrast is too low against default bg, then we flip to reverse
if ((contrast1 < fgFactor && contrast2 < fgFactor) || (contrastBG1 < bgFactor && contrastBG2 < bgFactor)) {
return {};
}
return (contrast1 < contrast2) ? blend1 : blend2;
}
void TerminalPainter::drawTextFragment(QPainter &painter,
const QRect &rect,
const QString &text,
const Character *style,
const QColor *colorTable,
const LineProperty lineProperty)
{
// setup painter
QColor foregroundColor = style->foregroundColor.color(colorTable);
const QColor backgroundColor = calculateBackgroundColor(style, colorTable).value_or(foregroundColor);
if (backgroundColor == foregroundColor) {
foregroundColor = style->backgroundColor.color(colorTable);
}
if (backgroundColor != colorTable[DEFAULT_BACK_COLOR]) {
drawBackground(painter, rect, backgroundColor, false);
}
QColor characterColor = foregroundColor;
if ((style->rendition & RE_CURSOR) != 0) {
drawCursor(painter, rect, foregroundColor, backgroundColor, characterColor);
}
// draw text
drawCharacters(painter, rect, text, style, characterColor, lineProperty);
}
void TerminalPainter::drawPrinterFriendlyTextFragment(QPainter &painter,
const QRect &rect,
const QString &text,
const Character *style,
const LineProperty lineProperty)
{
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(), lineProperty);
}
void TerminalPainter::drawBackground(QPainter &painter, const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
if (useOpacitySetting && !display->wallpaper()->isNull() && display->wallpaper()->draw(painter, rect, display->terminalColor()->opacity())) {
} else if (qAlpha(display->terminalColor()->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->terminalColor()->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)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
if (display->cursorBlinking()) {
return;
}
QRectF cursorRect = rect.adjusted(0, 1, 0, 0);
QColor color = display->terminalColor()->cursorColor();
QColor cursorColor = color.isValid() ? color : 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->terminalFont()->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);
QColor cursorTextColor = display->terminalColor()->cursorTextColor();
characterColor = cursorTextColor.isValid() ? 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,
const LineProperty lineProperty)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
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->terminalFont()->boldIntense());
const bool useUnderline = ((style->rendition & RE_UNDERLINE) != 0) || display->font().underline();
const bool useItalic = ((style->rendition & RE_ITALIC) != 0) || display->font().italic();
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) {
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->terminalColor()->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->terminalFont()->useFontLineCharacters()) {
int y = rect.y();
if (lineProperty & LINE_DOUBLEHEIGHT_BOTTOM) {
y -= display->terminalFont()->fontHeight() / 2;
}
drawLineCharString(display, painter, rect.x(), y, text, style);
} else {
painter.setLayoutDirection(Qt::LeftToRight);
int y = rect.y() + display->terminalFont()->fontAscent();
if (lineProperty & LINE_DOUBLEHEIGHT_BOTTOM) {
y -= display->terminalFont()->fontHeight() / 2;
} else {
y += display->terminalFont()->lineSpacing();
}
if (display->bidiEnabled()) {
painter.drawText(rect.x(), y, text);
} else {
painter.drawText(rect.x(), y, LTR_OVERRIDE_CHAR + text);
}
}
painter.setClipRegion(origClipRegion);
painter.setClipping(origClipping);
}
void TerminalPainter::drawLineCharString(TerminalDisplay *display, QPainter &painter, int x, int y, const QString &str, const Character *attributes)
{
painter.setRenderHint(QPainter::Antialiasing, display->terminalFont()->antialiasText());
const bool useBoldPen = (attributes->rendition & RE_BOLD) != 0 && display->terminalFont()->boldIntense();
QRect cellRect = {x, y, display->terminalFont()->fontWidth(), display->terminalFont()->fontHeight()};
for (int i = 0; i < str.length(); i++) {
LineBlockCharacters::draw(painter, cellRect.translated(i * display->terminalFont()->fontWidth(), 0), str[i], useBoldPen);
}
painter.setRenderHint(QPainter::Antialiasing, false);
}
void TerminalPainter::drawInputMethodPreeditString(QPainter &painter, const QRect &rect, TerminalDisplay::InputMethodData &inputMethodData, Character *image)
{
const auto display = qobject_cast<TerminalDisplay *>(sender());
if (inputMethodData.preeditString.isEmpty() || !display->isCursorOnDisplay()) {
return;
}
const QPoint cursorPos = display->cursorPosition();
QColor characterColor;
const QColor background = display->terminalColor()->colorTable()[DEFAULT_BACK_COLOR];
const QColor foreground = display->terminalColor()->colorTable()[DEFAULT_FORE_COLOR];
const Character *style = &image[display->loc(cursorPos.x(), cursorPos.y())];
drawBackground(painter, rect, background, true);
drawCursor(painter, rect, foreground, background, characterColor);
drawCharacters(painter, rect, inputMethodData.preeditString, style, characterColor, 0);
inputMethodData.previousPreeditRect = rect;
}
}