diff --git a/config/seed_runtime.json b/config/seed_runtime.json index 8dce457..42c7e3d 100644 --- a/config/seed_runtime.json +++ b/config/seed_runtime.json @@ -3,6 +3,16 @@ "window_height": 768, "lua_script": "scripts/cube_logic.lua", "scripts_directory": "scripts", + "mouse_grab": { + "enabled": true, + "grab_on_click": true, + "release_on_escape": true, + "start_grabbed": false, + "hide_cursor": true, + "relative_mode": true, + "grab_mouse_button": "left", + "release_key": "escape" + }, "input_bindings": { "move_forward": "W", "move_back": "S", diff --git a/scripts/cube_logic.lua b/scripts/cube_logic.lua index 9f6d664..d9e6963 100644 --- a/scripts/cube_logic.lua +++ b/scripts/cube_logic.lua @@ -106,8 +106,11 @@ function InputState:resetTransient() self.mouseDeltaY = 0.0 end -function InputState:setMouse(x, y, isDown) - if self.lastMouseX ~= nil and self.lastMouseY ~= nil then +function InputState:setMouse(x, y, isDown, deltaX, deltaY) + if type(deltaX) == "number" and type(deltaY) == "number" then + self.mouseDeltaX = deltaX + self.mouseDeltaY = deltaY + elseif self.lastMouseX ~= nil and self.lastMouseY ~= nil then self.mouseDeltaX = x - self.lastMouseX self.mouseDeltaY = y - self.lastMouseY end diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index 811580e..b9038df 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -111,10 +111,12 @@ void ServiceBasedApp::Run() { config.width = configService->GetWindowWidth(); config.height = configService->GetWindowHeight(); config.title = configService->GetWindowTitle(); + config.mouseGrab = configService->GetMouseGrabConfig(); } else { config.width = runtimeConfig_.width; config.height = runtimeConfig_.height; config.title = runtimeConfig_.windowTitle; + config.mouseGrab = runtimeConfig_.mouseGrab; } config.resizable = true; windowService->CreateWindow(config); diff --git a/src/services/impl/application_loop_service.cpp b/src/services/impl/application_loop_service.cpp index b3d1ed0..839999b 100644 --- a/src/services/impl/application_loop_service.cpp +++ b/src/services/impl/application_loop_service.cpp @@ -64,6 +64,12 @@ void ApplicationLoopService::HandleEvents() { if (logger_) { logger_->Trace("ApplicationLoopService", "HandleEvents"); } + if (inputService_) { + if (logger_) { + logger_->Trace("ApplicationLoopService", "HandleEvents", "resetInputState=true"); + } + inputService_->ResetFrameState(); + } if (windowService_) { windowService_->PollEvents(); } @@ -86,10 +92,6 @@ void ApplicationLoopService::ProcessFrame(float deltaTime, float elapsedTime) { "Entering"); } - if (inputService_) { - inputService_->ResetFrameState(); - } - if (physicsService_) { physicsService_->StepSimulation(deltaTime); } diff --git a/src/services/impl/gui_script_service.cpp b/src/services/impl/gui_script_service.cpp index 084c935..ee0a3ee 100644 --- a/src/services/impl/gui_script_service.cpp +++ b/src/services/impl/gui_script_service.cpp @@ -190,6 +190,8 @@ void GuiScriptService::UpdateGuiInput(const GuiInputSnapshot& input) { logger_->Trace("GuiScriptService", "UpdateGuiInput", "mouseX=" + std::to_string(input.mouseX) + ", mouseY=" + std::to_string(input.mouseY) + + ", mouseDeltaX=" + std::to_string(input.mouseDeltaX) + + ", mouseDeltaY=" + std::to_string(input.mouseDeltaY) + ", mouseDown=" + std::string(input.mouseDown ? "true" : "false") + ", wheel=" + std::to_string(input.wheel) + ", textInput.size=" + std::to_string(input.textInput.size()) + @@ -212,7 +214,9 @@ void GuiScriptService::UpdateGuiInput(const GuiInputSnapshot& input) { lua_pushnumber(L, input.mouseX); lua_pushnumber(L, input.mouseY); lua_pushboolean(L, input.mouseDown); - lua_call(L, 4, 0); + lua_pushnumber(L, input.mouseDeltaX); + lua_pushnumber(L, input.mouseDeltaY); + lua_call(L, 6, 0); lua_getfield(L, stateIndex, "setWheel"); lua_pushvalue(L, stateIndex); diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 6dd089d..ab81300 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -176,6 +176,41 @@ RuntimeConfig JsonConfigService::LoadFromJson(std::shared_ptr logger, c config.windowTitle = value.GetString(); } + if (document.HasMember("mouse_grab")) { + const auto& mouseGrabValue = document["mouse_grab"]; + if (!mouseGrabValue.IsObject()) { + throw std::runtime_error("JSON member 'mouse_grab' must be an object"); + } + auto readBool = [&](const char* name, bool& target) { + if (!mouseGrabValue.HasMember(name)) { + return; + } + const auto& value = mouseGrabValue[name]; + if (!value.IsBool()) { + throw std::runtime_error("JSON member 'mouse_grab." + std::string(name) + "' must be a boolean"); + } + target = value.GetBool(); + }; + auto readString = [&](const char* name, std::string& target) { + if (!mouseGrabValue.HasMember(name)) { + return; + } + const auto& value = mouseGrabValue[name]; + if (!value.IsString()) { + throw std::runtime_error("JSON member 'mouse_grab." + std::string(name) + "' must be a string"); + } + target = value.GetString(); + }; + readBool("enabled", config.mouseGrab.enabled); + readBool("grab_on_click", config.mouseGrab.grabOnClick); + readBool("release_on_escape", config.mouseGrab.releaseOnEscape); + readBool("start_grabbed", config.mouseGrab.startGrabbed); + readBool("hide_cursor", config.mouseGrab.hideCursor); + readBool("relative_mode", config.mouseGrab.relativeMode); + readString("grab_mouse_button", config.mouseGrab.grabMouseButton); + readString("release_key", config.mouseGrab.releaseKey); + } + if (document.HasMember("input_bindings")) { const auto& bindingsValue = document["input_bindings"]; if (!bindingsValue.IsObject()) { diff --git a/src/services/impl/json_config_service.hpp b/src/services/impl/json_config_service.hpp index f31146e..c5c4498 100644 --- a/src/services/impl/json_config_service.hpp +++ b/src/services/impl/json_config_service.hpp @@ -82,6 +82,12 @@ public: } return config_.inputBindings; } + const MouseGrabConfig& GetMouseGrabConfig() const override { + if (logger_) { + logger_->Trace("JsonConfigService", "GetMouseGrabConfig"); + } + return config_.mouseGrab; + } /** * @brief Get the full runtime configuration. diff --git a/src/services/impl/json_config_writer_service.cpp b/src/services/impl/json_config_writer_service.cpp index a525e4f..322df59 100644 --- a/src/services/impl/json_config_writer_service.cpp +++ b/src/services/impl/json_config_writer_service.cpp @@ -49,6 +49,21 @@ void JsonConfigWriterService::WriteConfig(const RuntimeConfig& config, const std std::filesystem::path scriptsDir = config.scriptPath.parent_path(); addStringMember("scripts_directory", scriptsDir.string()); + rapidjson::Value mouseGrabObject(rapidjson::kObjectType); + mouseGrabObject.AddMember("enabled", config.mouseGrab.enabled, allocator); + mouseGrabObject.AddMember("grab_on_click", config.mouseGrab.grabOnClick, allocator); + mouseGrabObject.AddMember("release_on_escape", config.mouseGrab.releaseOnEscape, allocator); + mouseGrabObject.AddMember("start_grabbed", config.mouseGrab.startGrabbed, allocator); + mouseGrabObject.AddMember("hide_cursor", config.mouseGrab.hideCursor, allocator); + mouseGrabObject.AddMember("relative_mode", config.mouseGrab.relativeMode, allocator); + mouseGrabObject.AddMember("grab_mouse_button", + rapidjson::Value(config.mouseGrab.grabMouseButton.c_str(), allocator), + allocator); + mouseGrabObject.AddMember("release_key", + rapidjson::Value(config.mouseGrab.releaseKey.c_str(), allocator), + allocator); + document.AddMember("mouse_grab", mouseGrabObject, allocator); + rapidjson::Value bindingsObject(rapidjson::kObjectType); auto addBindingMember = [&](const char* name, const std::string& value) { rapidjson::Value nameValue(name, allocator); diff --git a/src/services/impl/sdl_input_service.cpp b/src/services/impl/sdl_input_service.cpp index 97d0029..ac2b595 100644 --- a/src/services/impl/sdl_input_service.cpp +++ b/src/services/impl/sdl_input_service.cpp @@ -102,6 +102,8 @@ void SdlInputService::ProcessEvent(const SDL_Event& event) { // GUI input processing guiInputSnapshot_.mouseX = static_cast(event.motion.x); guiInputSnapshot_.mouseY = static_cast(event.motion.y); + guiInputSnapshot_.mouseDeltaX += static_cast(event.motion.xrel); + guiInputSnapshot_.mouseDeltaY += static_cast(event.motion.yrel); break; case SDL_EVENT_MOUSE_BUTTON_DOWN: @@ -148,6 +150,8 @@ void SdlInputService::ResetFrameState() { state_.textInput.clear(); // Reset GUI per-frame state + guiInputSnapshot_.mouseDeltaX = 0.0f; + guiInputSnapshot_.mouseDeltaY = 0.0f; guiInputSnapshot_.wheel = 0.0f; guiInputSnapshot_.textInput.clear(); } @@ -214,6 +218,8 @@ void SdlInputService::OnMouseMoved(const events::Event& event) { state_.mouseY = mouseEvent.y; guiInputSnapshot_.mouseX = mouseEvent.x; guiInputSnapshot_.mouseY = mouseEvent.y; + guiInputSnapshot_.mouseDeltaX += mouseEvent.deltaX; + guiInputSnapshot_.mouseDeltaY += mouseEvent.deltaY; } void SdlInputService::OnMouseButtonPressed(const events::Event& event) { diff --git a/src/services/impl/sdl_window_service.cpp b/src/services/impl/sdl_window_service.cpp index 5b5e3fe..5e1d5f2 100644 --- a/src/services/impl/sdl_window_service.cpp +++ b/src/services/impl/sdl_window_service.cpp @@ -1,6 +1,8 @@ #include "sdl_window_service.hpp" #include "../interfaces/i_logger.hpp" #include +#include +#include #include #include #include @@ -29,6 +31,40 @@ std::string BuildSdlErrorMessage(const char* context, const std::shared_ptr(std::tolower(c)); }); + return value; +} + +bool TryParseMouseButton(const std::string& name, uint8_t& buttonOut) { + if (name.empty()) { + return false; + } + std::string normalized = NormalizeMouseBindingName(name); + if (normalized == "left" || normalized == "button1") { + buttonOut = SDL_BUTTON_LEFT; + return true; + } + if (normalized == "right" || normalized == "button2") { + buttonOut = SDL_BUTTON_RIGHT; + return true; + } + if (normalized == "middle" || normalized == "button3") { + buttonOut = SDL_BUTTON_MIDDLE; + return true; + } + if (normalized == "x1" || normalized == "button4") { + buttonOut = SDL_BUTTON_X1; + return true; + } + if (normalized == "x2" || normalized == "button5") { + buttonOut = SDL_BUTTON_X2; + return true; + } + return false; +} + void ThrowSdlErrorIfFailed(bool success, const char* context, const std::shared_ptr& platformService) { if (!success) { throw std::runtime_error(BuildSdlErrorMessage(context, platformService)); @@ -106,7 +142,8 @@ void SdlWindowService::CreateWindow(const WindowConfig& config) { "config.width=" + std::to_string(config.width) + ", config.height=" + std::to_string(config.height) + ", config.title=" + config.title + - ", config.resizable=" + std::string(config.resizable ? "true" : "false")); + ", config.resizable=" + std::string(config.resizable ? "true" : "false") + + ", mouseGrab.enabled=" + std::string(config.mouseGrab.enabled ? "true" : "false")); if (!initialized_) { throw std::runtime_error("SdlWindowService not initialized"); @@ -174,6 +211,13 @@ void SdlWindowService::CreateWindow(const WindowConfig& config) { SDL_StartTextInput(window_); + mouseGrabConfig_ = config.mouseGrab; + mouseGrabbed_ = false; + ConfigureMouseGrabBindings(); + if (mouseGrabConfig_.enabled && mouseGrabConfig_.startGrabbed) { + ApplyMouseGrab(true); + } + logger_->TraceVariable("window_", reinterpret_cast(window_)); logger_->TraceVariable("width", static_cast(config.width)); logger_->TraceVariable("height", static_cast(config.height)); @@ -183,6 +227,9 @@ void SdlWindowService::DestroyWindow() { logger_->Trace("SdlWindowService", "DestroyWindow", "windowIsNull=" + std::string(window_ ? "false" : "true")); if (window_) { + if (mouseGrabbed_) { + ApplyMouseGrab(false); + } SDL_StopTextInput(window_); SDL_DestroyWindow(window_); window_ = nullptr; @@ -218,6 +265,7 @@ void SdlWindowService::PollEvents() { while (SDL_PollEvent(&event)) { // Convert SDL event to application event and publish PublishEvent(event); + HandleMouseGrabEvent(event); // Check for quit event if (event.type == SDL_EVENT_QUIT || event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) { @@ -234,6 +282,107 @@ void SdlWindowService::SetTitle(const std::string& title) { } } +void SdlWindowService::ConfigureMouseGrabBindings() { + grabMouseButton_ = SDL_BUTTON_LEFT; + releaseKey_ = SDLK_ESCAPE; + + if (!mouseGrabConfig_.grabMouseButton.empty()) { + uint8_t parsedButton = grabMouseButton_; + if (TryParseMouseButton(mouseGrabConfig_.grabMouseButton, parsedButton)) { + grabMouseButton_ = parsedButton; + } else if (logger_) { + logger_->Error("SdlWindowService: unknown mouse grab button '" + + mouseGrabConfig_.grabMouseButton + "'"); + } + } + + if (!mouseGrabConfig_.releaseKey.empty()) { + SDL_Keycode parsedKey = SDL_GetKeyFromName(mouseGrabConfig_.releaseKey.c_str()); + if (parsedKey != SDLK_UNKNOWN) { + releaseKey_ = parsedKey; + } else if (logger_) { + logger_->Error("SdlWindowService: unknown mouse release key '" + + mouseGrabConfig_.releaseKey + "'"); + } + } + + if (logger_) { + logger_->Trace("SdlWindowService", "ConfigureMouseGrabBindings", + "grabMouseButton=" + std::to_string(static_cast(grabMouseButton_)) + + ", releaseKey=" + std::to_string(static_cast(releaseKey_))); + } +} + +void SdlWindowService::HandleMouseGrabEvent(const SDL_Event& sdlEvent) { + if (!window_ || !mouseGrabConfig_.enabled) { + return; + } + + if (mouseGrabConfig_.grabOnClick && + sdlEvent.type == SDL_EVENT_MOUSE_BUTTON_DOWN && + sdlEvent.button.button == grabMouseButton_) { + ApplyMouseGrab(true); + return; + } + + if (mouseGrabConfig_.releaseOnEscape && + sdlEvent.type == SDL_EVENT_KEY_DOWN && + sdlEvent.key.key == releaseKey_ && + !sdlEvent.key.repeat) { + ApplyMouseGrab(false); + } +} + +void SdlWindowService::ApplyMouseGrab(bool grabbed) { + if (!window_ || !mouseGrabConfig_.enabled) { + return; + } + if (mouseGrabbed_ == grabbed) { + return; + } + + if (logger_) { + logger_->Trace("SdlWindowService", "ApplyMouseGrab", + "grabbed=" + std::string(grabbed ? "true" : "false") + + ", relativeMode=" + std::string(mouseGrabConfig_.relativeMode ? "true" : "false") + + ", hideCursor=" + std::string(mouseGrabConfig_.hideCursor ? "true" : "false")); + } + + bool success = true; + if (mouseGrabConfig_.relativeMode) { + if (!SDL_SetWindowRelativeMouseMode(window_, grabbed)) { + success = false; + if (logger_) { + logger_->Error("SdlWindowService: " + + BuildSdlErrorMessage("SDL_SetWindowRelativeMouseMode failed", platformService_)); + } + } + } + + if (!SDL_SetWindowMouseGrab(window_, grabbed)) { + success = false; + if (logger_) { + logger_->Error("SdlWindowService: " + + BuildSdlErrorMessage("SDL_SetWindowMouseGrab failed", platformService_)); + } + } + + if (mouseGrabConfig_.hideCursor) { + bool cursorResult = grabbed ? SDL_HideCursor() : SDL_ShowCursor(); + if (!cursorResult && logger_) { + logger_->Error("SdlWindowService: " + + BuildSdlErrorMessage(grabbed ? "SDL_HideCursor failed" : "SDL_ShowCursor failed", + platformService_)); + } + } + + if (success) { + mouseGrabbed_ = grabbed; + } else if (logger_) { + logger_->Trace("SdlWindowService", "ApplyMouseGrab", "grabChangeFailed=true"); + } +} + void SdlWindowService::PublishEvent(const SDL_Event& sdlEvent) { logger_->Trace("SdlWindowService", "PublishEvent", "eventType=" + std::to_string(static_cast(sdlEvent.type))); diff --git a/src/services/impl/sdl_window_service.hpp b/src/services/impl/sdl_window_service.hpp index 3a0c5cb..3763227 100644 --- a/src/services/impl/sdl_window_service.hpp +++ b/src/services/impl/sdl_window_service.hpp @@ -61,10 +61,17 @@ private: SDL_Window* window_ = nullptr; bool shouldClose_ = false; bool initialized_ = false; + MouseGrabConfig mouseGrabConfig_{}; + bool mouseGrabbed_ = false; + uint8_t grabMouseButton_ = SDL_BUTTON_LEFT; + SDL_Keycode releaseKey_ = SDLK_ESCAPE; // Helper methods void PublishEvent(const SDL_Event& sdlEvent); double GetCurrentTime() const; + void HandleMouseGrabEvent(const SDL_Event& sdlEvent); + void ApplyMouseGrab(bool grabbed); + void ConfigureMouseGrabBindings(); }; } // namespace sdl3cpp::services::impl diff --git a/src/services/interfaces/config_types.hpp b/src/services/interfaces/config_types.hpp index 163b047..0129dba 100644 --- a/src/services/interfaces/config_types.hpp +++ b/src/services/interfaces/config_types.hpp @@ -44,6 +44,20 @@ struct InputBindings { float gamepadAxisActionThreshold = 0.5f; }; +/** + * @brief Mouse grabbing behavior configuration. + */ +struct MouseGrabConfig { + bool enabled = false; + bool grabOnClick = true; + bool releaseOnEscape = true; + bool startGrabbed = false; + bool hideCursor = true; + bool relativeMode = true; + std::string grabMouseButton = "left"; + std::string releaseKey = "escape"; +}; + /** * @brief Runtime configuration values used across services. */ @@ -53,6 +67,7 @@ struct RuntimeConfig { std::filesystem::path scriptPath; bool luaDebug = false; std::string windowTitle = "SDL3 Vulkan Demo"; + MouseGrabConfig mouseGrab{}; InputBindings inputBindings{}; }; diff --git a/src/services/interfaces/gui_types.hpp b/src/services/interfaces/gui_types.hpp index 188e70a..920fd29 100644 --- a/src/services/interfaces/gui_types.hpp +++ b/src/services/interfaces/gui_types.hpp @@ -8,6 +8,8 @@ namespace sdl3cpp::services { struct GuiInputSnapshot { float mouseX = 0.0f; float mouseY = 0.0f; + float mouseDeltaX = 0.0f; + float mouseDeltaY = 0.0f; bool mouseDown = false; float wheel = 0.0f; std::string textInput; diff --git a/src/services/interfaces/i_config_service.hpp b/src/services/interfaces/i_config_service.hpp index 2a2bda5..71e4039 100644 --- a/src/services/interfaces/i_config_service.hpp +++ b/src/services/interfaces/i_config_service.hpp @@ -59,6 +59,12 @@ public: * @return Input bindings structure */ virtual const InputBindings& GetInputBindings() const = 0; + + /** + * @brief Get configured mouse grab settings. + * @return Mouse grab configuration + */ + virtual const MouseGrabConfig& GetMouseGrabConfig() const = 0; }; } // namespace sdl3cpp::services diff --git a/src/services/interfaces/i_window_service.hpp b/src/services/interfaces/i_window_service.hpp index 318a771..84d11ac 100644 --- a/src/services/interfaces/i_window_service.hpp +++ b/src/services/interfaces/i_window_service.hpp @@ -1,5 +1,6 @@ #pragma once +#include "config_types.hpp" #include #include #include @@ -17,6 +18,7 @@ struct WindowConfig { uint32_t height; std::string title; bool resizable; + MouseGrabConfig mouseGrab{}; }; /**