Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change printing to add ligature support #470

Closed
wants to merge 1 commit into from

Conversation

Nuclearo
Copy link
Contributor

@Nuclearo Nuclearo commented Oct 1, 2018

Solves #166

I've changed the text drawing function to

  1. Give QPainter::drawText strings of text instead of individual characters to make use of Qt's built in ligature support.
  2. Redraw whole lines instead of just changed cells. This makes sure we're drawing correct ligatures as new text is entered.

The new printing flow is similar to Konsole's. It collects new characters with the same font and colors, then draws that chunk all at once.

Testing:
I tested with code using the Fira Code font:, and some sample Chinese and Korean texts to test double width characters and all seemed to render fine.
image

@@ -294,7 +285,7 @@ int ShellWidget::put(const QString& text, int row, int column,
int cols_changed = m_contents.put(text, row, column, fg, bg, sp,
bold, italic, underline, undercurl);
if (cols_changed > 0) {
QRect rect = absoluteShellRect(row, column, 1, cols_changed);
QRect rect = absoluteShellRect(row, 0, 1, m_contents.columns());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So every update turns into a full line update. Any idea on much slower this will make things?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure. On the one hand it will make single character changes significantly longer but on the other hand, we're now calling one fillRect and one drawText for what could be entire lines, so in some cases it could make performance faster. I'm not sure how to reliably test it, but am willing to if you have a suggestion.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of tests in shellwidgets/test. The ones starting with bench_ should run repeated benchmarks.

For example to run the bench_scroll test you can call

$ ctest -R bench_scroll -VV
UpdateCTestConfiguration  from :/home/raf/Code/neovim-qt/build/DartConfiguration.tcl
UpdateCTestConfiguration  from :/home/raf/Code/neovim-qt/build/DartConfiguration.tcl
Test project /home/raf/Code/neovim-qt/build
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
test 11
    Start 11: bench_scroll

11: Test command: /home/raf/Code/neovim-qt/build/bin/bench_scroll
11: Test timeout computed to be: 10000000
11: ********* Start testing of Test *********
11: Config: Using QtTest library 5.10.1, Qt 5.10.1 (x86_64-little_endian-lp64 shared (dynamic) release build; by GCC 8.2.0)
11: PASS   : Test::initTestCase()
11: PASS   : Test::benchScrollRegion()
11: RESULT : Test::benchScrollRegion():
11:      0.42 msecs per iteration (total: 54, iterations: 128)
11: PASS   : Test::cleanupTestCase()
11: Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 219ms
11: ********* Finished testing of Test *********
1/1 Test #11: bench_scroll .....................   Passed    0.24 sec

The following tests passed:
	bench_scroll

100% tests passed, 0 tests failed out of 1

Which gives the average time for one test execution.

We could try something similar using the ShellWidget class directly. For comparison here what we would want is two tests, one with using ligatures and one without.

If you check test_shellcontents.cpp you'll see that you can load data from a file instead of doing it programaticaly.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this

diff --git a/src/gui/shellwidget/test/CMakeLists.txt b/src/gui/shellwidget/test/CMakeLists.txt
index 92ff991..7fe254b 100644
--- a/src/gui/shellwidget/test/CMakeLists.txt
+++ b/src/gui/shellwidget/test/CMakeLists.txt
@@ -15,3 +15,4 @@ add_xtest(test_shellcontents)
 add_xtest(test_shellwidget)
 add_xtest(bench_scroll)
 add_xtest(bench_cell)
+add_xtest(bench_widgetpaint)
diff --git a/src/gui/shellwidget/test/bench_widgetpaint.cpp b/src/gui/shellwidget/test/bench_widgetpaint.cpp
new file mode 100644
index 0000000..f220450
--- /dev/null
+++ b/src/gui/shellwidget/test/bench_widgetpaint.cpp
@@ -0,0 +1,23 @@
+#include <QtTest/QtTest>
+#include "shellwidget.h"
+
+#if defined(Q_OS_WIN) && defined(USE_STATIC_QT)
+#include <QtPlugin>
+Q_IMPORT_PLUGIN (QWindowsIntegrationPlugin);
+#endif
+
+class Test: public QObject
+{
+       Q_OBJECT
+private slots:
+       void benchPaint() {
+               QBENCHMARK {
+                       auto s = ShellWidget::fromFile("../test/buffer.c");
+                       s->show();
+                       s->repaint();
+               }
+       }
+};
+
+QTEST_MAIN(Test)
+#include "bench_widgetpaint.moc"

In this example I'm using one of the files in the repo which might not have many ligatures.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up on that example

  • that path is probably wrong
  • before forcing a repaint you will likely want to resize

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually has a decent amount of them, but I think this is more about rendering regular code anyway.
Time on master:

13: PASS   : Test::initTestCase()
13: PASS   : Test::benchPaint()
13: RESULT : Test::benchPaint():
13:      3.5 msecs per iteration (total: 56, iterations: 16)
13: PASS   : Test::cleanupTestCase()
13: Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 256ms

Time on ligature branch:

13: PASS   : Test::initTestCase()
13: PASS   : Test::benchPaint()
13: RESULT : Test::benchPaint():
13:      4.6 msecs per iteration (total: 74, iterations: 16)
13: PASS   : Test::cleanupTestCase()
13: Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 230ms

This isn't necessarily representative. I've run both a bunch of times and got higher and lower results with both. In general it would seem the new code has less consistent draw time. I'll add a few more lines to the test and try some more testing after adding the property.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A slightly better test case. Sorry about the formatting mess. The jpg file is just so I can examine what the test ended up doing.

#include <QtTest/QtTest>
#include <QScreen>
#include "shellwidget.h"
#include "helpers.h"

#if defined(Q_OS_WIN) && defined(USE_STATIC_QT)
#include <QtPlugin>
Q_IMPORT_PLUGIN (QWindowsIntegrationPlugin);
#endif

class Test: public QObject
{
       Q_OBJECT
		ShellWidget *s;
private slots:
		void init() {
			// FIXME: path
			QFileInfo fi("../../../../../src/gui/shellwidget/test/shellcontents/shell2.txt");
			s = ShellWidget::fromFile(fi.absoluteFilePath());
			s->resizeShell(80, 120);
			s->show();
		}
		void cleanupTestCase() {
			// save a image of the widget after the last test
			s->grab().save("widgetpaint.png");
		}
       void benchPaint() {
               QBENCHMARK {
				   s->repaint();
               }
       }
};

QTEST_MAIN(Test)
#include "bench_widgetpaint.moc"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I've included a working benchmark with fixes for the path here 736f57a

@equalsraf
Copy link
Owner

We should also check what happens when fonts are not monospace. I am very tempted to put this behind some kind of feature flag/option, I suspect the performance impact will be significant.

@Nuclearo
Copy link
Contributor Author

Nuclearo commented Oct 2, 2018

If the font is not monospace this will most likely be problematic. I assumed those weren't supported. A configurable flag sounds reasonable. If it's off we'll only update the changed sections as in the past and the loop will always stop after one step, otherwise we keep the new flow?

@equalsraf
Copy link
Owner

If the font is not monospace this will most likely be problematic. I assumed those weren't supported.

We kind of do, via :Guifont! fontname. On one hand some people really insist on it, on another some fonts are incorrectly detected as not being monospace, so the user needed a way to force it.

@Nuclearo
Copy link
Contributor Author

Nuclearo commented Oct 2, 2018

I could make the property be

  • default = on iff font is detected as monospace
  • on = print with ligatures even if it could come out broken
  • off = print the old way

Where would this flag go?

@equalsraf
Copy link
Owner

I could make the property be

Lets start with off by default.

The actual setting can be stored in the ShellWidget class. We would need a way for users change this at run time. Similar to what is done with setGuiFont.

@equalsraf
Copy link
Owner

I'm no exactly sure what is its expected behaviour with this particular font

Fira Mono

Nothing happens really.

firamono

Fira Code

Ligatures work, but some cases dont work exactly as expected.

firacode

@Nuclearo
Copy link
Contributor Author

Nuclearo commented Oct 4, 2018

Fira Mono doesn't have ligatures afaik. They're a font feature added by Fira Code.
I get that same behavior in Konsole and nvim-gtk. It seems to be related to Fira Code itself, since using FuraCode Nerd Font it comes as many single arrows.

@equalsraf
Copy link
Owner

Ok at least its consistent. If you get a feature flag going I'm ok with merging this.

@@ -6,6 +6,8 @@ endif ()

function(add_xtest SOURCE_NAME)
add_executable(${SOURCE_NAME} ${SOURCE_NAME}.cpp)
file(TO_NATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR} TEST_SOURCE_DIR)
add_compile_definitions(${SOURCE_NAME} TEST_SOURCE_DIR="${TEST_SOURCE_DIR}")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems add_compile_definitions does not work with older versions of cmake. This is why the tests are failing.

I think (untested) the alternative is to use add_definitions(${SOURCE_NAME} -DTEST_SOURCE_DIR="${TEST_SOURCE_DIR}")

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's that, but also bench_widgetpaint segfaults on win64.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That add_definitions line failed on linux. The following works fine, outside of the function:
add_definitions(-DTEST_SOURCE_DIR="${TEST_SOURCE_DIR}")
Since add_definitions only affects the directory and its subdirs I think that's fine.

@@ -6,12 +6,16 @@ endif ()

function(add_xtest SOURCE_NAME)
add_executable(${SOURCE_NAME} ${SOURCE_NAME}.cpp)
file(TO_NATIVE_PATH ${CMAKE_CURRENT_SOURCE_DIR} TEST_SOURCE_DIR)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this line out of this function, before the add_definition.

The segfault happens because the test fails to open the file. Since TEST_SOURCE_DIR is no defined.

@Nuclearo
Copy link
Contributor Author

TO_NATIVE_PATH results in malformed path strings in VS (the backslashes aren't escaped). Changing it to TO_CMAKE_PATH did the trick.

@Nuclearo
Copy link
Contributor Author

@equalsraf, the last commit I pushed added an RPC option to enable ligature rendering via
:call rpcnotify(0, 'Gui', 'Option', 'RenderLigatures', 1)
Is that alright as a feature flag?

} else {
pen.setColor(m_fgColor);
// end_row is inclusive
QChar *rowStr = new QChar[m_contents.columns()];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two nitpicks with how this is done here

  1. I would really like to avoid the rowStr copy when ligaturemode is off
  2. Part of this loop is used to fill rowStr from cell contents. Maybe we can just add a method ShellContents::getRow(n) that allocates the array(s) and fills it up return all the pieces that need to be draw.

@equalsraf
Copy link
Owner

Sorry @Nuclearo I dropped out the face of the earth. The option looks fine. I've left a comment about the painting.

@Nuclearo
Copy link
Contributor Author

Nuclearo commented Feb 3, 2019

No problem, I'm looking into what can be done about the copying.

@vasalf
Copy link

vasalf commented May 30, 2019

That's exactly the patch I've been looking for. Now I use nvim-qt version with ligature support locally. Hope it gets merged soon enough!

@x1314aq
Copy link

x1314aq commented Sep 2, 2019

any update?

@Nuclearo
Copy link
Contributor Author

Sorry about that first one, some git mess ups on my end.
I've fixed the merge issues, but I've found an issue with printing in my version which was either fixed or never present in master.
image
Top is master, bottom is mine. Note how the three lines character (right of 100%) is clipped.

@Nuclearo
Copy link
Contributor Author

I think this is done, circleci failure seems to be at the server...
Let me know if there's anything stopping this from being merged.

@stavlocker
Copy link

I'd love to see this feature! Any updates on this?

@0xADD1E
Copy link

0xADD1E commented Oct 28, 2019

@Nuclearo it looks like the build failure was stuff with the base image that got fixed with b25f0c6. It looks like a rebase to that commit or newer should resolve the circleci failure

I'm also super excited to see this happening. Thank you for your work on this

@Nuclearo
Copy link
Contributor Author

Nuclearo commented Nov 4, 2019

and there we go, squashed because it was getting too full of merge commits and passing tests nicely.

@pitbuster
Copy link

I noticed that undercurl (like the one in spell checking) seems to be broken with this PR 😔

@Nuclearo
Copy link
Contributor Author

Nuclearo commented Nov 7, 2019

@hellbuster Can you write a test or give me a config that causes this issue?

@pitbuster
Copy link

@Nuclearo I could reproduce with -u NONE and I am not sure how to write a test for this, but I could try to run in a debugger to investigate what is going on.

@pitbuster
Copy link

pitbuster commented Nov 12, 2019

I tried to get something useful by running through gdb. When I do :set spell, the program goes to the code that is supposed to draw the undercurl, so I don't have ideas on where the problem is 😞

@Nuclearo
Copy link
Contributor Author

Yeah, I moved the background painting outside of the loop as a bit of an optimization and it backfired, silly me... This new patch should fix it. Thanks again for testing, @hellbuster!

@skyegecko
Copy link

What's the status of this PR right now? It's been a while since I did C++ but the merge conflicts seem non-trivial.

@pianocomposer321
Copy link

Any particular reason this hasn't been merged yet? I seem to remember @equalsraf saying he was ok with merging.

@pianocomposer321
Copy link

BTW @dutchgecko, FVim is a frontend for neovim written in F# + Avalonia, and it supports ligatures. The latest three versions are broken on windows though, so you're gonna want v0.2-192-gb6a3318 if you're on windows. See yatli/fvim#132 and yatli/fvim#132 (comment).

@Nuclearo
Copy link
Contributor Author

Nuclearo commented May 2, 2020

I've been trying to keep the PR mergeable for a while, but refactoring in master makes these merges progressively more difficult. After all, this changes the entire flow of paintEvent, including indentations. At this point I might even prefer to just rewrite it from scratch, implementing a couple of improvements that I left for after it was merged.

@famiu
Copy link

famiu commented Jun 15, 2020

I'd like this to be merged soon, only thing stopping me from using neovim-qt is lack of ligature support.

@jgehrig
Copy link
Collaborator

jgehrig commented Jun 21, 2020

This feature is on my radar... We should support ligatures. Clearly there is enough user demand to justify the development work! 😁

Sorry for all of the merge conflicts, I am (mostly) the one responsible.

My plan is to modularize various rendering tasks into functions. This should at the least make re-writing the paintEvent code less work and easier to understand/follow.

I would also like to do some research on the QT Font Engine to see if ligatures can be pre-detected. The current rendering scheme would break certain non-monospace fonts and features (guifontwide). Not a big problem for now, since the change is gated in ginit.vim...

@Nuclearo

This is your feature, let me know how you would like to handle the work.

I am happy to do any of the following:

  1. All of the re-base work, giving you commit authorship/credit. I will handle everything.
  2. Do all of the refactor work, and leave this PR to you.
  3. Nothing, let you handle everything.

Thanks!

@jgehrig jgehrig mentioned this pull request Jul 14, 2020
@xihnik
Copy link

xihnik commented Jul 15, 2020

Ligatures
I may be blind, but this article did not help me solve the problem. In general, there is a solution at the moment, and if so, then how to make it work?

@diefans
Copy link
Contributor

diefans commented Jul 15, 2020

@RealXIHNIK

This branch has conflicts that must be resolved

jgehrig pushed a commit to jgehrig/neovim-qt that referenced this pull request Jul 29, 2020
Adds support for a :GuiRenderLigatures command, which will change the
underlying ShellWidget render logic to paint segments of text with the same
styling. Previously text was rendered character-by-character on the grid,
preventing font ligatures from rendering.

Code re-based from:
equalsraf#470
jgehrig added a commit to jgehrig/neovim-qt that referenced this pull request Jul 29, 2020
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.
@Nuclearo
Copy link
Contributor Author

Nuclearo commented Aug 2, 2020

Hi everyone, sorry for being so late, lost access to my account for a while.
Either way, it's great to see that the open source community always goes ahead with progressing. Thank you for picking up my slack @jgehrig :)
Since there's a new PR for this, should I close this one?

jgehrig added a commit to jgehrig/neovim-qt that referenced this pull request Aug 25, 2020
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.
jgehrig pushed a commit to jgehrig/neovim-qt that referenced this pull request Sep 20, 2020
Adds support for a :GuiRenderLigatures command, which will change the
underlying ShellWidget render logic to paint segments of text with the same
styling. Previously text was rendered character-by-character on the grid,
preventing font ligatures from rendering.

Code re-based from:
equalsraf#470
jgehrig added a commit to jgehrig/neovim-qt that referenced this pull request Sep 20, 2020
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.
jgehrig pushed a commit to jgehrig/neovim-qt that referenced this pull request Sep 28, 2020
Adds support for a :GuiRenderLigatures command, which will change the
underlying ShellWidget render logic to paint segments of text with the same
styling. Previously text was rendered character-by-character on the grid,
preventing font ligatures from rendering.

Code re-based from:
equalsraf#470
jgehrig added a commit to jgehrig/neovim-qt that referenced this pull request Sep 28, 2020
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.
jgehrig added a commit to jgehrig/neovim-qt that referenced this pull request Sep 28, 2020
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.
jgehrig added a commit to jgehrig/neovim-qt that referenced this pull request Sep 28, 2020
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.
jgehrig pushed a commit to jgehrig/neovim-qt that referenced this pull request Sep 29, 2020
Adds support for a :GuiRenderLigatures command, which will change the
underlying ShellWidget render logic to paint segments of text with the same
styling. Previously text was rendered character-by-character on the grid,
preventing font ligatures from rendering.

Code re-based from:
equalsraf#470
jgehrig added a commit to jgehrig/neovim-qt that referenced this pull request Sep 29, 2020
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.
jgehrig pushed a commit that referenced this pull request Sep 29, 2020
Adds support for a :GuiRenderLigatures command, which will change the
underlying ShellWidget render logic to paint segments of text with the same
styling. Previously text was rendered character-by-character on the grid,
preventing font ligatures from rendering.

Code re-based from:
#470
jgehrig added a commit that referenced this pull request Sep 29, 2020
This work would not have been done without @Nucearo 's efforts:
        #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.
@jgehrig
Copy link
Collaborator

jgehrig commented Sep 29, 2020

Thanks @Nuclearo!

Ligatures are now available in master, run :GuiRenderLigatures 1.

The feature should be available for release 0.2.17. Marking as closed.

@jgehrig jgehrig closed this Sep 29, 2020
equalsraf pushed a commit to equalsraf/neovim-gui-shim that referenced this pull request Mar 27, 2021
Adds support for a :GuiRenderLigatures command, which will change the
underlying ShellWidget render logic to paint segments of text with the same
styling. Previously text was rendered character-by-character on the grid,
preventing font ligatures from rendering.

Code re-based from:
equalsraf/neovim-qt#470
equalsraf pushed a commit to equalsraf/neovim-gui-shim that referenced this pull request Mar 27, 2021
Adds support for a :GuiRenderLigatures command, which will change the
underlying ShellWidget render logic to paint segments of text with the same
styling. Previously text was rendered character-by-character on the grid,
preventing font ligatures from rendering.

Code re-based from:
equalsraf/neovim-qt#470
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.