Пройшло півтора року, як я взявся створити у вільний час простий графічний інтерфейс користувача (ІК) до Neovim з допомогою C++. Проєкт nvim-ui став захоплюючою мандрівкою із істотною часткою відкриття, досвіду і висновками, вартими, щоб ними поділитися.

SpyUI у nvim-gdb

Передусім, ідея виникла коли було додано SpyUI для підтримки обслуговування мого розширення до Neovim nvim-gdb. SpyUI дозволяє відображувати і захоплювати зміни ІК під час виконання автоматичних тестів, або й під час справжнього вживання розширення за призначенням, щоб спростити відтворення вад. Не дивно, що сам Neovim активно застосовує клієнт ІК для автоматичного тестування. Скрипт був безсоромно простим, навіваючи, що повноцінний графічний ІК теж мав би бути не складним.

+----------------------------------------+
|                                        |
|~                                       |
|~                                       |
|~                                       |
|[No Name]             0,0-1          All|
|/src/test.cpp:17                        |
|/tmp/nvim-gdb/test/src/test.cpp:17:190:b|
|eg:0x5555555551e6                       |
|(gdb)                                   |
|<port -- gdb -q a.out 21,1           Bot|
|                                        |
|"/tmp/nvim-gdb/test/src/test.cpp"       |
+----------------------------------------+

Істотними підсумком SpyUI була модель даних, яку Neovim держить для представлення стану ІК. Сітка — це матриця текстових клітинок із асоційованими з ними ідентифікаторами підсвічування [[(текст, підсв)] * ширина] * висота. Тож найперша ідея — збирати цю матрицю із повідомлень Neovim і обробляти її далі для якнайкращого показу і продуктивності. Наприклад, сполучити сусідні комірки з однаковим підсв у слова, і пропустити невидимі пропуски взагалі: [[(крок, слово, підсв)]] * висота.

nvim-ui у SDL2

Отже, перша спроба була з SDL2. Певно, це дозволило б досягти назвичайної продуктивності за допомогою апаратного прискорення! Pango і Cairo зі стеку GNOME використовувалися для перетворення тексту в растр, а растр перетворювався на текстуру. Підготовлені текстури можна використовувати повторно кілька разів, якщо цей конкретний фрагмент тексту не змінювався. Ще одним способом зменшити використання процесора було пропускання відображень сітки. Справді, немає потреби показувати більше, ніж 25 кадрів за секунду.

nvim-ui з SDL2

Це працювало пристойно, тож було випущено версію 0.0.1 просто щоб запустити процес. Тоді скоро вийшла версія 0.0.2 з виправленням кількох вад, автоматизацією деяких частин інфрастуктури побудови коду.

Також було виявлено істотний недолік (проблема #20). Назвімо його текстурний шум. І не можна було знайти способу його виправити. Імовірно, це неминуче в процесі відображення з допомогою графічного процесора.

Текстурний шум у SDL2

Портування на GTK 4

Після невеликого дослідження я вирішив спробувати скористатися безпосередньо GTK 4. Передусім тому що Pango і Cairo вже є частиною GTK. Потім інструментарій GTK також дозволив би застосовувати готові елементи графічного ІК для діалогів конфігурування, наприклад, для вибору шрифту. І набагато більше. Але що важливіше, інструментарій радить вживати інвентарні елементи для будь-якого малювання, де це можливо. Отже, те, що було «текстурою» у SDL2, можна тепер легко реалізувати з допомогою мітки GTK! Більше того, стиль підсвічування можна було б зручно задати з допомогою CSS, обійшовшись без програмування низького рівня.

nvim-ui на GTK 4

У цей самий час застосунок отримав меню і можливість під’єднуватися до віддаленого процесу Neovim. Продуктивність була прийнятною зазвичай, крім тих випадків хіба що, коли було багато дрібних міток, які потрібно було створити за раз. Наприклад, коли переглядається список диграфів з допомогою :digraphs, близько потрібно створити і розмістити близько 1500 міток при кожному перегортуванні сторінки. Це спричиняло помітну затримку тривалістю приблизно 0.3 секунди.

Ява Gir2cpp

Ту версію не було випущено відразу, оскільки я усвідомив, що інтерфейс прикладного програмування GTK громіздкий і неприємний. Наприклад, потрібно знати наперед, що клас GtkEntry реалізовує інтерфейс Editable:

auto t = gtk_editable_get_text(GTK_EDITABLE(entry));

замість звичного

auto t = entry.get_text();

Залучення gtkmm здавалося перебором для застосування маленької підмножини елементів, які я збирався вжити. Але одна ідея здалася особливо привабливою: cppgir використовує інтроспекцію GObject щоб згенерувати обгортки C++. На жаль, мені не вдалося зібрати код з її допомогою бажаним чином. Але створити власний генератор обгорток С++ для бібліотек, що базуються на GObject, не здалося чимось надзвичайно складним. Тож ось ще один допоміжний проєкт, який потенційно може отримати окреме застосування: gir2cpp.

Це просто бібліотека мовою Пітон, яку потрібно належним чином сконфігурувати, задати білий і чорний список символів, які потрібно обгорнути у C++. На жаль, мені не вдалося її припасувати в систему збирання Meson без шва. Можливо, тому що наперед невідомо, які саме файли буде згенеровано внаслідок заданої конфігурації. Тож це відбувається як крок попередньої конфігурації перед залученням системи збирання Meson: prepare-gtkpp.sh.

Яким би кустарним спосіб не здавався, код поліпшився істотно:

Texture t{row, Gtk::Label::new_("").g_obj()};
t.label.set_markup(text.c_str());
t.label.set_sensitive(false);
t.label.set_can_focus(false);
t.label.set_focus_on_click(false);
t.label.get_style_context().add_provider(_css_provider.get(), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
_grid.put(t.label, 0, y);

Виправлення проблем із продуктивністю

Дрібні мітки/текстури бездоганні для мінімізації обсягу перемальовування, але проявляються кілька істотних недоліків. Найбільш надокучливий, коли потрібно поновити багато міток одночасно при перегляді повідомлень (дивись :digraphs). Інший — це горизонтальне суміщення окремих текстур, особливо тонких вертикальних ліній в суміжних рядках у телескопі.

asciicast

Тож я спробував спростити відображувач, щоб створював тільки одну велику мітку в одному рядку сітки. Це трохи ускладнює модель даних, але дуже спрощує код: [[(текст, підсв)] * стовпці] * рядки[[(слово, підсв)]] * рядки. Власне, розміщення тексту тепер здійснюється з допомогою розмітки Pango. Це виявилося чудовою ідеєю для зовнішнього вигляду і чутливості ІК, для підтримуваності коду.

nvim-ui з розміткою Pango

Перспектива

Тож ось ми тут після півтора року експериментування. Є проєкт, що триває, ІК Neovim з такими рисами:

  • Простота ідеї і реалізації мової C++
  • Пристойний зовнішній вигляд і продуктивність
  • Міцний фундамент для створення нових функцій
  • Хороша пісочниця для подальших експериментів.

Далі буде.