From 89a168f76d45f23fefa2181ea4094a6d95fc1eff Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Sat, 17 Oct 2026 16:25:09 +0200 Subject: [PATCH] feat: Add window class --- destrum/CMakeLists.txt | 1 + destrum/include/destrum/App.h | 5 +- destrum/include/destrum/Window.h | 98 ++++++ destrum/src/App.cpp | 54 ++- destrum/src/Window.cpp | 310 ++++++++++++++++++ lightkeeper/include/Lightkeeper.h | 1 + lightkeeper/src/Lightkeeper.cpp | 5 + lightkeeper/src/LightkeeperDebugUi.cpp | 72 ++++ .../src/components/InteractableButton.cpp | 3 +- 9 files changed, 513 insertions(+), 36 deletions(-) create mode 100644 destrum/include/destrum/Window.h create mode 100644 destrum/src/Window.cpp diff --git a/destrum/CMakeLists.txt b/destrum/CMakeLists.txt index 38b9401..7b3b534 100644 --- a/destrum/CMakeLists.txt +++ b/destrum/CMakeLists.txt @@ -34,6 +34,7 @@ target_compile_definitions(destrum_assets set(SRC_FILES "src/App.cpp" "src/Event.cpp" + "src/Window.cpp" "src/Components/Animator.cpp" "src/Components/CameraComponent.cpp" diff --git a/destrum/include/destrum/App.h b/destrum/include/destrum/App.h index 28789c9..188b128 100644 --- a/destrum/include/destrum/App.h +++ b/destrum/include/destrum/App.h @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -25,6 +26,8 @@ public: std::string windowTitle; std::filesystem::path exeDir; bool showDebugUi{true}; + WindowMode windowMode{WindowMode::Windowed}; + bool startMaximized{false}; }; App(); @@ -46,7 +49,7 @@ public: virtual void customWindowsMenu() {} protected: - SDL_Window* window{nullptr}; + Window window; AppParams m_params{}; GfxDevice gfxDevice; diff --git a/destrum/include/destrum/Window.h b/destrum/include/destrum/Window.h new file mode 100644 index 0000000..ab68ceb --- /dev/null +++ b/destrum/include/destrum/Window.h @@ -0,0 +1,98 @@ +#ifndef DESTRUM_WINDOW_H +#define DESTRUM_WINDOW_H + +#include +#include +#include + +#include + +#include + +enum class WindowMode { + Windowed, + Fullscreen, + BorderlessFullscreen, +}; + +struct WindowDesc { + std::string title{"Destrum"}; + glm::ivec2 size{1280, 720}; + glm::ivec2 minimumSize{320, 180}; + WindowMode mode{WindowMode::Windowed}; + bool resizable{true}; + bool maximized{false}; +}; + +class Window final { +public: + Window() = default; + ~Window(); + + Window(const Window&) = delete; + Window& operator=(const Window&) = delete; + Window(Window&&) = delete; + Window& operator=(Window&&) = delete; + + void Create(const WindowDesc& desc); + void Destroy(); + + void ProcessEvent(const SDL_Event& event); + [[nodiscard]] bool ConsumeResized(); + [[nodiscard]] bool ConsumeCloseRequested(); + + [[nodiscard]] bool IsCreated() const { return m_Handle != nullptr; } + [[nodiscard]] SDL_Window* GetHandle() const { return m_Handle; } + [[nodiscard]] std::uint32_t GetId() const; + + void SetTitle(std::string_view title); + [[nodiscard]] std::string GetTitle() const; + + void SetSize(const glm::ivec2& size); + [[nodiscard]] glm::ivec2 GetSize() const; + [[nodiscard]] glm::ivec2 GetDrawableSize() const; + [[nodiscard]] float GetAspectRatio() const; + void SetMinimumSize(const glm::ivec2& size); + void SetMaximumSize(const glm::ivec2& size); + + void SetPosition(const glm::ivec2& position); + [[nodiscard]] glm::ivec2 GetPosition() const; + void Center(); + + void Maximize(); + void Minimize(); + void Restore(); + void ToggleMaximized(); + [[nodiscard]] bool IsMaximized() const; + [[nodiscard]] bool IsMinimized() const; + + void SetMode(WindowMode mode); + [[nodiscard]] WindowMode GetMode() const; + void ToggleFullscreen(); + [[nodiscard]] bool IsFullscreen() const { return GetMode() != WindowMode::Windowed; } + + void SetResizable(bool resizable); + [[nodiscard]] bool IsResizable() const; + void SetBordered(bool bordered); + [[nodiscard]] bool IsBordered() const; + void SetAlwaysOnTop(bool alwaysOnTop); + + void Show(); + void Hide(); + void Focus(); + [[nodiscard]] bool IsVisible() const; + [[nodiscard]] bool HasFocus() const; + + [[nodiscard]] int GetDisplayIndex() const; + [[nodiscard]] glm::ivec2 GetDisplaySize() const; + +private: + [[nodiscard]] std::uint32_t getFlags() const; + [[nodiscard]] SDL_Window* requireHandle() const; + + SDL_Window* m_Handle{nullptr}; + bool m_Resized{false}; + bool m_CloseRequested{false}; +}; + +#endif // DESTRUM_WINDOW_H diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index 437e87c..c3a9dce 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -52,23 +51,15 @@ void App::init(const AppParams& params) TracySetProgramName(params.appName.c_str()); AssetFS::GetInstance().Init(params.exeDir); - window = SDL_CreateWindow( - params.windowTitle.c_str(), - SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, - params.windowSize.x, - params.windowSize.y, - SDL_WINDOW_VULKAN); + window.Create({ + .title = params.windowTitle, + .size = params.windowSize, + .mode = params.windowMode, + .maximized = params.startMaximized, + }); - if (!window) { - spdlog::error("Failed to create window. SDL Error: {}", SDL_GetError()); - throw std::runtime_error( - "Failed to create window: " + std::string{SDL_GetError()}); - } - SDL_SetWindowResizable(window, SDL_TRUE); - - gfxDevice.init(window, params.appName, false); - imguiPass.init(window, gfxDevice); + gfxDevice.init(window.GetHandle(), params.appName, false); + imguiPass.init(window.GetHandle(), gfxDevice); InputManager::GetInstance().Init(); Time::GetInstance().Update(); @@ -144,16 +135,16 @@ void App::run() break; } - if (event.type == SDL_WINDOWEVENT) + window.ProcessEvent(event); + if (window.ConsumeResized()) { - switch (event.window.event) - { - case SDL_WINDOWEVENT_SIZE_CHANGED: - case SDL_WINDOWEVENT_RESIZED: - resizePending = true; - lastResizeTime = std::chrono::steady_clock::now(); - break; - } + resizePending = true; + lastResizeTime = std::chrono::steady_clock::now(); + } + if (window.ConsumeCloseRequested()) + { + isRunning = false; + break; } const bool keyboardEvent = @@ -290,9 +281,9 @@ if (!isRunning) break; continue; } - int w = 0; - int h = 0; - SDL_Vulkan_GetDrawableSize(window, &w, &h); + const glm::ivec2 drawableSize = window.GetDrawableSize(); + const int w = drawableSize.x; + const int h = drawableSize.y; if (w == 0 || h == 0) { @@ -362,10 +353,7 @@ void App::cleanup() attempt([this] { resources.cleanup(gfxDevice); }); attempt([this] { gfxDevice.cleanup(); }); - if (window) { - SDL_DestroyWindow(window); - window = nullptr; - } + window.Destroy(); AssetFS::GetInstance().Reset(); customInitStarted = false; diff --git a/destrum/src/Window.cpp b/destrum/src/Window.cpp new file mode 100644 index 0000000..84ead7d --- /dev/null +++ b/destrum/src/Window.cpp @@ -0,0 +1,310 @@ +#include + +#include +#include + +#include + +#include + +namespace { + std::uint32_t modeFlags(WindowMode mode) + { + switch (mode) { + case WindowMode::Fullscreen: + return SDL_WINDOW_FULLSCREEN; + case WindowMode::BorderlessFullscreen: + return SDL_WINDOW_FULLSCREEN_DESKTOP; + case WindowMode::Windowed: + break; + } + return 0; + } +} + +Window::~Window() +{ + Destroy(); +} + +void Window::Create(const WindowDesc& desc) +{ + if (m_Handle) { + throw std::logic_error("Window::Create called on a window that already exists"); + } + + std::uint32_t flags = SDL_WINDOW_VULKAN | modeFlags(desc.mode); + if (desc.resizable) { + flags |= SDL_WINDOW_RESIZABLE; + } + if (desc.maximized) { + flags |= SDL_WINDOW_MAXIMIZED; + } + + m_Handle = SDL_CreateWindow( + desc.title.c_str(), + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + desc.size.x, + desc.size.y, + flags); + + if (!m_Handle) { + throw std::runtime_error("Failed to create window: " + std::string{SDL_GetError()}); + } + + SetMinimumSize(desc.minimumSize); + m_Resized = false; + m_CloseRequested = false; +} + +void Window::Destroy() +{ + if (m_Handle) { + SDL_DestroyWindow(m_Handle); + m_Handle = nullptr; + } +} + +void Window::ProcessEvent(const SDL_Event& event) +{ + if (!m_Handle || event.type != SDL_WINDOWEVENT || event.window.windowID != GetId()) { + return; + } + + switch (event.window.event) { + case SDL_WINDOWEVENT_SIZE_CHANGED: + case SDL_WINDOWEVENT_RESIZED: + case SDL_WINDOWEVENT_MAXIMIZED: + case SDL_WINDOWEVENT_RESTORED: + m_Resized = true; + break; + case SDL_WINDOWEVENT_CLOSE: + m_CloseRequested = true; + break; + default: + break; + } +} + +bool Window::ConsumeResized() +{ + const bool resized = m_Resized; + m_Resized = false; + return resized; +} + +bool Window::ConsumeCloseRequested() +{ + const bool closeRequested = m_CloseRequested; + m_CloseRequested = false; + return closeRequested; +} + +std::uint32_t Window::GetId() const +{ + return SDL_GetWindowID(requireHandle()); +} + +void Window::SetTitle(std::string_view title) +{ + SDL_SetWindowTitle(requireHandle(), std::string{title}.c_str()); +} + +std::string Window::GetTitle() const +{ + return SDL_GetWindowTitle(requireHandle()); +} + +void Window::SetSize(const glm::ivec2& size) +{ + SDL_SetWindowSize(requireHandle(), size.x, size.y); +} + +glm::ivec2 Window::GetSize() const +{ + glm::ivec2 size{0}; + SDL_GetWindowSize(requireHandle(), &size.x, &size.y); + return size; +} + +glm::ivec2 Window::GetDrawableSize() const +{ + glm::ivec2 size{0}; + SDL_Vulkan_GetDrawableSize(requireHandle(), &size.x, &size.y); + return size; +} + +float Window::GetAspectRatio() const +{ + const glm::ivec2 size = GetDrawableSize(); + return size.y > 0 ? static_cast(size.x) / static_cast(size.y) : 1.0f; +} + +void Window::SetMinimumSize(const glm::ivec2& size) +{ + SDL_SetWindowMinimumSize(requireHandle(), size.x, size.y); +} + +void Window::SetMaximumSize(const glm::ivec2& size) +{ + SDL_SetWindowMaximumSize(requireHandle(), size.x, size.y); +} + +void Window::SetPosition(const glm::ivec2& position) +{ + SDL_SetWindowPosition(requireHandle(), position.x, position.y); +} + +glm::ivec2 Window::GetPosition() const +{ + glm::ivec2 position{0}; + SDL_GetWindowPosition(requireHandle(), &position.x, &position.y); + return position; +} + +void Window::Center() +{ + const int display = GetDisplayIndex(); + SDL_SetWindowPosition( + requireHandle(), + static_cast(SDL_WINDOWPOS_CENTERED_DISPLAY(display)), + static_cast(SDL_WINDOWPOS_CENTERED_DISPLAY(display))); +} + +void Window::Maximize() +{ + SDL_MaximizeWindow(requireHandle()); +} + +void Window::Minimize() +{ + SDL_MinimizeWindow(requireHandle()); +} + +void Window::Restore() +{ + SDL_RestoreWindow(requireHandle()); +} + +void Window::ToggleMaximized() +{ + if (IsMaximized()) { + Restore(); + } else { + Maximize(); + } +} + +bool Window::IsMaximized() const +{ + return (getFlags() & SDL_WINDOW_MAXIMIZED) != 0; +} + +bool Window::IsMinimized() const +{ + return (getFlags() & SDL_WINDOW_MINIMIZED) != 0; +} + +void Window::SetMode(WindowMode mode) +{ + if (SDL_SetWindowFullscreen(requireHandle(), modeFlags(mode)) != 0) { + spdlog::warn("Failed to change the window mode: {}", SDL_GetError()); + return; + } + m_Resized = true; +} + +WindowMode Window::GetMode() const +{ + const std::uint32_t flags = getFlags(); + if ((flags & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP) { + return WindowMode::BorderlessFullscreen; + } + if ((flags & SDL_WINDOW_FULLSCREEN) != 0) { + return WindowMode::Fullscreen; + } + return WindowMode::Windowed; +} + +void Window::ToggleFullscreen() +{ + SetMode(IsFullscreen() ? WindowMode::Windowed : WindowMode::BorderlessFullscreen); +} + +void Window::SetResizable(bool resizable) +{ + SDL_SetWindowResizable(requireHandle(), resizable ? SDL_TRUE : SDL_FALSE); +} + +bool Window::IsResizable() const +{ + return (getFlags() & SDL_WINDOW_RESIZABLE) != 0; +} + +void Window::SetBordered(bool bordered) +{ + SDL_SetWindowBordered(requireHandle(), bordered ? SDL_TRUE : SDL_FALSE); +} + +bool Window::IsBordered() const +{ + return (getFlags() & SDL_WINDOW_BORDERLESS) == 0; +} + +void Window::SetAlwaysOnTop(bool alwaysOnTop) +{ + SDL_SetWindowAlwaysOnTop(requireHandle(), alwaysOnTop ? SDL_TRUE : SDL_FALSE); +} + +void Window::Show() +{ + SDL_ShowWindow(requireHandle()); +} + +void Window::Hide() +{ + SDL_HideWindow(requireHandle()); +} + +void Window::Focus() +{ + SDL_RaiseWindow(requireHandle()); +} + +bool Window::IsVisible() const +{ + return (getFlags() & SDL_WINDOW_SHOWN) != 0 && (getFlags() & SDL_WINDOW_HIDDEN) == 0; +} + +bool Window::HasFocus() const +{ + return (getFlags() & SDL_WINDOW_INPUT_FOCUS) != 0; +} + +int Window::GetDisplayIndex() const +{ + return std::max(SDL_GetWindowDisplayIndex(requireHandle()), 0); +} + +glm::ivec2 Window::GetDisplaySize() const +{ + SDL_DisplayMode displayMode{}; + if (SDL_GetDesktopDisplayMode(GetDisplayIndex(), &displayMode) != 0) { + return GetSize(); + } + return {displayMode.w, displayMode.h}; +} + +std::uint32_t Window::getFlags() const +{ + return m_Handle ? SDL_GetWindowFlags(m_Handle) : 0; +} + +SDL_Window* Window::requireHandle() const +{ + if (!m_Handle) { + throw std::logic_error("Window used before Create or after Destroy"); + } + return m_Handle; +} diff --git a/lightkeeper/include/Lightkeeper.h b/lightkeeper/include/Lightkeeper.h index 28bc1be..2677b8d 100644 --- a/lightkeeper/include/Lightkeeper.h +++ b/lightkeeper/include/Lightkeeper.h @@ -37,6 +37,7 @@ private: // LightkeeperDebugUi.cpp void updateDebugUi(); void drawDebugMenuBar(); + void drawWindowMenu(); void drawTestWindow(); void spawnBall(); void saveCurrentScene(); diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index b0ad4c6..671863c 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -68,6 +68,11 @@ void LightKeeper::customUpdate(float dt) { camera.Update(dt); } + if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F11)) + { + window.ToggleFullscreen(); + } + SceneManager::GetInstance().Update(dt); SceneManager::GetInstance().LateUpdate(dt); diff --git a/lightkeeper/src/LightkeeperDebugUi.cpp b/lightkeeper/src/LightkeeperDebugUi.cpp index 6cb9ecc..2ec32fe 100644 --- a/lightkeeper/src/LightkeeperDebugUi.cpp +++ b/lightkeeper/src/LightkeeperDebugUi.cpp @@ -158,9 +158,81 @@ void LightKeeper::drawDebugMenuBar() ImGui::EndMenu(); } + drawWindowMenu(); + ImGui::EndMainMenuBar(); } +void LightKeeper::drawWindowMenu() +{ + if (!ImGui::BeginMenu("Window")) { + return; + } + + const WindowMode mode = window.GetMode(); + const bool fullscreen = window.IsFullscreen(); + const bool maximized = window.IsMaximized(); + + if (ImGui::MenuItem("Fullscreen", "F11", fullscreen)) { + window.ToggleFullscreen(); + } + if (ImGui::BeginMenu("Mode")) { + if (ImGui::MenuItem("Windowed", nullptr, mode == WindowMode::Windowed)) { + window.SetMode(WindowMode::Windowed); + } + if (ImGui::MenuItem("Borderless Fullscreen", nullptr, mode == WindowMode::BorderlessFullscreen)) { + window.SetMode(WindowMode::BorderlessFullscreen); + } + if (ImGui::MenuItem("Exclusive Fullscreen", nullptr, mode == WindowMode::Fullscreen)) { + window.SetMode(WindowMode::Fullscreen); + } + ImGui::EndMenu(); + } + + ImGui::Separator(); + + if (ImGui::MenuItem(maximized ? "Restore" : "Maximize", nullptr, false, !fullscreen)) { + window.ToggleMaximized(); + } + if (ImGui::MenuItem("Minimize", nullptr, false, !fullscreen)) { + window.Minimize(); + } + if (ImGui::MenuItem("Center", nullptr, false, !fullscreen && !maximized)) { + window.Center(); + } + + if (ImGui::BeginMenu("Size", !fullscreen)) { + const glm::ivec2 sizes[] = {glm::ivec2{1280, 720}, glm::ivec2{1600, 900}, glm::ivec2{1920, 1080}}; + for (const glm::ivec2& size : sizes) { + const std::string label = std::to_string(size.x) + " x " + std::to_string(size.y); + if (ImGui::MenuItem(label.c_str(), nullptr, window.GetSize() == size)) { + if (maximized) { + window.Restore(); + } + window.SetSize(size); + window.Center(); + } + } + ImGui::EndMenu(); + } + + ImGui::Separator(); + + if (bool resizable = window.IsResizable(); ImGui::MenuItem("Resizable", nullptr, &resizable, !fullscreen)) { + window.SetResizable(resizable); + } + if (bool bordered = window.IsBordered(); ImGui::MenuItem("Bordered", nullptr, &bordered, !fullscreen)) { + window.SetBordered(bordered); + } + + ImGui::Separator(); + + const glm::ivec2 drawableSize = window.GetDrawableSize(); + ImGui::TextDisabled("Drawable: %d x %d", drawableSize.x, drawableSize.y); + + ImGui::EndMenu(); +} + void LightKeeper::saveCurrentScene() { try { diff --git a/lightkeeper/src/components/InteractableButton.cpp b/lightkeeper/src/components/InteractableButton.cpp index 390d057..396e702 100644 --- a/lightkeeper/src/components/InteractableButton.cpp +++ b/lightkeeper/src/components/InteractableButton.cpp @@ -43,8 +43,7 @@ void InteractableButton::Interact(GameObject& interactor) void InteractableButton::ApplyColor() { const auto* meshRenderer = GetGameObject()->GetComponent(); - if (meshRenderer == nullptr || meshRenderer->GetMaterialID() == NULL_MATERIAL_ID || - !GameState::GetInstance().HasRenderer()) { + if (meshRenderer == nullptr || meshRenderer->GetMaterialID() == NULL_MATERIAL_ID || !GameState::GetInstance().HasRenderer()) { return; }