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.
1013 lines
42 KiB
1013 lines
42 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 "../Screen.h" |
|
#include "../characters/ExtendedCharTable.h" |
|
#include "../characters/LineBlockCharacters.h" |
|
#include "../filterHotSpots/FilterChain.h" |
|
#include "../session/SessionManager.h" |
|
#include "TerminalColor.h" |
|
#include "TerminalFonts.h" |
|
#include "TerminalScrollBar.h" |
|
|
|
// Qt |
|
#include <QChar> |
|
#include <QColor> |
|
#include <QDebug> |
|
#include <QElapsedTimer> |
|
#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(TerminalDisplay *parent) |
|
: QObject(parent) |
|
, m_parentDisplay(parent) |
|
{ |
|
} |
|
|
|
static inline bool isLineCharString(const QString &string) |
|
{ |
|
if (string.length() == 0) { |
|
return false; |
|
} |
|
if (LineBlockCharacters::canDraw(string.at(0).unicode())) { |
|
return true; |
|
} |
|
if (string.length() <= 1 || !string[0].isSurrogate()) { |
|
return false; |
|
} |
|
uint ucs4; |
|
if (string[0].isHighSurrogate()) { |
|
ucs4 = QChar::surrogateToUcs4(string[0], string[1]); |
|
} else { |
|
ucs4 = QChar::surrogateToUcs4(string[1], string[0]); |
|
} |
|
return LineBlockCharacters::isLegacyComputingSymbol(ucs4); |
|
} |
|
|
|
bool isInvertedRendition(const TerminalDisplay *display) |
|
{ |
|
auto currentProfile = SessionManager::instance()->sessionProfile(display->session()); |
|
return currentProfile ? currentProfile->property<bool>(Profile::InvertSelectionColors) : false; |
|
} |
|
|
|
void TerminalPainter::drawContents(Character *image, |
|
QPainter &paint, |
|
const QRect &rect, |
|
bool printerFriendly, |
|
int imageSize, |
|
bool bidiEnabled, |
|
QVector<LineProperty> lineProperties, |
|
CharacterColor const *ulColorTable) |
|
{ |
|
const bool invertedRendition = isInvertedRendition(m_parentDisplay); |
|
|
|
QVector<uint> univec; |
|
univec.reserve(m_parentDisplay->usedColumns()); |
|
int placementIdx = 0; |
|
|
|
const int leftPadding = m_parentDisplay->contentRect().left() + m_parentDisplay->contentsRect().left(); |
|
const int topPadding = m_parentDisplay->contentRect().top() + m_parentDisplay->contentsRect().top(); |
|
const int fontWidth = m_parentDisplay->terminalFont()->fontWidth(); |
|
const int fontHeight = m_parentDisplay->terminalFont()->fontHeight(); |
|
const QRect textArea(QPoint(leftPadding + fontWidth * rect.x(), topPadding + rect.y() * fontHeight), |
|
QSize(rect.width() * fontWidth, rect.height() * fontHeight)); |
|
if (!printerFriendly) { |
|
drawImagesBelowText(paint, textArea, fontWidth, fontHeight, placementIdx); |
|
} |
|
|
|
static const QFont::Weight FontWeights[] = { |
|
QFont::Thin, |
|
QFont::Light, |
|
QFont::Normal, |
|
QFont::Bold, |
|
QFont::Black, |
|
}; |
|
const auto normalWeight = m_parentDisplay->font().weight(); |
|
auto it = std::upper_bound(std::begin(FontWeights), std::end(FontWeights), normalWeight); |
|
const QFont::Weight boldWeight = it != std::end(FontWeights) ? *it : QFont::Black; |
|
paint.setRenderHint(QPainter::Antialiasing, m_parentDisplay->terminalFont()->antialiasText()); |
|
paint.setLayoutDirection(Qt::LeftToRight); |
|
|
|
for (int y = rect.y(); y <= rect.bottom(); y++) { |
|
int pos = m_parentDisplay->loc(0, y); |
|
if (pos > imageSize) { |
|
break; |
|
} |
|
int right = rect.right(); |
|
if (pos + right > imageSize) { |
|
right = imageSize - pos; |
|
} |
|
|
|
const int textY = topPadding + fontHeight * y; |
|
bool doubleHeightLinePair = false; |
|
int x = rect.x(); |
|
LineProperty lineProperty = y < lineProperties.size() ? lineProperties[y] : 0; |
|
|
|
// Search for start of multi-column character |
|
if (image[m_parentDisplay->loc(rect.x(), y)].isRightHalfOfDoubleWide() && (x != 0)) { |
|
x--; |
|
} |
|
QTransform textScale; |
|
bool doubleHeight = false; |
|
bool doubleWidthLine = false; |
|
|
|
if ((lineProperty & LINE_DOUBLEWIDTH) != 0) { |
|
textScale.scale(2, 1); |
|
doubleWidthLine = true; |
|
} |
|
|
|
doubleHeight = lineProperty & (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(textScale, true); |
|
// Calculate the area in which the text will be drawn |
|
const int textX = leftPadding + fontWidth * rect.x() * (doubleWidthLine ? 2 : 1); |
|
const int textWidth = fontWidth * rect.width(); |
|
const int textHeight = doubleHeight && !doubleHeightLinePair ? fontHeight / 2 : 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) |
|
QString line; |
|
#define MAX_LINE_WIDTH 1024 |
|
int log2line[MAX_LINE_WIDTH]; |
|
int line2log[MAX_LINE_WIDTH]; |
|
uint16_t shapemap[MAX_LINE_WIDTH]; |
|
int32_t vis2line[MAX_LINE_WIDTH]; |
|
bool shaped; |
|
int lastNonSpace = m_parentDisplay->bidiMap(image + pos, line, log2line, line2log, shapemap, vis2line, shaped, bidiEnabled, bidiEnabled); |
|
const QRect textArea(textScale.inverted().map(QPoint(textX, textY)), QSize(textWidth, textHeight)); |
|
if (!printerFriendly) { |
|
drawBelowText(paint, |
|
textArea, |
|
image + pos, |
|
rect.x(), |
|
rect.width(), |
|
fontWidth, |
|
m_parentDisplay->terminalColor()->colorTable(), |
|
invertedRendition, |
|
vis2line, |
|
line2log, |
|
bidiEnabled); |
|
} |
|
|
|
RenditionFlags oldRendition = -1; |
|
QColor oldColor = QColor(); |
|
for (; x <= right; x++) { |
|
if (x > lastNonSpace) { |
|
// What about the cursor? |
|
// break; |
|
} |
|
int log_x; |
|
int line_x; |
|
if (bidiEnabled) { |
|
line_x = vis2line[x]; |
|
log_x = line2log[line_x]; |
|
} else { |
|
log_x = x; |
|
} |
|
|
|
const Character char_value = image[pos + log_x]; |
|
|
|
if (!printerFriendly && char_value.isSpace() && char_value.rendition.f.cursor == 0) { |
|
continue; |
|
} |
|
|
|
QString unistr = line.mid(log2line[log_x], log2line[log_x + 1] - log2line[log_x]); |
|
if (shaped) { |
|
unistr[0] = shapemap[log2line[log_x]]; |
|
} |
|
|
|
// paint text fragment |
|
if (unistr.length() && unistr[0] != QChar(0)) { |
|
int textWidth = fontWidth * 1; |
|
int textX = leftPadding + fontWidth * x * (doubleWidthLine ? 2 : 1); |
|
const QRect textAreaOneChar(textScale.inverted().map(QPoint(textX, textY)), QSize(textWidth, textHeight)); |
|
drawTextCharacters(paint, |
|
textAreaOneChar, |
|
unistr, |
|
image[pos + log_x], |
|
m_parentDisplay->terminalColor()->colorTable(), |
|
invertedRendition, |
|
lineProperty, |
|
printerFriendly, |
|
oldRendition, |
|
oldColor, |
|
normalWeight, |
|
boldWeight); |
|
} |
|
} |
|
if (!printerFriendly) { |
|
drawAboveText(paint, |
|
textArea, |
|
image + pos, |
|
rect.x(), |
|
rect.width(), |
|
fontWidth, |
|
m_parentDisplay->terminalColor()->colorTable(), |
|
invertedRendition, |
|
vis2line, |
|
line2log, |
|
bidiEnabled, |
|
ulColorTable); |
|
} |
|
|
|
paint.setRenderHint(QPainter::Antialiasing, false); |
|
paint.setWorldTransform(textScale.inverted(), true); |
|
if ((lineProperty & LINE_PROMPT_START) |
|
&& ((m_parentDisplay->semanticHints() == Enum::SemanticHintsURL && m_parentDisplay->filterChain()->showUrlHint()) |
|
|| m_parentDisplay->semanticHints() == Enum::SemanticHintsAlways)) { |
|
QPen pen(m_parentDisplay->terminalColor()->foregroundColor()); |
|
paint.setPen(pen); |
|
paint.drawLine(leftPadding, textY, m_parentDisplay->contentRect().right(), textY); |
|
} |
|
|
|
if (doubleHeightLinePair) { |
|
y++; |
|
} |
|
} |
|
if (!printerFriendly) { |
|
drawImagesAboveText(paint, textArea, fontWidth, fontHeight, placementIdx); |
|
} |
|
} |
|
|
|
void TerminalPainter::drawCurrentResultRect(QPainter &painter, const QRect &searchResultRect) |
|
{ |
|
painter.fillRect(searchResultRect, QColor(0, 0, 255, 80)); |
|
} |
|
|
|
void TerminalPainter::highlightScrolledLines(QPainter &painter, bool isTimerActive, QRect rect) |
|
{ |
|
QColor color = QColor(m_parentDisplay->terminalColor()->colorTable()[Color4Index]); |
|
color.setAlpha(isTimerActive ? 255 : 150); |
|
painter.fillRect(rect, color); |
|
} |
|
|
|
QRegion TerminalPainter::highlightScrolledLinesRegion(TerminalScrollBar *scrollBar) |
|
{ |
|
QRegion dirtyRegion; |
|
const int highlightLeftPosition = m_parentDisplay->scrollBar()->scrollBarPosition() == Enum::ScrollBarLeft ? m_parentDisplay->scrollBar()->width() : 0; |
|
|
|
int start = 0; |
|
int nb_lines = abs(m_parentDisplay->screenWindow()->scrollCount()); |
|
if (nb_lines > 0 && m_parentDisplay->scrollBar()->maximum() > 0) { |
|
QRect new_highlight; |
|
bool addToCurrentHighlight = scrollBar->highlightScrolledLines().isTimerActive() |
|
&& (m_parentDisplay->screenWindow()->scrollCount() * scrollBar->highlightScrolledLines().getPreviousScrollCount() > 0); |
|
if (addToCurrentHighlight) { |
|
const int oldScrollCount = scrollBar->highlightScrolledLines().getPreviousScrollCount(); |
|
if (m_parentDisplay->screenWindow()->scrollCount() > 0) { |
|
start = -1 * (oldScrollCount + m_parentDisplay->screenWindow()->scrollCount()) + m_parentDisplay->screenWindow()->windowLines(); |
|
} else { |
|
start = -1 * oldScrollCount; |
|
} |
|
scrollBar->highlightScrolledLines().setPreviousScrollCount(oldScrollCount + m_parentDisplay->screenWindow()->scrollCount()); |
|
} else { |
|
start = m_parentDisplay->screenWindow()->scrollCount() > 0 ? m_parentDisplay->screenWindow()->windowLines() - nb_lines : 0; |
|
scrollBar->highlightScrolledLines().setPreviousScrollCount(m_parentDisplay->screenWindow()->scrollCount()); |
|
} |
|
|
|
new_highlight.setRect(highlightLeftPosition, |
|
m_parentDisplay->contentRect().top() + start * m_parentDisplay->terminalFont()->fontHeight(), |
|
scrollBar->highlightScrolledLines().HIGHLIGHT_SCROLLED_LINES_WIDTH, |
|
nb_lines * m_parentDisplay->terminalFont()->fontHeight()); |
|
new_highlight.setTop(std::max(new_highlight.top(), m_parentDisplay->contentRect().top())); |
|
new_highlight.setBottom(std::min(new_highlight.bottom(), m_parentDisplay->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(); |
|
} |
|
|
|
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); |
|
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; |
|
} |
|
|
|
static void reverseRendition(Character &p) |
|
{ |
|
CharacterColor f = p.foregroundColor; |
|
CharacterColor b = p.backgroundColor; |
|
|
|
p.foregroundColor = b; |
|
p.backgroundColor = f; |
|
} |
|
|
|
void TerminalPainter::drawBackground(QPainter &painter, const QRect &rect, const QColor &backgroundColor, bool useOpacitySetting) |
|
{ |
|
if (useOpacitySetting && !m_parentDisplay->wallpaper()->isNull() |
|
&& m_parentDisplay->wallpaper()->draw(painter, rect, m_parentDisplay->terminalColor()->opacity(), backgroundColor)) { |
|
} else if (qAlpha(m_parentDisplay->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(m_parentDisplay->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) |
|
{ |
|
if (m_parentDisplay->cursorBlinking()) { |
|
return; |
|
} |
|
|
|
// shift rectangle top down one pixel to leave some space |
|
// between top and bottom |
|
// noticeable when linespace>1 |
|
QRectF cursorRect = rect.adjusted(0, 1, 0, 0); |
|
|
|
QColor color = m_parentDisplay->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(m_parentDisplay->terminalFont()->fontWidth() / 12.0, 1.0); |
|
const qreal halfWidth = width / 2.0; |
|
pen.setWidthF(width); |
|
painter.setPen(pen); |
|
|
|
if (m_parentDisplay->cursorShape() == Enum::BlockCursor) { |
|
if (m_parentDisplay->hasFocus()) { |
|
painter.fillRect(cursorRect, cursorColor); |
|
|
|
// invert the color used to draw the text to ensure that the character at |
|
// the cursor position is readable |
|
QColor cursorTextColor = m_parentDisplay->terminalColor()->cursorTextColor(); |
|
|
|
characterColor = cursorTextColor.isValid() ? cursorTextColor : backgroundColor; |
|
} else { |
|
// draw the cursor outline, adjusting the area so that |
|
// it is drawn entirely inside cursorRect |
|
painter.drawRect(cursorRect.adjusted(halfWidth, halfWidth, -halfWidth, -halfWidth)); |
|
} |
|
} else if (m_parentDisplay->cursorShape() == Enum::UnderlineCursor) { |
|
QLineF line(cursorRect.left() + halfWidth, cursorRect.bottom() - halfWidth, cursorRect.right() - halfWidth, cursorRect.bottom() - halfWidth); |
|
painter.drawLine(line); |
|
|
|
} else if (m_parentDisplay->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) |
|
{ |
|
if (m_parentDisplay->textBlinking() && (style.rendition.f.blink != 0)) { |
|
return; |
|
} |
|
|
|
if (style.rendition.f.conceal != 0) { |
|
return; |
|
} |
|
|
|
static const QFont::Weight FontWeights[] = { |
|
QFont::Thin, |
|
QFont::Light, |
|
QFont::Normal, |
|
QFont::Bold, |
|
QFont::Black, |
|
}; |
|
|
|
// The weight used as bold depends on selected font's weight. |
|
// "Regular" will use "Bold", but e.g. "Thin" will use "Light". |
|
// Note that QFont::weight/setWeight() returns/takes an int in Qt5, |
|
// and a QFont::Weight in Qt6 |
|
const auto normalWeight = m_parentDisplay->font().weight(); |
|
auto it = std::upper_bound(std::begin(FontWeights), std::end(FontWeights), normalWeight); |
|
const QFont::Weight boldWeight = it != std::end(FontWeights) ? *it : QFont::Black; |
|
|
|
const bool useBold = ((style.rendition.f.bold != 0) && m_parentDisplay->terminalFont()->boldIntense()); |
|
const bool useUnderline = (style.rendition.f.underline != 0) || m_parentDisplay->font().underline(); |
|
const bool useItalic = (style.rendition.f.italic != 0) || m_parentDisplay->font().italic(); |
|
const bool useStrikeOut = (style.rendition.f.strikeout != 0) || m_parentDisplay->font().strikeOut(); |
|
const bool useOverline = (style.rendition.f.overline != 0) || m_parentDisplay->font().overline(); |
|
|
|
QFont currentFont = painter.font(); |
|
|
|
const bool isCurrentBold = currentFont.weight() >= boldWeight; |
|
// clang-format off |
|
if (isCurrentBold != useBold |
|
|| currentFont.underline() != useUnderline |
|
|| currentFont.italic() != useItalic |
|
|| currentFont.strikeOut() != useStrikeOut |
|
|| currentFont.overline() != useOverline |
|
) |
|
{ // clang-format on |
|
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(m_parentDisplay->terminalColor()->colorTable()); |
|
const QColor color = characterColor.isValid() ? characterColor : foregroundColor; |
|
QPen pen = painter.pen(); |
|
if (pen.color() != color) { |
|
pen.setColor(color); |
|
painter.setPen(color); |
|
} |
|
// draw text |
|
if (isLineCharString(text) && !m_parentDisplay->terminalFont()->useFontLineCharacters()) { |
|
int y = rect.y(); |
|
|
|
if (lineProperty & LINE_DOUBLEHEIGHT_BOTTOM) { |
|
y -= m_parentDisplay->terminalFont()->fontHeight() / 2; |
|
} |
|
|
|
drawLineCharString(m_parentDisplay, painter, rect.x(), y, text, style); |
|
} else { |
|
painter.setLayoutDirection(Qt::LeftToRight); |
|
int y = rect.y() + m_parentDisplay->terminalFont()->fontAscent(); |
|
|
|
int shifted = 0; |
|
if (lineProperty & LINE_DOUBLEHEIGHT_BOTTOM) { |
|
y -= m_parentDisplay->terminalFont()->fontHeight() / 2; |
|
} else { |
|
// We shift half way down here to center |
|
shifted = m_parentDisplay->terminalFont()->lineSpacing() / 2; |
|
y += shifted; |
|
} |
|
|
|
if (m_parentDisplay->bidiEnabled()) { |
|
painter.drawText(rect.x(), y, text); |
|
} else { |
|
painter.drawText(rect.x(), y, LTR_OVERRIDE_CHAR + text); |
|
} |
|
|
|
if (shifted > 0) { |
|
// To avoid rounding errors we shift the rest this way |
|
y += m_parentDisplay->terminalFont()->lineSpacing() - shifted; |
|
} |
|
} |
|
} |
|
|
|
void TerminalPainter::drawLineCharString(TerminalDisplay *display, QPainter &painter, int x, int y, const QString &str, const Character attributes) |
|
{ |
|
const bool useBoldPen = (attributes.rendition.f.bold) != 0 && display->terminalFont()->boldIntense(); |
|
QRect cellRect = {x, y, display->terminalFont()->fontWidth(), display->terminalFont()->fontHeight()}; |
|
QVector<uint> ucs4str = str.toUcs4(); |
|
for (int i = 0; i < ucs4str.length(); i++) { |
|
LineBlockCharacters::draw(painter, cellRect.translated(i * display->terminalFont()->fontWidth(), 0), ucs4str[i], useBoldPen); |
|
} |
|
} |
|
|
|
void TerminalPainter::drawInputMethodPreeditString(QPainter &painter, const QRect &rect, TerminalDisplay::InputMethodData &inputMethodData, Character *image) |
|
{ |
|
if (inputMethodData.preeditString.isEmpty() || !m_parentDisplay->isCursorOnDisplay()) { |
|
return; |
|
} |
|
|
|
const QPoint cursorPos = m_parentDisplay->cursorPosition(); |
|
|
|
QColor characterColor; |
|
const QColor background = m_parentDisplay->terminalColor()->colorTable()[DEFAULT_BACK_COLOR]; |
|
const QColor foreground = m_parentDisplay->terminalColor()->colorTable()[DEFAULT_FORE_COLOR]; |
|
const Character style = image[m_parentDisplay->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; |
|
} |
|
|
|
void TerminalPainter::drawBelowText(QPainter &painter, |
|
const QRect &rect, |
|
Character *style, |
|
int startX, |
|
int width, |
|
int fontWidth, |
|
const QColor *colorTable, |
|
const bool invertedRendition, |
|
int *vis2line, |
|
int *line2log, |
|
bool bidiEnabled) |
|
{ |
|
// setup painter |
|
|
|
bool first = true; |
|
QRect constRect(0, 0, 0, 0); |
|
QColor backgroundColor; |
|
QColor foregroundColor; |
|
bool drawBG = false; |
|
int lastX = 0; |
|
|
|
for (int i = 0;; i++) { |
|
int x; |
|
if (bidiEnabled && i < width) { |
|
x = line2log[vis2line[i + startX]]; |
|
} else { |
|
x = i + startX; |
|
} |
|
|
|
if (first || i == width || style[x].rendition.all != style[lastX].rendition.all || style[x].foregroundColor != style[lastX].foregroundColor |
|
|| style[x].backgroundColor != style[lastX].backgroundColor) { |
|
if (first) { |
|
first = false; |
|
} else { |
|
if (drawBG) { |
|
painter.fillRect(constRect, backgroundColor); |
|
} |
|
} |
|
if (i == width) { |
|
return; |
|
} |
|
// Sets the text selection colors, either: |
|
// - using reverseRendition(), which inverts the foreground/background |
|
// colors OR |
|
// - blending the foreground/background colors |
|
if (style[x].rendition.f.selected && invertedRendition) { |
|
backgroundColor = style[x].foregroundColor.color(colorTable); |
|
foregroundColor = style[x].backgroundColor.color(colorTable); |
|
} else { |
|
foregroundColor = style[x].foregroundColor.color(colorTable); |
|
backgroundColor = style[x].backgroundColor.color(colorTable); |
|
} |
|
|
|
if (style[x].rendition.f.selected) { |
|
if (!invertedRendition) { |
|
backgroundColor = calculateBackgroundColor(style[x], colorTable).value_or(foregroundColor); |
|
if (backgroundColor == foregroundColor) { |
|
foregroundColor = style[x].backgroundColor.color(colorTable); |
|
} |
|
} |
|
} |
|
drawBG = backgroundColor != colorTable[DEFAULT_BACK_COLOR]; |
|
if (style[x].rendition.f.transparent) { |
|
drawBG = false; |
|
} |
|
constRect = QRect(rect.x() + fontWidth * i, rect.y(), fontWidth, rect.height()); |
|
} else { |
|
constRect.setWidth(constRect.width() + fontWidth); |
|
} |
|
lastX = x; |
|
} |
|
} |
|
|
|
void TerminalPainter::drawAboveText(QPainter &painter, |
|
const QRect &rect, |
|
Character *style, |
|
int startX, |
|
int width, |
|
int fontWidth, |
|
const QColor *colorTable, |
|
const bool invertedRendition, |
|
int *vis2line, |
|
int *line2log, |
|
bool bidiEnabled, |
|
CharacterColor const *ulColorTable) |
|
{ |
|
bool first = true; |
|
QRect constRect(0, 0, 0, 0); |
|
QColor backgroundColor; |
|
QColor foregroundColor; |
|
int lastX = 0; |
|
int startUnderline = -1; |
|
int startOverline = -1; |
|
int startStrikeOut = -1; |
|
|
|
for (int i = 0;; i++) { |
|
int x; |
|
if (bidiEnabled && i < width) { |
|
x = line2log[vis2line[i + startX]]; |
|
} else { |
|
x = i + startX; |
|
} |
|
|
|
if (first || i == width || ((style[x].rendition.all ^ style[lastX].rendition.all) & RE_MASK_ABOVE) |
|
|| ((style[x].flags ^ style[lastX].flags) & EF_UNDERLINE_COLOR) || style[x].foregroundColor != style[lastX].foregroundColor |
|
|| style[x].backgroundColor != style[lastX].backgroundColor) { |
|
if (first) { |
|
first = false; |
|
} else { |
|
if ((i == width || style[x].rendition.f.strikeout == 0) && startStrikeOut >= 0) { |
|
QPen pen(foregroundColor); |
|
int y = rect.y() + m_parentDisplay->terminalFont()->fontAscent(); |
|
pen.setWidth(m_parentDisplay->terminalFont()->lineWidth()); |
|
painter.setPen(pen); |
|
painter.drawLine(rect.x() + fontWidth * startStrikeOut, |
|
y - m_parentDisplay->terminalFont()->strikeOutPos(), |
|
rect.x() + fontWidth * i - 1, |
|
y - m_parentDisplay->terminalFont()->strikeOutPos()); |
|
startStrikeOut = -1; |
|
} |
|
if ((i == width || style[x].rendition.f.overline == 0) && startOverline >= 0) { |
|
QPen pen(foregroundColor); |
|
int y = rect.y() + m_parentDisplay->terminalFont()->fontAscent(); |
|
pen.setWidth(m_parentDisplay->terminalFont()->lineWidth()); |
|
painter.setPen(pen); |
|
painter.drawLine(rect.x() + fontWidth * startOverline, |
|
y - m_parentDisplay->terminalFont()->overlinePos(), |
|
rect.x() + fontWidth * i - 1, |
|
y - m_parentDisplay->terminalFont()->overlinePos()); |
|
startOverline = -1; |
|
} |
|
int underline = style[lastX].rendition.f.underline; |
|
if ((i == width || style[x].rendition.f.underline != underline || ((style[x].flags ^ style[lastX].flags) & EF_UNDERLINE_COLOR)) |
|
&& startUnderline >= 0) { |
|
QPen pen(foregroundColor); |
|
if (ulColorTable != nullptr && (style[lastX].flags & EF_UNDERLINE_COLOR) != 0) { |
|
pen.setColor(ulColorTable[((style[lastX].flags & EF_UNDERLINE_COLOR)) / EF_UNDERLINE_COLOR_1 - 1].color(colorTable)); |
|
} |
|
int y = rect.y() + m_parentDisplay->terminalFont()->fontAscent() + m_parentDisplay->terminalFont()->underlinePos() |
|
+ m_parentDisplay->terminalFont()->lineWidth(); |
|
int lw = m_parentDisplay->terminalFont()->lineWidth(); |
|
if (underline == RE_UNDERLINE_DOUBLE || underline == RE_UNDERLINE_CURL) { |
|
y = rect.bottom() - 1; |
|
lw = 1; |
|
} else { |
|
if (lw + lw + m_parentDisplay->terminalFont()->fontAscent() + m_parentDisplay->terminalFont()->underlinePos() > rect.height()) { |
|
lw = rect.height() - m_parentDisplay->terminalFont()->fontAscent() - m_parentDisplay->terminalFont()->underlinePos() - lw; |
|
} |
|
} |
|
pen.setWidth(lw); |
|
if (underline == RE_UNDERLINE_DOT) { |
|
pen.setStyle(Qt::DotLine); |
|
} else if (underline == RE_UNDERLINE_DASH) { |
|
pen.setStyle(Qt::DashLine); |
|
} |
|
if (underline == RE_UNDERLINE_CURL) { |
|
QVector<qreal> dashes(2, 2); |
|
pen.setDashPattern(dashes); |
|
} |
|
painter.setPen(pen); |
|
painter.drawLine(rect.x() + fontWidth * startUnderline, y, rect.x() + fontWidth * i - 1, y); |
|
if (underline == RE_UNDERLINE_DOUBLE) { |
|
painter.drawLine(rect.x() + fontWidth * startUnderline, y - 2, rect.x() + fontWidth * i - 1, y - 2); |
|
} |
|
if (underline == RE_UNDERLINE_CURL) { |
|
painter.drawLine(rect.x() + fontWidth * startUnderline + 2, y - 1, rect.x() + fontWidth * i - 1, y - 1); |
|
} |
|
|
|
startUnderline = -1; |
|
} |
|
} |
|
if (i == width) { |
|
return; |
|
} |
|
// Sets the text selection colors, either: |
|
// - using reverseRendition(), which inverts the foreground/background |
|
// colors OR |
|
// - blending the foreground/background colors |
|
if (style[x].rendition.f.selected && invertedRendition) { |
|
backgroundColor = style[x].foregroundColor.color(colorTable); |
|
foregroundColor = style[x].backgroundColor.color(colorTable); |
|
} else { |
|
foregroundColor = style[x].foregroundColor.color(colorTable); |
|
backgroundColor = style[x].backgroundColor.color(colorTable); |
|
} |
|
|
|
if (style[x].rendition.f.selected) { |
|
if (!invertedRendition) { |
|
backgroundColor = calculateBackgroundColor(style[x], colorTable).value_or(foregroundColor); |
|
if (backgroundColor == foregroundColor) { |
|
foregroundColor = style[x].backgroundColor.color(colorTable); |
|
} |
|
} |
|
} |
|
if (style[x].rendition.f.strikeout && startStrikeOut == -1) { |
|
startStrikeOut = i; |
|
} |
|
if (style[x].rendition.f.overline && startOverline == -1) { |
|
startOverline = i; |
|
} |
|
if (style[x].rendition.f.underline && startUnderline == -1) { |
|
startUnderline = i; |
|
} |
|
lastX = x; |
|
} |
|
} |
|
} |
|
|
|
void TerminalPainter::drawImagesBelowText(QPainter &painter, const QRect &rect, int fontWidth, int fontHeight, int &placementIdx) |
|
{ |
|
Screen *screen = m_parentDisplay->screenWindow()->screen(); |
|
|
|
placementIdx = 0; |
|
qreal opacity = painter.opacity(); |
|
int scrollDelta = m_parentDisplay->terminalFont()->fontHeight() * (m_parentDisplay->screenWindow()->currentLine() - screen->getHistLines()); |
|
const bool origClipping = painter.hasClipping(); |
|
const auto origClipRegion = painter.clipRegion(); |
|
if (screen->hasGraphics()) { |
|
painter.setClipRect(rect); |
|
while (1) { |
|
TerminalGraphicsPlacement_t *p = screen->getGraphicsPlacement(placementIdx); |
|
if (!p || p->z >= 0) { |
|
break; |
|
} |
|
int x = p->col * fontWidth + p->X + m_parentDisplay->contentRect().left(); |
|
int y = p->row * fontHeight + p->Y + m_parentDisplay->contentRect().top(); |
|
QRectF srcRect(0, 0, p->pixmap.width(), p->pixmap.height()); |
|
QRectF dstRect(x, y - scrollDelta, p->pixmap.width(), p->pixmap.height()); |
|
painter.setOpacity(p->opacity); |
|
painter.drawPixmap(dstRect, p->pixmap, srcRect); |
|
placementIdx++; |
|
} |
|
painter.setOpacity(opacity); |
|
painter.setClipRegion(origClipRegion); |
|
painter.setClipping(origClipping); |
|
} |
|
} |
|
|
|
void TerminalPainter::drawImagesAboveText(QPainter &painter, const QRect &rect, int fontWidth, int fontHeight, int &placementIdx) |
|
{ |
|
// setup painter |
|
Screen *screen = m_parentDisplay->screenWindow()->screen(); |
|
|
|
qreal opacity = painter.opacity(); |
|
int scrollDelta = fontHeight * (m_parentDisplay->screenWindow()->currentLine() - screen->getHistLines()); |
|
const bool origClipping = painter.hasClipping(); |
|
const auto origClipRegion = painter.clipRegion(); |
|
|
|
if (screen->hasGraphics()) { |
|
painter.setClipRect(rect); |
|
while (1) { |
|
TerminalGraphicsPlacement_t *p = screen->getGraphicsPlacement(placementIdx); |
|
if (!p) { |
|
break; |
|
} |
|
QPixmap image = p->pixmap; |
|
int x = p->col * fontWidth + p->X + m_parentDisplay->contentRect().left(); |
|
int y = p->row * fontHeight + p->Y + m_parentDisplay->contentRect().top(); |
|
QRectF srcRect(0, 0, image.width(), image.height()); |
|
QRectF dstRect(x, y - scrollDelta, image.width(), image.height()); |
|
painter.setOpacity(p->opacity); |
|
painter.drawPixmap(dstRect, image, srcRect); |
|
placementIdx++; |
|
} |
|
painter.setOpacity(opacity); |
|
painter.setClipRegion(origClipRegion); |
|
painter.setClipping(origClipping); |
|
} |
|
} |
|
|
|
void TerminalPainter::drawTextCharacters(QPainter &painter, |
|
const QRect &rect, |
|
const QString &text, |
|
Character style, |
|
const QColor *colorTable, |
|
const bool invertedRendition, |
|
const LineProperty lineProperty, |
|
bool printerFriendly, |
|
RenditionFlags &oldRendition, |
|
QColor oldColor, |
|
int normalWeight, |
|
QFont::Weight boldWeight) |
|
{ |
|
// setup painter |
|
if (style.rendition.f.conceal != 0) { |
|
return; |
|
} |
|
QColor characterColor; |
|
if (!printerFriendly) { |
|
// Sets the text selection colors, either: |
|
// - invertedRendition, which inverts the foreground/background colors OR |
|
// - blending the foreground/background colors |
|
if (m_parentDisplay->textBlinking() && (style.rendition.f.blink != 0)) { |
|
return; |
|
} |
|
|
|
if (style.rendition.f.selected) { |
|
if (invertedRendition) { |
|
reverseRendition(style); |
|
} |
|
} |
|
|
|
QColor foregroundColor = style.foregroundColor.color(colorTable); |
|
QColor backgroundColor = style.backgroundColor.color(colorTable); |
|
|
|
if (style.rendition.f.selected) { |
|
if (!invertedRendition) { |
|
backgroundColor = calculateBackgroundColor(style, colorTable).value_or(foregroundColor); |
|
if (backgroundColor == foregroundColor) { |
|
foregroundColor = style.backgroundColor.color(colorTable); |
|
} |
|
} |
|
} |
|
|
|
characterColor = foregroundColor; |
|
if (style.rendition.f.cursor != 0) { |
|
drawCursor(painter, rect, foregroundColor, backgroundColor, characterColor); |
|
} |
|
if (m_parentDisplay->filterChain()->showUrlHint()) { |
|
if ((style.flags & EF_REPL) == EF_REPL_PROMPT) { |
|
int h, s, v; |
|
characterColor.getHsv(&h, &s, &v); |
|
s = s / 2; |
|
v = v / 2; |
|
characterColor.setHsv(h, s, v); |
|
} |
|
if ((style.flags & EF_REPL) == EF_REPL_INPUT) { |
|
int h, s, v; |
|
characterColor.getHsv(&h, &s, &v); |
|
s = (511 + s) / 3; |
|
v = (511 + v) / 3; |
|
characterColor.setHsv(h, s, v); |
|
} |
|
} |
|
} else { |
|
characterColor = QColor(0, 0, 0); |
|
} |
|
|
|
// The weight used as bold depends on selected font's weight. |
|
// "Regular" will use "Bold", but e.g. "Thin" will use "Light". |
|
// Note that QFont::weight/setWeight() returns/takes an int in Qt5, |
|
// and a QFont::Weight in Qt6 |
|
QFont savedFont; |
|
bool restoreFont = false; |
|
if ((style.flags & EF_EMOJI_REPRESENTATION) && m_parentDisplay->terminalFont()->hasExtraFont(0)) { |
|
savedFont = painter.font(); |
|
restoreFont = true; |
|
painter.setFont(m_parentDisplay->terminalFont()->getExtraFont(0)); |
|
} else { |
|
if (oldRendition != style.rendition.all) { |
|
const bool useBold = ((style.rendition.f.bold != 0) && m_parentDisplay->terminalFont()->boldIntense()); |
|
const bool useItalic = (style.rendition.f.italic != 0) || m_parentDisplay->font().italic(); |
|
|
|
QFont currentFont = painter.font(); |
|
|
|
const bool isCurrentBold = currentFont.weight() >= boldWeight; |
|
if (isCurrentBold != useBold || currentFont.italic() != useItalic) { |
|
currentFont.setWeight(useBold ? boldWeight : normalWeight); |
|
currentFont.setItalic(useItalic); |
|
painter.setFont(currentFont); |
|
} |
|
} |
|
} |
|
|
|
if (characterColor != oldColor) { |
|
QPen pen = painter.pen(); |
|
if (pen.color() != characterColor) { |
|
painter.setPen(characterColor); |
|
} |
|
} |
|
// const bool origClipping = painter.hasClipping(); |
|
// const auto origClipRegion = painter.clipRegion(); |
|
// painter.setClipRect(rect); |
|
// draw text |
|
if (isLineCharString(text) && !m_parentDisplay->terminalFont()->useFontLineCharacters()) { |
|
int y = rect.y(); |
|
|
|
if (lineProperty & LINE_DOUBLEHEIGHT_BOTTOM) { |
|
y -= m_parentDisplay->terminalFont()->fontHeight() / 2; |
|
} |
|
|
|
drawLineCharString(m_parentDisplay, painter, rect.x(), y, text, style); |
|
} else { |
|
int y = rect.y() + m_parentDisplay->terminalFont()->fontAscent(); |
|
|
|
if (lineProperty & LINE_DOUBLEHEIGHT_BOTTOM) { |
|
y -= m_parentDisplay->terminalFont()->fontHeight() / 2; |
|
} else { |
|
// We shift half way down here to center |
|
y += m_parentDisplay->terminalFont()->lineSpacing() / 2; |
|
} |
|
painter.drawText(rect.x(), y, text); |
|
if (0 && text.toUcs4().length() >= 1) { |
|
fprintf(stderr, " %i ", text.toUcs4().length()); |
|
for (int i = 0; i < text.toUcs4().length(); i++) { |
|
fprintf(stderr, " %04x ", text.toUcs4()[i]); |
|
} |
|
fprintf(stderr, "\n"); |
|
} |
|
} |
|
if (restoreFont) { |
|
painter.setFont(savedFont); |
|
} |
|
} |
|
}
|
|
|