Skip to content

Commit

Permalink
TODO WIP: Add Font Ligature Support
Browse files Browse the repository at this point in the history
This work would not have been done without @Nucearo 's efforts:
	equalsraf#470

Uses QTextLayout to render segments of similarly styled cells. QTextLayout
provides greater control over Qt's font-rendering engine via QGlyphRun. With
QGlyphRun, we are able to identify individual characters and their positions.

QGlyphRun should provide a means of correcting the spacing for non-perfect
monospace fonts. It also allows for identification of when a character
sequence is converted to a glyph.
  • Loading branch information
jgehrig committed Jul 29, 2020
1 parent 34e6ce6 commit 85e912e
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 49 deletions.
262 changes: 214 additions & 48 deletions src/gui/shellwidget/shellwidget.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#include "shellwidget.h"

#include <QDebug>
#include <QPainter>
#include <QPainterPath>
#include <QPaintEvent>
#include <QDebug>
#include "shellwidget.h"
#include "helpers.h"
#include <QTextLayout>

ShellWidget::ShellWidget(QWidget* parent)
: QWidget(parent)
Expand Down Expand Up @@ -331,9 +332,10 @@ void ShellWidget::paintForegroundCellText(
QPainter& p,
const Cell& cell,
QRect cellRect,
bool isCursorCell) noexcept
const QString& text,
QVariant cursorPos) noexcept
{
if (cell.GetCharacter() == ' ') {
if (text == " ") {
return;
}

Expand All @@ -344,15 +346,93 @@ void ShellWidget::paintForegroundCellText(
p.setPen(fgColor);
p.setFont(GetCellFont(cell));

if (isLigatureModeEnabled())
{
// Draw chars at the baseline
// FIXME Resolve cellTextOffset with -1.0F, probably a better way to do this...
const int cellTextOffset{ m_ascent + (m_lineSpace / 2) };
const QPoint pos{ cellRect.left(), cellRect.top() + cellTextOffset };

QTextLayout textLayout{ text, GetCellFont(cell), p.device() };
textLayout.setCacheEnabled(true);
textLayout.beginLayout();
QTextLine line = textLayout.createLine();
if (!line.isValid()) {
return;
}
line.setNumColumns(text.length());
QFontMetrics metrics{ GetCellFont(cell) };
line.setPosition(QPointF{ 0, -1.0F*(metrics.height() - metrics.descent()) });
textLayout.endLayout();

if (!cursorPos.isValid() || !cursorPos.canConvert<int>()) {
textLayout.draw(&p, pos);
return;
}

// FIXME cursorPos? varCursorPos? this cursorPos?
// Bad names...
int cursorIndex{ cursorPos.toInt() };

auto glyphRunList = textLayout.glyphRuns();

QPainterPath glyphPath;
// FIXME assumes only 1 glyphRun... not acurate. substr text instead
if (glyphRunList.size() != 1) {
qDebug() << "Hitting Lazy assumption... Ignore for now!";
return;
}

auto& glyphRun = glyphRunList[0];
if (glyphRun.glyphIndexes().size() != text.size()) {
qDebug() << "Hitting Lazy assumption 2... Ignore for now!";
return;
}

auto glyphIndexList{ glyphRun.glyphIndexes() };
auto glyphIndexListNoLigatures{ glyphRun.rawFont().glyphIndexesForString(text) };

if (glyphIndexList.at(cursorIndex) != glyphIndexListNoLigatures.at(cursorIndex)) {
for(int i=cursorIndex; i>=0; i--) {
if (glyphIndexList.at(i) == glyphIndexListNoLigatures.at(i)) {
break;
}

glyphIndexList.data()[i] = glyphIndexListNoLigatures[i];
}

for(int i=cursorIndex+1; i<glyphIndexList.size(); i++) {
if (glyphIndexList.at(i) == glyphIndexListNoLigatures.at(i)) {
break;
}

glyphIndexList.data()[i] = glyphIndexListNoLigatures[i];
}

glyphRun.setGlyphIndexes(glyphIndexList);
}

p.drawGlyphRun(pos, glyphRun);

// FIXME Double byte?
QRect cursorCellRect{ absoluteShellRect(m_cursor_pos.y(),m_cursor_pos.x(),1,1) };

paintNeovimCursorBackground(p, cursorCellRect);
paintNeovimCursorForeground(
p, cursorCellRect, glyphRun.positions()[cursorIndex].toPoint() + pos,
text.at(cursorIndex));

return;
}

// Draw chars at the baseline
const int cellTextOffset{ m_ascent + (m_lineSpace / 2) };
const QPoint pos{ cellRect.left(), cellRect.top() + cellTextOffset};
const uint character{ cell.GetCharacter() };
const QString text{ QString::fromUcs4(&character, 1) };

p.drawText(pos, text);

if (isCursorCell) {
// FIXME previously a bool isCursorCell... This abstraction isn't great.
if (cursorPos.isValid()) {
paintNeovimCursorForeground(p, cellRect, pos, text);
}
}
Expand All @@ -364,44 +444,11 @@ void ShellWidget::paintEvent(QPaintEvent *ev)
p.setClipping(true);

for(const auto& rect : ev->region().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;
if (isLigatureModeEnabled()) {
paintRectLigatures(p, rect);
}
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 };

paintBackgroundClearCell(p, cell, r, isCursorVisibleAtCell);
paintForegroundCellText(p, cell, r, isCursorVisibleAtCell);
}

paintUnderline(p, cell, r);

paintUndercurl(p, cell, r);
}
else {
paintRectNoLigatures(p, rect);
}
}

Expand All @@ -410,7 +457,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());
}

Expand All @@ -427,6 +474,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 QVariant cursorPos{ isCursorVisibleAtCell ? QPoint{ 0, 0} : QVariant{} };

const uint character{ cell.GetCharacter() };
const QString charText{ QString::fromUcs4(&character, 1) };

paintBackgroundClearCell(p, cell, r, isCursorVisibleAtCell);
paintForegroundCellText(p, cell, r, charText, cursorPos);
}

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) };
int chars = firstCell.IsDoubleWidth() ? 2 : 1;
QRect firstCellRect = absoluteShellRect(i, j, 1, chars);

QString blockText;
QVariant blockCursorPos;

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 nextCharacter{ checkCell.GetCharacter() };
blockText += QString::fromUcs4(&nextCharacter, 1);

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() };

QVariant optionalCursorPos{
(m_cursor.IsVisible()) ? blockCursorPos : QVariant{} };

paintBackgroundClearCell(p, firstCell, blockRect, false);
paintForegroundCellText(p, firstCell, blockRect, blockText, optionalCursorPos);
paintUnderline(p, firstCell, blockRect);
paintUndercurl(p, firstCell, blockRect);
}
}
}

void ShellWidget::resizeEvent(QResizeEvent *ev)
{
int cols = ev->size().width() / m_cellSize.width();
Expand Down Expand Up @@ -545,8 +702,17 @@ 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()) {
// FIXME The following doesn't work.
// Ex) Insert !=, press Esc, cursor moves to !=, first char doesn't re-paint. Not sure why?
//
//update(absoluteShellRect(row, 0, 1, m_contents.columns()));
update();
}
else {
QRect rect = absoluteShellRect(row, column, 1, cols_changed);
update(rect);
}
}
return cols_changed;
}
Expand Down
10 changes: 9 additions & 1 deletion src/gui/shellwidget/shellwidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ 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();
Expand All @@ -153,12 +154,19 @@ 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 paintForegroundCellText(
QPainter& p,
const Cell& cell,
QRect cellRect,
const QString& text,
QVariant cursorPos) noexcept;
QFont GetCellFont(const Cell& cell) const noexcept;

ShellContents m_contents{ 0, 0 };
Expand Down

0 comments on commit 85e912e

Please sign in to comment.