0xc3 2 周之前
當前提交
03b40c185b

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+.cache/
+build/
+vcpkg_installed/
+imgui.ini

+ 8 - 0
.zed/debug.json

@@ -0,0 +1,8 @@
+[
+  {
+    "label": "Debug native binary",
+    "program": "$ZED_WORKTREE_ROOT/build/Debug/sandbox",
+    "request": "launch",
+    "adapter": "CodeLLDB"
+  }
+]

+ 69 - 0
CMakeLists.txt

@@ -0,0 +1,69 @@
+cmake_minimum_required(VERSION 3.15...4.0)
+project(app)
+
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+# === CCACHE АВТОМАТИЧЕСКИЙ ЗАПУСК ===
+find_program(CCACHE_PROGRAM ccache)
+if(CCACHE_PROGRAM)
+ message(STATUS "ccache найден: ${CCACHE_PROGRAM}")
+
+ # Это работает и с Ninja, и с Makefiles
+ set(CMAKE_C_COMPILER_LAUNCHER   "${CCACHE_PROGRAM}")
+ set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}")
+
+ # Для старых версий CMake (< 3.4) можно использовать альтернативу:
+ # set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE "${CCACHE_PROGRAM}")
+endif()
+
+set(OUTPUT_DIR ${CMAKE_BINARY_DIR}/$<CONFIG>)
+set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${OUTPUT_DIR})
+set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${OUTPUT_DIR})
+set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${OUTPUT_DIR})
+
+file(GLOB_RECURSE LIB_SOURCES
+    "${PROJECT_SOURCE_DIR}/src/saura/app/*/*.cpp"
+    "${PROJECT_SOURCE_DIR}/src/saura/core/*/*.cpp"
+    "${PROJECT_SOURCE_DIR}/src/saura/main/main.cpp"
+)
+
+# Создаем библиотеку
+add_library(${PROJECT_NAME}_lib STATIC ${LIB_SOURCES})
+
+# Пути
+set(SRC_RES_DIR "${CMAKE_SOURCE_DIR}/res")
+set(RES_SYMLINK "${OUTPUT_DIR}/res")
+
+find_package(SDL3 CONFIG REQUIRED)
+find_package(spdlog CONFIG REQUIRED)
+find_package(imgui CONFIG REQUIRED)
+find_package(glm CONFIG REQUIRED)
+find_package(cpr CONFIG REQUIRED)
+find_package(nlohmann_json CONFIG REQUIRED)
+find_package(GTest CONFIG REQUIRED)
+
+# Добавляем include-пути
+target_include_directories(${PROJECT_NAME}_lib PUBLIC
+    ${PROJECT_SOURCE_DIR}
+    ${PROJECT_SOURCE_DIR}/src
+)
+
+target_include_directories(${PROJECT_NAME}_lib PRIVATE
+    ${PROJECT_SOURCE_DIR}
+    ${PROJECT_SOURCE_DIR}/src
+)
+
+target_link_libraries(${PROJECT_NAME}_lib PUBLIC
+    SDL3::SDL3
+    spdlog::spdlog
+    imgui::imgui
+    glm::glm
+    cpr::cpr
+    nlohmann_json::nlohmann_json
+    GTest::gtest GTest::gtest_main
+)
+
+add_subdirectory(src/saura/cmd/sandbox)
+add_subdirectory(src/saura/cmd/tests)

+ 4 - 0
README.md

@@ -0,0 +1,4 @@
+```
+cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=$HOME/.local/share/vcpkg/scripts/buildsystems/vcpkg.cmake
+cmake --build build --config Debug -j $(nproc)
+```

二進制
res/fonts/InterRegular.ttf


+ 10 - 0
src/saura/cmd/sandbox/CMakeLists.txt

@@ -0,0 +1,10 @@
+get_filename_component(TARGET_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME)
+
+file(GLOB SOURCES
+ "${CMAKE_CURRENT_SOURCE_DIR}/*/*.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/sandbox.cpp"
+)
+
+add_executable(${TARGET_NAME} ${SOURCES})
+
+target_link_libraries(${TARGET_NAME} PRIVATE ${PROJECT_NAME}_lib)

+ 59 - 0
src/saura/cmd/sandbox/sandbox.cpp

@@ -0,0 +1,59 @@
+#include <algorithm>
+
+#include <cstddef>
+#include <cstring>
+#include <fmt/format.h>
+#include <memory>
+#include <span>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include "saura/core/base/types.hpp"
+#include "saura/core/memory/arena.hpp"
+#include <saura/core/app_base/app_base.hpp>
+#include <saura/core/os/os.hpp>
+
+#include <imgui.h>
+#include <imgui_internal.h>
+#include <spdlog/spdlog.h>
+
+namespace saura {
+
+struct SandboxApp : AppBase {
+  std::unique_ptr<MemoryArena> def_mem_arena;
+  std::unique_ptr<MemoryArenaTemp> mem_arena_temp;
+
+  void start() override {
+    spdlog::info("Sandbox start!");
+
+    spdlog::info("DPI scale: {}", os_ctx->get_dpi_scale());
+
+    this->def_mem_arena = std::make_unique<MemoryArena>();
+    this->mem_arena_temp = std::make_unique<MemoryArenaTemp>();
+  }
+
+  void update(double dt) override {}
+
+  void draw() override {
+    ImGui::SetNextWindowPos({0.0f, 0.0f});
+    ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize);
+
+    ImGui::Begin("Demo", 0,
+                 ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
+                     ImGuiWindowFlags_NoFocusOnAppearing |
+                     ImGuiWindowFlags_NoSavedSettings);
+    ImGui::End();
+
+    ImGui::ShowDemoWindow();
+  }
+
+  void stop() override { spdlog::info("Sandbox stop!"); }
+};
+} // namespace saura
+
+int main() {
+  auto app = std::make_unique<saura::SandboxApp>();
+  app->run();
+  return 0;
+}

+ 7 - 0
src/saura/cmd/tests/CMakeLists.txt

@@ -0,0 +1,7 @@
+get_filename_component(TARGET_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME)
+
+file(GLOB SOURCES "*.cpp")
+
+add_executable(${TARGET_NAME} ${SOURCES})
+
+target_link_libraries(${TARGET_NAME} PRIVATE ${PROJECT_NAME}_lib)

+ 6 - 0
src/saura/cmd/tests/tests.cpp

@@ -0,0 +1,6 @@
+#include <gtest/gtest.h>
+
+int main(int argc, char **argv) {
+  ::testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}

+ 47 - 0
src/saura/core/app_base/app_base.cpp

@@ -0,0 +1,47 @@
+#include "app_base.hpp"
+#include <memory>
+
+namespace saura {
+void AppBase::run() {
+  os_ctx = std::make_unique<os::Context>();
+
+  this->start();
+
+  const double fps_max = 60.0;
+  const double period_max = 1.0 / fps_max;
+  const double perf_frequency = os_ctx->get_performance_frequency();
+
+  double time = 0.0;
+  double begin_counter = 0.0;
+  double end_counter = 0.0;
+
+  while (!is_quit) {
+
+    double counter_elapsed = begin_counter - end_counter;
+    double dt = counter_elapsed / perf_frequency;
+    double fps = perf_frequency / counter_elapsed;
+
+    begin_counter = os_ctx->get_performance_counter();
+
+    if (dt >= period_max) {
+      if (dt >= 1.0) {
+        dt = period_max;
+      }
+
+      if (!os_ctx->update_events()) {
+        is_quit = true;
+      }
+      os_ctx->update();
+      this->update(dt);
+      os_ctx->draw_begin();
+      this->draw();
+      os_ctx->draw_end();
+
+      end_counter = begin_counter;
+    }
+
+    os_ctx->sleep(period_max);
+  }
+  this->stop();
+}
+} // namespace saura

+ 35 - 0
src/saura/core/app_base/app_base.hpp

@@ -0,0 +1,35 @@
+#ifndef SAURA_APP_BASE_HPP_
+#define SAURA_APP_BASE_HPP_
+
+#include "saura/core/os/os.hpp"
+#include <SDL3/SDL_render.h>
+#include <SDL3/SDL_video.h>
+#include <memory>
+#include <spdlog/spdlog.h>
+#include <string>
+
+namespace saura {
+struct AppBase;
+
+struct AppBase {
+private:
+  std::string title;
+  bool is_quit;
+
+public:
+  std::unique_ptr<os::Context> os_ctx;
+
+public:
+  virtual ~AppBase() = default;
+
+  virtual void start() {}
+  virtual void update(double dt) {}
+  virtual void draw() {}
+  virtual void stop() {}
+
+  void run();
+};
+
+} // namespace saura
+
+#endif

+ 12 - 0
src/saura/core/base/types.hpp

@@ -0,0 +1,12 @@
+#ifndef SAURA_BASE_TYPES_HPP_
+#define SAURA_BASE_TYPES_HPP_
+
+#include <cstdint>
+
+#define Bytes(value) (value)
+#define Kilobytes(value) (value << 10)
+#define Megabytes(value) (value << 20)
+#define Gigabytes(value) ((uint64_t)(value) << 30)
+#define Terabytes(value) ((uint64_t)(value) << 40)
+
+#endif

+ 99 - 0
src/saura/core/memory/arena.cpp

@@ -0,0 +1,99 @@
+#include "arena.hpp"
+
+#include <spdlog/spdlog.h>
+
+#include <SDL3/SDL_stdinc.h>
+
+namespace saura {
+MemoryArena::MemoryArena() {
+  this->data = (uint8_t *)std::malloc(DEFAULT_ARENA_SIZE);
+  if (this->data == nullptr) {
+    spdlog::critical("Failed malloc!");
+  }
+
+  this->size = DEFAULT_ARENA_SIZE;
+  this->offset = 0;
+}
+
+MemoryArena::MemoryArena(uint64_t size) {
+  this->data = (uint8_t *)std::malloc(size);
+  if (this->data == nullptr) {
+    spdlog::critical("Failed malloc!");
+  }
+  this->size = size;
+  this->offset = 0;
+}
+
+MemoryArena::~MemoryArena() {}
+
+uint8_t *MemoryArena::push(uint64_t size) {
+  if (this->offset + size > this->size) {
+    spdlog::critical("Handle out-of-memory");
+  }
+  uint8_t *pos = (uint8_t *)this->data + this->offset;
+  this->offset += size;
+  return pos;
+}
+
+uint8_t *MemoryArena::push_zero(uint64_t size) {
+  uint8_t *memory = this->push(size);
+  if (memory == 0) {
+    return 0;
+  }
+  std::memset(memory, 0, size);
+  return memory;
+}
+
+uint8_t *MemoryArena::pop(uint64_t size) {
+  if (this->offset == 0) {
+    spdlog::critical("Handle out-of-memory");
+  }
+  this->offset -= size;
+  uint8_t *pos = (uint8_t *)this->offset - size;
+  return pos;
+}
+
+uint8_t *MemoryArena::pop_zero(uint64_t size) {
+  if (this->offset == 0) {
+    spdlog::critical("Handle out-of-memory");
+  }
+
+  std::memset((uint8_t *)this->data + this->offset - size, 0, size);
+
+  this->offset -= size;
+  uint8_t *pos = (uint8_t *)this->offset - size;
+  return pos;
+}
+
+void MemoryArena::reset() { this->offset = 0; }
+
+uint64_t MemoryArena::get_offset() { return this->offset; }
+
+void MemoryArenaTemp::begin(MemoryArena *arena) {
+  this->arena = arena;
+  this->offset = arena->offset;
+}
+
+void MemoryArenaTemp::end() { this->arena->offset = this->offset; }
+
+uint8_t *MemoryArenaTemp::push(uint64_t size) {
+  return this->arena->push(size);
+}
+
+uint8_t *MemoryArenaTemp::push_zero(uint64_t size) {
+  return this->arena->push_zero(size);
+}
+
+uint8_t *MemoryArenaTemp::pop() {
+  return this->arena->pop(this->arena->offset - this->offset);
+}
+
+uint8_t *MemoryArenaTemp::pop_zero() {
+  return this->arena->pop_zero(this->arena->offset - this->offset);
+}
+
+uint8_t *MemoryArenaTemp::pop_zero_all() {
+  return this->arena->pop_zero(this->arena->offset);
+}
+
+} // namespace saura

+ 52 - 0
src/saura/core/memory/arena.hpp

@@ -0,0 +1,52 @@
+#ifndef SAURA_MEMORY_ARENA_HPP_
+#define SAURA_MEMORY_ARENA_HPP_
+
+#include "saura/core/base/types.hpp"
+
+/*
+ - Permanent Allocation
+ - Transient Allocation
+ - Scratch/Temporary Allocation
+*/
+
+#define DEFAULT_ARENA_SIZE Gigabytes(4)
+#define DEFAULT_ALIGNMENT (8 * sizeof(void *))
+
+namespace saura {
+class MemoryArena {
+ public:
+  uint8_t *data;
+  uint64_t size;
+  uint64_t offset;
+
+public:
+  MemoryArena();
+  MemoryArena(uint64_t size);
+  ~MemoryArena();
+
+  uint8_t *push(uint64_t size);
+  uint8_t *push_zero(uint64_t size);
+  uint8_t *pop(uint64_t size);
+  uint8_t *pop_zero(uint64_t size);
+  void reset();
+  uint64_t get_offset();
+};
+
+class MemoryArenaTemp {
+ public:
+  MemoryArena *arena;
+  uint64_t offset;
+
+public:
+  void begin(MemoryArena *arena);
+  void end();
+
+  uint8_t *push(uint64_t size);
+  uint8_t *push_zero(uint64_t size);
+  uint8_t *pop();
+  uint8_t *pop_zero();
+  uint8_t *pop_zero_all();
+};
+} // namespace saura
+
+#endif

+ 141 - 0
src/saura/core/os/os.cpp

@@ -0,0 +1,141 @@
+#include "os.hpp"
+
+#include <spdlog/spdlog.h>
+
+namespace saura::os {
+Context::Context() {
+  this->root_window_ctx = std::make_shared<os::Window_Context>();
+  this->root_window_ctx->title = "(Saura Studios) [Dev]";
+  this->root_window_ctx->w = 1280;
+  this->root_window_ctx->h = 720;
+
+  ui_ctx = std::make_unique<ui::Context>(this->root_window_ctx);
+
+  std::locale::global(std::locale("en_US.UTF-8"));
+
+  if (!SDL_Init(SDL_INIT_VIDEO)) {
+    throw std::runtime_error("Failed SDL_Init!");
+  }
+
+  SDL_WindowFlags window_flags =
+      SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY;
+
+  this->root_window_ctx->handle = SDL_CreateWindow(
+      this->root_window_ctx->title.c_str(), this->root_window_ctx->w,
+      this->root_window_ctx->h, window_flags);
+  if (this->root_window_ctx->handle == nullptr) {
+    throw std::runtime_error("Failed SDL_CreateWindow!");
+  }
+
+  this->root_window_ctx->renderer =
+      SDL_CreateRenderer(this->root_window_ctx->handle, nullptr);
+  if (this->root_window_ctx->renderer == nullptr) {
+    throw std::runtime_error("Failed SDL_CreateRenderer!");
+  }
+  SDL_SetRenderVSync(this->root_window_ctx->renderer,
+                     SDL_RENDERER_VSYNC_DISABLED);
+  SDL_SetWindowPosition(this->root_window_ctx->handle, SDL_WINDOWPOS_CENTERED,
+                        SDL_WINDOWPOS_CENTERED);
+
+  ui_ctx->init();
+
+  SDL_ShowWindow(this->root_window_ctx->handle);
+}
+
+Context::~Context() {
+  ui_ctx->deinit();
+
+  SDL_DestroyRenderer(this->root_window_ctx->renderer);
+  SDL_DestroyWindow(this->root_window_ctx->handle);
+  SDL_Quit();
+}
+
+void Context::draw_begin() { this->ui_ctx->begin(); }
+
+void Context::draw_end() { this->ui_ctx->end(); }
+
+void Context::update() {
+  int w, h = 0;
+  SDL_GetWindowSize(this->root_window_ctx->handle, &w, &h);
+  this->root_window_ctx->w = w;
+  this->root_window_ctx->h = h;
+}
+
+bool Context::update_events() {
+  SDL_Event event = {};
+  while (SDL_PollEvent(&event)) {
+    switch (event.type) {
+    case SDL_EVENT_KEY_UP: {
+      if (event.key.key == SDLK_ESCAPE) {
+        return false;
+      }
+      break;
+    }
+    case SDL_EVENT_QUIT: {
+      return false;
+    }
+    case SDL_EVENT_WINDOW_CLOSE_REQUESTED: {
+      if (event.window.windowID ==
+          SDL_GetWindowID(this->root_window_ctx->handle)) {
+        return false;
+      }
+      break;
+    }
+    }
+    ui_ctx->update_events(event);
+  }
+  return true;
+}
+
+void Context::sleep(double ms) { SDL_Delay((Uint32)ms * 1000); }
+
+int Context::get_root_window_width() { return this->root_window_ctx->w; }
+
+int Context::get_root_window_height() { return this->root_window_ctx->h; }
+
+void Context::set_root_window_title(std::string title) {
+  SDL_SetWindowTitle(this->root_window_ctx.get()->handle, title.c_str());
+}
+
+double Context::get_performance_frequency() {
+  return (double)SDL_GetPerformanceFrequency();
+}
+
+double Context::get_performance_counter() {
+  return (double)SDL_GetPerformanceCounter();
+}
+
+uint32_t Context::get_dpi_scale() {
+  return SDL_GetWindowDisplayScale(this->root_window_ctx.get()->handle);
+}
+
+std::string Context::get_safe_getenv(const std::string key) {
+  return SDL_getenv(key.c_str());
+}
+
+fs::path Context::get_home_config_path() {
+  fs::path res = {};
+
+#if defined(_WIN32)
+  auto config = get_safe_getenv("APPDATA");
+
+  if (config.empty()) {
+    throw std::runtime_error("APPDATA environment variable not found");
+  }
+
+  res = fs::path(config);
+#elif defined(__linux__)
+  auto config = get_safe_getenv("HOME");
+
+  if (config.empty()) {
+    throw std::runtime_error("HOME environment variable not found");
+  }
+
+  res = fs::path(config) / ".config";
+#endif
+
+  return res;
+}
+
+std::string Context::get_user_name() { return get_safe_getenv("USERNAME"); }
+} // namespace saura::os

+ 47 - 0
src/saura/core/os/os.hpp

@@ -0,0 +1,47 @@
+#ifndef SAURA_OS_HPP_
+#define SAURA_OS_HPP_
+
+#include <SDL3/SDL.h>
+
+#include <filesystem>
+#include <memory>
+
+#include "saura/core/os/os_types.hpp"
+#include "saura/core/ui/ui.hpp"
+
+namespace fs = std::filesystem;
+
+namespace saura::os {
+class Context {
+private:
+  std::shared_ptr<Window_Context> root_window_ctx;
+  std::unique_ptr<ui::Context> ui_ctx;
+
+public:
+  Context();
+  ~Context();
+
+  void draw_begin();
+  void draw_end();
+
+  void update();
+  bool update_events();
+  void sleep(double ms);
+
+  int get_root_window_width();
+  int get_root_window_height();
+
+  void set_root_window_title(std::string title);
+
+  double get_performance_frequency();
+  double get_performance_counter();
+
+  uint32_t get_dpi_scale();
+
+  static std::string get_safe_getenv(const std::string key);
+  static fs::path get_home_config_path();
+  static std::string get_user_name();
+};
+} // namespace saura::os
+
+#endif

+ 19 - 0
src/saura/core/os/os_types.hpp

@@ -0,0 +1,19 @@
+#ifndef SAURA_OS_TYPES_HPP_
+#define SAURA_OS_TYPES_HPP_
+
+#include <SDL3/SDL_render.h>
+#include <SDL3/SDL_video.h>
+
+#include <cstdint>
+#include <string>
+
+namespace saura::os {
+struct Window_Context {
+  uint32_t w, h;
+  std::string title;
+  SDL_Window *handle;
+  SDL_Renderer *renderer;
+};
+} // namespace saura
+
+#endif

+ 74 - 0
src/saura/core/ui/ui.cpp

@@ -0,0 +1,74 @@
+#include "ui.hpp"
+
+#include <SDL3/SDL_video.h>
+#include <imgui.h>
+#include <imgui_impl_sdl3.h>
+#include <imgui_impl_sdlrenderer3.h>
+
+namespace saura::ui {
+Context::Context(std::weak_ptr<os::Window_Context> window_ctx) {
+  this->window_ctx = window_ctx;
+}
+
+void Context::init() {
+  IMGUI_CHECKVERSION();
+  ImGui::CreateContext();
+  ImGuiIO &io = ImGui::GetIO();
+
+  float dpi_scale = SDL_GetWindowDisplayScale(window_ctx.lock()->handle);
+  float font_size = 16.0f;
+
+  io.Fonts->AddFontFromFileTTF("res/fonts/InterRegular.ttf", font_size * dpi_scale, nullptr,
+                               io.Fonts->GetGlyphRangesCyrillic());
+
+  (void)io;
+  io.ConfigFlags |=
+      ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
+  io.ConfigFlags |=
+      ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
+
+  // Setup Dear ImGui style
+  ImGui::StyleColorsDark();
+  ImGui::GetStyle().ScaleAllSizes(dpi_scale);
+
+  // Setup Platform/Renderer backends
+  ImGui_ImplSDL3_InitForSDLRenderer(this->window_ctx.lock()->handle,
+                                    this->window_ctx.lock()->renderer);
+  ImGui_ImplSDLRenderer3_Init(this->window_ctx.lock()->renderer);
+}
+
+void Context::deinit() {
+  ImGui_ImplSDLRenderer3_Shutdown();
+  ImGui_ImplSDL3_Shutdown();
+  ImGui::DestroyContext();
+}
+
+void Context::update_events(SDL_Event &event) {
+  ImGui_ImplSDL3_ProcessEvent(&event);
+}
+
+void Context::begin() {
+  ImGuiIO *io = &ImGui::GetIO();
+
+  ImGui_ImplSDLRenderer3_NewFrame();
+  ImGui_ImplSDL3_NewFrame();
+  ImGui::NewFrame();
+}
+
+void Context::end() {
+  ImGuiIO *io = &ImGui::GetIO();
+  ImVec4 clear_color = {0.15f, 0.15f, 0.15f, 0.0f};
+
+  ImGui::Render();
+  SDL_SetRenderScale(this->window_ctx.lock()->renderer,
+                     io->DisplayFramebufferScale.x,
+                     io->DisplayFramebufferScale.y);
+  SDL_SetRenderDrawColorFloat(this->window_ctx.lock()->renderer, clear_color.x,
+                              clear_color.y, clear_color.z, clear_color.w);
+  SDL_RenderClear(this->window_ctx.lock()->renderer);
+  ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(),
+                                        this->window_ctx.lock()->renderer);
+
+  SDL_RenderPresent(this->window_ctx.lock()->renderer);
+}
+} // namespace saura::ui

+ 28 - 0
src/saura/core/ui/ui.hpp

@@ -0,0 +1,28 @@
+#ifndef SAURA_UI_HPP_
+#define SAURA_UI_HPP_
+
+#include <memory>
+
+#include <SDL3/SDL_events.h>
+
+#include "saura/core/os/os_types.hpp"
+
+namespace saura::ui {
+class Context {
+private:
+  std::weak_ptr<os::Window_Context> window_ctx;
+
+public:
+  Context(std::weak_ptr<os::Window_Context> window_ctx);
+
+  void init();
+  void deinit();
+
+  void update_events(SDL_Event &event);
+
+  void begin();
+  void end();
+};
+} // namespace saura::ui
+
+#endif

+ 10 - 0
tools/build-debug.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+
+VCPKG_ROOT="$HOME/.local/share/vcpkg"
+PATH="$PATH:$VCPKG_ROOT"
+
+mkdir -p ./build/Debug
+ln -s $PWD/res $PWD/build/Debug/
+
+cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
+cmake --build build --config Debug -j $(nproc)

+ 10 - 0
tools/build-release.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+
+VCPKG_ROOT="$HOME/.local/share/vcpkg"
+PATH="$PATH:$VCPKG_ROOT"
+
+mkdir -p ./build/Release
+ln -s $PWD/res $PWD/build/Release/
+
+cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
+cmake --build build --config Release -j $(nproc)

+ 3 - 0
tools/run-debug.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+./build/Debug/$1

+ 3 - 0
tools/run-release.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+./build/Release/$1

+ 13 - 0
tools/setup.sh

@@ -0,0 +1,13 @@
+#!/bin/sh
+
+VCPKG_ROOT="$HOME/.local/share/vcpkg"
+PATH="$PATH:$VCPKG_ROOT"
+
+# Создаём директорию и сразу клонируем официальный репозиторий Microsoft
+git clone https://github.com/microsoft/vcpkg $VCPKG_ROOT
+
+# Делаем первый bootstrap (собирает сам vcpkg)
+$VCPKG_ROOT/bootstrap-vcpkg.sh
+
+vcpkg version
+vcpkg install

+ 17 - 0
vcpkg.json

@@ -0,0 +1,17 @@
+{
+  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
+  "name": "bookmarks",
+  "version": "0.1.0",
+  "dependencies": [
+    {
+      "name": "imgui",
+      "features": ["sdl3-binding", "sdl3-renderer-binding"]
+    },
+    "spdlog",
+    "sdl3",
+    "glm",
+    "cpr",
+    "nlohmann-json",
+    "gtest"
+  ]
+}