Вступление

Это третья статья из цикла статей о создании компактной кросс‑платформенной библиотеки для разработки приложений с графическим интерфейсом на языке C++ — Frenchie. Для тех, кто привык изучать исходный код самостоятельно, репозиторий с исходным кодом библиотеки можно найти по ссылке. В данном материале я продолжу рассказывать про ту часть библиотеки, которая управляет состоянием контекстного окна и отслеживает связанные с ним события.

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

Что осталось доделать в platform backend-е ?

Итак, в предыдущей статье мы разобрались, как моделируются события, связанные с контекстным окном, где они сохраняются, как фиксируются.

В коде выше реализовано все кроме функций, работающих в цикле приложения: awake(), frame_start(), frame_update() и.т.д Это те самые функции, реализация которых зависит от конкретной операционной системы. Писать реализацю этих функций мы будем с использованием библиотек GLFW и SDL3. В виду того, что функция инициализации awake() завязана на конкретный графический API, в этом материале я покажу как дописать недостающую логику в привязке к OpenGL. Про то, как реализовать инициализацию для других графических API я покажу в материале, посвященному rendering backend-у. Итак, начнем.

Дописываем недостающую логику при помощи библиотеки GLFW

Что такое GLFW? GLFW - это кроссплатформенная open-source библиотека, предназначенная для создания контекстных окон и отслеживания их событий. Ровно то, что нужно.

Функция awake()

Функция awake() выполняет запуск контекстного окна и инициализацию в нем графического API. Помимом этого, нам нужно не забыть создать хранилище состояния platform backend-а, в котором у нас лежит указатель на контекстное окно, а также кеш его событий. Если мы создаем контекстное окно, которое использует OpenGL, то для Unix-подобных систем нужно указать используемую версию OpenGL, включить профилирование и обратную совместимость.

Для контекстного окна GLFW отслеживает события и, если какое-то из них происходит, GLFW может вызвать пользовательскую функцию-callback, в которую передадутся параметры события (какая кнопка нажата, отпущена и.т.д). Вот эти пользовательские callback-и на отслеживание событий нам также нужно не забыть установить. Таким образом, на C++, функция awake() будет выглядеть так:

bool ApplicationPlatformBackend::awake()
{
    // если инициализация уже выполнена, то выходим
    if(m_Api != nullptr && m_Api->Window != nullptr)
        return true;

    // Запускаем GLFW
    if(glfwInit() == GLFW_FALSE)
        return false;

    // создаем состояние platform backend-а
    m_Api = std::make_shared<FrenchieApplicationPlatformApi>();

    // для Unix-подобных систем выставляем используемую версию OpenGL, включаем профилирование и обратную совместимость
#ifdef FRENCHIE_APPLICATION_PLATFORM_IS_UNIX
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // выставляем свойства запускаемого контекстного окна

    // делаем окно видимым
    glfwWindowHint(GLFW_VISIBLE, GLFW_TRUE);

    // включаем рамку
    glfwWindowHint(GLFW_DECORATED, GLFW_TRUE);

    // включаем возможность изменения размера контекстного окна
    glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);

    // включаем возможность сворачивания контекстного окна
    glfwWindowHint(GLFW_ICONIFIED, GLFW_TRUE);

    // включаем возможность отслеживания событий в контекстно окне
    glfwWindowHint(GLFW_FOCUSED, GLFW_TRUE);

    // создаем контекстное окно и сохраняем ссылку на него
    m_Api->Window = glfwCreateWindow(512, 256, "Application", nullptr, nullptr);

    if(m_Api->Window == nullptr)
    {
        glfwTerminate();
        return false;
    }

    // Здесь мы говорим OpenGL API, что рисовать будем в созданном контекстном окне
    glfwMakeContextCurrent(reinterpret_cast<GLFWwindow*>(m_Api->Window));

    // устанавливаем пользовательские callback-и на отслеживание событий в контекстном окне
    glfwSetWindowSizeCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_window_resize_callback);
    glfwSetFramebufferSizeCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_window_resize_callback);
    glfwSetWindowMaximizeCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_window_maximized_callback);
    glfwSetWindowFocusCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_window_focused_callback);
    glfwSetCursorEnterCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_cursor_enter_callback);
    glfwSetCursorPosCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_cursor_moved_callback);
    glfwSetMouseButtonCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_mouse_button_callback);
    glfwSetScrollCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_mouse_sroll_offset_changed_callback);
    glfwSetKeyCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_key_callback);
    glfwSetCharCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::glfw_on_character_input_callback);
    glfwSetWindowIconifyCallback(reinterpret_cast<GLFWwindow*>(m_Api->Window), FrenchieApplicationGLFWInputHandler::window_iconify_callback);

    // запускаем инициализацию графического API через rendering backend
    if(!ApplicationRenderingBackend::awake((void*(*)(const char*))glfwGetProcAddress))
    {
        glfwTerminate();
        return false;
    }

    // растягиваем контекстное окно на весь экран
    glfwMaximizeWindow(reinterpret_cast<GLFWwindow*>(m_Api->Window));

    return true;
}

Пользовательские callback-и GLFW для фиксации событий, связанных с контекстным окном

Как я писал ранее, GLFW передает нам информацию о произошедших в контекстном окне событиях через пользтовательские callback-и. В нашем случае нам нужно ослеживать следующие события:

  1. Изменение размера контекстного окна

  2. Изменение размера frame buffer-а контекстного окна

  3. Растягивание окна на весь экран

  4. Передача фокуса нашему контекстному окну

  5. Вхождение курсора в область нашего контектного окна

  6. Перемещение курсора в контекстном окне

  7. Нажатие/отпускание кнопки мыши

  8. Прокручивание колесика мыши

  9. Текстовый ввод

  10. Сворачивание контекстного окна

Пользовательские callback для GLFW для отслеживания указанных выше событий будут реализованы на C++ вот так:

// Эта функция конвертирует идентификатор кнопки мыши GLFW в идентификатор Frenchie
ApplicationPlatformBackendMouseButton::Button FrenchieApplicationGLFWInputHandler::glfw_mouse_button_to_application_mouse_button(int _MouseButton)
{
    switch (_MouseButton)
    {
    case GLFW_MOUSE_BUTTON_LEFT:
        return ApplicationPlatformBackendMouseButton::Button::ApplicationPlatformBackendMouseButtonLeft;
    case GLFW_MOUSE_BUTTON_RIGHT:
        return ApplicationPlatformBackendMouseButton::Button::ApplicationPlatformBackendMouseButtonRight;
    case GLFW_MOUSE_BUTTON_MIDDLE:
        return ApplicationPlatformBackendMouseButton::Button::ApplicationPlatformBackendMouseButtonMiddle;
    }

    return ApplicationPlatformBackendMouseButton::Button::ApplicationPlatformBackendMouseButtonEnd;
}

// Эта функция конвертирует идентификатор кнопки клавиатуры GLFW в идентификатор Frenchie
ApplicationPlatformBackendKey::Key FrenchieApplicationGLFWInputHandler::glfw_key_to_application_key(int _KeyKode, int)
{
    switch (_KeyKode)
    {
        case GLFW_KEY_TAB: return ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_Tab;
        case GLFW_KEY_LEFT: return ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_LeftArrow;
        case GLFW_KEY_RIGHT: return ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_RightArrow;
        // И так далее. Ключей очень много...
        default: return ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_None;
    }
}

// Эта функция конвертирует булев TRUE/FALSE от GLFW в обычный булев TRUE/FALSE C++
bool FrenchieApplicationGLFWInputHandler::glfw_boolean_to_application_boolean(int _Boolean)
{
    return _Boolean == GLFW_TRUE;
}

// Этот callback реагирует на изменение размера контекстного окна
void FrenchieApplicationGLFWInputHandler::glfw_on_window_resize_callback(GLFWwindow* _Window, int _Width, int _Height)
{
    // Здесь мы выставляем актуальный размер frame buffer-а контекстного окна
    (void)_Window;
    int width  = 0;
    int height = 0;
    glfwGetFramebufferSize(_Window, &width, &height);
    ApplicationRenderingBackend::set_viewport(gs_vec2f(0, 0), gs_vec2f(width, height));
}

// Этот callback реагирует на растяжение контекстного окна во весь экран
void FrenchieApplicationGLFWInputHandler::glfw_on_window_maximized_callback(GLFWwindow* _Window, int _Maximized)
{
    // Здесь мы выставляем актуальный размер frame buffer-а контекстного окна
    (void)_Window;
    int width  = 0;
    int height = 0;
    glfwGetFramebufferSize(_Window, &width, &height);
    ApplicationRenderingBackend::set_viewport(gs_vec2f(0, 0), gs_vec2f(width, height));
}

// Этот callback реагирует на передачу операционной системой фокуса нашему контекстному окну
void FrenchieApplicationGLFWInputHandler::glfw_on_window_focused_callback(GLFWwindow* _Window, int _Focused)
{
    ApplicationPlatformBackend::platform_api()->Input.Window.Focused =
        glfw_boolean_to_application_boolean(_Focused);
}

//  Этот callback реагирует на перемещение курсора внутри контекстного окна
void FrenchieApplicationGLFWInputHandler::glfw_on_cursor_moved_callback(GLFWwindow* _Window, double _X, double _Y)
{
    (void)_Window;
    if(ApplicationPlatformBackend::platform_api()->Input.MouseCursor.Entered)
        ApplicationPlatformBackend::platform_api()->Input.MouseCursor.Position = gs_vec2f(_X, _Y);
}

//  Этот callback запускается, когда курсор заходит в область контекстного окна
void FrenchieApplicationGLFWInputHandler::glfw_on_cursor_enter_callback(GLFWwindow* _Window, int _Entered)
{
    (void)_Window;
    ApplicationPlatformBackend::platform_api()->Input.MouseCursor.Entered =
        glfw_boolean_to_application_boolean(_Entered);
}

// В этом callback-е мы фиксируем нажатие и отпускание кнопок мыши
void FrenchieApplicationGLFWInputHandler::glfw_on_mouse_button_callback(GLFWwindow* _Window, int button, int action, int mods)
{
    (void)_Window;

    ApplicationPlatformBackend::platform_api()->Input.MouseButtons[glfw_mouse_button_to_application_mouse_button(button)].Pressed  = action == GLFW_PRESS;
    ApplicationPlatformBackend::platform_api()->Input.MouseButtons[glfw_mouse_button_to_application_mouse_button(button)].Released = action == GLFW_RELEASE;
}

// В этом callback-е мы фиксируем нажатие и отпускание кнопок клавиатуры
void FrenchieApplicationGLFWInputHandler::glfw_on_key_callback(GLFWwindow* _Window, int _Keycode, int _Scancode, int _Action, int _Mods)
{
    (void)_Window;
    (void)_Mods;

    ApplicationPlatformBackend::platform_api()->Input.Keys[glfw_key_to_application_key(_Keycode, _Scancode)].Pressed  = _Action == GLFW_PRESS;
    ApplicationPlatformBackend::platform_api()->Input.Keys[glfw_key_to_application_key(_Keycode, _Scancode)].Released = _Action == GLFW_RELEASE;
}

// В этом callback-е мы сохраняем последний введенный в контекстном окне символ
void FrenchieApplicationGLFWInputHandler::glfw_on_character_input_callback(GLFWwindow* _Window, unsigned int _Character)
{
    (void)_Window;
    ApplicationPlatformBackend::platform_api()->Input.Character = _Character;
}

// В этом callback-е мы сохраняем нормированное смещение колесика мыши
void FrenchieApplicationGLFWInputHandler::glfw_on_mouse_sroll_offset_changed_callback(GLFWwindow* _Window, double _dX, double _dY)
{
    (void)_Window;
    ApplicationPlatformBackend::platform_api()->Input.MouseScrollOffset = gs_vector_normalize(gs_vec2f(_dX, _dY));
}

// В этом callback-е мы фиксируем событие сворачивания контекстного окна
void FrenchieApplicationGLFWInputHandler::window_iconify_callback(GLFWwindow* _Window, int _Iconified)
{
    // фиксируем свернуто ли контекстное окно
    ApplicationPlatformBackend::platform_api()->Input.Window.Iconified =
        glfw_boolean_to_application_boolean(_Iconified);

    // ексли контекстное окно разворачивается, мы должны вернуть ему фокус и указать графическому API, что мы продолжаем рисовать в этом контекстном окне
    if(!glfw_boolean_to_application_boolean(_Iconified))
    {
        glfwFocusWindow(_Window);
        glfwMakeContextCurrent(_Window);
    }
}

Функция frame_start()

В функции frame_start() мы готовим графический API к рисованию в контекстном окне, подхватываем события от контекстного окна (т.е просим GLFW запустить наши callback-и), после чего мы сохраняем события в кеш контекстного окна и запускаем нашу платформо-независимую логику фиксации событий, которую обсуждали в прошлой статье.

При работе с GLFW есть один очень интересный момент, связанный со сворачиванием контекстного окна. Когда контекстное окно сворачивается, операционная система забирает у него фокус и устанавливает размер контекстного окна, равный нулю. В таком случае, если не поставить цепочку обработки событий GLFW на паузу, она попросту зависнет вместе с циклом работы нашего приложения. В таком случае мы не сможем развернуть контекстное окно обратно. Таким образом, с учетом описанных нюансов, функция frame_start() будет выглядеть вот так:

void ApplicationPlatformBackend::frame_start()
{
    // проверяем, свернуто ли наше контекстное окно. Если оно свернуто, мы ставим на паузу цепочку отслеживания событий GLFW, чтобы она не зависла и выходим
    if(ApplicationPlatformBackend::is_window_iconified())
    {
        glfwWaitEventsTimeout(0.1);
        return;
    }
    
    // здесь мы сохраняем текущий размер frame buffer-а контекстного окна
    int x = 0;
    int y = 0;
    glfwGetFramebufferSize(reinterpret_cast<GLFWwindow*>(m_Api->Window), &x, &y);
    m_Api->Input.FrameBufferSize = gs_vec2f(x, y);

    // здесь мы сохраняем текущий размер контекстного окна
    glfwGetWindowSize(reinterpret_cast<GLFWwindow*>(m_Api->Window), &x, &y);
    m_Api->Input.WindowSize = gs_vec2f(x, y);

    // здесь мы сохраняем текущую позицию контекстного окна
    glfwGetWindowPos(reinterpret_cast<GLFWwindow*>(m_Api->Window), &x, &y);
    m_Api->Input.WindowPosition = gs_vec2f(x, y);

    // здесь мы запускаем цепочку отслеживания событий GLFW, т.е просим GLFW запустить наши callback-и
    // и заодно устанавливаем интервал смены текущего frame buffer-а контекстного окна и свеженарисованного
    glfwPollEvents();
    glfwSwapInterval(1);

    // здесь, используя rendering backend, мы просим готовим графический API к рисованию текущего кадра
    ApplicationRenderingBackend::frame_start();

    // здесь мы запускаем платформо-независимую логику фиксации событий
    ApplicationPlatformBackend::collect_input();
}

Функция frame_update()

Это самая изящная и лаконичная функция platform backend-а при использовании библиотеки GLFW:

void ApplicationPlatformBackend::frame_update(){}

Почему эта функция ничего не делает ? Дело в том, что GLFW предусматривает автоматическое включение/отключение текстового ввода в контекстном окне и нам самим делать ничего не нужно.

Функция frame_finish()

В функции frame_finish() мы должны попросить rendering backend отдать нам то, что нарисовала видеокарта, отобразить это в контекстном окне, а потом сбросить состояние графического API в исходное вместе с кешем событий контекстного окна:

void ApplicationPlatformBackend::frame_finish()
{
    // Если окно свернуто, то не делаем ничего
    if(ApplicationPlatformBackend::is_window_iconified())
        return;

    // просим rendering backend отдать нам то, что нарисовала видеокарта и сбрасываем состояние графического API в исходное
    ApplicationRenderingBackend::frame_finish();

    // отображаем то, что нарисовала видеокарта, в нашем контекстном окне
    glfwSwapBuffers(reinterpret_cast<GLFWwindow*>(m_Api->Window));

    // сбрасываем кеш событий контекстного окна
    ApplicationPlatformBackend::restore_input();
}

Функции quit(), close(), is_closed()

Функция quit() деинициализирует графический API и закрывает контекстное окно и делает это именно в таком порядке, т.к графический API привязан к контекстному окну и поэтому уничтожаться он должен первым:

void ApplicationPlatformBackend::quit()
{
    // деинициализируем графический API
    ApplicationRenderingBackend::quit();

    // уничтожаем контекстное окно
    glfwDestroyWindow(reinterpret_cast<GLFWwindow*>(m_Api->Window));
    glfwTerminate();
    m_Api->Window = nullptr;
}

Функция close() говорит GLFW, что надо закрыть контекстное окно, а функция is_closed() проверяет, не собирается ли контекстное окно закрыться:

bool ApplicationPlatformBackend::is_closed()
{
    return glfwWindowShouldClose(reinterpret_cast<GLFWwindow*>(m_Api->Window));
}

void ApplicationPlatformBackend::close()
{
    glfwSetWindowShouldClose(reinterpret_cast<GLFWwindow*>(m_Api->Window), GL_TRUE);
}

Таким образом, мы дописали недостающие функции platform backend-а с использованием библиотеки GLFW, теперь посмотрим, как сделать тоже самое, имея под рукой библиотеку SDL3.

Дописываем недостающую логику с использованием библиотеки SDL3

SDL3 - это библиотека, предоставляющая низкоуровневые функции для работы с аудио, вводом с клавиатуры, мыши, игрового контроллера, а также для работы с различными графическими API типа OpenGL, Vulkan и.т.д. Сам бибилиотека больше чем GLFW и ее функционал намного шире, но, нас интересует только работа с графическими API.

Основное отличие SDL3 от GLFW в части работы с событиями контекстного окна - это то, что вместо callback-ов, SDL3 использует переменные состояния. Эти переменные состояние являются дополнительной информацией, которую нужно хранить в состоянии platform backend-а:

// Application
#include <FrenchieApplicationPlatformBackend.hpp>

// SDL3
#include "SDL3/SDL.h"

namespace Frenchie
{
    namespace Application
    {
        // структура-наследник базового состояния контекстного окна
        struct FrenchieApplicationPlatformSDL3 : public FrenchieApplicationPlatformApi
        {
            FrenchieApplicationPlatformSDL3(){}
            virtual ~FrenchieApplicationPlatformSDL3(){}

            // в этой переменной хранится информация обо всех событиях, связанных с контекстным окном
            SDL_Event     Event;

            // в этой переменной хранится контекст OpenGL, она нужна только при работе с OpenGL
            SDL_GLContext Context;
        };
    }
}

Функция awake()

В функции awake() при использовании SDL3 нам нужно запустить контекстное окно, создать для него контекст OpenGL, привязать этот контекст к контекстному окну и потом инициализировать графический API:

bool ApplicationPlatformBackend::awake()
{
    // если инициализация уже выполнена, ничего не делаем
    if(m_Api != nullptr && m_Api->Window != nullptr)
        return true;

    // инициализируем модуль SDL3 для работы с графикой
    if(!SDL_Init(SDL_INIT_VIDEO))
        return false;

    // создаем состояние platform backend-а
    m_Api = std::make_shared<FrenchieApplicationPlatformSDL3>();

    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    // на unix-подобных системах при работе с OpenGL нужно не забыть включить профилирование и режим обратной совместимости.
    // при работе с SDL3 версию OpenGL API указывать не нужно
#ifdef FRENCHIE_APPLICATION_PLATFORM_IS_UNIX
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
#endif

    // создаем контекстное окно
    SDL3->Window =
        SDL_CreateWindow("Application", 512, 256, SDL_WINDOW_OPENGL | SDL_WINDOW_MAXIMIZED | SDL_WINDOW_RESIZABLE);

    if(m_Api->Window == nullptr)
    {
        SDL_Quit();
        return false;
    }

    // создаем OpenGL контекст и привязываем его к контекстному окну
    SDL3->Context = SDL_GL_CreateContext(reinterpret_cast<SDL_Window*>(SDL3->Window));

    if(SDL3->Context == nullptr)
    {
        SDL_DestroyWindow(reinterpret_cast<SDL_Window*>(SDL3->Window));
        SDL_Quit();
        return false;
    }

    if(!SDL_GL_MakeCurrent(reinterpret_cast<SDL_Window*>(SDL3->Window), SDL3->Context))
    {
        SDL_DestroyWindow(reinterpret_cast<SDL_Window*>(SDL3->Window));
        SDL_GL_DestroyContext(SDL3->Context);
        SDL_Quit();
        return false;
    }

    // инициализируем графический API
    if(!ApplicationRenderingBackend::awake((void*(*)(const char*))SDL_GL_GetProcAddress))
    {
        SDL_Quit();
        return false;
    }

    return true;
}

Функция frame_start()

В функции frame_start() нам нужно подготовить графический API к рисованию, отследить события, связанные с контекстным окном, из переменных состояния SDL3, а также запустить текстовый ввод и подхватить размеры котнектсного окна, его frame buffer-а и положение:

void ApplicationPlatformBackend::frame_start()
{
    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    if(SDL3 == nullptr)
        return;

    // сохраняем размер контекстного окна в пикселях - это и есть размер frame buffer-а
    int x = 0;
    int y = 0;

    SDL_GetWindowSizeInPixels(reinterpret_cast<SDL_Window*>(SDL3->Window), &x, &y);
    SDL3->Input.FrameBufferSize = gs_vec2f(x, y);

    // здесь мы сохраняем размер контекстного окна
    SDL_GetWindowSize(reinterpret_cast<SDL_Window*>(SDL3->Window), &x, &y);
    SDL3->Input.WindowSize = gs_vec2f(x, y);

    // сохраняем положение контекстного окна
    SDL_GetWindowPosition(reinterpret_cast<SDL_Window*>(SDL3->Window), &x, &y);
    SDL3->Input.WindowPosition = gs_vec2f(x, y);

    // запускаем отслеживание текстового ввода
    SDL_StartTextInput(reinterpret_cast<SDL_Window*>(SDL3->Window));

    // запускаем сохранение событий, связанных с контекстным окном, в переменные состояния SDL3
    SDL_PollEvent(&SDL3->Event);
    SDL_GL_SetSwapInterval(1);

    // фиксируем события, связанные с контекстным окном, использую уже подготовленную переменную состояния SDL3, отвечающую за события
    switch (SDL3->Event.type)
    {
        case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
        case SDL_EVENT_WINDOW_RESIZED:
            ApplicationRenderingBackend::set_viewport(gs_vec2f(0, 0), SDL3->Input.FrameBufferSize);
    }
        
    switch (SDL3->Event.type)
    {
        case SDL_EVENT_MOUSE_MOTION:
        {
            SDL3->Input.MouseCursor.Position = gs_vec2f((float)SDL3->Event.motion.x, (float)SDL3->Event.motion.y);
            break;
        }
        case SDL_EVENT_MOUSE_WHEEL:
        {
            SDL3->Input.MouseScrollOffset = gs_vec2f(SDL3->Event.wheel.x, SDL3->Event.wheel.y);
            break;
        }
        case SDL_EVENT_MOUSE_BUTTON_DOWN:
        case SDL_EVENT_MOUSE_BUTTON_UP:
        {
            SDL3->Input.MouseButtons[SDLApplicationInputHandler::glfw_mouse_button_to_application_mouse_button(SDL3->Event.button.button)].Pressed =
                SDL3->Event.type == SDL_EVENT_MOUSE_BUTTON_DOWN;
            
            SDL3->Input.MouseButtons[SDLApplicationInputHandler::glfw_mouse_button_to_application_mouse_button(SDL3->Event.button.button)].Released =
                SDL3->Event.type == SDL_EVENT_MOUSE_BUTTON_UP;

            break;
        }
        case SDL_EVENT_TEXT_INPUT:
        {
            std::u32string utf32 = Frenchie::Core::String::convert_utf8_to_utf32(SDL3->Event.text.text);

            if(!utf32.empty())
            {
                SDL3->Input.Character = utf32[0];
                break;
            }
        }
        case SDL_EVENT_KEY_DOWN:
        case SDL_EVENT_KEY_UP:
        {
            SDL3->Input.Keys[SDLApplicationInputHandler::glfw_key_to_application_key(SDL3->Event.key.key, SDL3->Event.key.scancode)].Pressed =
                SDL3->Event.type == SDL_EVENT_KEY_DOWN;

            SDL3->Input.Keys[SDLApplicationInputHandler::glfw_key_to_application_key(SDL3->Event.key.key, SDL3->Event.key.scancode)].Released =
                SDL3->Event.type == SDL_EVENT_KEY_UP;

            break;
        }
        case SDL_EVENT_DISPLAY_ORIENTATION:
        case SDL_EVENT_DISPLAY_ADDED:
        case SDL_EVENT_DISPLAY_REMOVED:
        case SDL_EVENT_DISPLAY_MOVED:
        case SDL_EVENT_DISPLAY_CONTENT_SCALE_CHANGED:
        {
            break;
        }
        case SDL_EVENT_WINDOW_MOUSE_ENTER:
        {
            SDL3->Input.MouseCursor.Entered = true;
            break;
        }
        case SDL_EVENT_WINDOW_MOUSE_LEAVE:
        {
            SDL3->Input.MouseCursor.Entered = false;
            break;
        }
        case SDL_WINDOW_MINIMIZED:
        {
            SDL3->Input.Window.Iconified = SDL3->Event.type == SDL_WINDOW_MINIMIZED;
            break;
        }
        case SDL_EVENT_WINDOW_FOCUS_GAINED:
        case SDL_EVENT_WINDOW_FOCUS_LOST:
        {
            SDL3->Input.Window.Focused = SDL3->Event.type == SDL_EVENT_WINDOW_FOCUS_GAINED;
            break;
        }
        case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
        case SDL_EVENT_WINDOW_MOVED:
        case SDL_EVENT_WINDOW_RESIZED:
        {
            if(SDL3->Event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)
            {
            }
            else if(SDL3->Event.type == SDL_EVENT_WINDOW_MOVED)
            {
            }
            else if(SDL3->Event.type == SDL_EVENT_WINDOW_RESIZED)
            {
            }

            break;
        }
        case SDL_EVENT_GAMEPAD_ADDED:
        case SDL_EVENT_GAMEPAD_REMOVED:
            break;
        default:
            break;
    }

    // готовим графический API к рисованию, используюя rendering_backend()
    ApplicationRenderingBackend::frame_start();

    // запускаем платформо-независимую логику отслеживания событий
    ApplicationPlatformBackend::collect_input();
}

Функция frame_update()

В случае с SDL3, функция frame_update() выполняет функцию выключения отслеживания текстового ввода в контекстном окне, SDL3 не делает этого автоматически:

void ApplicationPlatformBackend::frame_update()
{
    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    if(SDL3 == nullptr)
        return;

    // выключаем отслеживание текстового ввода в контекстном окне
    if(SDL_TextInputActive(reinterpret_cast<SDL_Window*>(SDL3->Window)))
        SDL_StopTextInput(reinterpret_cast<SDL_Window*>(SDL3->Window));
}

Функция frame_update()

В случае с SDL3, функция frame_update() выполняет функцию выключения отслеживания текстового ввода в контекстном окне, SDL3 не делает этого автоматически:

void ApplicationPlatformBackend::frame_update()
{
    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    if(SDL3 == nullptr)
        return;

    // выключаем отслеживание текстового ввода в контекстном окне
    if(SDL_TextInputActive(reinterpret_cast<SDL_Window*>(SDL3->Window)))
        SDL_StopTextInput(reinterpret_cast<SDL_Window*>(SDL3->Window));
}

Функция frame_finish()

В функции frame_finish() мы должны попросить rendering backend отдать нам то, что нарисовала видеокарта, отобразить это в контекстном окне, а потом сбросить состояние графического API в исходне вместе с кешем событий контекстного окна:

void ApplicationPlatformBackend::frame_finish()
{
    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    if(SDL3 == nullptr)
        return;

    // execute rendering backend
    ApplicationRenderingBackend::frame_finish();

    // swap frame buffers
    SDL_GL_SwapWindow(reinterpret_cast<SDL_Window*>(SDL3->Window));

    // restore input
    ApplicationPlatformBackend::restore_input();
}

Функции quit(), close(), is_closed()

Функция quit() деинициализирует графический API и закрывает контекстное окно и делает это именно в таком порядке, т.к графический API привязан к контекстному окну и поэтому уничтожаться он должен первым:

void ApplicationPlatformBackend::quit()
{
    // terminate rendering API
    ApplicationRenderingBackend::quit();

    // terminate self
    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    if(SDL3 != nullptr)
    {
        SDL_GL_DestroyContext(SDL3->Context);
        SDL_DestroyWindow(reinterpret_cast<SDL_Window*>(SDL3->Window));
    }

    SDL_Quit();
}

Функция close() говорит SDL3, что надо закрыть контекстное окно, а функция is_closed() проверяет, не собирается ли контекстное окно закрыться:

bool ApplicationPlatformBackend::is_closed()
{
    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    return SDL3 == nullptr || SDL3->Event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED;
}

void ApplicationPlatformBackend::close()
{
    auto SDL3 = platform_api<FrenchieApplicationPlatformSDL3>();

    if(SDL3 == nullptr)
        return;

    SDL_Event close_event;
    SDL_zero(close_event);
    close_event.type = SDL_EVENT_WINDOW_CLOSE_REQUESTED;
    close_event.window.windowID = SDL_GetWindowID(reinterpret_cast<SDL_Window*>(SDL3->Window)); 
    SDL_PushEvent(&close_event);
}

Таким образом, мы дописали недостающую логику platform backend-а, используя библиотеки GLFW и SDL3. В следующем материале мы перейдём к самой сложной и при этом самой интересной части проекта Frenchie - rendering backend-у. А на этом пока-что все, спасибо за внимание, надеюсь, было интересно.