diff --git a/src/gui/shellwidget/cell.cpp b/src/gui/shellwidget/cell.cpp index e046b255e..9aa22223f 100644 --- a/src/gui/shellwidget/cell.cpp +++ b/src/gui/shellwidget/cell.cpp @@ -23,6 +23,10 @@ bool Cell::IsStyleEquivalent(const Cell& other) const return false; } + if (IsDoubleWidth() != other.IsDoubleWidth()) { + return false; + } + return m_highlight == other.m_highlight; } diff --git a/src/gui/shellwidget/shellwidget.cpp b/src/gui/shellwidget/shellwidget.cpp index 3d7a3998f..7a84b3554 100644 --- a/src/gui/shellwidget/shellwidget.cpp +++ b/src/gui/shellwidget/shellwidget.cpp @@ -1,9 +1,10 @@ +#include "shellwidget.h" + +#include #include #include #include -#include -#include "shellwidget.h" -#include "helpers.h" +#include ShellWidget::ShellWidget(QWidget* parent) : QWidget(parent) @@ -357,51 +358,170 @@ void ShellWidget::paintForegroundCellText( } } -void ShellWidget::paintEvent(QPaintEvent *ev) +static bool AreGlyphPositionsUniform( + const QVector& glyphPositionList, + int cellWidth) noexcept { - QPainter p(this); + if (glyphPositionList.size() <= 1) { + return true; + } - p.setClipping(true); + qreal lastPos{ glyphPositionList[0].x() }; + for (int i=1; iregion().rects()) { - int start_row = rect.top() / m_cellSize.height(); - int end_row = rect.bottom() / m_cellSize.height(); - int start_col = rect.left() / m_cellSize.width(); - int end_col = rect.right() / m_cellSize.width(); - - // Paint margins - if (end_col >= m_contents.columns()) { - end_col = m_contents.columns() - 1; + return true; +} + +static QVector DistributeGlyphPositions( + QVector&& glyphPositionList, + int cellWidth) noexcept +{ + if (glyphPositionList.size() > 1) { + qreal adjustPositionX{ glyphPositionList[0].x() }; + for (auto& glyphPos : glyphPositionList) { + glyphPos.setX(adjustPositionX); + adjustPositionX += cellWidth; } - if (end_row >= m_contents.rows()) { - end_row = m_contents.rows() - 1; + } + + return std::move(glyphPositionList); +} + +static QVector RemoveLigaturesUnderCursor( + const QGlyphRun& glyphRun, + const QString& textGlyphRun, + int cursorGlyphRunPos) noexcept +{ + auto glyphIndexList{ glyphRun.glyphIndexes() }; + auto glyphIndexListNoLigatures{ glyphRun.rawFont().glyphIndexesForString(textGlyphRun) }; + + if (cursorGlyphRunPos < 0 + || cursorGlyphRunPos >= glyphIndexList.size() + || cursorGlyphRunPos >= glyphIndexListNoLigatures.size()) { + qDebug() << "ERROR: Invalid cursorGlyphRunPos!"; + return {}; + } + + if (glyphIndexList.at(cursorGlyphRunPos) != glyphIndexListNoLigatures.at(cursorGlyphRunPos)) { + for(int i=cursorGlyphRunPos; i>=0; i--) { + if (glyphIndexList.at(i) == glyphIndexListNoLigatures.at(i)) { + break; + } + + glyphIndexList.data()[i] = glyphIndexListNoLigatures[i]; } - // end_col/row is inclusive - for (int i=start_row; i<=end_row; i++) { - for (int j=end_col; j>=start_col; j--) { + for(int i=cursorGlyphRunPos+1; i= 0 + && cursorPos < sizeGlyphRun + glyphsRendered + && cursorPos >= glyphsRendered }; + + // When the cursor is NOT within the glyph run, render as-is. + if (!isCursorVisibleInGlyphRun) { + p.drawGlyphRun(pos, glyphRun); + glyphsRendered += sizeGlyphRun; + continue; + } + + // When the cursor IS within the glyph run, decompose individual characters under the cursor. + const int cursorGlyphRunPos { cursorPos - glyphsRendered }; + const QString textGlyphRun{ QStringRef{ &text, glyphsRendered, sizeGlyphRun }.toString() }; + + // Compares a glyph run with and without ligatures. Ligature glyphs are detected as differences + // in these two lists. A non-empty newCursorGlyphList indicates glyph substitution is required. + const auto newCursorGlyphList { RemoveLigaturesUnderCursor(glyphRun, textGlyphRun, cursorGlyphRunPos) }; + if (!newCursorGlyphList.empty()) { + glyphRun.setGlyphIndexes(newCursorGlyphList); + } + + p.drawGlyphRun(pos, glyphRun); + glyphsRendered += sizeGlyphRun; + + const QRect cursorCellRect{ neovimCursorRect() }; + paintNeovimCursorBackground(p, cursorCellRect); + paintNeovimCursorForeground( + p, cursorCellRect, glyphRun.positions()[cursorGlyphRunPos].toPoint() + pos, + textGlyphRun.at(cursorGlyphRunPos)); + } +} + +void ShellWidget::paintEvent(QPaintEvent *ev) +{ + QPainter p(this); + + p.setClipping(true); + + for(const auto& rect : ev->region().rects()) { + if (isLigatureModeEnabled()) { + paintRectLigatures(p, rect); + } + else { + paintRectNoLigatures(p, rect); } } @@ -410,7 +530,7 @@ void ShellWidget::paintEvent(QPaintEvent *ev) QRect shellArea = absoluteShellRect(0, 0, m_contents.rows(), m_contents.columns()); QRegion margins = QRegion(rect()).subtracted(shellArea); - foreach(QRect margin, margins.intersected(ev->region()).rects()) { + for (QRect margin : margins.intersected(ev->region()).rects()) { p.fillRect(margin, background()); } @@ -427,6 +547,116 @@ void ShellWidget::paintEvent(QPaintEvent *ev) #endif } +void ShellWidget::paintRectNoLigatures(QPainter& p, const QRect rect) noexcept +{ + int start_row = rect.top() / m_cellSize.height(); + int end_row = rect.bottom() / m_cellSize.height(); + int start_col = rect.left() / m_cellSize.width(); + int end_col = rect.right() / m_cellSize.width(); + + // Paint margins + if (end_col >= m_contents.columns()) { + end_col = m_contents.columns() - 1; + } + if (end_row >= m_contents.rows()) { + end_row = m_contents.rows() - 1; + } + + // end_col/row is inclusive + for (int i=start_row; i<=end_row; i++) { + for (int j=end_col; j>=start_col; j--) { + + const Cell& cell = m_contents.constValue(i,j); + int chars = cell.IsDoubleWidth() ? 2 : 1; + QRect r = absoluteShellRect(i, j, 1, chars); + QRect ovflw = absoluteShellRect(i, j, 1, chars + 1); + + p.setClipRegion(ovflw); + + // Only paint bg/fg if this is not the second cell of a wide char + if (j <= 0 || !contents().constValue(i, j-1).IsDoubleWidth()) { + + const QPoint curPos{ j, i }; + const bool isCursorVisibleAtCell{ m_cursor.IsVisible() && m_cursor_pos == curPos }; + + const uint character{ cell.GetCharacter() }; + const QString charText{ QString::fromUcs4(&character, 1) }; + + paintBackgroundClearCell(p, cell, r, isCursorVisibleAtCell); + paintForegroundCellText(p, cell, r, isCursorVisibleAtCell); + } + + paintUnderline(p, cell, r); + + paintUndercurl(p, cell, r); + } + } +} + +void ShellWidget::paintRectLigatures(QPainter& p, const QRect rect) noexcept +{ + int start_row = rect.top() / m_cellSize.height(); + int end_row = rect.bottom() / m_cellSize.height(); + int start_col = 0; + int end_col = m_contents.columns() - 1; + + // Paint margins + if (end_col >= m_contents.columns()) { + end_col = m_contents.columns() - 1; + } + if (end_row >= m_contents.rows()) { + end_row = m_contents.rows() - 1; + } + + // end_col/row is inclusive + for (int i=start_row; i<=end_row; i++) { + for (int j=start_col; j<=end_col; j++) { + + const Cell& firstCell{ m_contents.constValue(i,j) }; + const int chars{ firstCell.IsDoubleWidth() ? 2 : 1 }; + const QRect firstCellRect{ absoluteShellRect(i, j, 1, chars) }; + + QString blockText; + int blockCursorPos{ -1 }; + + while (j <= end_col) + { + const Cell& checkCell{ m_contents.constValue(i, j) }; + + if (!firstCell.IsStyleEquivalent(checkCell)) { + j--; + break; + } + + const QPoint checkPos{ j, i }; + if (m_cursor_pos == checkPos) { + blockCursorPos = blockText.size(); + } + + const uint cellCharacter{ checkCell.GetCharacter() }; + blockText += QString::fromUcs4(&cellCharacter, 1); + + if (checkCell.IsDoubleWidth()) { + j++; + } + + j++; + } + + const Cell& lastCell{ m_contents.constValue(i,j) }; + int lastCellChars = lastCell.IsDoubleWidth() ? 2 : 1; + QRect lastCellRect = absoluteShellRect(i, j, 1, lastCellChars); + + QRect blockRect{ firstCellRect.topLeft(), lastCellRect.bottomRight() }; + + paintBackgroundClearCell(p, firstCell, blockRect, false); + paintForegroundTextBlock(p, firstCell, blockRect, blockText, blockCursorPos); + paintUnderline(p, firstCell, blockRect); + paintUndercurl(p, firstCell, blockRect); + } + } +} + void ShellWidget::resizeEvent(QResizeEvent *ev) { int cols = ev->size().width() / m_cellSize.width(); @@ -545,8 +775,13 @@ int ShellWidget::put( { int cols_changed = m_contents.put(text, row, column, hl_attr); if (cols_changed > 0) { - QRect rect = absoluteShellRect(row, column, 1, cols_changed); - update(rect); + if (isLigatureModeEnabled()) { + update(absoluteShellRectRow(row)); + } + else { + QRect rect = absoluteShellRect(row, column, 1, cols_changed); + update(rect); + } } return cols_changed; } @@ -554,7 +789,7 @@ int ShellWidget::put( void ShellWidget::clearRow(int row) { m_contents.clearRow(row); - QRect rect = absoluteShellRect(row, 0, 1, m_contents.columns()); + QRect rect = absoluteShellRectRow(row); update(rect); } void ShellWidget::clearShell(QColor bg) @@ -580,6 +815,7 @@ void ShellWidget::scrollShell(int rows) scroll(0, -rows*m_cellSize.height()); } } + /// Scroll an area, count rows (positive numbers move content up) void ShellWidget::scrollShellRegion(int row0, int row1, int col0, int col1, int rows) @@ -592,15 +828,17 @@ void ShellWidget::scrollShellRegion(int row0, int row1, int col0, } } -/// Convert Area in row/col coordinates into pixel coordinates -/// -/// (row0, col0) is the start position and rowcount/colcount the size -QRect ShellWidget::absoluteShellRect(int row0, int col0, int rowcount, int colcount) +QRect ShellWidget::absoluteShellRect(int row0, int col0, int rowcount, int colcount) const noexcept { return QRect(col0*m_cellSize.width(), row0*m_cellSize.height(), colcount*m_cellSize.width(), rowcount*m_cellSize.height()); } +QRect ShellWidget::absoluteShellRectRow(int row) const noexcept +{ + return absoluteShellRect(row, 0, 1, m_contents.columns()); +} + int ShellWidget::rows() const { return m_contents.rows(); @@ -614,12 +852,25 @@ int ShellWidget::columns() const void ShellWidget::setNeovimCursor(uint64_t row, uint64_t col) noexcept { // Clear the stale cursor - update(neovimCursorRect()); + if (isLigatureModeEnabled()) { + uint64_t oldCursorRow{ static_cast(m_cursor_pos.y()) }; - // Update cursor position, draw at new location + // Ligature mode only requires clear during cursor row changes. + if (row != oldCursorRow) { + update(absoluteShellRectRow(oldCursorRow)); + } + } + else { + update(neovimCursorRect()); + } + + // Update cursor position m_cursor_pos = QPoint(col, row); m_cursor.ResetTimer(); - update(neovimCursorRect()); + + // Draw cursor at new location + update((isLigatureModeEnabled()) ? + absoluteShellRectRow(row) : neovimCursorRect()); } /// The top left corner position (pixel) for the cursor diff --git a/src/gui/shellwidget/shellwidget.h b/src/gui/shellwidget/shellwidget.h index d6d734aab..b699375c3 100644 --- a/src/gui/shellwidget/shellwidget.h +++ b/src/gui/shellwidget/shellwidget.h @@ -130,7 +130,7 @@ public slots: /// The top left corner position (pixel) for the cursor QPoint neovimCursorTopLeft() const noexcept; - /// Get the area filled by the cursor + /// Get the area filled by the cursor FIXME Comment full vs getNeovimCursor rect actual QRect neovimCursorRect() const noexcept; std::vector m_guifontwidelist; @@ -139,10 +139,18 @@ public slots: void setNeovimCursor(uint64_t col, uint64_t row) noexcept; virtual void paintEvent(QPaintEvent *ev) Q_DECL_OVERRIDE; + virtual void resizeEvent(QResizeEvent *ev) Q_DECL_OVERRIDE; void setCellSize(); - QRect absoluteShellRect(int row0, int col0, int rowcount, int colcount); + + /// Converts an area in row/col coordinates into pixel coordinates. + /// + /// (row0, col0) is the start position and rowcount/colcount the size + QRect absoluteShellRect(int row0, int col0, int rowcount, int colcount) const noexcept; + + /// Computes the entire row position and size in pixel coordinates. + QRect absoluteShellRectRow(int row) const noexcept; void setGuiFontList(const std::vector&& fontList) noexcept { @@ -153,12 +161,22 @@ public slots: void setFont(const QFont&); void handleCursorChanged(); QRect getNeovimCursorRect(QRect cellRect) noexcept; + void paintRectLigatures(QPainter& p, QRect rect) noexcept; + void paintRectNoLigatures(QPainter& p, QRect rect) noexcept; void paintNeovimCursorBackground(QPainter& p, QRect cellRect) noexcept; void paintNeovimCursorForeground(QPainter& p, QRect cellRect, QPoint pos, const QString& character) noexcept; void paintUnderline(QPainter& p, const Cell& cell, QRect cellRect) noexcept; void paintUndercurl(QPainter& p, const Cell& cell, QRect cellRect) noexcept; void paintBackgroundClearCell(QPainter& p, const Cell& cell, QRect cellRect, bool isCursorCell) noexcept; void paintForegroundCellText(QPainter& p, const Cell& cell, QRect cellRect, bool isCursorCell) noexcept; + + void paintForegroundTextBlock( + QPainter& p, + const Cell& cell, + QRect blockRect, + const QString& text, + int cursorPos) noexcept; + QFont GetCellFont(const Cell& cell) const noexcept; ShellContents m_contents{ 0, 0 };