aboutsummaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-20 02:27:02 +0200
committerhachem <im@hachem.wtf>2026-08-20 02:40:07 +0200
commit5b361d81dbd2af0d0e99b9eb1adebea76ff05db0 (patch)
tree34db1df17e8c7248aeeb92156d635d31c2484e3f /src/core
parente3abaaf59777258ba06705135a0cafcb5918ad12 (diff)
[chore]: MASSIVE refactor + more vulkan bs
Diffstat (limited to 'src/core')
-rw-r--r--src/core/application.cpp238
-rw-r--r--src/core/application.h53
-rw-r--r--src/core/camera.cpp225
-rw-r--r--src/core/camera.h131
-rw-r--r--src/core/event.h362
-rw-r--r--src/core/hdri_manager.cpp64
-rw-r--r--src/core/hdri_manager.h48
-rw-r--r--src/core/log.cpp156
-rw-r--r--src/core/log.h156
-rw-r--r--src/core/memory.h24
-rw-r--r--src/core/settings_manager.cpp200
-rw-r--r--src/core/settings_manager.h82
-rw-r--r--src/core/state.h19
-rw-r--r--src/core/state_manager.cpp88
-rw-r--r--src/core/state_manager.h41
-rw-r--r--src/core/theme_manager.cpp291
-rw-r--r--src/core/theme_manager.h27
-rw-r--r--src/core/window.cpp341
-rw-r--r--src/core/window.h78
19 files changed, 2624 insertions, 0 deletions
diff --git a/src/core/application.cpp b/src/core/application.cpp
new file mode 100644
index 0000000..667fc73
--- /dev/null
+++ b/src/core/application.cpp
@@ -0,0 +1,238 @@
+#include "application.h"
+
+#include "rendering/renderer.h"
+#include "settings_manager.h"
+
+#include "states/simulation_state.h"
+#include "states/config_state.h"
+#include "states/world_builder_state.h"
+
+#include <GLFW/glfw3.h>
+#include <imgui.h>
+#include <ImGuizmo.h>
+
+#include "platform/vulkan/vulkan_renderer.h"
+
+#ifdef __APPLE__
+#include "platform/metal/metal_context.h"
+#include "platform/vulkan/vulkan_context.h"
+#endif
+
+namespace Donut
+{
+ Application* Application::s_instance = nullptr;
+
+ Application::Application(const std::string& name, int width, int height)
+ : m_running(true), m_minimized(false)
+ {
+ s_instance = this;
+
+ // Select the render API before the window is created: the window is
+ // built differently for Vulkan (GLFW_NO_API) than for OpenGL.
+ Logger::init();
+ SettingsManager::initialize();
+ {
+ const auto& settings = SettingsManager::get_settings_const();
+ RendererAPI::set_api(settings.graphics.render_api == "Vulkan"
+ ? RendererAPI::API::Vulkan : RendererAPI::API::OpenGL);
+ }
+
+ if (RendererAPI::get_api() == RendererAPI::API::Vulkan)
+ vulkan_prepare_glfw(); // must precede glfwInit() inside the Window ctor
+
+ m_window = create_scope<Window>(name, width, height);
+ m_window->set_event_callback([this](Event& event)
+ {
+ on_event(event);
+ });
+
+ on_init();
+ }
+
+ Application::~Application()
+ {
+ on_shutdown();
+ s_instance = nullptr;
+ }
+
+ auto Application::run() -> void
+ {
+ while (m_running)
+ {
+ if (m_vulkan_renderer)
+ {
+ glfwPollEvents(); // before the ImGui frame so input is current
+ if (!m_minimized)
+ m_vulkan_renderer->draw_frame(glm::vec4(0.05f, 0.06f, 0.10f, 1.0f),
+ [this] { build_vulkan_ui(); });
+ }
+ else
+ {
+ if (!m_minimized)
+ {
+ on_update();
+ on_render();
+ }
+ m_window->on_update(); // polls events + swaps buffers (OpenGL)
+ }
+ }
+ }
+
+ auto Application::build_vulkan_ui() -> void
+ {
+ // Placeholder UI proving the ImGui Vulkan backend renders. The real
+ // application UI (states' on_im_ui_render) moves here once the scene is
+ // ported to Vulkan (B-3).
+ ImGui::Begin("Donut - Vulkan backend");
+ ImGui::Text("Geodesic black hole through Vulkan (MoltenVK).");
+ ImGui::Text("%.1f FPS", ImGui::GetIO().Framerate);
+ ImGui::Separator();
+ ImGui::TextWrapped("Drag to orbit, scroll to zoom. HDRI starfield is live; "
+ "next is porting the simulation UI to Vulkan.");
+ ImGui::End();
+ }
+
+ auto Application::close() -> void
+ {
+ m_running = false;
+ }
+
+ auto Application::on_event(Event& event) -> void
+ {
+ EventDispatcher dispatcher(event);
+ dispatcher.dispatch<WindowCloseEvent>([this, &event](WindowCloseEvent& e)
+ {
+ m_running = false;
+ event.handled = true;
+ return true;
+ });
+
+ dispatcher.dispatch<WindowResizeEvent>([this, &event](WindowResizeEvent& e)
+ {
+ if (e.get_width() == 0 || e.get_height() == 0)
+ m_minimized = true;
+ else
+ m_minimized = false;
+
+ if (m_vulkan_renderer)
+ {
+ m_vulkan_renderer->on_resize(e.get_width(), e.get_height());
+ }
+ else
+ {
+ Renderer::on_window_resize(e.get_width(), e.get_height());
+ if (m_engine) m_engine->set_window_dimensions(e.get_width(), e.get_height());
+ }
+ event.handled = true;
+ return true;
+ });
+
+ dispatcher.dispatch<KeyPressedEvent>([this, &event](KeyPressedEvent& e)
+ {
+ if (!m_state_manager)
+ return true;
+ if (e.get_key_code() == GLFW_KEY_1)
+ {
+ m_state_manager->switch_to_state("Config");
+ event.handled = true;
+ return true;
+ }
+ else if (e.get_key_code() == GLFW_KEY_2)
+ {
+ m_state_manager->switch_to_state("Simulation");
+ event.handled = true;
+ return true;
+ }
+ else if (e.get_key_code() == GLFW_KEY_3)
+ {
+ m_state_manager->switch_to_state("WorldBuilder");
+ event.handled = true;
+ return true;
+ }
+
+ return true;
+ });
+
+ if (m_state_manager)
+ m_state_manager->on_event(event);
+ }
+
+ auto Application::on_init() -> void
+ {
+ // (Logger, settings and API selection happen in the constructor.)
+
+ if (RendererAPI::get_api() == RendererAPI::API::Vulkan)
+ {
+ int w = 0, h = 0;
+ glfwGetFramebufferSize((GLFWwindow*)m_window->get_native_window(), &w, &h);
+ m_vulkan_renderer = create_scope<VulkanRenderer>();
+ if (!m_vulkan_renderer->init(m_window->get_native_window(), w, h))
+ DONUT_ERROR("Vulkan renderer initialization failed");
+ else
+ m_vulkan_renderer->init_im_gui();
+ // Scene rendering hooks in here in the B-3 phase; the Vulkan path now
+ // clears, presents, and draws the ImGui UI.
+ return;
+ }
+
+ Renderer::init();
+ m_window->init_im_gui();
+
+ Renderer::on_window_resize(1280, 720);
+ RenderCommand::set_face_culling(false);
+
+ m_engine = create_scope<Engine>();
+ m_engine->set_window_dimensions(1280, 720);
+
+ m_state_manager = create_scope<StateManager>();
+ m_state_manager->register_state("Config", create_scope<ConfigState>());
+ m_state_manager->register_state("Simulation", create_scope<SimulationState>());
+ m_state_manager->register_state("WorldBuilder", create_scope<WorldBuilderState>());
+ m_state_manager->switch_to_state("Config");
+ }
+
+ auto Application::on_shutdown() -> void
+ {
+ if (m_vulkan_renderer)
+ {
+ m_vulkan_renderer->shutdown();
+ m_vulkan_renderer.reset();
+ }
+ else
+ {
+ if (m_state_manager)
+ m_state_manager->shutdown();
+ Renderer::shutdown();
+ }
+ SettingsManager::shutdown();
+ Logger::shutdown();
+ }
+
+ auto Application::on_update() -> void
+ {
+ float current_frame = (float)glfwGetTime();
+ m_delta_time = current_frame - m_last_frame;
+ m_last_frame = current_frame;
+
+ m_state_manager->update(m_delta_time);
+ }
+
+ auto Application::on_render() -> void
+ {
+ m_state_manager->render();
+ m_window->begin_im_gui_frame();
+
+ ImGuizmo::BeginFrame();
+ ImGuizmo::SetOrthographic(false);
+
+ setup_docking_layout();
+ m_state_manager->on_im_ui_render();
+ m_window->end_im_gui_frame();
+ }
+
+ auto Application::setup_docking_layout() -> void
+ {
+ ImGuiViewport* viewport = ImGui::GetMainViewport();
+ ImGui::DockSpaceOverViewport(0, viewport, ImGuiDockNodeFlags_PassthruCentralNode);
+ }
+};
diff --git a/src/core/application.h b/src/core/application.h
new file mode 100644
index 0000000..06c4e94
--- /dev/null
+++ b/src/core/application.h
@@ -0,0 +1,53 @@
+#pragma once
+
+#include "state_manager.h"
+#include "memory.h"
+#include "window.h"
+#include "event.h"
+#include "log.h"
+
+#include "engine/engine.h"
+
+namespace Donut
+{
+ class VulkanRenderer; // live-window Vulkan backend (platform/vulkan)
+
+ class Application
+ {
+ public:
+ Application(const std::string& name = "Donut",
+ int width = 1280, int height = 720);
+ ~Application();
+
+ auto run() -> void;
+ auto close() -> void;
+
+ auto get_window() -> Window& { return *m_window; }
+ auto get_state_manager() -> StateManager& { return *m_state_manager; }
+ auto get_engine() -> Engine& { return *m_engine; }
+ static auto get() -> Application& { return *s_instance; }
+
+ private:
+ auto on_init() -> void;
+ auto on_shutdown() -> void;
+ auto on_update() -> void;
+ auto on_render() -> void;
+ auto on_event(Event& event) -> void;
+ auto setup_docking_layout() -> void;
+ auto build_vulkan_ui() -> void; // ImGui UI built each frame on the Vulkan path
+
+ private:
+ Scope<StateManager> m_state_manager;
+ Scope<Window> m_window;
+ Scope<Engine> m_engine;
+ Scope<VulkanRenderer> m_vulkan_renderer; // non-null only when the Vulkan API is selected
+
+ bool m_running;
+ bool m_minimized;
+
+ float m_delta_time = 0.0f;
+ float m_last_frame = 0.0f;
+
+ static Application* s_instance;
+ };
+}
diff --git a/src/core/camera.cpp b/src/core/camera.cpp
new file mode 100644
index 0000000..35b7a2f
--- /dev/null
+++ b/src/core/camera.cpp
@@ -0,0 +1,225 @@
+#include "camera.h"
+
+#include <glm/gtc/quaternion.hpp>
+#include <GLFW/glfw3.h>
+
+namespace Donut
+{
+ Camera::Camera(float fov, float aspect_ratio, float near_plane, float far_plane)
+ : m_fov(fov), m_aspect_ratio(aspect_ratio), m_near_plane(near_plane), m_far_plane(far_plane)
+ {
+ recalculate_projection_matrix();
+ recalculate_view_matrix();
+ }
+
+ auto Camera::set_projection(float fov, float aspect_ratio, float near_plane, float far_plane) -> void
+ {
+ m_fov = fov;
+ m_aspect_ratio = aspect_ratio;
+ m_near_plane = near_plane;
+ m_far_plane = far_plane;
+ recalculate_projection_matrix();
+ }
+
+ auto Camera::recalculate_projection_matrix() -> void
+ {
+ m_projection_matrix = glm::perspective(glm::radians(m_fov), m_aspect_ratio, m_near_plane, m_far_plane);
+ m_view_projection_matrix = m_projection_matrix * m_view_matrix;
+ }
+
+ auto Camera::recalculate_view_matrix() -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ float pitch = glm::radians(m_rotation.x);
+ float yaw = glm::radians(m_rotation.y);
+ float roll = glm::radians(m_rotation.z);
+
+ glm::vec3 direction;
+ direction.x = cos(yaw) * cos(pitch);
+ direction.y = sin(pitch);
+ direction.z = sin(yaw) * cos(pitch);
+
+ glm::vec3 world_up(0.0f, 1.0f, 0.0f);
+ glm::vec3 front = glm::normalize(direction);
+ glm::vec3 right = glm::normalize(glm::cross(front, world_up));
+ glm::vec3 up = glm::normalize(glm::cross(right, front));
+
+ m_view_matrix = glm::lookAt(m_position, m_position + front, up);
+ m_view_projection_matrix = m_projection_matrix * m_view_matrix;
+ }
+ else if (m_camera_mode == CameraMode::Orbital)
+ {
+ glm::vec3 position = get_orbital_position();
+ glm::vec3 target = m_orbital_target;
+ glm::vec3 up(0.0f, 1.0f, 0.0f);
+
+ m_view_matrix = glm::lookAt(position, target, up);
+ m_view_projection_matrix = m_projection_matrix * m_view_matrix;
+ }
+ }
+
+ auto Camera::get_forward_direction() const -> glm::vec3
+ {
+ float pitch = glm::radians(m_rotation.x);
+ float yaw = glm::radians(m_rotation.y);
+
+ glm::vec3 direction;
+ direction.x = cos(yaw) * cos(pitch);
+ direction.y = sin(pitch);
+ direction.z = sin(yaw) * cos(pitch);
+
+ return glm::normalize(direction);
+ }
+
+ auto Camera::get_right_direction() const -> glm::vec3
+ {
+ glm::vec3 world_up(0.0f, 1.0f, 0.0f);
+ return glm::normalize(glm::cross(get_forward_direction(), world_up));
+ }
+
+ auto Camera::get_up_direction() const -> glm::vec3
+ {
+ return glm::normalize(glm::cross(get_right_direction(), get_forward_direction()));
+ }
+
+ auto Camera::on_mouse_move(float x_offset, float y_offset, bool constrain_pitch) -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ x_offset *= m_mouse_sensitivity;
+ y_offset *= m_mouse_sensitivity;
+
+ m_rotation.y += x_offset;
+ m_rotation.x += y_offset;
+
+ if (constrain_pitch)
+ {
+ if (m_rotation.x > 89.0f)
+ m_rotation.x = 89.0f;
+ if (m_rotation.x < -89.0f)
+ m_rotation.x = -89.0f;
+ }
+
+ recalculate_view_matrix();
+ }
+ }
+
+ auto Camera::move_forward(float delta_time) -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ m_position += get_forward_direction() * m_movement_speed * delta_time;
+ recalculate_view_matrix();
+ }
+ }
+
+ auto Camera::move_backward(float delta_time) -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ m_position -= get_forward_direction() * m_movement_speed * delta_time;
+ recalculate_view_matrix();
+ }
+ }
+
+ auto Camera::move_right(float delta_time) -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ m_position += get_right_direction() * m_movement_speed * delta_time;
+ recalculate_view_matrix();
+ }
+ }
+
+ auto Camera::move_left(float delta_time) -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ m_position -= get_right_direction() * m_movement_speed * delta_time;
+ recalculate_view_matrix();
+ }
+ }
+
+ auto Camera::move_up(float delta_time) -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ glm::vec3 world_up(0.0f, 1.0f, 0.0f);
+ m_position += world_up * m_movement_speed * delta_time;
+ recalculate_view_matrix();
+ }
+ }
+
+ auto Camera::move_down(float delta_time) -> void
+ {
+ if (m_camera_mode == CameraMode::FPS)
+ {
+ glm::vec3 world_up(0.0f, 1.0f, 0.0f);
+ m_position -= world_up * m_movement_speed * delta_time;
+ recalculate_view_matrix();
+ }
+ }
+
+ auto Camera::get_orbital_position() const -> glm::vec3
+ {
+ float clamped_elevation = glm::clamp(m_elevation, 0.01f, float(std::numbers::pi) - 0.01f);
+ return glm::vec3
+ (
+ m_orbital_radius * sin(clamped_elevation) * cos(m_azimuth),
+ m_orbital_radius * cos(clamped_elevation),
+ m_orbital_radius * sin(clamped_elevation) * sin(m_azimuth)
+ );
+ }
+
+ auto Camera::update_orbital() -> void
+ {
+ m_orbital_target = glm::vec3(0.0f, 0.0f, 0.0f);
+ if (m_dragging || m_panning)
+ m_moving = true;
+ else
+ m_moving = false;
+ recalculate_view_matrix();
+ }
+
+ auto Camera::process_orbital_mouse_move(double x, double y) -> void
+ {
+ if (m_dragging && !m_panning)
+ {
+ float dx = float(x - m_last_x_orbital);
+ float dy = float(y - m_last_y_orbital);
+
+ m_azimuth += dx * m_orbital_speed;
+ m_elevation -= dy * m_orbital_speed;
+ m_elevation = glm::clamp(m_elevation, 0.01f, float(std::numbers::pi) - 0.01f);
+ }
+
+ m_last_x_orbital = x;
+ m_last_y_orbital = y;
+ update_orbital();
+ }
+
+ auto Camera::process_orbital_mouse_button(int button, int action, int mods) -> void
+ {
+ if (button == GLFW_MOUSE_BUTTON_LEFT)
+ {
+ if (action == GLFW_PRESS)
+ {
+ m_dragging = true;
+ m_panning = false;
+ }
+ else if (action == GLFW_RELEASE)
+ {
+ m_dragging = false;
+ m_panning = false;
+ }
+ }
+ }
+
+ auto Camera::process_orbital_scroll(double x_offset, double y_offset) -> void
+ {
+ m_orbital_radius -= y_offset * m_zoom_speed;
+ m_orbital_radius = glm::clamp(m_orbital_radius, m_orbital_min_radius, m_orbital_max_radius);
+ update_orbital();
+ }
+}
diff --git a/src/core/camera.h b/src/core/camera.h
new file mode 100644
index 0000000..c997c35
--- /dev/null
+++ b/src/core/camera.h
@@ -0,0 +1,131 @@
+#pragma once
+
+#include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+
+#include <numbers>
+
+namespace Donut
+{
+ enum class CameraMode
+ {
+ FPS,
+ Orbital
+ };
+
+ class Camera
+ {
+ public:
+ Camera(float fov = 45.0f, float aspect_ratio = 16.0f / 9.0f,
+ float near_plane = 0.1f, float far_plane = 100.0f);
+ ~Camera() = default;
+
+ auto set_position(const glm::vec3& position) -> void { m_position = position; recalculate_view_matrix(); }
+ auto set_rotation(const glm::vec3& rotation) -> void { m_rotation = rotation; recalculate_view_matrix(); }
+
+ auto get_position() const -> const glm::vec3& { return m_position; }
+ auto get_rotation() const -> const glm::vec3& { return m_rotation; }
+
+ auto get_forward_direction() const -> glm::vec3;
+ auto get_right_direction() const -> glm::vec3;
+ auto get_up_direction() const -> glm::vec3;
+
+ auto get_projection_matrix() const -> const glm::mat4& { return m_projection_matrix; }
+ auto get_view_matrix() const -> const glm::mat4& { return m_view_matrix; }
+ auto get_view_projection_matrix() const -> const glm::mat4& { return m_view_projection_matrix; }
+
+ auto set_projection(float fov, float aspect_ratio, float near_plane, float far_plane) -> void;
+ auto on_mouse_move(float x_offset, float y_offset, bool constrain_pitch = true) -> void;
+
+ auto move_forward(float delta_time) -> void;
+ auto move_backward(float delta_time) -> void;
+ auto move_right(float delta_time) -> void;
+ auto move_left(float delta_time) -> void;
+ auto move_up(float delta_time) -> void;
+ auto move_down(float delta_time) -> void;
+
+ auto set_mouse_sensitivity(float sensitivity) -> void { m_mouse_sensitivity = sensitivity; }
+ auto get_mouse_sensitivity() const -> float { return m_mouse_sensitivity; }
+
+ auto set_movement_speed(float speed) -> void { m_movement_speed = speed; }
+ auto get_movement_speed() const -> float { return m_movement_speed; }
+
+ auto set_orbital_target(const glm::vec3& target) -> void { m_orbital_target = target; }
+ auto get_orbital_target() const -> const glm::vec3& { return m_orbital_target; }
+
+ auto set_orbital_radius(double radius) -> void { m_orbital_radius = radius; }
+ auto get_orbital_radius() const -> double { return m_orbital_radius; }
+
+ auto set_orbital_limits(double min_radius, double max_radius) -> void
+ {
+ m_orbital_min_radius = min_radius;
+ m_orbital_max_radius = max_radius;
+ }
+
+ auto set_orbital_speed(float speed) -> void { m_orbital_speed = speed; }
+ auto get_orbital_speed() const -> float { return m_orbital_speed; }
+
+ auto set_zoom_speed(double speed) -> void { m_zoom_speed = speed; }
+ auto get_zoom_speed() const -> double { return m_zoom_speed; }
+
+ auto set_azimuth(float azimuth) -> void { m_azimuth = azimuth; }
+ auto get_azimuth() const -> float { return m_azimuth; }
+
+ auto set_elevation(float elevation) -> void { m_elevation = elevation; }
+ auto get_elevation() const -> float { return m_elevation; }
+
+ auto get_orbital_position() const -> glm::vec3;
+ auto update_orbital() -> void;
+ auto process_orbital_mouse_move(double x, double y) -> void;
+ auto process_orbital_mouse_button(int button, int action, int mods) -> void;
+ auto process_orbital_scroll(double x_offset, double y_offset) -> void;
+
+ auto set_camera_mode(CameraMode mode) -> void { m_camera_mode = mode; }
+ auto get_camera_mode() const -> CameraMode { return m_camera_mode; }
+
+ auto is_dragging() const -> bool { return m_dragging; }
+ auto is_panning() const -> bool { return m_panning; }
+ auto is_moving() const -> bool { return m_moving; }
+ auto get_last_x() const -> double { return m_last_x; }
+ auto get_last_y() const -> double { return m_last_y; }
+
+ private:
+ auto recalculate_view_matrix() -> void;
+ auto recalculate_projection_matrix() -> void;
+
+ private:
+ CameraMode m_camera_mode = CameraMode::FPS;
+
+ glm::mat4 m_projection_matrix;
+ glm::mat4 m_view_matrix;
+ glm::mat4 m_view_projection_matrix;
+
+ glm::vec3 m_position = { 0.0f, 0.0f, 3.0f };
+ glm::vec3 m_rotation = { 0.0f, 0.0f, 0.0f };
+
+ float m_fov = 45.0f;
+ float m_aspect_ratio = 16.0f / 9.0f;
+ float m_near_plane = 0.1f;
+ float m_far_plane = 100.0f;
+
+ float m_mouse_sensitivity = 0.1f;
+ float m_movement_speed = 5.0f;
+ bool m_first_mouse = true;
+ float m_last_x = 0.0f;
+ float m_last_y = 0.0f;
+
+ glm::vec3 m_orbital_target = glm::vec3(0.0f, 0.0f, 0.0f);
+ double m_orbital_radius = 6.34194e10;
+ double m_orbital_min_radius = 1e10;
+ double m_orbital_max_radius = 1e12;
+ float m_azimuth = 0.0f;
+ float m_elevation = static_cast<float>(std::numbers::pi) / 2.0f;
+ float m_orbital_speed = 0.01f;
+ double m_zoom_speed = 25e9f;
+ bool m_dragging = false;
+ bool m_panning = false;
+ bool m_moving = false;
+ double m_last_x_orbital = 0.0;
+ double m_last_y_orbital = 0.0;
+ };
+}
diff --git a/src/core/event.h b/src/core/event.h
new file mode 100644
index 0000000..1891206
--- /dev/null
+++ b/src/core/event.h
@@ -0,0 +1,362 @@
+#pragma once
+
+#include <functional>
+#include <unordered_map>
+#include <vector>
+#include <string>
+#include <sstream>
+
+namespace Donut
+{
+ class Event;
+ class EventDispatcher;
+
+ enum class EventType
+ {
+ None = 0,
+
+ WindowClose,
+ WindowResize,
+ WindowFocus,
+ WindowLostFocus,
+ WindowMoved,
+
+ KeyPressed,
+ KeyReleased,
+ KeyTyped,
+
+ MouseButtonPressed,
+ MouseButtonReleased,
+ MouseMoved,
+ MouseScrolled
+ };
+
+ enum EventCategory
+ {
+ None = 0,
+ EventCategoryApplication = 1 << 0,
+ EventCategoryInput = 1 << 1,
+ EventCategoryKeyboard = 1 << 2,
+ EventCategoryMouse = 1 << 3,
+ EventCategoryMouseButton = 1 << 4
+ };
+
+ class Event
+ {
+ public:
+ virtual ~Event() = default;
+
+ virtual auto get_event_type() const -> EventType = 0;
+ virtual auto get_name() const -> const char* = 0;
+ virtual auto get_category_flags() const -> int = 0;
+ virtual auto to_string() const -> std::string { return get_name(); }
+
+ auto is_in_category(EventCategory category) -> bool { return get_category_flags() & category; }
+
+ bool handled = false;
+ };
+
+ #define EVENT_CLASS_TYPE(type) \
+ static auto get_static_type() -> EventType { return EventType::type; } \
+ virtual auto get_event_type() const -> EventType override { return get_static_type(); } \
+ virtual auto get_name() const -> const char* override { return #type; }
+
+ #define APPLICATION_EVENT_CLASS_TYPE(type) \
+ EVENT_CLASS_TYPE(type) \
+ virtual auto get_category_flags() const -> int override { return EventCategoryApplication; }
+
+ #define MOUSE_EVENT_CLASS_TYPE(type) \
+ EVENT_CLASS_TYPE(type) \
+ virtual auto get_category_flags() const -> int override { return EventCategoryMouse | \
+ EventCategoryInput; }
+
+ #define MOUSE_BUTTON_EVENT_CLASS_TYPE(type) \
+ EVENT_CLASS_TYPE(type) \
+ virtual auto get_category_flags() const -> int override { return EventCategoryMouseButton | \
+ EventCategoryMouse | \
+ EventCategoryInput; }
+
+ #define KEYBOARD_EVENT_CLASS_TYPE(type) \
+ EVENT_CLASS_TYPE(type) \
+ virtual auto get_category_flags() const -> int override { return EventCategoryKeyboard | \
+ EventCategoryInput; }
+
+ class WindowResizeEvent
+ : public Event
+ {
+ public:
+ WindowResizeEvent(unsigned int width, unsigned int height)
+ : m_width(width), m_height(height) { }
+
+ auto get_width() const -> unsigned int { return m_width; }
+ auto get_height() const -> unsigned int { return m_height; }
+
+ auto to_string() const -> std::string override
+ {
+ return "WindowResizeEvent: " + std::to_string(m_width) + ", " + std::to_string(m_height);
+ }
+
+ APPLICATION_EVENT_CLASS_TYPE(WindowResize)
+
+ private:
+ unsigned int m_width, m_height;
+ };
+
+ class WindowCloseEvent
+ : public Event
+ {
+ public:
+ WindowCloseEvent() = default;
+ APPLICATION_EVENT_CLASS_TYPE(WindowClose)
+ };
+
+ class WindowFocusEvent
+ : public Event
+ {
+ public:
+ WindowFocusEvent() = default;
+ APPLICATION_EVENT_CLASS_TYPE(WindowFocus)
+ };
+
+ class WindowLostFocusEvent
+ : public Event
+ {
+ public:
+ WindowLostFocusEvent() = default;
+ APPLICATION_EVENT_CLASS_TYPE(WindowLostFocus)
+ };
+
+ class WindowMovedEvent
+ : public Event
+ {
+ public:
+ WindowMovedEvent(int x, int y)
+ : m_x(x), m_y(y) { }
+
+ auto get_x() const -> int { return m_x; }
+ auto get_y() const -> int { return m_y; }
+
+ auto to_string() const -> std::string override
+ {
+ return "WindowMovedEvent: " + std::to_string(m_x) + ", " + std::to_string(m_y);
+ }
+
+ APPLICATION_EVENT_CLASS_TYPE(WindowMoved)
+
+ private:
+ int m_x, m_y;
+ };
+
+ class KeyEvent
+ : public Event
+ {
+ public:
+ auto get_key_code() const -> int { return m_key_code; }
+ virtual auto get_category_flags() const -> int override { return EventCategoryKeyboard |
+ EventCategoryInput; }
+
+ protected:
+ KeyEvent(int keycode)
+ : m_key_code(keycode) { }
+ int m_key_code;
+ };
+
+ class KeyPressedEvent
+ : public KeyEvent
+ {
+ public:
+ KeyPressedEvent(int keycode, bool is_repeat = false)
+ : KeyEvent(keycode), m_is_repeat(is_repeat) { }
+
+ auto is_repeat() const -> bool { return m_is_repeat; }
+
+ auto to_string() const -> std::string override
+ {
+ return "KeyPressedEvent: " + std::to_string(m_key_code) +
+ " (repeat = " + std::to_string(m_is_repeat) + ")";
+ }
+
+ KEYBOARD_EVENT_CLASS_TYPE(KeyPressed)
+
+ private:
+ bool m_is_repeat;
+ };
+
+ class KeyReleasedEvent
+ : public KeyEvent
+ {
+ public:
+ KeyReleasedEvent(int keycode)
+ : KeyEvent(keycode) { }
+
+ auto to_string() const -> std::string override
+ {
+ return "KeyReleasedEvent: " + std::to_string(m_key_code);
+ }
+
+ KEYBOARD_EVENT_CLASS_TYPE(KeyReleased)
+ };
+
+ class KeyTypedEvent
+ : public KeyEvent
+ {
+ public:
+ KeyTypedEvent(int keycode)
+ : KeyEvent(keycode) { }
+
+ auto to_string() const -> std::string override
+ {
+ return "KeyTypedEvent: " + std::to_string(m_key_code);
+ }
+
+ KEYBOARD_EVENT_CLASS_TYPE(KeyTyped)
+ };
+
+ class MouseMovedEvent
+ : public Event
+ {
+ public:
+ MouseMovedEvent(float x, float y)
+ : m_mouse_x(x), m_mouse_y(y) { }
+
+ auto get_x() const -> float { return m_mouse_x; }
+ auto get_y() const -> float { return m_mouse_y; }
+
+ auto to_string() const -> std::string override
+ {
+ return "MouseMovedEvent: " + std::to_string(m_mouse_x) + ", " + std::to_string(m_mouse_y);
+ }
+
+ MOUSE_EVENT_CLASS_TYPE(MouseMoved)
+
+ private:
+ float m_mouse_x, m_mouse_y;
+ };
+
+ class MouseScrolledEvent
+ : public Event
+ {
+ public:
+ MouseScrolledEvent(float x_offset, float y_offset)
+ : m_x_offset(x_offset), m_y_offset(y_offset) { }
+
+ auto get_x_offset() const -> float { return m_x_offset; }
+ auto get_y_offset() const -> float { return m_y_offset; }
+
+ auto to_string() const -> std::string override
+ {
+ return "MouseScrolledEvent: " + std::to_string(m_x_offset) +
+ ", " + std::to_string(m_y_offset);
+ }
+
+ MOUSE_EVENT_CLASS_TYPE(MouseScrolled)
+
+ private:
+ float m_x_offset, m_y_offset;
+ };
+
+ class MouseButtonEvent
+ : public Event
+ {
+ public:
+ auto get_mouse_button() const -> int { return m_button; }
+ virtual auto get_category_flags() const -> int override { return EventCategoryMouseButton |
+ EventCategoryMouse |
+ EventCategoryInput; }
+
+ protected:
+ MouseButtonEvent(int button)
+ : m_button(button) { }
+ int m_button;
+ };
+
+ class MouseButtonPressedEvent
+ : public MouseButtonEvent
+ {
+ public:
+ MouseButtonPressedEvent(int button)
+ : MouseButtonEvent(button) { }
+
+ auto to_string() const -> std::string override
+ {
+ return "MouseButtonPressedEvent: " + std::to_string(m_button);
+ }
+
+ MOUSE_BUTTON_EVENT_CLASS_TYPE(MouseButtonPressed)
+ };
+
+ class MouseButtonReleasedEvent
+ : public MouseButtonEvent
+ {
+ public:
+ MouseButtonReleasedEvent(int button)
+ : MouseButtonEvent(button) { }
+
+ auto to_string() const -> std::string override
+ {
+ return "MouseButtonReleasedEvent: " + std::to_string(m_button);
+ }
+
+ MOUSE_BUTTON_EVENT_CLASS_TYPE(MouseButtonReleased)
+ };
+
+ class EventDispatcher
+ {
+ public:
+ EventDispatcher(Event& event)
+ : m_event(event) { }
+
+ template<typename T, typename F>
+ auto dispatch(const F& func) -> bool
+ {
+ if (m_event.get_event_type() == T::get_static_type())
+ {
+ m_event.handled = func(static_cast<T&>(m_event));
+ return true;
+ }
+ return false;
+ }
+
+ private:
+ Event& m_event;
+ };
+
+ class EventHandler
+ {
+ public:
+ using EventCallbackFn = std::function<void(Event&)>;
+
+ EventHandler() = default;
+ ~EventHandler() = default;
+
+ auto set_event_callback(const EventCallbackFn& callback) -> void
+ {
+ m_event_callback = callback;
+ }
+
+ auto on_event(Event& event) -> void
+ {
+ if (m_event_callback)
+ m_event_callback(event);
+ }
+
+ template<typename T>
+ auto bind_event(const std::function<void(T&)>& callback) -> void
+ {
+ m_event_callback = [callback](Event& event)
+ {
+ if (event.get_event_type() == T::get_static_type())
+ callback(static_cast<T&>(event));
+ };
+ }
+
+ private:
+ EventCallbackFn m_event_callback;
+ };
+
+ #undef EVENT_CLASS_TYPE
+ #undef APPLICATION_EVENT_CLASS_TYPE
+ #undef MOUSE_EVENT_CLASS_TYPE
+ #undef MOUSE_BUTTON_EVENT_CLASS_TYPE
+ #undef KEYBOARD_EVENT_CLASS_TYPE
+}
diff --git a/src/core/hdri_manager.cpp b/src/core/hdri_manager.cpp
new file mode 100644
index 0000000..605dcf6
--- /dev/null
+++ b/src/core/hdri_manager.cpp
@@ -0,0 +1,64 @@
+#include "hdri_manager.h"
+#include <filesystem>
+
+namespace Donut
+{
+ auto HDRIManager::load_hdri(const std::string& path) -> Ref<CubemapTexture>
+ {
+ auto it = m_hdri_cache.find(path);
+ if (it != m_hdri_cache.end())
+ {
+ DONUT_INFO("HDRI already cached: {}", path);
+ return it->second;
+ }
+
+ DONUT_INFO("Loading HDRI: {}", path);
+ auto hdri = CubemapTexture::create_from_hdri(path);
+
+ if (hdri)
+ {
+ m_hdri_cache[path] = hdri;
+ DONUT_INFO("Successfully loaded and cached HDRI: {}", path);
+ }
+ else
+ DONUT_ERROR("Failed to load HDRI: {}", path);
+
+ return hdri;
+ }
+
+ auto HDRIManager::set_current_hdri(const std::string& path) -> void
+ {
+ auto hdri = load_hdri(path);
+ if (hdri)
+ {
+ m_current_hdri = hdri;
+ DONUT_INFO("Set current HDRI to: {}", path);
+ }
+ else
+ DONUT_ERROR("Failed to set current HDRI: {}", path);
+ }
+
+ auto HDRIManager::get_hdri_name(const std::string& path) const -> std::string
+ {
+ std::filesystem::path file_path(path);
+ std::string filename = file_path.stem().string();
+ std::string name = filename;
+
+ for (size_t i = 0; i < name.length(); ++i)
+ {
+ if (name[i] == '_')
+ name[i] = ' ';
+ else if (i == 0 || name[i-1] == ' ')
+ name[i] = std::toupper(name[i]);
+ }
+
+ return name;
+ }
+
+ auto HDRIManager::clear_cache() -> void
+ {
+ m_hdri_cache.clear();
+ m_current_hdri.reset();
+ DONUT_INFO("HDRI cache cleared");
+ }
+};
diff --git a/src/core/hdri_manager.h b/src/core/hdri_manager.h
new file mode 100644
index 0000000..fa05330
--- /dev/null
+++ b/src/core/hdri_manager.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include "core/log.h"
+#include "rendering/texture.h"
+
+#include <string>
+#include <unordered_map>
+#include <memory>
+
+namespace Donut
+{
+ class HDRIManager
+ {
+ public:
+ static auto get() -> HDRIManager&
+ {
+ static HDRIManager instance;
+ return instance;
+ }
+
+ auto load_hdri(const std::string& path) -> Ref<CubemapTexture>;
+ auto get_current_hdri() const -> Ref<CubemapTexture> { return m_current_hdri; }
+
+ auto set_current_hdri(const std::string& path) -> void;
+ auto get_available_hdri() const -> const std::vector<std::string>& { return m_available_hdri; }
+ auto get_hdri_name(const std::string& path) const -> std::string;
+ auto clear_cache() -> void;
+
+ private:
+ HDRIManager() = default;
+ ~HDRIManager() = default;
+
+ HDRIManager(const HDRIManager&) = delete;
+ HDRIManager& operator=(const HDRIManager&) = delete;
+
+ std::unordered_map<std::string, Ref<CubemapTexture>> m_hdri_cache;
+
+ Ref<CubemapTexture> m_current_hdri;
+
+ std::vector<std::string> m_available_hdri =
+ {
+ "assets/hdri/HDR_blue_nebulae-1.hdr",
+ "assets/hdri/HDR_subdued_blue_nebulae.hdr",
+ "assets/hdri/HDR_subdued_multi_nebulae.hdr",
+ "assets/hdri/night_sky.hdr"
+ };
+ };
+};
diff --git a/src/core/log.cpp b/src/core/log.cpp
new file mode 100644
index 0000000..5a0434f
--- /dev/null
+++ b/src/core/log.cpp
@@ -0,0 +1,156 @@
+#include "log.h"
+
+#include <filesystem>
+
+#if defined(DONUT_WINDOWS)
+ #include <windows.h>
+#endif
+
+namespace Donut
+{
+ Ref<Logger> Logger::s_logger;
+
+ auto Logger::init() -> void
+ {
+ s_logger = create_ref<Logger>();
+ s_logger->set_log_level(LogLevel::Info);
+ s_logger->enable_console_output(true);
+ s_logger->enable_file_output(true);
+ s_logger->set_log_file("logs/donut.log");
+
+ s_logger->log_message(LogLevel::Info, "Logging system initialized");
+ }
+
+ auto Logger::shutdown() -> void
+ {
+ if (s_logger)
+ s_logger->log_message(LogLevel::Info, "Shutting down logging system");
+ s_logger.reset();
+ }
+
+ auto Logger::get_logger() -> Ref<Logger>
+ {
+ return s_logger;
+ }
+
+ Logger::Logger()
+ : m_log_level(LogLevel::Info),
+ m_console_output(true),
+ m_file_output(false) { }
+
+ Logger::~Logger()
+ {
+ if (m_log_file.is_open())
+ m_log_file.close();
+ }
+
+ auto Logger::set_log_file(const std::string& filename) -> void
+ {
+ std::filesystem::path log_path(filename);
+ std::filesystem::create_directories(log_path.parent_path());
+
+ m_log_file.open(filename, std::ios::app);
+ if (!m_log_file.is_open())
+ std::cerr << "Failed to open log file: " << filename << std::endl;
+ }
+
+ auto Logger::get_time_stamp() -> std::string
+ {
+ auto now = std::chrono::system_clock::now();
+ auto time = std::chrono::system_clock::to_time_t(now);
+
+ std::stringstream ss;
+ ss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S");
+
+ return ss.str();
+ }
+
+ auto Logger::get_log_level_string(LogLevel level) -> std::string
+ {
+ switch (level)
+ {
+ case LogLevel::trace: return "TRACE";
+ case LogLevel::Info: return "INFO";
+ case LogLevel::Warn: return "WARN";
+ case LogLevel::Err: return "ERROR";
+ case LogLevel::Fatal: return "FATAL";
+ default: return "UNKNOWN";
+ }
+ }
+
+ auto Logger::set_console_color(LogLevel level) -> void
+ {
+#if defined(DONUT_WINDOWS)
+ HANDLE h_console = GetStdHandle(STD_OUTPUT_HANDLE);
+ WORD color;
+
+ switch (level) {
+ case LogLevel::trace:
+ color = FOREGROUND_RED |
+ FOREGROUND_GREEN |
+ FOREGROUND_BLUE;
+ break;
+ case LogLevel::Info:
+ color = FOREGROUND_GREEN |
+ FOREGROUND_INTENSITY;
+ break;
+ case LogLevel::Warn:
+ color = FOREGROUND_RED |
+ FOREGROUND_GREEN |
+ FOREGROUND_INTENSITY;
+ break;
+ case LogLevel::Err:
+ color = FOREGROUND_RED |
+ FOREGROUND_INTENSITY;
+ break;
+ case LogLevel::Fatal:
+ color = FOREGROUND_RED |
+ FOREGROUND_BLUE |
+ FOREGROUND_INTENSITY;
+ break;
+ default:
+ color = FOREGROUND_RED |
+ FOREGROUND_GREEN |
+ FOREGROUND_BLUE;
+ break;
+ }
+
+ SetConsoleTextAttribute(h_console, color);
+#else
+ const char* color_code;
+ switch (level) {
+ case LogLevel::trace:
+ color_code = "\033[37m";
+ break;
+ case LogLevel::Info:
+ color_code = "\033[32;1m";
+ break;
+ case LogLevel::Warn:
+ color_code = "\033[33;1m";
+ break;
+ case LogLevel::Err:
+ color_code = "\033[31;1m";
+ break;
+ case LogLevel::Fatal:
+ color_code = "\033[35;1m";
+ break;
+ default:
+ color_code = "\033[37m";
+ break;
+ }
+ std::cout << color_code;
+#endif
+ }
+
+ auto Logger::reset_console_color() -> void
+ {
+#if defined(DONUT_WINDOWS)
+ HANDLE h_console = GetStdHandle(STD_OUTPUT_HANDLE);
+ SetConsoleTextAttribute(h_console, FOREGROUND_RED |
+ FOREGROUND_GREEN |
+ FOREGROUND_BLUE);
+#else
+ std::cout << "\033[0m";
+#endif
+ }
+}
diff --git a/src/core/log.h b/src/core/log.h
new file mode 100644
index 0000000..db6b4b4
--- /dev/null
+++ b/src/core/log.h
@@ -0,0 +1,156 @@
+#pragma once
+
+#include "core/memory.h"
+
+#include <string>
+#include <vector>
+#include <sstream>
+#include <iostream>
+#include <fstream>
+#include <chrono>
+#include <iomanip>
+#include <mutex>
+#include <string_view>
+
+namespace Donut
+{
+ enum class LogLevel
+ {
+ trace = 0,
+ Info = 2,
+ Warn = 3,
+ Err = 4,
+ Fatal = 5
+ };
+
+ class Logger
+ {
+ public:
+ Logger();
+ ~Logger();
+
+ static auto init() -> void;
+ static auto shutdown() -> void;
+
+ static auto get_logger() -> Ref<Logger>;
+
+ template<typename... Args>
+ static auto trace(const std::string_view& format, const Args&... args) -> void
+ {
+ auto logger = get_logger();
+ if (logger) logger->log_message(LogLevel::trace, format, args...);
+ }
+
+ template<typename... Args>
+ static auto info(const std::string_view& format, const Args&... args) -> void
+ {
+ auto logger = get_logger();
+ if (logger) logger->log_message(LogLevel::Info, format, args...);
+ }
+
+ template<typename... Args>
+ static auto warn(const std::string_view& format, const Args&... args) -> void
+ {
+ auto logger = get_logger();
+ if (logger) logger->log_message(LogLevel::Warn, format, args...);
+ }
+
+ template<typename... Args>
+ static auto error(const std::string_view& format, const Args&... args) -> void
+ {
+ auto logger = get_logger();
+ if (logger) logger->log_message(LogLevel::Err, format, args...);
+ }
+
+ template<typename... Args>
+ static auto fatal(const std::string_view& format, const Args&... args) -> void
+ {
+ auto logger = get_logger();
+ if (logger) logger->log_message(LogLevel::Fatal, format, args...);
+ }
+
+ template<typename... Args>
+ auto log_message(LogLevel level, const std::string_view& format, const Args&... args) -> void
+ {
+ if (level < m_log_level)
+ return;
+
+ std::lock_guard<std::mutex> lock(m_mutex);
+ std::string message = format_string(format, args...);
+ std::string full_message = get_time_stamp() + " [" + get_log_level_string(level) + "] " + message;
+
+ if (m_console_output)
+ {
+ set_console_color(level);
+ std::cout << full_message << std::endl;
+ reset_console_color();
+ }
+
+ if (m_file_output && m_log_file.is_open())
+ {
+ m_log_file << full_message << std::endl;
+ m_log_file.flush();
+ }
+ }
+
+ template<typename... Args>
+ auto format_string(const std::string_view& format, const Args&... args) -> std::string
+ {
+ std::string result = format.data();
+ std::vector<std::string> arg_strings = { to_string(args)... };
+
+ size_t arg_index = 0;
+ size_t pos = 0;
+
+ while ((pos = result.find("{}", pos)) != std::string::npos && arg_index < arg_strings.size())
+ {
+ result.replace(pos, 2, arg_strings[arg_index]);
+ pos += arg_strings[arg_index].length();
+ arg_index++;
+ }
+
+ return result;
+ }
+
+ template<typename T>
+ auto to_string(const T& value) -> std::string
+ {
+ std::stringstream ss;
+ ss << value;
+ return ss.str();
+ }
+
+ auto set_log_level(LogLevel level) -> void { m_log_level = level; }
+ auto enable_console_output(bool enable) -> void { m_console_output = enable; }
+ auto enable_file_output(bool enable) -> void { m_file_output = enable; }
+ auto set_log_file(const std::string& filename) -> void;
+
+ auto get_time_stamp() -> std::string;
+ auto get_log_level_string(LogLevel level) -> std::string;
+ auto set_console_color(LogLevel level) -> void;
+ auto reset_console_color() -> void;
+
+ private:
+ LogLevel m_log_level;
+ bool m_console_output;
+ bool m_file_output;
+ std::ofstream m_log_file;
+ std::mutex m_mutex;
+
+ static Ref<Logger> s_logger;
+ };
+}
+
+#if defined(DONUT_DEBUG)
+ #define DONUT_TRACE(format, ...) ::Donut::Logger::trace(format, ##__VA_ARGS__)
+ #define DONUT_INFO(format, ...) ::Donut::Logger::info(format, ##__VA_ARGS__)
+ #define DONUT_WARN(format, ...) ::Donut::Logger::warn(format, ##__VA_ARGS__)
+ #define DONUT_ERROR(format, ...) ::Donut::Logger::error(format, ##__VA_ARGS__)
+ #define DONUT_FATAL(format, ...) ::Donut::Logger::fatal(format, ##__VA_ARGS__)
+#else
+ #define DONUT_TRACE(format, ...) {}
+ #define DONUT_INFO(format, ...) {}
+ #define DONUT_WARN(format, ...) {}
+ #define DONUT_ERROR(format, ...) {}
+ #define DONUT_FATAL(format, ...) {}
+#endif
diff --git a/src/core/memory.h b/src/core/memory.h
new file mode 100644
index 0000000..3240c71
--- /dev/null
+++ b/src/core/memory.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <memory>
+
+namespace Donut
+{
+ template<typename T>
+ using Ref = std::shared_ptr<T>;
+
+ template<typename T>
+ using Scope = std::unique_ptr<T>;
+
+ template<typename T, typename ... Args>
+ constexpr auto create_scope(Args&& ... args) -> Scope<T>
+ {
+ return std::make_unique<T>(std::forward<Args>(args)...);
+ }
+
+ template<typename T, typename ... Args>
+ constexpr auto create_ref(Args&& ... args) -> Ref<T>
+ {
+ return std::make_shared<T>(std::forward<Args>(args)...);
+ }
+};
diff --git a/src/core/settings_manager.cpp b/src/core/settings_manager.cpp
new file mode 100644
index 0000000..ca4cac5
--- /dev/null
+++ b/src/core/settings_manager.cpp
@@ -0,0 +1,200 @@
+#include "settings_manager.h"
+#include "log.h"
+#include "theme_manager.h"
+#include "rendering/renderer.h"
+
+#include <fstream>
+#include <filesystem>
+#include <algorithm>
+
+namespace Donut
+{
+ Settings SettingsManager::s_settings;
+ bool SettingsManager::s_initialized = false;
+
+ auto SettingsManager::initialize() -> void
+ {
+ if (s_initialized)
+ return;
+
+ load_settings();
+ s_initialized = true;
+ DONUT_INFO("Settings Manager initialized");
+ }
+
+ auto SettingsManager::shutdown() -> void
+ {
+ if (!s_initialized)
+ return;
+
+ save_settings();
+ s_initialized = false;
+ DONUT_INFO("Settings Manager shutdown");
+ }
+
+ auto SettingsManager::load_settings() -> void
+ {
+ std::string file_path = get_settings_file_path();
+
+ try
+ {
+ if (std::filesystem::exists(file_path))
+ {
+ auto config = toml::parse(file_path);
+
+ if (config.contains("simulation"))
+ {
+ auto sim = config["simulation"];
+ s_settings.simulation.target_fps = toml::find_or(sim, "target_fps", 60);
+ s_settings.simulation.compute_height = toml::find_or(sim, "compute_height", 512);
+ s_settings.simulation.max_steps_moving = toml::find_or(sim, "max_steps_moving", 30000);
+ s_settings.simulation.max_steps_static = toml::find_or(sim, "max_steps_static", 15000);
+ s_settings.simulation.early_exit_distance = toml::find_or(sim, "early_exit_distance", 5e12f);
+ s_settings.simulation.gravity_enabled = toml::find_or(sim, "gravity_enabled", true);
+ s_settings.simulation.disk_thickness = toml::find_or(sim, "disk_thickness", 0.1f);
+ s_settings.simulation.disk_density = toml::find_or(sim, "disk_density", 0.1f);
+ s_settings.simulation.rotation_speed = toml::find_or(sim, "rotation_speed", 1.0f);
+ s_settings.simulation.blur_strength = toml::find_or(sim, "blur_strength", 2.0f);
+ s_settings.simulation.glow_intensity = toml::find_or(sim, "glow_intensity", 0.1f);
+
+ s_settings.simulation.target_fps = std::max(30, std::min(120, s_settings.simulation.target_fps));
+ s_settings.simulation.compute_height = std::max(64, std::min(2048, s_settings.simulation.compute_height));
+ s_settings.simulation.max_steps_moving = std::max(1000, std::min(60000, s_settings.simulation.max_steps_moving));
+ s_settings.simulation.max_steps_static = std::max(1000, std::min(30000, s_settings.simulation.max_steps_static));
+ s_settings.simulation.early_exit_distance = std::max(1e11f, std::min(1e13f, s_settings.simulation.early_exit_distance));
+ s_settings.simulation.disk_thickness = std::max(0.01f, std::min(5.0f, s_settings.simulation.disk_thickness));
+ s_settings.simulation.disk_density = std::max(0.01f, std::min(5.0f, s_settings.simulation.disk_density));
+ s_settings.simulation.rotation_speed = std::max(0.0f, std::min(5.0f, s_settings.simulation.rotation_speed));
+ s_settings.simulation.blur_strength = std::max(0.1f, std::min(10.0f, s_settings.simulation.blur_strength));
+ s_settings.simulation.glow_intensity = std::max(0.01f, std::min(5.0f, s_settings.simulation.glow_intensity));
+ }
+
+ if (config.contains("graphics"))
+ {
+ auto gfx = config["graphics"];
+ s_settings.graphics.render_api = toml::find_or(gfx, "render_api", std::string("OpenGL"));
+ s_settings.graphics.v_sync_enabled = toml::find_or(gfx, "vsync_enabled", true);
+ s_settings.graphics.show_fps = toml::find_or(gfx, "show_fps", true);
+ s_settings.graphics.show_performance_metrics = toml::find_or(gfx, "show_performance_metrics", true);
+ s_settings.graphics.show_debug_info = toml::find_or(gfx, "show_debug_info", false);
+ s_settings.graphics.enable_anti_aliasing = toml::find_or(gfx, "enable_anti_aliasing", true);
+ s_settings.graphics.selected_theme = toml::find_or(gfx, "selected_theme", std::string("Dark"));
+
+ if (s_settings.graphics.render_api != "OpenGL" &&
+ s_settings.graphics.render_api != "Vulkan")
+ s_settings.graphics.render_api = "OpenGL";
+ if (s_settings.graphics.selected_theme != "Dark" &&
+ s_settings.graphics.selected_theme != "Light" &&
+ s_settings.graphics.selected_theme != "Blue")
+ s_settings.graphics.selected_theme = "Dark";
+ }
+
+ DONUT_INFO("Settings loaded from {}", file_path);
+ }
+ else
+ {
+ load_default_settings();
+ save_settings();
+ DONUT_INFO("No settings file found, created default settings");
+ }
+ }
+ catch (const std::exception& e)
+ {
+ DONUT_ERROR("Failed to load settings: {}", e.what());
+ load_default_settings();
+ }
+ }
+
+ auto SettingsManager::save_settings() -> void
+ {
+ std::string file_path = get_settings_file_path();
+
+ try
+ {
+ std::filesystem::path path(file_path);
+ std::filesystem::create_directories(path.parent_path());
+
+ toml::value simulation = toml::table
+ {
+ {"target_fps", s_settings.simulation.target_fps },
+ {"compute_height", s_settings.simulation.compute_height },
+ {"max_steps_moving", s_settings.simulation.max_steps_moving },
+ {"max_steps_static", s_settings.simulation.max_steps_static },
+ {"early_exit_distance", s_settings.simulation.early_exit_distance},
+ {"gravity_enabled", s_settings.simulation.gravity_enabled },
+ {"disk_thickness", s_settings.simulation.disk_thickness },
+ {"disk_density", s_settings.simulation.disk_density },
+ {"rotation_speed", s_settings.simulation.rotation_speed },
+ {"blur_strength", s_settings.simulation.blur_strength },
+ {"glow_intensity", s_settings.simulation.glow_intensity }
+ };
+
+ toml::value graphics = toml::table
+ {
+ {"render_api", s_settings.graphics.render_api },
+ {"vsync_enabled", s_settings.graphics.v_sync_enabled },
+ {"show_fps", s_settings.graphics.show_fps },
+ {"show_performance_metrics", s_settings.graphics.show_performance_metrics},
+ {"show_debug_info", s_settings.graphics.show_debug_info },
+ {"enable_anti_aliasing", s_settings.graphics.enable_anti_aliasing },
+ {"selected_theme", s_settings.graphics.selected_theme }
+ };
+
+ toml::value config = toml::table
+ {
+ {"simulation", simulation},
+ {"graphics", graphics}
+ };
+
+ std::ofstream file(file_path);
+ file << config;
+ file.close();
+
+ DONUT_INFO("Settings saved to {}", file_path);
+ }
+ catch (const std::exception& e)
+ {
+ DONUT_ERROR("Failed to save settings: {}", e.what());
+ }
+ }
+
+ auto SettingsManager::set_simulation_settings(const SimulationSettings& settings) -> void
+ {
+ s_settings.simulation = settings;
+ save_settings();
+ }
+
+ auto SettingsManager::set_graphics_settings(const GraphicsSettings& settings) -> void
+ {
+ s_settings.graphics = settings;
+ save_settings();
+ }
+
+ auto SettingsManager::get_settings_file_path() -> std::string
+ {
+ return "config/settings.toml";
+ }
+
+ auto SettingsManager::load_default_settings() -> void
+ {
+ s_settings.simulation.target_fps = 60;
+ s_settings.simulation.compute_height = 512;
+ s_settings.simulation.max_steps_moving = 30000;
+ s_settings.simulation.max_steps_static = 15000;
+ s_settings.simulation.early_exit_distance = 5e12f;
+ s_settings.simulation.gravity_enabled = true;
+ s_settings.simulation.disk_thickness = 0.1f;
+ s_settings.simulation.disk_density = 0.1f;
+ s_settings.simulation.rotation_speed = 1.0f;
+ s_settings.simulation.blur_strength = 2.0f;
+ s_settings.simulation.glow_intensity = 0.1f;
+
+ s_settings.graphics.render_api = "OpenGL";
+ s_settings.graphics.v_sync_enabled = true;
+ s_settings.graphics.show_fps = true;
+ s_settings.graphics.show_performance_metrics = true;
+ s_settings.graphics.show_debug_info = false;
+ s_settings.graphics.enable_anti_aliasing = true;
+ s_settings.graphics.selected_theme = "Dark";
+ }
+}
diff --git a/src/core/settings_manager.h b/src/core/settings_manager.h
new file mode 100644
index 0000000..95826c7
--- /dev/null
+++ b/src/core/settings_manager.h
@@ -0,0 +1,82 @@
+#pragma once
+
+#include <string>
+#include <toml.hpp>
+
+namespace Donut
+{
+ struct SimulationSettings
+ {
+ int target_fps = 60;
+ int compute_height = 512;
+ int max_steps_moving = 30000;
+ int max_steps_static = 15000;
+ float early_exit_distance = 5e12f;
+ bool gravity_enabled = true;
+ float disk_thickness = 0.1f;
+ float disk_density = 0.1f;
+ float rotation_speed = 1.0f;
+ float blur_strength = 2.0f;
+ float glow_intensity = 0.1f;
+ };
+
+ struct GraphicsSettings
+ {
+ std::string render_api = "OpenGL";
+ bool v_sync_enabled = true;
+ bool show_fps = true;
+ bool show_performance_metrics = true;
+ bool show_debug_info = false;
+ bool enable_anti_aliasing = true;
+ std::string selected_theme = "Dark";
+ };
+
+ struct Settings
+ {
+ SimulationSettings simulation;
+ GraphicsSettings graphics;
+ };
+
+ class SettingsManager
+ {
+ public:
+ static auto initialize() -> void;
+ static auto shutdown() -> void;
+
+ static auto load_settings() -> void;
+ static auto save_settings() -> void;
+
+ static auto get_settings() -> Settings& { return s_settings; }
+ static auto get_settings_const() -> const Settings& { return s_settings; }
+
+ static auto set_simulation_settings(const SimulationSettings& settings) -> void;
+ static auto set_graphics_settings(const GraphicsSettings& settings) -> void;
+
+ static auto get_target_fps() -> int { return s_settings.simulation.target_fps; }
+ static auto get_compute_height() -> int { return s_settings.simulation.compute_height; }
+ static auto get_max_steps_moving() -> int { return s_settings.simulation.max_steps_moving; }
+ static auto get_max_steps_static() -> int { return s_settings.simulation.max_steps_static; }
+ static auto get_early_exit_distance() -> float { return s_settings.simulation.early_exit_distance; }
+ static auto get_gravity_enabled() -> bool { return s_settings.simulation.gravity_enabled; }
+ static auto get_disk_thickness() -> float { return s_settings.simulation.disk_thickness; }
+ static auto get_disk_density() -> float { return s_settings.simulation.disk_density; }
+ static auto get_rotation_speed() -> float { return s_settings.simulation.rotation_speed; }
+ static auto get_blur_strength() -> float { return s_settings.simulation.blur_strength; }
+ static auto get_glow_intensity() -> float { return s_settings.simulation.glow_intensity; }
+ static auto get_render_api() -> std::string { return s_settings.graphics.render_api; }
+ static auto get_v_sync_enabled() -> bool { return s_settings.graphics.v_sync_enabled; }
+ static auto get_show_fps() -> bool { return s_settings.graphics.show_fps; }
+ static auto get_show_performance_metrics() -> bool { return s_settings.graphics.show_performance_metrics; }
+ static auto get_show_debug_info() -> bool { return s_settings.graphics.show_debug_info; }
+ static auto get_enable_anti_aliasing() -> bool { return s_settings.graphics.enable_anti_aliasing; }
+ static auto get_selected_theme() -> std::string { return s_settings.graphics.selected_theme; }
+
+ private:
+ static auto get_settings_file_path() -> std::string;
+ static auto load_default_settings() -> void;
+
+ private:
+ static Settings s_settings;
+ static bool s_initialized;
+ };
+}
diff --git a/src/core/state.h b/src/core/state.h
new file mode 100644
index 0000000..623f1c2
--- /dev/null
+++ b/src/core/state.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "event.h"
+
+namespace Donut
+{
+ class State
+ {
+ public:
+ virtual ~State() = default;
+
+ virtual auto on_enter() -> void = 0;
+ virtual auto on_exit() -> void = 0;
+ virtual auto on_update(float delta_time) -> void = 0;
+ virtual auto on_render() -> void = 0;
+ virtual auto on_im_ui_render() -> void = 0;
+ virtual auto on_event(Event& event) -> void = 0;
+ };
+};
diff --git a/src/core/state_manager.cpp b/src/core/state_manager.cpp
new file mode 100644
index 0000000..8018401
--- /dev/null
+++ b/src/core/state_manager.cpp
@@ -0,0 +1,88 @@
+#include "state_manager.h"
+#include "log.h"
+
+namespace Donut
+{
+ auto StateManager::shutdown() -> void
+ {
+ if (m_current_state)
+ m_current_state->on_exit();
+
+ destroy_states();
+ DONUT_INFO("StateManager shutdown");
+ }
+
+ auto StateManager::destroy_states() -> void
+ {
+ m_states.clear();
+ m_current_state = nullptr;
+ m_current_state_name = "";
+ }
+
+ auto StateManager::register_state(const std::string& state_name, Scope<State> state) -> void
+ {
+ if (m_states.find(state_name) != m_states.end())
+ DONUT_WARN("State '{}' already exists, overwriting", state_name);
+ m_states[state_name] = std::move(state);
+ DONUT_INFO("Registered state: {}", state_name);
+ }
+
+ auto StateManager::switch_to_state(const std::string& state_name) -> void
+ {
+ auto it = m_states.find(state_name);
+ if (it == m_states.end())
+ {
+ DONUT_ERROR("Attempted to switch to unknown state: {}", state_name);
+ return;
+ }
+
+ State* new_state = it->second.get();
+ if (new_state == m_current_state)
+ return;
+
+ if (m_current_state)
+ m_current_state->on_exit();
+
+ m_current_state = new_state;
+ m_current_state_name = state_name;
+ m_current_state->on_enter();
+ DONUT_INFO("Switched to state: {}", state_name);
+ }
+
+ auto StateManager::get_state(const std::string& state_name) -> State*
+ {
+ auto it = m_states.find(state_name);
+ if (it != m_states.end())
+ return it->second.get();
+ return nullptr;
+ }
+
+ auto StateManager::has_state(const std::string& state_name) const -> bool
+ {
+ return m_states.find(state_name) != m_states.end();
+ }
+
+ auto StateManager::update(float delta_time) -> void
+ {
+ if (m_current_state)
+ m_current_state->on_update(delta_time);
+ }
+
+ auto StateManager::render() -> void
+ {
+ if (m_current_state)
+ m_current_state->on_render();
+ }
+
+ auto StateManager::on_im_ui_render() -> void
+ {
+ if (m_current_state)
+ m_current_state->on_im_ui_render();
+ }
+
+ auto StateManager::on_event(Event& event) -> void
+ {
+ if (m_current_state)
+ m_current_state->on_event(event);
+ }
+}
diff --git a/src/core/state_manager.h b/src/core/state_manager.h
new file mode 100644
index 0000000..446841e
--- /dev/null
+++ b/src/core/state_manager.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "memory.h"
+#include "state.h"
+
+#include <string>
+#include <unordered_map>
+
+namespace Donut
+{
+ class StateManager
+ {
+ public:
+ ~StateManager() = default;
+ auto shutdown() -> void;
+
+ auto update(float delta_time) -> void;
+ auto render() -> void;
+ auto on_im_ui_render() -> void;
+ auto on_event(Event& event) -> void;
+
+ auto register_state(const std::string& state_name, Scope<State> state) -> void;
+ auto switch_to_state(const std::string& state_name) -> void;
+
+ auto get_current_state_name() const -> std::string { return m_current_state_name; }
+ auto get_current_state() const -> State* { return m_current_state; }
+
+ auto get_state(const std::string& state_name) -> State*;
+ auto has_state(const std::string& state_name) const -> bool;
+
+ private:
+ auto create_states() -> void;
+ auto destroy_states() -> void;
+
+ private:
+ State* m_current_state = nullptr;
+ std::string m_current_state_name = "";
+
+ std::unordered_map<std::string, Scope<State>> m_states;
+ };
+}
diff --git a/src/core/theme_manager.cpp b/src/core/theme_manager.cpp
new file mode 100644
index 0000000..4ae025f
--- /dev/null
+++ b/src/core/theme_manager.cpp
@@ -0,0 +1,291 @@
+#include "theme_manager.h"
+
+namespace Donut
+{
+ Theme ThemeManager::s_current_theme = Theme::Dark;
+
+ auto ThemeManager::set_theme(Theme theme) -> void
+ {
+ s_current_theme = theme;
+
+ switch (theme)
+ {
+ case Theme::Dark:
+ apply_dark_theme();
+ break;
+ case Theme::Light:
+ apply_light_theme();
+ break;
+ case Theme::Blue:
+ apply_blue_theme();
+ break;
+ }
+ }
+
+ auto ThemeManager::apply_dark_theme() -> void
+ {
+ ImGuiStyle& style = ImGui::GetStyle();
+ ImVec4* colors = style.Colors;
+
+ colors[ImGuiCol_Text] = ImVec4(0.95f, 0.95f, 0.95f, 1.00f);
+ colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
+ colors[ImGuiCol_WindowBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.95f);
+ colors[ImGuiCol_ChildBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.95f);
+ colors[ImGuiCol_PopupBg] = ImVec4(0.08f, 0.08f, 0.08f, 0.95f);
+ colors[ImGuiCol_Border] = ImVec4(0.25f, 0.25f, 0.25f, 0.54f);
+ colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
+ colors[ImGuiCol_FrameBg] = ImVec4(0.12f, 0.12f, 0.12f, 0.95f);
+ colors[ImGuiCol_FrameBgHovered] = ImVec4(0.18f, 0.18f, 0.18f, 0.95f);
+ colors[ImGuiCol_FrameBgActive] = ImVec4(0.22f, 0.22f, 0.22f, 0.95f);
+ colors[ImGuiCol_TitleBg] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
+ colors[ImGuiCol_TitleBgActive] = ImVec4(0.08f, 0.08f, 0.08f, 1.00f);
+ colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f);
+ colors[ImGuiCol_MenuBarBg] = ImVec4(0.14f, 0.14f, 0.14f, 1.00f);
+ colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.02f, 0.02f, 0.53f);
+ colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.31f, 0.31f, 0.31f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.41f, 0.41f, 0.41f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.51f, 0.51f, 0.51f, 1.00f);
+ colors[ImGuiCol_CheckMark] = ImVec4(0.60f, 0.60f, 0.60f, 0.95f);
+ colors[ImGuiCol_SliderGrab] = ImVec4(0.60f, 0.60f, 0.60f, 0.95f);
+ colors[ImGuiCol_SliderGrabActive] = ImVec4(0.70f, 0.70f, 0.70f, 0.95f);
+ colors[ImGuiCol_Button] = ImVec4(0.15f, 0.15f, 0.15f, 0.95f);
+ colors[ImGuiCol_ButtonHovered] = ImVec4(0.20f, 0.20f, 0.20f, 0.95f);
+ colors[ImGuiCol_ButtonActive] = ImVec4(0.25f, 0.25f, 0.25f, 0.95f);
+ colors[ImGuiCol_Header] = ImVec4(0.15f, 0.15f, 0.15f, 0.95f);
+ colors[ImGuiCol_HeaderHovered] = ImVec4(0.20f, 0.20f, 0.20f, 0.95f);
+ colors[ImGuiCol_HeaderActive] = ImVec4(0.25f, 0.25f, 0.25f, 0.95f);
+ colors[ImGuiCol_Separator] = ImVec4(0.43f, 0.43f, 0.43f, 0.50f);
+ colors[ImGuiCol_SeparatorHovered] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
+ colors[ImGuiCol_SeparatorActive] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f);
+ colors[ImGuiCol_ResizeGrip] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
+ colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.29f, 0.30f, 0.31f, 0.67f);
+ colors[ImGuiCol_ResizeGripActive] = ImVec4(0.60f, 0.60f, 0.60f, 0.95f);
+ colors[ImGuiCol_Tab] = ImVec4(0.08f, 0.08f, 0.08f, 0.83f);
+ colors[ImGuiCol_TabHovered] = ImVec4(0.15f, 0.15f, 0.15f, 0.83f);
+ colors[ImGuiCol_TabActive] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f);
+ colors[ImGuiCol_TabUnfocused] = ImVec4(0.08f, 0.08f, 0.08f, 0.97f);
+ colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.13f, 0.13f, 0.13f, 1.00f);
+ colors[ImGuiCol_DockingPreview] = ImVec4(0.60f, 0.60f, 0.60f, 0.70f);
+ colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f);
+ colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f);
+ colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f);
+ colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f);
+ colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f);
+ colors[ImGuiCol_TextSelectedBg] = ImVec4(0.60f, 0.60f, 0.60f, 0.35f);
+ colors[ImGuiCol_DragDropTarget] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f);
+ colors[ImGuiCol_NavHighlight] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f);
+ colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
+ colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
+ colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f);
+
+ style.WindowPadding = ImVec2(8, 8);
+ style.FramePadding = ImVec2(5, 2);
+ style.CellPadding = ImVec2(6, 4);
+ style.ItemSpacing = ImVec2(6, 4);
+ style.ItemInnerSpacing = ImVec2(4, 4);
+ style.IndentSpacing = 21;
+ style.ScrollbarSize = 14;
+ style.GrabMinSize = 10;
+
+ style.WindowBorderSize = 1;
+ style.ChildBorderSize = 1;
+ style.PopupBorderSize = 1;
+ style.FrameBorderSize = 0;
+ style.TabBorderSize = 0;
+
+ style.WindowRounding = 8.0f;
+ style.ChildRounding = 6.0f;
+ style.FrameRounding = 4.0f;
+ style.PopupRounding = 4.0f;
+ style.ScrollbarRounding = 4.0f;
+ style.GrabRounding = 4.0f;
+ style.TabRounding = 4.0f;
+
+ style.WindowTitleAlign = ImVec2(0.5f, 0.5f);
+ style.ButtonTextAlign = ImVec2(0.5f, 0.5f);
+ style.SelectableTextAlign = ImVec2(0.0f, 0.0f);
+
+ style.DisplaySafeAreaPadding = ImVec2(3, 3);
+
+ style.Alpha = 1.0f;
+ style.DisabledAlpha = 0.6f;
+ }
+
+ auto ThemeManager::apply_light_theme() -> void
+ {
+ ImGuiStyle& style = ImGui::GetStyle();
+ ImVec4* colors = style.Colors;
+
+ colors[ImGuiCol_Text] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f);
+ colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f);
+ colors[ImGuiCol_WindowBg] = ImVec4(0.95f, 0.95f, 0.95f, 0.95f);
+ colors[ImGuiCol_ChildBg] = ImVec4(0.92f, 0.92f, 0.92f, 0.95f);
+ colors[ImGuiCol_PopupBg] = ImVec4(0.95f, 0.95f, 0.95f, 0.95f);
+ colors[ImGuiCol_Border] = ImVec4(0.70f, 0.70f, 0.70f, 0.54f);
+ colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
+ colors[ImGuiCol_FrameBg] = ImVec4(0.85f, 0.85f, 0.85f, 0.95f);
+ colors[ImGuiCol_FrameBgHovered] = ImVec4(0.80f, 0.80f, 0.80f, 0.95f);
+ colors[ImGuiCol_FrameBgActive] = ImVec4(0.75f, 0.75f, 0.75f, 0.95f);
+ colors[ImGuiCol_TitleBg] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
+ colors[ImGuiCol_TitleBgActive] = ImVec4(0.85f, 0.85f, 0.85f, 1.00f);
+ colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.95f, 0.95f, 0.95f, 0.51f);
+ colors[ImGuiCol_MenuBarBg] = ImVec4(0.88f, 0.88f, 0.88f, 1.00f);
+ colors[ImGuiCol_ScrollbarBg] = ImVec4(0.90f, 0.90f, 0.90f, 0.53f);
+ colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.70f, 0.70f, 0.70f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
+ colors[ImGuiCol_CheckMark] = ImVec4(0.20f, 0.60f, 0.90f, 0.95f);
+ colors[ImGuiCol_SliderGrab] = ImVec4(0.20f, 0.60f, 0.90f, 0.95f);
+ colors[ImGuiCol_SliderGrabActive] = ImVec4(0.15f, 0.50f, 0.80f, 0.95f);
+ colors[ImGuiCol_Button] = ImVec4(0.80f, 0.80f, 0.80f, 0.95f);
+ colors[ImGuiCol_ButtonHovered] = ImVec4(0.75f, 0.75f, 0.75f, 0.95f);
+ colors[ImGuiCol_ButtonActive] = ImVec4(0.70f, 0.70f, 0.70f, 0.95f);
+ colors[ImGuiCol_Header] = ImVec4(0.85f, 0.85f, 0.85f, 0.95f);
+ colors[ImGuiCol_HeaderHovered] = ImVec4(0.80f, 0.80f, 0.80f, 0.95f);
+ colors[ImGuiCol_HeaderActive] = ImVec4(0.75f, 0.75f, 0.75f, 0.95f);
+ colors[ImGuiCol_Separator] = ImVec4(0.70f, 0.70f, 0.70f, 0.50f);
+ colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f);
+ colors[ImGuiCol_SeparatorActive] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
+ colors[ImGuiCol_ResizeGrip] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
+ colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.60f, 0.60f, 0.60f, 0.67f);
+ colors[ImGuiCol_ResizeGripActive] = ImVec4(0.20f, 0.60f, 0.90f, 0.95f);
+ colors[ImGuiCol_Tab] = ImVec4(0.85f, 0.85f, 0.85f, 0.83f);
+ colors[ImGuiCol_TabHovered] = ImVec4(0.80f, 0.80f, 0.80f, 0.83f);
+ colors[ImGuiCol_TabActive] = ImVec4(0.75f, 0.75f, 0.75f, 1.00f);
+ colors[ImGuiCol_TabUnfocused] = ImVec4(0.90f, 0.90f, 0.90f, 0.97f);
+ colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.85f, 0.85f, 0.85f, 1.00f);
+ colors[ImGuiCol_DockingPreview] = ImVec4(0.20f, 0.60f, 0.90f, 0.70f);
+ colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f);
+ colors[ImGuiCol_PlotLines] = ImVec4(0.40f, 0.40f, 0.40f, 1.00f);
+ colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.30f, 0.20f, 1.00f);
+ colors[ImGuiCol_PlotHistogram] = ImVec4(0.70f, 0.50f, 0.00f, 1.00f);
+ colors[ImGuiCol_PlotHistogramHovered] = ImVec4(0.80f, 0.40f, 0.00f, 1.00f);
+ colors[ImGuiCol_TextSelectedBg] = ImVec4(0.20f, 0.60f, 0.90f, 0.35f);
+ colors[ImGuiCol_DragDropTarget] = ImVec4(0.10f, 0.50f, 0.80f, 1.00f);
+ colors[ImGuiCol_NavHighlight] = ImVec4(0.20f, 0.60f, 0.90f, 1.00f);
+ colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.10f, 0.10f, 0.10f, 0.70f);
+ colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.20f);
+ colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f);
+
+ style.WindowPadding = ImVec2(8, 8);
+ style.FramePadding = ImVec2(5, 2);
+ style.CellPadding = ImVec2(6, 4);
+ style.ItemSpacing = ImVec2(6, 4);
+ style.ItemInnerSpacing = ImVec2(4, 4);
+ style.IndentSpacing = 21;
+ style.ScrollbarSize = 14;
+ style.GrabMinSize = 10;
+
+ style.WindowBorderSize = 1;
+ style.ChildBorderSize = 1;
+ style.PopupBorderSize = 1;
+ style.FrameBorderSize = 0;
+ style.TabBorderSize = 0;
+
+ style.WindowRounding = 8.0f;
+ style.ChildRounding = 6.0f;
+ style.FrameRounding = 4.0f;
+ style.PopupRounding = 4.0f;
+ style.ScrollbarRounding = 4.0f;
+ style.GrabRounding = 4.0f;
+ style.TabRounding = 4.0f;
+
+ style.WindowTitleAlign = ImVec2(0.5f, 0.5f);
+ style.ButtonTextAlign = ImVec2(0.5f, 0.5f);
+ style.SelectableTextAlign = ImVec2(0.0f, 0.0f);
+
+ style.DisplaySafeAreaPadding = ImVec2(3, 3);
+
+ style.Alpha = 1.0f;
+ style.DisabledAlpha = 0.6f;
+ }
+
+ auto ThemeManager::apply_blue_theme() -> void
+ {
+ ImGuiStyle& style = ImGui::GetStyle();
+ ImVec4* colors = style.Colors;
+
+ colors[ImGuiCol_Text] = ImVec4(0.95f, 0.95f, 0.95f, 1.00f);
+ colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.50f, 1.00f);
+ colors[ImGuiCol_WindowBg] = ImVec4(0.05f, 0.10f, 0.20f, 0.95f);
+ colors[ImGuiCol_ChildBg] = ImVec4(0.03f, 0.08f, 0.18f, 0.95f);
+ colors[ImGuiCol_PopupBg] = ImVec4(0.05f, 0.10f, 0.20f, 0.95f);
+ colors[ImGuiCol_Border] = ImVec4(0.20f, 0.30f, 0.50f, 0.54f);
+ colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
+ colors[ImGuiCol_FrameBg] = ImVec4(0.08f, 0.15f, 0.30f, 0.95f);
+ colors[ImGuiCol_FrameBgHovered] = ImVec4(0.12f, 0.20f, 0.40f, 0.95f);
+ colors[ImGuiCol_FrameBgActive] = ImVec4(0.15f, 0.25f, 0.45f, 0.95f);
+ colors[ImGuiCol_TitleBg] = ImVec4(0.05f, 0.10f, 0.20f, 1.00f);
+ colors[ImGuiCol_TitleBgActive] = ImVec4(0.05f, 0.10f, 0.20f, 1.00f);
+ colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f);
+ colors[ImGuiCol_MenuBarBg] = ImVec4(0.08f, 0.15f, 0.25f, 1.00f);
+ colors[ImGuiCol_ScrollbarBg] = ImVec4(0.02f, 0.05f, 0.10f, 0.53f);
+ colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.30f, 0.40f, 0.60f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.40f, 0.50f, 0.70f, 1.00f);
+ colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.50f, 0.60f, 0.80f, 1.00f);
+ colors[ImGuiCol_CheckMark] = ImVec4(0.20f, 0.60f, 1.00f, 0.95f);
+ colors[ImGuiCol_SliderGrab] = ImVec4(0.20f, 0.60f, 1.00f, 0.95f);
+ colors[ImGuiCol_SliderGrabActive] = ImVec4(0.30f, 0.70f, 1.00f, 0.95f);
+ colors[ImGuiCol_Button] = ImVec4(0.10f, 0.20f, 0.40f, 0.95f);
+ colors[ImGuiCol_ButtonHovered] = ImVec4(0.15f, 0.25f, 0.45f, 0.95f);
+ colors[ImGuiCol_ButtonActive] = ImVec4(0.20f, 0.30f, 0.50f, 0.95f);
+ colors[ImGuiCol_Header] = ImVec4(0.10f, 0.20f, 0.40f, 0.95f);
+ colors[ImGuiCol_HeaderHovered] = ImVec4(0.15f, 0.25f, 0.45f, 0.95f);
+ colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.30f, 0.50f, 0.95f);
+ colors[ImGuiCol_Separator] = ImVec4(0.30f, 0.40f, 0.60f, 0.50f);
+ colors[ImGuiCol_SeparatorHovered] = ImVec4(0.40f, 0.50f, 0.70f, 1.00f);
+ colors[ImGuiCol_SeparatorActive] = ImVec4(0.50f, 0.60f, 0.80f, 1.00f);
+ colors[ImGuiCol_ResizeGrip] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
+ colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.30f, 0.40f, 0.60f, 0.67f);
+ colors[ImGuiCol_ResizeGripActive] = ImVec4(0.20f, 0.60f, 1.00f, 0.95f);
+ colors[ImGuiCol_Tab] = ImVec4(0.08f, 0.15f, 0.25f, 0.83f);
+ colors[ImGuiCol_TabHovered] = ImVec4(0.12f, 0.20f, 0.35f, 0.83f);
+ colors[ImGuiCol_TabActive] = ImVec4(0.15f, 0.25f, 0.40f, 1.00f);
+ colors[ImGuiCol_TabUnfocused] = ImVec4(0.05f, 0.10f, 0.20f, 0.97f);
+ colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.08f, 0.15f, 0.25f, 1.00f);
+ colors[ImGuiCol_DockingPreview] = ImVec4(0.20f, 0.60f, 1.00f, 0.70f);
+ colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.05f, 0.10f, 0.20f, 1.00f);
+ colors[ImGuiCol_PlotLines] = ImVec4(0.60f, 0.70f, 0.90f, 1.00f);
+ colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.60f, 0.40f, 1.00f);
+ colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.30f, 1.00f);
+ colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.80f, 0.40f, 1.00f);
+ colors[ImGuiCol_TextSelectedBg] = ImVec4(0.20f, 0.60f, 1.00f, 0.35f);
+ colors[ImGuiCol_DragDropTarget] = ImVec4(0.10f, 0.50f, 0.90f, 1.00f);
+ colors[ImGuiCol_NavHighlight] = ImVec4(0.20f, 0.60f, 1.00f, 1.00f);
+ colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f);
+ colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f);
+ colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f);
+
+ style.WindowPadding = ImVec2(8, 8);
+ style.FramePadding = ImVec2(5, 2);
+ style.CellPadding = ImVec2(6, 4);
+ style.ItemSpacing = ImVec2(6, 4);
+ style.ItemInnerSpacing = ImVec2(4, 4);
+ style.IndentSpacing = 21;
+ style.ScrollbarSize = 14;
+ style.GrabMinSize = 10;
+
+ style.WindowBorderSize = 1;
+ style.ChildBorderSize = 1;
+ style.PopupBorderSize = 1;
+ style.FrameBorderSize = 0;
+ style.TabBorderSize = 0;
+
+ style.WindowRounding = 8.0f;
+ style.ChildRounding = 6.0f;
+ style.FrameRounding = 4.0f;
+ style.PopupRounding = 4.0f;
+ style.ScrollbarRounding = 4.0f;
+ style.GrabRounding = 4.0f;
+ style.TabRounding = 4.0f;
+
+ style.WindowTitleAlign = ImVec2(0.5f, 0.5f);
+ style.ButtonTextAlign = ImVec2(0.5f, 0.5f);
+ style.SelectableTextAlign = ImVec2(0.0f, 0.0f);
+
+ style.DisplaySafeAreaPadding = ImVec2(3, 3);
+
+ style.Alpha = 1.0f;
+ style.DisabledAlpha = 0.6f;
+ }
+}
diff --git a/src/core/theme_manager.h b/src/core/theme_manager.h
new file mode 100644
index 0000000..d067906
--- /dev/null
+++ b/src/core/theme_manager.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <imgui.h>
+
+namespace Donut
+{
+ enum class Theme
+ {
+ Dark = 0,
+ Light = 1,
+ Blue = 2
+ };
+
+ class ThemeManager
+ {
+ public:
+ static auto set_theme(Theme theme) -> void;
+ static auto get_current_theme() -> Theme { return s_current_theme; }
+
+ static auto apply_dark_theme() -> void;
+ static auto apply_light_theme() -> void;
+ static auto apply_blue_theme() -> void;
+
+ private:
+ static Theme s_current_theme;
+ };
+}
diff --git a/src/core/window.cpp b/src/core/window.cpp
new file mode 100644
index 0000000..047ef0d
--- /dev/null
+++ b/src/core/window.cpp
@@ -0,0 +1,341 @@
+#include <glad/glad.h>
+#include "window.h"
+#include "theme_manager.h"
+#include "rendering/renderer.h" // RendererAPI::get_api()
+
+#include <cstdint>
+
+#include <imgui.h>
+#include <imgui_impl_glfw.h>
+#include <imgui_impl_opengl3.h>
+
+namespace Donut
+{
+ static bool s_glfw_initialized = false;
+ static uint32_t s_glfw_window_count = 0;
+
+ Window::Window(const std::string& title, int width, int height)
+ : m_title(title), m_width(width),
+ m_height(height), m_is_closed(false)
+ {
+ init();
+ }
+
+ Window::~Window()
+ {
+ shutdown();
+ }
+
+ auto Window::init() -> void
+ {
+ DONUT_INFO("Initializing window: ", m_title, " (", m_width, "x", m_height, ")");
+
+ if (!s_glfw_initialized)
+ {
+ int success = glfwInit();
+ if (!success)
+ {
+ DONUT_ERROR("Could not initialize GLFW!");
+ return;
+ }
+
+ s_glfw_initialized = true;
+ DONUT_INFO("GLFW initialized successfully");
+ }
+
+ if (RendererAPI::get_api() == RendererAPI::API::Vulkan)
+ {
+ // Vulkan manages presentation itself; GLFW must not create a GL context.
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+ }
+ else
+ {
+#ifdef __APPLE__
+ // macOS only exposes OpenGL up to 4.1 Core Profile, and requires a
+ // forward-compatible core-profile context for any modern (>= 3.3)
+ // shader to compile. Without these hints GLFW hands back a legacy
+ // 2.1 context and every GLSL shader in the project fails to build.
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
+ glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
+#endif
+ }
+
+ m_window = glfwCreateWindow(m_width, m_height, m_title.c_str(), nullptr, nullptr);
+ if (!m_window)
+ {
+ DONUT_ERROR("Could not create GLFW window!");
+ glfwTerminate();
+ return;
+ }
+
+ DONUT_INFO("GLFW window created successfully");
+
+ if (RendererAPI::get_api() != RendererAPI::API::Vulkan)
+ glfwMakeContextCurrent(m_window);
+ glfwSetWindowUserPointer(m_window, this);
+
+ glfwSetErrorCallback(glfw_error_callback);
+ glfwSetWindowCloseCallback(m_window, glfw_window_close_callback);
+ glfwSetWindowSizeCallback(m_window, glfw_window_size_callback);
+ glfwSetWindowFocusCallback(m_window, glfw_window_focus_callback);
+ glfwSetWindowPosCallback(m_window, glfw_window_pos_callback);
+ glfwSetKeyCallback(m_window, glfw_key_callback);
+ glfwSetCharCallback(m_window, glfw_char_callback);
+ glfwSetMouseButtonCallback(m_window, glfw_mouse_button_callback);
+ glfwSetScrollCallback(m_window, glfw_mouse_scroll_callback);
+ glfwSetCursorPosCallback(m_window, glfw_cursor_pos_callback);
+
+ s_glfw_window_count++;
+ }
+
+ auto Window::shutdown() -> void
+ {
+ DONUT_INFO("Shutting down window: ", m_title);
+
+ shutdown_im_gui();
+
+ glfwDestroyWindow(m_window);
+ s_glfw_window_count--;
+
+ if (s_glfw_window_count == 0)
+ {
+ glfwTerminate();
+ s_glfw_initialized = false;
+ DONUT_INFO("GLFW terminated (no more windows)");
+ }
+ }
+
+ auto Window::on_update() const -> void
+ {
+ glfwPollEvents();
+ if (RendererAPI::get_api() != RendererAPI::API::Vulkan)
+ glfwSwapBuffers(m_window); // Vulkan presents via the swapchain instead
+ }
+
+ auto Window::should_close() const -> bool
+ {
+ return glfwWindowShouldClose(m_window) || m_is_closed;
+ }
+
+ auto Window::set_cursor_locked(bool locked) -> void
+ {
+ m_cursor_locked = locked;
+ glfwSetInputMode(m_window, GLFW_CURSOR, locked ?
+ GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
+ }
+
+ auto Window::set_cursor_visible(bool visible) -> void
+ {
+ m_cursor_visible = visible;
+ glfwSetInputMode(m_window, GLFW_CURSOR, visible ?
+ GLFW_CURSOR_NORMAL : GLFW_CURSOR_HIDDEN);
+ }
+
+ auto Window::init_im_gui() -> void
+ {
+ IMGUI_CHECKVERSION();
+ ImGui::CreateContext();
+ ImGuiIO& io = ImGui::GetIO();
+ io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
+ io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
+#ifndef __APPLE__
+ // Multi-viewport (dragging ImGui panels out as separate OS windows)
+ // relies on a populated platform-monitor list and is unreliable on
+ // macOS, where it intermittently asserts (Monitors.Size > 0) and
+ // aborts. Docking stays enabled; panels just remain inside the window.
+ io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
+#endif
+
+ setup_im_gui_fonts();
+ ThemeManager::set_theme(Theme::Dark);
+
+ ImGuiStyle& style = ImGui::GetStyle();
+ if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
+ {
+ style.WindowRounding = 0.0f;
+ style.Colors[ImGuiCol_WindowBg].w = 1.0f;
+ }
+
+ ImGui_ImplGlfw_InitForOpenGL(m_window, true);
+#ifdef __APPLE__
+ // macOS uses a core-profile context, which rejects the legacy
+ // "#version 130" GLSL the ImGui backend defaults to. 150 is the
+ // minimum core-profile GLSL that macOS's OpenGL 4.1 accepts.
+ ImGui_ImplOpenGL3_Init("#version 150");
+#else
+ ImGui_ImplOpenGL3_Init("#version 130");
+#endif
+
+ DONUT_INFO("ImGUI initialized successfully");
+ }
+
+ auto Window::setup_im_gui_fonts() -> void
+ {
+ ImGuiIO& io = ImGui::GetIO();
+ io.Fonts->Clear();
+
+ m_main_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Regular.ttf", 16.0f);
+ if (!m_main_font)
+ {
+ DONUT_WARN("Failed to load Inter font, falling back to default");
+ m_main_font = io.Fonts->AddFontDefault();
+ }
+ else
+ DONUT_INFO("Successfully loaded Inter font");
+
+ m_large_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Bold.ttf", 20.0f);
+ if (!m_large_font)
+ m_large_font = m_main_font;
+
+ m_small_font = io.Fonts->AddFontFromFileTTF("assets/fonts/inter/static/Inter_18pt-Light.ttf", 12.0f);
+ if (!m_small_font)
+ m_small_font = m_main_font;
+
+ io.FontDefault = m_main_font;
+ DONUT_INFO("ImGUI fonts loaded successfully");
+ }
+
+ auto Window::shutdown_im_gui() -> void
+ {
+ ImGui_ImplOpenGL3_Shutdown();
+ ImGui_ImplGlfw_Shutdown();
+ ImGui::DestroyContext();
+
+ DONUT_INFO("ImGUI shutdown");
+ }
+
+ auto Window::begin_im_gui_frame() -> void
+ {
+ ImGui_ImplOpenGL3_NewFrame();
+ ImGui_ImplGlfw_NewFrame();
+ ImGui::NewFrame();
+ }
+
+ auto Window::end_im_gui_frame() -> void
+ {
+ ImGui::Render();
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+
+ ImGuiIO& io = ImGui::GetIO();
+ if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
+ {
+ GLFWwindow* backup_current_context = glfwGetCurrentContext();
+ ImGui::UpdatePlatformWindows();
+ ImGui::RenderPlatformWindowsDefault();
+ glfwMakeContextCurrent(backup_current_context);
+ }
+ }
+
+ auto Window::glfw_error_callback(int error, const char* description) -> void
+ {
+ DONUT_ERROR("GLFW Error ({}): {}", error, description ? description : "");
+ }
+
+ auto Window::glfw_window_close_callback(GLFWwindow* window) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+ WindowCloseEvent event;
+ win->m_event_handler.on_event(event);
+ }
+
+ auto Window::glfw_window_size_callback(GLFWwindow* window, int width, int height) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+ win->m_width = width;
+ win->m_height = height;
+
+ WindowResizeEvent event(width, height);
+ win->m_event_handler.on_event(event);
+ }
+
+ auto Window::glfw_window_focus_callback(GLFWwindow* window, int focused) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+
+ if (focused)
+ {
+ WindowFocusEvent event;
+ win->m_event_handler.on_event(event);
+ }
+ else
+ {
+ WindowLostFocusEvent event;
+ win->m_event_handler.on_event(event);
+ }
+ }
+
+ auto Window::glfw_window_pos_callback(GLFWwindow* window, int xpos, int ypos) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+ WindowMovedEvent event(xpos, ypos);
+ win->m_event_handler.on_event(event);
+ }
+
+ auto Window::glfw_key_callback(GLFWwindow* window, int key, int scancode,
+ int action, int mods) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+
+ switch (action)
+ {
+ case GLFW_PRESS:
+ {
+ KeyPressedEvent event(key, false);
+ win->m_event_handler.on_event(event);
+ } break;
+ case GLFW_RELEASE:
+ {
+ KeyReleasedEvent event(key);
+ win->m_event_handler.on_event(event);
+ } break;
+ case GLFW_REPEAT:
+ {
+ KeyPressedEvent event(key, true);
+ win->m_event_handler.on_event(event);
+ } break;
+ }
+ }
+
+ auto Window::glfw_char_callback(GLFWwindow* window, unsigned int keycode) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+ KeyTypedEvent event(keycode);
+ win->m_event_handler.on_event(event);
+ }
+
+ auto Window::glfw_mouse_button_callback(GLFWwindow* window, int button,
+ int action, int mods) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+
+ switch (action)
+ {
+ case GLFW_PRESS:
+ {
+ MouseButtonPressedEvent event(button);
+ win->m_event_handler.on_event(event);
+ } break;
+ case GLFW_RELEASE:
+ {
+ MouseButtonReleasedEvent event(button);
+ win->m_event_handler.on_event(event);
+ } break;
+ }
+ }
+
+ auto Window::glfw_mouse_scroll_callback(GLFWwindow* window, double x_offset, double y_offset) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+ MouseScrolledEvent event((float)x_offset, (float)y_offset);
+ win->m_event_handler.on_event(event);
+ }
+
+ auto Window::glfw_cursor_pos_callback(GLFWwindow* window, double x_pos, double y_pos) -> void
+ {
+ Window* win = static_cast<Window*>(glfwGetWindowUserPointer(window));
+ MouseMovedEvent event((float)x_pos, (float)y_pos);
+ win->m_event_handler.on_event(event);
+ }
+}
diff --git a/src/core/window.h b/src/core/window.h
new file mode 100644
index 0000000..9b7d7b9
--- /dev/null
+++ b/src/core/window.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "event.h"
+#include "log.h"
+
+#include <GLFW/glfw3.h>
+#include <imgui.h>
+
+#include <string>
+
+namespace Donut
+{
+ class Window
+ {
+ public:
+ using EventCallbackFn = std::function<void(Event&)>;
+
+ Window(const std::string& title, int width, int height);
+ ~Window();
+
+ auto should_close() const -> bool;
+ auto on_update() const -> void;
+
+ auto set_event_callback(const EventCallbackFn& callback) -> void
+ {
+ m_event_handler.set_event_callback(callback);
+ }
+
+ auto get_event_handler() -> EventHandler& { return m_event_handler; }
+
+ auto get_width() const -> unsigned int { return m_width; }
+ auto get_height() const -> unsigned int { return m_height; }
+
+ auto get_native_window() const -> void* { return m_window; }
+
+ auto set_cursor_locked(bool locked) -> void;
+ auto set_cursor_visible(bool visible) -> void;
+ auto is_cursor_locked() const -> bool { return m_cursor_locked; }
+ auto is_cursor_visible() const -> bool { return m_cursor_visible; }
+
+ auto init_im_gui() -> void;
+ auto begin_im_gui_frame() -> void;
+ auto end_im_gui_frame() -> void;
+
+ private:
+ auto init() -> void;
+ auto shutdown() -> void;
+
+ auto setup_im_gui_fonts() -> void;
+ auto shutdown_im_gui() -> void;
+
+ static auto glfw_error_callback(int error, const char* description) -> void;
+ static auto glfw_window_close_callback(GLFWwindow* window) -> void;
+ static auto glfw_window_size_callback(GLFWwindow* window, int width, int height) -> void;
+ static auto glfw_window_focus_callback(GLFWwindow* window, int focused) -> void;
+ static auto glfw_window_pos_callback(GLFWwindow* window, int xpos, int ypos) -> void;
+ static auto glfw_key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) -> void;
+ static auto glfw_char_callback(GLFWwindow* window, unsigned int keycode) -> void;
+ static auto glfw_mouse_button_callback(GLFWwindow* window, int button, int action, int mods) -> void;
+ static auto glfw_mouse_scroll_callback(GLFWwindow* window, double x_offset, double y_offset) -> void;
+ static auto glfw_cursor_pos_callback(GLFWwindow* window, double x_pos, double y_pos) -> void;
+
+ private:
+ GLFWwindow* m_window;
+ EventHandler m_event_handler;
+
+ std::string m_title;
+ unsigned int m_width;
+ unsigned int m_height;
+ bool m_is_closed;
+ bool m_cursor_locked = false;
+ bool m_cursor_visible = true;
+
+ ImFont* m_main_font = nullptr;
+ ImFont* m_small_font = nullptr;
+ ImFont* m_large_font = nullptr;
+ };
+}