В 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);

Но самое интересное – это выбор виджетов, который просто невероятно огромный! Помимо стандартных окошек, меню, кнопок и слайдеров, есть различные виды таблиц, модальные окна, вкладки, графики и гистограммы, диалог выбора цвета с поддержкой цветового круга и палитры, многокомпонентные слайдеры для векторных значений, вертикальные слайдеры, поддержка перетаскивания, канвас для рисования произвольных фигур и многое другое.

Код примера из этой статьи можно найти тут.

Written by Gecko

Разработчик компьютерной графики

Оставить комментарий

Ваш адрес email не будет опубликован.