Как я делал компактную библиотеку для создания приложений с графическим интерфейсом на языке C++. Часть 2

от автора

Вступление

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

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

Как будем делать platform backend ?

Как говорили в прошлой статье, platform backend — это та часть библиотеки, которая отвечает за открытие контекстного окна и отслеживание связанных с ним событий. На разных операционных системах все эти функции реализованы по разному и единого API не существует. Поэтому, при разработке platform backend-а мы можем либо прокачать навыки системного программирования и реализовать весь нужный функционал самостоятельно для каждой операционной системы, либо взять готовую библиотеку, в которой уже реализован нужный нам функционал.

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

Набор функций platform backend-а

Не смотря на то, что, platform backend работает внутри жизненного цикла приложения, функций у него чуточку меньше. Что же это за функции ?

Первая функция, работающая при запуске приложения, — это функция открытия контекстного окна и запуска процесса инициализации в нем графического API.

Как только приложение запустилось, начинается цикл его работы. Вначале итерации цикла, называемой кадром, platform backend отслеживает события, связанные с контекстным окном и сохраняет информацию об этих событиях в кеш, чтобы далее в цикле можно было на эти события как-то отреагировать.

Помимо сбора кеша, platform backend запускает процесс подготовки графического API к рисованию через rendering backend, т.е выставляет нужное состояние графического API: включает alpha blending, depth testing и.т.д.

В конце кадра, platform backend сбрасывает кеш контекстного окна, сбрасывает состояние графического API в исходное и отображает то, что нарисовала видеокарта в контекстном окне.

Когда приложение закрывается, platform backend деинициализирует графический API через rendering backend и закрывает контекстное окно.

Помимо того, что описано выше, иногда требуется запускать и останавливать поток ввода в контекстном окне (т.е. печатанье текста). Запуск ввода реализуется в начале итерации цикла, а останов — во время процесса расчета геометрии рисуемых графический примитивов (frame_update(), см. предыдущую статью).

А теперь все вместе на C++

В коде на C++ вся базовая логика platform backend-а описанная выше, будет выглядеть вот так:

namespace Frenchie{    namespace Application    {        // статический класс platform backend-а, отвечающего менеджмент жизненного цикла контекстного окна и отслеживание событий, связанных с ним        class ApplicationPlatformBackend final        {        public:            ApplicationPlatformBackend() = delete;            ApplicationPlatformBackend(const ApplicationPlatformBackend&) = delete;            ApplicationPlatformBackend& operator=(const ApplicationPlatformBackend&) = delete;            // открывает контекстное окно и запускает процесс инициализации в нем графического API            static bool awake();            // Отслеживает события, связанные с контекстным окном, и сохраняет их в кеш, чтобы можно было потом на них как-то отреагировать. Также, запускает отслеживание потока ввода (т.е печатанье текста), если это требуется            static void frame_start();            // Останавливает отслеживание потока ввода (т.е печатанья текста), если это требуется            static void frame_update();            // сбрасывает кеш событий, связанных с контекстным окном, сбрасывает состояние графического API в исходное через rendering backend и отображает то, что нарисовала видеокарта в контекстном окне также через rendering backend            static void frame_finish();            // деинициализирует графический API через rendering backend и закрывает контекстное окно            static void quit();            // проверяет, не нужно ли закрыть контекстное окно и само приложение            static bool is_closed();            // запускает процесс закрытия контекстного окна и самого приложения            static void close();        };    }}

Код выше содержит исключительно функции, которые вызываются внутри жизненного цикла приложения, но, в нем нет кеша, о котором мы упоминали ранее. Так что же такое кеш контекстного окна, где он хранится и что в нем есть ?

Состояние platform backend-а

Как уже мог догадаться опытный читатель, кеш контекстного окна — это сущность, являющаяся частью состояния platform backend-а. Состояние platform backend-а — это хранилище, в котором, как минимум, хранится ссылка на само контекстное окно, а также его кеш. В коде на C++, класс platform backend-а и хранилища его состояния будут выглядеть вот так:

namespace Frenchie{    namespace Application    {        // переопределяемое хранилище состояния platform backend-а        struct FrenchieApplicationPlatformApi        {            FrenchieApplicationPlatformApi(){}            virtual ~FrenchieApplicationPlatformApi(){}            // указатель на контекстное окно            void*                           Window = nullptr;            // кеш контекстного окна            ApplicationPlatformBackendInput Input;        };        // статический класс platform backend-а, отвечающего менеджмент жизненного цикла контекстного окна и отслеживание событий, связанных с ним        class ApplicationPlatformBackend final        {        public:            ApplicationPlatformBackend() = delete;            ApplicationPlatformBackend(const ApplicationPlatformBackend&) = delete;            ApplicationPlatformBackend& operator=(const ApplicationPlatformBackend&) = delete;            // открывает контекстное окно и запускает процесс инициализации в нем графического API            static bool awake();            // Отслеживает события, связанные с контекстным окном, и сохраняет их в кеш, чтобы можно было потом на них как-то отреагировать. Также, запускает отслеживание потока ввода (т.е печатанья текста), если это требуется            static void frame_start();            // Останавливает отслеживание потока ввода (т.е печатанья текста), если это требуется            static void frame_update();            // сбрасывает кеш контекстного окна, сбрасывает состояние графического API в исходное через rendering backend и отображает то, что нарисовала видеокарта в контекстном окне также через rendering backend            static void frame_finish();            // деинициализирует графический API через rendering backend и закрывает контекстное окно            static void quit();            // проверяет, не нужно ли закрыть контекстное окно и само приложение            static bool is_closed();            // запускает процесс закрытия контекстного окна и самого приложения            static void close();            // выдает переопределенное состояние platform backend-а, созданное в функции awake(). По умолчанию возвращает состояние с контекстным окном и кешем событий            template <typename T = FrenchieApplicationPlatformApi>            static std::shared_ptr<T> platform_api()            {                return std::dynamic_pointer_cast<T>(m_Api);            }        private:            static std::shared_ptr<FrenchieApplicationPlatformApi> m_Api;        };    }}

Хранилище состояния platform backend-а в общем случае может содержать больше данных чем одно только контекстное окно и кеш его событий, поэтому, от него можно наследоваться и в структуру-наследник уже добавлять все, что нужно. По этой причине, platform backend должен предусматривать возможность достать переопределенное состояние platform backend-а. Эта возможность реализована в шаблонной функции platform_api(). С состоянием разобрались, теперь посмотрим, что содержит в себе кеш событий.

Кеш контекстного окна

Типы событий, связанных с кнопками мыши и клавиатуры

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

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

Второй тип событий — это отпускание кнопки мыши. Отпускание кнопки мыши — это событие, когда пользователь отпустил кнопку. Данное событие фиксируется в той итерации цикла работы приложения, когда пользователь отпустил кнопку и существует исключительно внутри этой итерации.

Третий тип событий — это зажатие кнопки мыши. Зажатие кнопки мыши — это событие, когда пользователь нажал на кнопку и удерживает ее в зажатом состоянии. Данное событие фиксируется в той итерации цикла приложения, когда пользователь нажал на кнопку и существует до тех пор, пока пользователь не отпустит ее.

С отпусканием, нажатием и зажатием, думаю, что все понятно, однако есть более интересные события — удержание и скликивание. Что же это за события ?

Сперва может показаться, что зажатие кнопки мыши и ее удержание — это одно и то же, однако, это не так. Событие удержания кнопки мыши — это тоже самое, что зажатие, однако оно фиксируется после того, как пользователь продержал кнопку мыши в зажатом состоянии некоторое время, называемое временем удержания. После фиксации, событие удержания существует до тех пор, пока пользователь не отпустит кнопку мыши (здесь логика полностью идентична той, что используется для события зажатия). Теперь разберемся с тем, что такое скликивание.

Событие скликивания — это событие, когда пользователь нажал на кнопку, подержал ее в зажатом состоянии какое-то время, а потом отпустил. Время удержания кнопки при фиксации события скликивания называется времением фиксации скликавания.

Со всеми типами событий, связанных с кнопками мыши, разобрались. Что касается клавиш клавиатуры, то для них все типы событий абсолютно идентичны. Теперь посмотрим, как это моделируется в коде на C++.

Моделирование событий, связанных с кнопками мыши и клавиатуры на C++

Не смотря на то, что типы событий для кнопок мыши и клавиатуры абсолютно идентичны, я все-таки решил моделировать их отдельными структурами, дабы не было путаницы при их фиксации. Также, перечисления с названиями кнопок мыши и клавиатуры я заимствовал из библиотеки ImGUI дабы не набивать их самостоятельно. Итак, структуры для хранения событий, связанных с кнопками мыши и клавиатуры будут выглядеть так:

{    namespace Application    {        // Структура, моделирующая клавишу клавиатуры        struct ApplicationPlatformBackendKey        {            // Перечисление со всеми возсожными клавишами клавиатуры            enum Key : int            {                ApplicationPlatformBackendKey_NamedKey_BEGIN = 0,                // Здесь я опустил названия кнопок т.к их очень много, интересующийся читатель может все посмотреть в репозитории с исходным кодом библиотеки - https://github.com/Alexey-cpu/Frenchie/blob/v1/release/frenchie/source/application/FrenchieApplicationPlatformBackend.hpp                ApplicationPlatformBackendKey_NamedKey_END            };            // количество скликиваний            int                              Clicks       {0    };            // показывает зажата ли кнопка            bool                             Down         {false};            // показывает удерживается ли кнопка            bool                             Hold         {false};            // показывает нажата ли кнопка в текущей итерации цикла работы приложения            bool                             Pressed      {false};            // показывает отпущена ли кнопка в текущей итерации цикла работы приложения            bool                             Released     {false};            // показывает скликнута ли кнопка            bool                             Clicked      {false};            // момент времени нажатия на кнопку            Frenchie::Core::Clock::TimePoint PressTime    {Frenchie::Core::Clock::TimePoint()};            // момент времени отпускания кнопки            Frenchie::Core::Clock::TimePoint ReleaseTime  {Frenchie::Core::Clock::TimePoint()};        };        // Структура, моделирующая кнопку мыши        struct ApplicationPlatformBackendMouseButton        {            /**             * @brief This enum encodes platform backend mouse button code.             * @enum Button             */            enum Button : int            {                ApplicationPlatformBackendMouseButtonBegin,                // Здесь я опустил названия кнопок т.к их очень много, интересующийся читатель может все посмотреть в репозитории с исходным кодом библиотеки - https://github.com/Alexey-cpu/Frenchie/blob/v1/release/frenchie/source/application/FrenchieApplicationPlatformBackend.hpp                ApplicationPlatformBackendMouseButtonEnd            };            // количество скликиваний            int                              Clicks       {0    };            // показывает зажата ли кнопка            bool                             Down         {false};            // показывает удерживается ли кнопка            bool                             Hold         {false};            // показывает нажата ли кнопка в текущей итерации цикла работы приложения            bool                             Pressed      {false};            // показывает отпущена ли кнопка в текущей итерации цикла работы приложения            bool                             Released     {false};            // показывает скликнута ли кнопка            bool                             Clicked      {false};            // показывает была ли кнопка нажата два раз            bool                             DoubleClicked{false};            // момент времени нажатия на кнопку            Frenchie::Core::Clock::TimePoint PressTime    {Frenchie::Core::Clock::TimePoint()};            // момент времени отпускания кнопки            Frenchie::Core::Clock::TimePoint ReleaseTime  {Frenchie::Core::Clock::TimePoint()};        };    }}

При нажатии на кнопки мыши и клавиатуры нам обычно предоставляется возможность наложения на них различных модификаторов типа CTRL, ALT, SHIFT и.т.д. Наличие модификаторов мы также должны как-то контролировать. Структура данных для контроля наличия модификаторов выглядит вот так:

namespace Frenchie{    namespace Application    {        // Структура, моделирующая модификатор для кнопок мыши и клавиатуры        struct ApplicationPlatformBackendKeyModifier        {            // Перечисление со всеми возможными модификаторами            enum Modifier : int            {                ApplicationPlatformBackendKeyModifier_Begin,                // Здесь я опустил названия модификаторов т.к их очень много, интересующийся читатель может все посмотреть в репозитории с исходным кодом библиотеки - https://github.com/Alexey-cpu/Frenchie/blob/v1/release/frenchie/source/application/FrenchieApplicationPlatformBackend.hpp                ApplicationPlatformBackendKeyModifier_End,            };            // показывает наложен модификатор или нет            bool Active = false;        };    }}

В общем и целом, с моделированием кнопок мыши, клавиатуры и их модификаторов мы разобрались, однако в кеше контекстного окна есть еще кое-что, с чем нужно ознакомиться — курсор мыши. О том, как моделируется курсор поговорим далее.

Моделирование событий, связанных с курсором мыши на C++

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

Первое и самое главное, что есть у курсора — это его положение, т.к оно играет ключевую роль в определении того, с каким элементом интерфейса мы сейчас взаимодействуем.

Второе не менее важное свойство курсора — это величина его перемещения относительно позиции нажатия кнопки мыши — drag delta. Почему drag delta так важна ? drag delta — это ключевое свойство курсора, используемое для того, чтобы определить, насколько переместить объект относительно его изначального положения или насколько этот объект растянуть относительно его исходного размера.

Для того, чтобы вычислить drag delta, нужно зафиксировать положение курсора в момент нажатия на кнопку мыши (что такое нажатие, мы уже знаем). После того, как мы зафиксировали нажатие, мы можем считать drag delta как разницу между текущим положением курсора и его положением в момент нажатия. При этом, считать эту разницу нужно только тогда, когда кнопка мыши находится в зажатом состоянии.

И самое главное, как всегда в конце, — это фиксация того, что курсор находится внутри контекстного окна, т.к если курсор находится вне области контекстного окна, то вообще нет смысла фиксировать его положение, drag delta и.т.д Таким образом, модель курсора мыши для фиксации связанных с ним событий в кеше контекстного окна будет выглядеть вот так:

namespace Frenchie{    namespace Application    {        // Структура для моделирования событий, связанных с курсором        struct ApplicationPlatformBackendMouseCursor        {            // Определяет, вошел ли курсор внутрь области контекстного окна            bool      Entered           {false};            // Позиция курсора            gs_vec2f  Position          {gs_vec2f(0.f)};            // Позиция курсора в момент нажатия кнопки мыши внутри контекстного окна            gs_vec2f  MousePressPosition{gs_vec2f(0.f)};            // Разница между позицией курсора в момент нажатия кнопки мыши и его текущим положением в режиме, когда любая кнопка мыши зажата            gs_vec2f  DragDelta         {gs_vec2f(0.f)};        };    }}

На этом с моделированием событий можно было бы выдохнуть и перейти непосредственно к кешу контекстного окна, но, к сожалению, это еще не все, ибо у нас есть еще само контекстное окно.

Моделирование событий самого контекстного окна на C++

Итак, давайте разбираться, что же такого есть у самого контекстного онка, что требует фиксации ? У контекстного окна всего два события, которые нужно фиксировать — это фокус и сворачивание.

Фокус — это событие, когда операционная система передает управление нашему контекстному окну. Сворачивание — это, когда операционная система забирает фокус у нашего контекстного окна и ставит его размер, равным нулю. Первое событие наступает, когда среди множества окон различных приложений мы выбираем окно нашего приложения. Второе событие наступает, когда мы сворачиваем окно нашего приложения нажатием на соответствующую кнопку на обрамляющей рамке окна. Таким образом, структура данных событий самого контекстного окна будет иметь вид:

namespace Frenchie{    namespace Application    {        // Структура для моделирования событий, связанных с самим контекстным окном        struct ApplicationPlatformBackendWindow        {            // Определяет, передала ли операционная система фокус нашему контекстному окну            bool Focused  {false};            // Определяет, свернула ли операционная система наше контекстное окно            bool Iconified{false};        };    }}

Вот теперь точно все: мы умеем моделировать события клавиш клавиатуры, кнопок мыши, курсора, модификаторы, а также события, связанные с самим контекстным окном. Теперь можно перейти к кешу контекстного окна.

Модель кеша контекстного окна

В предыдущих разделах мы разобрались, как моделировать различные события, связанные с контекстным окном — начиная от событий кнопок мыши и клавиш клавиатуры, заканчивая курсором и самим контекстным окном. Теперь разберемся, что находится в самом кеше.

Кеш контекстного окна, очевидно, хранит в себе события, связанные с контекстным окном. Однако, помимо этого, в кеше также находится информация о том, какой символ пользователь только что ввел в контекстном окне (информация о вводе), размер контекстного окна, его позиция, а также размер области рисования — frame buffer.

Область рисования контекстного окна — это внутренняя область контекстного окна, ограниченная рамкой. В принципе, мы перечислили все, кроме колесика мыши. Колесико мыши имеет только смещение, которое может быть либо отрицательным, либо положительным в диапазоне от минус единицы до плюс единицы (нормированное смещение). Таким образом, кеш контекстного окна на C++ будет выглядеть вот так:

namespace Frenchie{    namespace Application    {        // Структура, моделирующая кеш контекстного окна        struct ApplicationPlatformBackendInput        {            // вектор событий кнопок мыши            ApplicationPlatformBackendMouseButton  MouseButtons     [ApplicationPlatformBackendMouseButton::Button::ApplicationPlatformBackendMouseButtonEnd]{};            // модель курсора мыши            ApplicationPlatformBackendMouseCursor  MouseCursor      {ApplicationPlatformBackendMouseCursor()};            // события самого контекстного окна            ApplicationPlatformBackendWindow       Window           {ApplicationPlatformBackendWindow()};            // вектор с модификаторами кнопок мыши и клавиатуры            ApplicationPlatformBackendKeyModifier  Modifiers        [ApplicationPlatformBackendKeyModifier::ApplicationPlatformBackendKeyModifier_End]{};            // вектор с событиями кнопок клавиатуры            ApplicationPlatformBackendKey          Keys             [ApplicationPlatformBackendKey::Key::ApplicationPlatformBackendKey_NamedKey_END]{};            // послений введенный пользователем символ            Frenchie::Core::Optional<unsigned int> Character;            // смещение колесика мыши            gs_vec2f                               MouseScrollOffset{gs_vec2f(0.f, 0.f)};            // размер контекстного окна            gs_vec2f                               WindowSize;            // положение контекстного окна            gs_vec2f                               WindowPosition;            // размер области рисования контекстного окна            gs_vec2f                               FrameBufferSize;        };    }}

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

Логика фиксации/сброса событий

Итак, после того, как мы сохранили события контекстного окна в кеш, мы можем их зафиксировать, т.е определить, была ли какая-то кнопка удержана, скликнута и.т.д Мы уже частично затрагивали логику фиксации событий в разделе, где разбирали типы событий, теперь поговорим об этом более подробно.

По факту, такие события, как нажатие кнопок, отпускание кнопок, текстовый ввод, изменение положения курсора, положения окна, его размера, а также положение колесика мыши фиксируются логикой, реализуемой при помощи сторонних библиотек типа GLFW и SDL3. Подробно эту логику мы рассмотрим в следующем материале, а здесь пока-что будем считать, что эти события как-то фиксируются. Таким образом, остается лишь разобраться с тем, как работает логика, которая не зависит от сторонних библиотек.

Как уже говорилось ранее, событие скликивания фиксируется в случае, если пользователь нажал на кнопку, подержал ее какое-то время и потом отпустил. Время удержания кнопки при фиксации события скликивания называется временем скликивания. Событие удержания кнопки мыши или клавиатуры фиксируется в случае, если пользователь зажал кнопку и удерживал ее какое-то время, называемое времением фиксации удержания.

Количество скликиваний кнопок мыши или клавиатуры фиксируется при условии, если промежуток времени между скликивания меньше какого-то заданного промежутка времени, называемого временем фиксации количества скликиваний. В коде на C++, вся описанная логика будет выглядеть вот так:

// Полезные макросы для определения, на какой операционной системе мы работаем#if defined(unix) || defined(__unix) || defined(__unix__)#define FRENCHIE_APPLICATION_PLATFORM_IS_LINUX#endif#if defined(__APPLE__) || defined(__MACH__)#define FRENCHIE_APPLICATION_PLATFORM_IS_MACOS#endif#if defined(_WIN32) || defined(_WIN64)#define FRENCHIE_APPLICATION_PLATFORM_IS_WINDOWS#endif#if defined(FRENCHIE_APPLICATION_PLATFORM_IS_LINUX) || defined(FRENCHIE_APPLICATION_PLATFORM_IS_MACOS)#define FRENCHIE_APPLICATION_PLATFORM_IS_UNIX#endifnamespace Frenchie{    namespace Application    {        // статический класс platform backend-а, отвечающего менеджмент жизненного цикла контекстного окна и отслеживание событий, связанных с ним        class ApplicationPlatformBackend final        {            // Здесь уже знакомая часть...            // функция, отвечающая за фиксацию событий            void collect_input()            {                // Интервал фиксации количества скликиваний кнопок мыши/клавиатуры                const double KeyClicksCountResetTime = 200; // ms                // Время фиксации удерживания кнопок мыши/клавиатуры                const double KeyHoldDetectionTime    = 100; // ms                // Время фиксации скликивания кнопок мыши/клавиатуры                const double KeyClickDetectionTime   = 500; // ms                // handle mouse buttons events                for (int mouseButton = ApplicationPlatformBackendMouseButton::Button::ApplicationPlatformBackendMouseButtonBegin;                        mouseButton < ApplicationPlatformBackendMouseButton::Button::ApplicationPlatformBackendMouseButtonEnd;                        mouseButton++)                {                    // считаем к-во скликиваний кнопки мыши при условии, что интервал времени между отпусканиями кнопки мыши меньше чем интервал фиксации к-ва скликиваний                    if(Frenchie::Core::Clock::elapsed<Frenchie::Core::Clock::Milliseconds>(                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].ReleaseTime,                        Frenchie::Core::Clock::tic()) > KeyClicksCountResetTime)                    {                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Clicks = 0;                    }                    // Здесь мы фиксируем событие зажатия кнопки мыши,                    // сохраняем позицию, где была нажата кнопка,                    // сохраняем момент времени нажатия                    if(ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Pressed)                    {                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Down      = true;                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].PressTime = Frenchie::Core::Clock::tic();                        ApplicationPlatformBackend::platform_api()->Input.MouseCursor.MousePressPosition = ApplicationPlatformBackend::platform_api()->Input.MouseCursor.Position;                    }                                        // Фиксируем событие удержания кнопки мыши и рассчитываем drag delta у курсора мыши                    if(ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Down)                    {                        ApplicationPlatformBackend::platform_api()->Input.MouseCursor.DragDelta =                            ApplicationPlatformBackend::platform_api()->Input.MouseCursor.Position - ApplicationPlatformBackend::platform_api()->Input.MouseCursor.MousePressPosition;                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Hold =                            Frenchie::Core::Clock::elapsed<Frenchie::Core::Clock::Milliseconds>(                                ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].PressTime,                                Frenchie::Core::Clock::tic()) > KeyHoldDetectionTime;                    }                    // фиксируем событие скликивания кнопки мыши, отпускания кнопки мыши и сбрасываем события зажатия и удерживания                    if(ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Released)                    {                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Down        = false;                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Hold        = false;                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].ReleaseTime = Frenchie::Core::Clock::tic();                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Clicked =                            Frenchie::Core::Clock::elapsed<Frenchie::Core::Clock::Milliseconds>(                                ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].PressTime,                                ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].ReleaseTime) < KeyClickDetectionTime;                        ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].DoubleClicked =                            ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Clicked && ++ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Clicks >= 2;                    }                        }                // логика фиксации событий кнопок клавиатуры                for (int mouseButton = ApplicationPlatformBackendKey::Key::ApplicationPlatformBackendKey_NamedKey_BEGIN;                        mouseButton < ApplicationPlatformBackendKey::Key::ApplicationPlatformBackendKey_NamedKey_END;                        mouseButton++)                {                    if(Frenchie::Core::Clock::elapsed<Frenchie::Core::Clock::Milliseconds>(                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].ReleaseTime,                        Frenchie::Core::Clock::tic()) > KeyClicksCountResetTime)                    {                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Clicks = 0;                    }                    if(ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Pressed)                    {                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Down      = true;                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].PressTime = Frenchie::Core::Clock::tic();                    }                                        if(ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Down)                    {                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Hold =                            Frenchie::Core::Clock::elapsed<Frenchie::Core::Clock::Milliseconds>(                                ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].PressTime,                                Frenchie::Core::Clock::tic()) > KeyHoldDetectionTime; // TODO: MUST BE A SETTING                    }                    if(ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Released)                    {                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Down        = false;                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Hold        = false;                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].ReleaseTime = Frenchie::Core::Clock::tic();                        ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Clicked =                            Frenchie::Core::Clock::elapsed<Frenchie::Core::Clock::Milliseconds>(                                ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].PressTime,                                ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].ReleaseTime) < KeyClickDetectionTime;                        ++ApplicationPlatformBackend::platform_api()->Input.Keys[mouseButton].Clicks;                    }                }                // фиксируем модификаторы            #ifdef FRENCHIE_APPLICATION_PLATFORM_IS_MACOS                ApplicationPlatformBackend::platform_api()->Input.Modifiers[ApplicationPlatformBackendKeyModifier::Modifier::ApplicationPlatformBackendKeyModifier_Ctrl].Active =                    ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_LeftSuper].Down ||                        ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_RightSuper].Down;            #else                ApplicationPlatformBackend::platform_api()->Input.Modifiers[ApplicationPlatformBackendKeyModifier::Modifier::ApplicationPlatformBackendKeyModifier_Ctrl].Active =                    ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_LeftCtrl].Down ||                        ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_RightCtrl].Down;            #endif                // Alt (Option on MacOS)                ApplicationPlatformBackend::platform_api()->Input.Modifiers[ApplicationPlatformBackendKeyModifier::Modifier::ApplicationPlatformBackendKeyModifier_Alt].Active =                    ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_LeftAlt].Down ||                        ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_RightAlt].Down;                // Shift                ApplicationPlatformBackend::platform_api()->Input.Modifiers[ApplicationPlatformBackendKeyModifier::Modifier::ApplicationPlatformBackendKeyModifier_Shift].Active =                    ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_LeftShift].Down ||                        ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_RightShift].Down;                // фиксируем ввод символов '\n' и '\t'                // catch character '\n'                if(ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::Key::ApplicationPlatformBackendKey_Enter].Pressed)                    ApplicationPlatformBackend::platform_api()->Input.Character = '\n';                // catch character '\t'                if(ApplicationPlatformBackend::platform_api()->Input.Keys[ApplicationPlatformBackendKey::Key::ApplicationPlatformBackendKey_Tab].Pressed)                    ApplicationPlatformBackend::platform_api()->Input.Character = '\t';            }            // здесь находится логика сброса кеша событий, связанных с контекстным окном            void restore_input()            {                // сбрасываем кеш событий кнопок мыши                for (int mouseButton = ApplicationPlatformBackendMouseButton::ApplicationPlatformBackendMouseButtonBegin;                        mouseButton < ApplicationPlatformBackendMouseButton::ApplicationPlatformBackendMouseButtonEnd;                        mouseButton++)                {                    ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Released      = false;                    ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Pressed       = false;                    ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].Clicked       = false;                    ApplicationPlatformBackend::platform_api()->Input.MouseButtons[mouseButton].DoubleClicked = false;                }                // сбрасываем кеш событий кнопок клавиатуры                for (int key = ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_NamedKey_BEGIN;                        key < ApplicationPlatformBackendKey::ApplicationPlatformBackendKey_NamedKey_END;                        key++)                {                    ApplicationPlatformBackend::platform_api()->Input.Keys[key].Released = false;                    ApplicationPlatformBackend::platform_api()->Input.Keys[key].Pressed  = false;                    ApplicationPlatformBackend::platform_api()->Input.Keys[key].Clicked  = false;                }                // сбрасываем кеш с модификаторами событий кнопок мыши и клавиатуры                for (int key = ApplicationPlatformBackendKeyModifier::ApplicationPlatformBackendKeyModifier_Begin;                        key < ApplicationPlatformBackendKeyModifier::ApplicationPlatformBackendKeyModifier_End;                        key++)                {                    ApplicationPlatformBackend::platform_api()->Input.Modifiers[key].Active = false;                }                // сбрасываем колесико мыши, курсор и текстовый ввод                ApplicationPlatformBackend::platform_api()->Input.MouseScrollOffset = gs_vec2f(0.f, 0.f);                ApplicationPlatformBackend::platform_api()->Input.MouseCursor.DragDelta = gs_vec2f(0.f, 0.f);                ApplicationPlatformBackend::platform_api()->Input.Character.reset();            }        };    }}

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

ссылка на оригинал статьи https://habr.com/ru/articles/1055870/