Пример ImGui в Dagon
В Dagon 0.14 появилось расширение для работы с популярным UI-тулкитом ImGui – dagon:imgui. Это у меня уже второй инструмент для создания пользовательских интерфейсов после Nuklear, и во многом ImGui оказался проще и удобнее – хотя, конечно, оба тулкита имеют свои особенности, и нельзя сказать, что один однозначно лучше другого. Но ImGui на сегодняшний день является де-факто стандартом, поэтому его поддержка стала довольно важной вехой в развитии движка. Расширение основано на биндинге BindBC ImGui, модуль bindbc.imgui.ogl (ImGuiOpenGLBackend) оказался полностью совместим с Dagon.
ImGui, как и Nuklear, – это тулкит немедленного режима (immediate mode), это означает, что интерфейс не хранится в виде графа, а формируется в реальном времени вызовами функций, добавляющих виджеты в очередь на отрисовку. Как обычно в таких тулкитах, внешняя C-библиотека отвечает только за обработку событий и формирование очереди – собственно отрисовка примитивов реализуется на стороне приложения штатными средствами вывода графики, в моем случае OpenGL. Основной плюс такого подхода – реактивность из коробки: интерфейс автоматически реагирует на изменение переменных внешним кодом и при этом сам может их менять. Изменяемое состояние для отображения в виджетах вы можете хранить как вам угодно. Подробнее о принципах работы тулкитов немедленного режима на примере Nuklear можно почитать в моей статье на Medium.
Интерфейсы я рекомендую создавать при помощи отдельного класса-контроллера, который слушает события и передает их в ImGui. Объект такого класса лучше создавать в классе, наследующем от Game, чтобы можно было переключать 3D-сцены, и интерфейс не зависел от логики сцен:
import dagon;
import dagon.ext.imgui;
class ImGui: EventListener
{
Application application;
ImGuiContext* igContext;
ImGuiIO* io;
ImFont* font;
this(Application application)
{
super(application.eventManager, application);
this.application = application;
igContext = igCreateContext(null);
igSetCurrentContext(igContext);
io = igGetIO();
io.ConfigFlags |= ImGuiConfigFlags.DockingEnable;
io.ConfigWindowsMoveFromTitleBarOnly = true;
igStyleColorsDark(null);
ImGui_ImplSDL2_InitForOpenGL(application.window, application.glcontext);
ImGuiOpenGLBackend.init();
}
void onProcessEvent(SDL_Event* event)
{
ImGui_ImplSDL2_ProcessEvent(event);
}
bool capturesMouse() @property
{
return io.WantCaptureMouse;
}
bool capturesKeyboard() @property
{
return io.WantCaptureKeyboard;
}
bool showDemoWindow = true;
void update(Time t)
{
processEvents();
ImGuiOpenGLBackend.new_frame();
ImGui_ImplSDL2_NewFrame();
igNewFrame();
if (showDemoWindow)
igShowDemoWindow(&showDemoWindow);
igRender();
}
void render()
{
ImGuiOpenGLBackend.render_draw_data(igGetDrawData());
}
}
class MyGame: Game
{
ImGui ui;
this(uint windowWidth, uint windowHeight, bool fullscreen, string title, string[] args)
{
super(windowWidth, windowHeight, fullscreen, title, args);
currentScene = New!MyScene(this);
ui = New!ImGui(this);
eventManager.onProcessEvent = &ui.onProcessEvent;
}
override void onUpdate(Time t)
{
super.onUpdate(t);
ui.update(t);
currentScene.focused = !ui.capturesMouse;
}
override void onRender()
{
super.onRender();
ui.render();
}
}
Функция igShowDemoWindow используется для демонстрации возможностей тулкита – в реальном приложении вместо нее будут пользовательские вызовы для создания окон и виджетов.
Приятно удивило, что в ImGui не нужно писать склеивающий код для передачи в тулкит событий ввода SDL – это делается при помощи функции ImGui_ImplSDL2_ProcessEvent. Также очень полезными оказались свойства io.WantCaptureMouse и io.WantCaptureKeyboard – я их использую для того, чтобы управление сценой не конфликтовало с мышиными и клавиатурными событиями ImGui. Например, можно блокировать навигацию по сцене, если ImGui сообщает, что мышь захвачена для управления виджетом. Со встроенным компонентом FreeviewComponent это работает следующим образом:
class MyScene: Scene
{
Game game;
FreeviewComponent freeview;
this(Game game)
{
super(game);
this.game = game;
}
// ...
override void onUpdate(Time t)
{
freeview.active = focused;
}
}
У меня сразу возник вопрос, можно ли в ImGui выводить нелатинский текст. Оказалось, можно – путем использования кастомного TTF-шрифта, но работает это по той же схеме, что и в Nuklear. То есть, нужно явным образом указать диапазоны символов Юникода, которые должны загружаться из шрифта. Насколько я понял, возможности загружать символы на лету, в зависимости от того, на каком языке печатает пользователь, тут нет, как и в Nuklear.
ImWchar[] ranges = [
0x0020, 0x00FF, // Basic Latin + Latin Supplement
0x0370, 0x03FF, // Greek
0x0400, 0x044F, // Cyrillic
0
];
font = ImFontAtlas_AddFontFromFileTTF(io.Fonts, "data/font/Roboto.ttf", 16, null, ranges.ptr);
Но самое интересное – это выбор виджетов, который просто невероятно огромный! Помимо стандартных окошек, меню, кнопок и слайдеров, есть различные виды таблиц, модальные окна, вкладки, графики и гистограммы, диалог выбора цвета с поддержкой цветового круга и палитры, многокомпонентные слайдеры для векторных значений, вертикальные слайдеры, поддержка перетаскивания, канвас для рисования произвольных фигур и многое другое.
Код примера из этой статьи можно найти тут.