From 5fec5e954499a220d51d9c703142a88b5c2ea5f7 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Mon, 5 Jan 2026 14:49:40 +0000 Subject: [PATCH] feat: Enhance input handling and window services - Added mouse delta tracking to InputState for improved mouse movement feedback. - Introduced FPS mode toggle in GUI demo, allowing for relative mouse movement and cursor visibility control. - Implemented input service methods for mouse position, delta, wheel, and key/action state checks. - Updated SDL input service to handle relative mouse mode and mouse grabbing more effectively. - Enhanced window service with methods to manage mouse grabbing and cursor visibility. - Updated interface definitions for IInputService and IWindowService to include new functionalities. --- scripts/gui.lua | 8 +- scripts/gui_demo.lua | 88 +++++- src/app/service_based_app.cpp | 2 + src/services/impl/script_engine_service.cpp | 294 ++++++++++++++++++- src/services/impl/script_engine_service.hpp | 24 ++ src/services/impl/sdl_input_service.cpp | 119 ++++++-- src/services/impl/sdl_input_service.hpp | 12 + src/services/impl/sdl_window_service.cpp | 101 ++++++- src/services/impl/sdl_window_service.hpp | 9 +- src/services/interfaces/i_input_service.hpp | 26 ++ src/services/interfaces/i_window_service.hpp | 42 +++ 11 files changed, 688 insertions(+), 37 deletions(-) diff --git a/scripts/gui.lua b/scripts/gui.lua index dfd7cf7..9a7983e 100644 --- a/scripts/gui.lua +++ b/scripts/gui.lua @@ -59,6 +59,8 @@ function InputState:new() local instance = { mouseX = 0, mouseY = 0, + mouseDeltaX = 0, + mouseDeltaY = 0, mouseDown = false, mouseDownPrevious = false, wheel = 0, @@ -68,10 +70,12 @@ function InputState:new() return setmetatable(instance, self) end -function InputState:setMouse(x, y, isDown) +function InputState:setMouse(x, y, isDown, deltaX, deltaY) self.mouseDownPrevious = self.mouseDown self.mouseX = x self.mouseY = y + self.mouseDeltaX = deltaX or 0 + self.mouseDeltaY = deltaY or 0 self.mouseDown = isDown end @@ -102,6 +106,8 @@ end function InputState:resetTransient() self.textInput = "" self.wheel = 0 + self.mouseDeltaX = 0 + self.mouseDeltaY = 0 end local Context = {} diff --git a/scripts/gui_demo.lua b/scripts/gui_demo.lua index 8803293..671dcd2 100644 --- a/scripts/gui_demo.lua +++ b/scripts/gui_demo.lua @@ -20,6 +20,38 @@ local buttonStates = { local statusMessage = 'Ready' local viewProjectionLogged = false +local fpsMode = false +local fpsToggleWasDown = false + +local function setFpsMode(enabled) + fpsMode = enabled + if window_set_relative_mouse_mode then + window_set_relative_mouse_mode(enabled) + end + if window_set_mouse_grabbed then + window_set_mouse_grabbed(enabled) + end + if window_set_cursor_visible then + window_set_cursor_visible(not enabled) + end + statusMessage = enabled and "FPS mode enabled" or "FPS mode disabled" + log_trace("FPS mode toggled: %s", enabled and "on" or "off") +end + +local function updateFpsModeToggle() + if not input_is_key_down then + return + end + local down = input_is_key_down("F1") + if down and not fpsToggleWasDown then + setFpsMode(not fpsMode) + end + fpsToggleWasDown = down + if fpsMode and window_get_mouse_grabbed and not window_get_mouse_grabbed() then + fpsMode = false + statusMessage = "FPS mode disabled" + end +end local shader_variants = { default = { @@ -83,14 +115,37 @@ local function drawTestButtons() buttonStates.button2 and "ON" or "OFF", buttonStates.button3 and "ON" or "OFF", buttonStates.button4 and "ON" or "OFF") - Gui.text(ctx, {x = 70, y = 320, width = 360, height = 30}, statesText, { + Gui.text(ctx, {x = 70, y = 315, width = 360, height = 22}, statesText, { fontSize = 16, alignX = "center", color = {1.0, 0.8, 0.8, 1.0}, }) + local deltaX = input.mouseDeltaX or 0 + local deltaY = input.mouseDeltaY or 0 + Gui.text(ctx, {x = 70, y = 340, width = 360, height = 20}, + string.format("Mouse d: %.1f, %.1f", deltaX, deltaY), { + fontSize = 14, + alignX = "center", + color = {0.85, 0.9, 0.85, 1.0}, + }) + + local grabState = window_get_mouse_grabbed and window_get_mouse_grabbed() or false + local fpsLabel = fpsMode and "ON" or "OFF" + Gui.text(ctx, {x = 70, y = 362, width = 360, height = 20}, + string.format("FPS Mode: %s (grab=%s, F1 toggle)", fpsLabel, grabState and "on" or "off"), { + fontSize = 14, + alignX = "center", + color = {0.85, 0.85, 0.95, 1.0}, + }) + + if Gui.button(ctx, "fps_toggle", {x = 155, y = 385, width = 140, height = 28}, + fpsMode and "FPS: ON" or "FPS: OFF") then + setFpsMode(not fpsMode) + end + -- Reset button - if Gui.button(ctx, "reset", {x = 175, y = 370, width = 100, height = 40}, "Reset") then + if Gui.button(ctx, "reset", {x = 175, y = 418, width = 100, height = 28}, "Reset") then buttonStates.button1 = false buttonStates.button2 = false buttonStates.button3 = false @@ -123,14 +178,31 @@ function get_view_projection(aspect) end function get_gui_commands() + updateFpsModeToggle() ctx:beginFrame(input) drawTestButtons() - Gui.cursor(ctx, input, { - size = 16, - thickness = 2, - color = {1.0, 0.9, 0.2, 1.0}, - activeColor = {1.0, 0.35, 0.15, 1.0}, - }) + if fpsMode then + local width, height = 1024, 768 + if window_get_size then + local w, h = window_get_size() + if type(w) == "number" and type(h) == "number" then + width, height = w, h + end + end + Gui.cursor(ctx, {mouseX = width * 0.5, mouseY = height * 0.5, mouseDown = input.mouseDown}, { + size = 18, + thickness = 2, + color = {0.9, 0.95, 1.0, 1.0}, + activeColor = {1.0, 0.4, 0.2, 1.0}, + }) + else + Gui.cursor(ctx, input, { + size = 16, + thickness = 2, + color = {1.0, 0.9, 0.2, 1.0}, + activeColor = {1.0, 0.35, 0.15, 1.0}, + }) + end ctx:endFrame() return ctx:getCommands() end diff --git a/src/app/service_based_app.cpp b/src/app/service_based_app.cpp index 5cf88c5..c3f8c3b 100644 --- a/src/app/service_based_app.cpp +++ b/src/app/service_based_app.cpp @@ -262,6 +262,8 @@ void ServiceBasedApp::RegisterServices() { registry_.GetService(), registry_.GetService(), registry_.GetService(), + registry_.GetService(), + registry_.GetService(), runtimeConfig_.luaDebug); // Script-facing services diff --git a/src/services/impl/script_engine_service.cpp b/src/services/impl/script_engine_service.cpp index 4223a79..e3c4b97 100644 --- a/src/services/impl/script_engine_service.cpp +++ b/src/services/impl/script_engine_service.cpp @@ -5,12 +5,64 @@ #include #include +#include +#include #include +#include #include #include #include +namespace { +bool TryParseMouseButtonName(const char* name, uint8_t& button) { + if (!name) { + return false; + } + std::string lower(name); + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char ch) { return static_cast(std::tolower(ch)); }); + if (lower == "left") { + button = SDL_BUTTON_LEFT; + return true; + } + if (lower == "right") { + button = SDL_BUTTON_RIGHT; + return true; + } + if (lower == "middle") { + button = SDL_BUTTON_MIDDLE; + return true; + } + if (lower == "x1" || lower == "extra1") { + button = SDL_BUTTON_X1; + return true; + } + if (lower == "x2" || lower == "extra2") { + button = SDL_BUTTON_X2; + return true; + } + return false; +} + +SDL_Keycode ReadKeycodeFromLua(lua_State* L, int index, bool& ok) { + ok = true; + if (lua_isnumber(L, index)) { + return static_cast(lua_tointeger(L, index)); + } + if (lua_isstring(L, index)) { + const char* name = lua_tostring(L, index); + SDL_Keycode key = SDL_GetKeyFromName(name); + if (key == SDLK_UNKNOWN) { + ok = false; + } + return key; + } + ok = false; + return SDLK_UNKNOWN; +} +} // namespace + namespace sdl3cpp::services::impl { ScriptEngineService::ScriptEngineService(const std::filesystem::path& scriptPath, @@ -18,11 +70,15 @@ ScriptEngineService::ScriptEngineService(const std::filesystem::path& scriptPath std::shared_ptr meshService, std::shared_ptr audioCommandService, std::shared_ptr physicsBridgeService, + std::shared_ptr inputService, + std::shared_ptr windowService, bool debugEnabled) : logger_(std::move(logger)), meshService_(std::move(meshService)), audioCommandService_(std::move(audioCommandService)), physicsBridgeService_(std::move(physicsBridgeService)), + inputService_(std::move(inputService)), + windowService_(std::move(windowService)), scriptPath_(scriptPath), debugEnabled_(debugEnabled) { if (logger_) { @@ -31,7 +87,9 @@ ScriptEngineService::ScriptEngineService(const std::filesystem::path& scriptPath ", debugEnabled=" + std::string(debugEnabled_ ? "true" : "false") + ", meshService=" + std::string(meshService_ ? "set" : "null") + ", audioCommandService=" + std::string(audioCommandService_ ? "set" : "null") + - ", physicsBridgeService=" + std::string(physicsBridgeService_ ? "set" : "null")); + ", physicsBridgeService=" + std::string(physicsBridgeService_ ? "set" : "null") + + ", inputService=" + std::string(inputService_ ? "set" : "null") + + ", windowService=" + std::string(windowService_ ? "set" : "null")); } } @@ -56,6 +114,8 @@ void ScriptEngineService::Initialize() { bindingContext_->meshService = meshService_; bindingContext_->audioCommandService = audioCommandService_; bindingContext_->physicsBridgeService = physicsBridgeService_; + bindingContext_->inputService = inputService_; + bindingContext_->windowService = windowService_; bindingContext_->logger = logger_; luaState_ = luaL_newstate(); @@ -160,6 +220,22 @@ void ScriptEngineService::RegisterBindings(lua_State* L) { bind("audio_play_background", &ScriptEngineService::AudioPlayBackground); bind("audio_play_sound", &ScriptEngineService::AudioPlaySound); bind("audio_stop_background", &ScriptEngineService::AudioStopBackground); + bind("input_get_mouse_position", &ScriptEngineService::InputGetMousePosition); + bind("input_get_mouse_delta", &ScriptEngineService::InputGetMouseDelta); + bind("input_get_mouse_wheel", &ScriptEngineService::InputGetMouseWheel); + bind("input_is_key_down", &ScriptEngineService::InputIsKeyDown); + bind("input_is_action_down", &ScriptEngineService::InputIsActionDown); + bind("input_is_mouse_down", &ScriptEngineService::InputIsMouseDown); + bind("input_get_text", &ScriptEngineService::InputGetText); + bind("window_get_size", &ScriptEngineService::WindowGetSize); + bind("window_set_title", &ScriptEngineService::WindowSetTitle); + bind("window_is_minimized", &ScriptEngineService::WindowIsMinimized); + bind("window_set_mouse_grabbed", &ScriptEngineService::WindowSetMouseGrabbed); + bind("window_get_mouse_grabbed", &ScriptEngineService::WindowGetMouseGrabbed); + bind("window_set_relative_mouse_mode", &ScriptEngineService::WindowSetRelativeMouseMode); + bind("window_get_relative_mouse_mode", &ScriptEngineService::WindowGetRelativeMouseMode); + bind("window_set_cursor_visible", &ScriptEngineService::WindowSetCursorVisible); + bind("window_is_cursor_visible", &ScriptEngineService::WindowIsCursorVisible); } int ScriptEngineService::LoadMeshFromFile(lua_State* L) { @@ -384,6 +460,222 @@ int ScriptEngineService::AudioStopBackground(lua_State* L) { return 1; } +int ScriptEngineService::InputGetMousePosition(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->inputService) { + lua_pushnil(L); + lua_pushstring(L, "Input service not available"); + return 2; + } + auto [x, y] = context->inputService->GetMousePosition(); + lua_pushnumber(L, x); + lua_pushnumber(L, y); + return 2; +} + +int ScriptEngineService::InputGetMouseDelta(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->inputService) { + lua_pushnil(L); + lua_pushstring(L, "Input service not available"); + return 2; + } + const auto& state = context->inputService->GetState(); + lua_pushnumber(L, state.mouseDeltaX); + lua_pushnumber(L, state.mouseDeltaY); + return 2; +} + +int ScriptEngineService::InputGetMouseWheel(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->inputService) { + lua_pushnil(L); + lua_pushstring(L, "Input service not available"); + return 2; + } + const auto& state = context->inputService->GetState(); + lua_pushnumber(L, state.mouseWheelDeltaX); + lua_pushnumber(L, state.mouseWheelDeltaY); + return 2; +} + +int ScriptEngineService::InputIsKeyDown(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->inputService) { + lua_pushnil(L); + lua_pushstring(L, "Input service not available"); + return 2; + } + bool ok = false; + SDL_Keycode key = ReadKeycodeFromLua(L, 1, ok); + if (!ok) { + lua_pushboolean(L, 0); + return 1; + } + lua_pushboolean(L, context->inputService->IsKeyPressed(key)); + return 1; +} + +int ScriptEngineService::InputIsActionDown(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->inputService) { + lua_pushnil(L); + lua_pushstring(L, "Input service not available"); + return 2; + } + const char* action = luaL_checkstring(L, 1); + lua_pushboolean(L, context->inputService->IsActionPressed(action)); + return 1; +} + +int ScriptEngineService::InputIsMouseDown(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->inputService) { + lua_pushnil(L); + lua_pushstring(L, "Input service not available"); + return 2; + } + uint8_t button = SDL_BUTTON_LEFT; + if (lua_isnumber(L, 1)) { + button = static_cast(lua_tointeger(L, 1)); + } else if (lua_isstring(L, 1)) { + const char* name = lua_tostring(L, 1); + if (!TryParseMouseButtonName(name, button)) { + lua_pushboolean(L, 0); + return 1; + } + } else { + lua_pushboolean(L, 0); + return 1; + } + lua_pushboolean(L, context->inputService->IsMouseButtonPressed(button)); + return 1; +} + +int ScriptEngineService::InputGetText(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->inputService) { + lua_pushnil(L); + lua_pushstring(L, "Input service not available"); + return 2; + } + const auto& state = context->inputService->GetState(); + lua_pushstring(L, state.textInput.c_str()); + return 1; +} + +int ScriptEngineService::WindowGetSize(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + auto [width, height] = context->windowService->GetSize(); + lua_pushinteger(L, static_cast(width)); + lua_pushinteger(L, static_cast(height)); + return 2; +} + +int ScriptEngineService::WindowSetTitle(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + const char* title = luaL_checkstring(L, 1); + context->windowService->SetTitle(title); + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::WindowIsMinimized(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + lua_pushboolean(L, context->windowService->IsMinimized()); + return 1; +} + +int ScriptEngineService::WindowSetMouseGrabbed(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + bool grabbed = lua_toboolean(L, 1); + context->windowService->SetMouseGrabbed(grabbed); + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::WindowGetMouseGrabbed(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + lua_pushboolean(L, context->windowService->IsMouseGrabbed()); + return 1; +} + +int ScriptEngineService::WindowSetRelativeMouseMode(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + bool enabled = lua_toboolean(L, 1); + context->windowService->SetRelativeMouseMode(enabled); + if (context->inputService) { + context->inputService->SetRelativeMouseMode(enabled); + } + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::WindowGetRelativeMouseMode(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + lua_pushboolean(L, context->windowService->IsRelativeMouseMode()); + return 1; +} + +int ScriptEngineService::WindowSetCursorVisible(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + bool visible = lua_toboolean(L, 1); + context->windowService->SetCursorVisible(visible); + lua_pushboolean(L, 1); + return 1; +} + +int ScriptEngineService::WindowIsCursorVisible(lua_State* L) { + auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); + if (!context || !context->windowService) { + lua_pushnil(L); + lua_pushstring(L, "Window service not available"); + return 2; + } + lua_pushboolean(L, context->windowService->IsCursorVisible()); + return 1; +} + int ScriptEngineService::GlmMatrixFromTransform(lua_State* L) { auto* context = static_cast(lua_touserdata(L, lua_upvalueindex(1))); auto logger = context ? context->logger : nullptr; diff --git a/src/services/impl/script_engine_service.hpp b/src/services/impl/script_engine_service.hpp index 2f57d05..e7b5b75 100644 --- a/src/services/impl/script_engine_service.hpp +++ b/src/services/impl/script_engine_service.hpp @@ -4,6 +4,8 @@ #include "../interfaces/i_audio_command_service.hpp" #include "../interfaces/i_mesh_service.hpp" #include "../interfaces/i_physics_bridge_service.hpp" +#include "../interfaces/i_input_service.hpp" +#include "../interfaces/i_window_service.hpp" #include "../interfaces/i_logger.hpp" #include "../../di/lifecycle.hpp" #include @@ -25,6 +27,8 @@ public: std::shared_ptr meshService, std::shared_ptr audioCommandService, std::shared_ptr physicsBridgeService, + std::shared_ptr inputService, + std::shared_ptr windowService, bool debugEnabled = false); ~ScriptEngineService() override; @@ -47,6 +51,8 @@ private: std::shared_ptr meshService; std::shared_ptr audioCommandService; std::shared_ptr physicsBridgeService; + std::shared_ptr inputService; + std::shared_ptr windowService; std::shared_ptr logger; }; @@ -59,11 +65,29 @@ private: static int AudioPlaySound(lua_State* L); static int AudioStopBackground(lua_State* L); static int GlmMatrixFromTransform(lua_State* L); + static int InputGetMousePosition(lua_State* L); + static int InputGetMouseDelta(lua_State* L); + static int InputGetMouseWheel(lua_State* L); + static int InputIsKeyDown(lua_State* L); + static int InputIsActionDown(lua_State* L); + static int InputIsMouseDown(lua_State* L); + static int InputGetText(lua_State* L); + static int WindowGetSize(lua_State* L); + static int WindowSetTitle(lua_State* L); + static int WindowIsMinimized(lua_State* L); + static int WindowSetMouseGrabbed(lua_State* L); + static int WindowGetMouseGrabbed(lua_State* L); + static int WindowSetRelativeMouseMode(lua_State* L); + static int WindowGetRelativeMouseMode(lua_State* L); + static int WindowSetCursorVisible(lua_State* L); + static int WindowIsCursorVisible(lua_State* L); std::shared_ptr logger_; std::shared_ptr meshService_; std::shared_ptr audioCommandService_; std::shared_ptr physicsBridgeService_; + std::shared_ptr inputService_; + std::shared_ptr windowService_; std::filesystem::path scriptPath_; std::filesystem::path scriptDirectory_; bool debugEnabled_ = false; diff --git a/src/services/impl/sdl_input_service.cpp b/src/services/impl/sdl_input_service.cpp index 84ed3ae..05ac623 100644 --- a/src/services/impl/sdl_input_service.cpp +++ b/src/services/impl/sdl_input_service.cpp @@ -66,6 +66,10 @@ SdlInputService::SdlInputService(std::shared_ptr eventBus, OnTextInput(e); }); + eventBus_->Subscribe(events::EventType::WindowResized, [this](const events::Event& e) { + OnWindowResized(e); + }); + eventBus_->Subscribe(events::EventType::WindowFocusGained, [this](const events::Event& e) { OnWindowFocusGained(e); }); @@ -86,9 +90,23 @@ SdlInputService::SdlInputService(std::shared_ptr eventBus, const auto& mouseGrabConfig = configService_->GetMouseGrabConfig(); mouseGrabGatesLook_ = mouseGrabConfig.enabled && (mouseGrabConfig.grabOnClick || mouseGrabConfig.startGrabbed); + mouseRelativeMode_ = mouseGrabConfig.relativeMode; + guiWindowWidth_ = configService_->GetWindowWidth(); + guiWindowHeight_ = configService_->GetWindowHeight(); + if (guiWindowWidth_ > 0 && guiWindowHeight_ > 0) { + guiCursorX_ = static_cast(guiWindowWidth_) * 0.5f; + guiCursorY_ = static_cast(guiWindowHeight_) * 0.5f; + state_.mouseX = guiCursorX_; + state_.mouseY = guiCursorY_; + guiInputSnapshot_.mouseX = guiCursorX_; + guiInputSnapshot_.mouseY = guiCursorY_; + } if (logger_) { logger_->Trace("SdlInputService", "SdlInputService", - "mouseGrabGatesLook=" + std::string(mouseGrabGatesLook_ ? "true" : "false")); + "mouseGrabGatesLook=" + std::string(mouseGrabGatesLook_ ? "true" : "false") + + ", relativeMode=" + std::string(mouseRelativeMode_ ? "true" : "false") + + ", guiWindow=" + std::to_string(guiWindowWidth_) + "x" + + std::to_string(guiWindowHeight_)); } } BuildActionKeyMapping(); @@ -118,15 +136,10 @@ void SdlInputService::ProcessEvent(const SDL_Event& event) { break; case SDL_EVENT_MOUSE_MOTION: - state_.mouseX = event.motion.x; - state_.mouseY = event.motion.y; - // GUI input processing - guiInputSnapshot_.mouseX = static_cast(event.motion.x); - guiInputSnapshot_.mouseY = static_cast(event.motion.y); - if (ShouldCaptureMouseDelta()) { - guiInputSnapshot_.mouseDeltaX += static_cast(event.motion.xrel); - guiInputSnapshot_.mouseDeltaY += static_cast(event.motion.yrel); - } + UpdateMousePosition(static_cast(event.motion.x), + static_cast(event.motion.y), + static_cast(event.motion.xrel), + static_cast(event.motion.yrel)); break; case SDL_EVENT_MOUSE_BUTTON_DOWN: @@ -168,6 +181,8 @@ void SdlInputService::ResetFrameState() { logger_->Trace("SdlInputService", "ResetFrameState"); } // Reset per-frame state + state_.mouseDeltaX = 0.0f; + state_.mouseDeltaY = 0.0f; state_.mouseWheelDeltaX = 0.0f; state_.mouseWheelDeltaY = 0.0f; state_.textInput.clear(); @@ -195,6 +210,14 @@ bool SdlInputService::IsMouseButtonPressed(uint8_t button) const { return state_.mouseButtonsPressed.count(button) > 0; } +bool SdlInputService::IsActionPressed(const std::string& action) const { + if (logger_) { + logger_->Trace("SdlInputService", "IsActionPressed", + "action=" + action); + } + return IsActionKeyPressed(action); +} + std::pair SdlInputService::GetMousePosition() const { if (logger_) { logger_->Trace("SdlInputService", "GetMousePosition"); @@ -202,6 +225,21 @@ std::pair SdlInputService::GetMousePosition() const { return {state_.mouseX, state_.mouseY}; } +void SdlInputService::SetRelativeMouseMode(bool enabled) { + if (logger_) { + logger_->Trace("SdlInputService", "SetRelativeMouseMode", + "enabled=" + std::string(enabled ? "true" : "false")); + } + mouseRelativeMode_ = enabled; +} + +bool SdlInputService::IsRelativeMouseMode() const { + if (logger_) { + logger_->Trace("SdlInputService", "IsRelativeMouseMode"); + } + return mouseRelativeMode_; +} + void SdlInputService::OnKeyPressed(const events::Event& event) { const auto& keyEvent = event.GetData(); if (logger_) { @@ -237,14 +275,7 @@ void SdlInputService::OnMouseMoved(const events::Event& event) { ", deltaX=" + std::to_string(mouseEvent.deltaX) + ", deltaY=" + std::to_string(mouseEvent.deltaY)); } - state_.mouseX = mouseEvent.x; - state_.mouseY = mouseEvent.y; - guiInputSnapshot_.mouseX = mouseEvent.x; - guiInputSnapshot_.mouseY = mouseEvent.y; - if (ShouldCaptureMouseDelta()) { - guiInputSnapshot_.mouseDeltaX += mouseEvent.deltaX; - guiInputSnapshot_.mouseDeltaY += mouseEvent.deltaY; - } + UpdateMousePosition(mouseEvent.x, mouseEvent.y, mouseEvent.deltaX, mouseEvent.deltaY); } void SdlInputService::OnMouseButtonPressed(const events::Event& event) { @@ -300,6 +331,18 @@ void SdlInputService::OnTextInput(const events::Event& event) { guiInputSnapshot_.textInput += textEvent.text; } +void SdlInputService::OnWindowResized(const events::Event& event) { + const auto& resized = event.GetData(); + guiWindowWidth_ = resized.width; + guiWindowHeight_ = resized.height; + ClampGuiCursor(); + if (logger_) { + logger_->Trace("SdlInputService", "OnWindowResized", + "width=" + std::to_string(guiWindowWidth_) + + ", height=" + std::to_string(guiWindowHeight_)); + } +} + void SdlInputService::OnWindowFocusGained(const events::Event& event) { (void)event; windowFocused_ = true; @@ -530,6 +573,46 @@ bool SdlInputService::ShouldCaptureMouseDelta() const { return true; } +void SdlInputService::UpdateMousePosition(float x, float y, float deltaX, float deltaY) { + if (mouseRelativeMode_ && mouseGrabbed_) { + guiCursorX_ += deltaX; + guiCursorY_ += deltaY; + ClampGuiCursor(); + state_.mouseX = guiCursorX_; + state_.mouseY = guiCursorY_; + guiInputSnapshot_.mouseX = guiCursorX_; + guiInputSnapshot_.mouseY = guiCursorY_; + if (logger_ && !relativeCursorLogged_) { + logger_->Trace("SdlInputService", "UpdateMousePosition", + "relativeCursor=true, startX=" + std::to_string(guiCursorX_) + + ", startY=" + std::to_string(guiCursorY_)); + relativeCursorLogged_ = true; + } + } else { + state_.mouseX = x; + state_.mouseY = y; + guiInputSnapshot_.mouseX = x; + guiInputSnapshot_.mouseY = y; + guiCursorX_ = x; + guiCursorY_ = y; + } + + if (ShouldCaptureMouseDelta()) { + guiInputSnapshot_.mouseDeltaX += deltaX; + guiInputSnapshot_.mouseDeltaY += deltaY; + } + state_.mouseDeltaX += deltaX; + state_.mouseDeltaY += deltaY; +} + +void SdlInputService::ClampGuiCursor() { + if (guiWindowWidth_ == 0 || guiWindowHeight_ == 0) { + return; + } + guiCursorX_ = std::clamp(guiCursorX_, 0.0f, static_cast(guiWindowWidth_ - 1)); + guiCursorY_ = std::clamp(guiCursorY_, 0.0f, static_cast(guiWindowHeight_ - 1)); +} + void SdlInputService::EnsureGamepadSubsystem() { uint32_t initialized = SDL_WasInit(0); if ((initialized & SDL_INIT_GAMEPAD) != 0) { diff --git a/src/services/impl/sdl_input_service.hpp b/src/services/impl/sdl_input_service.hpp index 7ac78c5..e61f34d 100644 --- a/src/services/impl/sdl_input_service.hpp +++ b/src/services/impl/sdl_input_service.hpp @@ -41,7 +41,10 @@ public: } bool IsKeyPressed(SDL_Keycode key) const override; bool IsMouseButtonPressed(uint8_t button) const override; + bool IsActionPressed(const std::string& action) const override; std::pair GetMousePosition() const override; + void SetRelativeMouseMode(bool enabled) override; + bool IsRelativeMouseMode() const override; void SetGuiScriptService(IGuiScriptService* guiScriptService) override; void UpdateGuiInput() override; @@ -56,6 +59,12 @@ private: bool windowFocused_ = true; bool mouseGrabbed_ = false; bool mouseGrabGatesLook_ = false; + bool mouseRelativeMode_ = false; + float guiCursorX_ = 0.0f; + float guiCursorY_ = 0.0f; + uint32_t guiWindowWidth_ = 0; + uint32_t guiWindowHeight_ = 0; + bool relativeCursorLogged_ = false; SDL_GamepadButton musicToggleButton_ = SDL_GAMEPAD_BUTTON_START; SDL_GamepadButton dpadUpButton_ = SDL_GAMEPAD_BUTTON_DPAD_UP; SDL_GamepadButton dpadDownButton_ = SDL_GAMEPAD_BUTTON_DPAD_DOWN; @@ -78,6 +87,7 @@ private: void OnMouseButtonReleased(const events::Event& event); void OnMouseWheel(const events::Event& event); void OnTextInput(const events::Event& event); + void OnWindowResized(const events::Event& event); void OnWindowFocusGained(const events::Event& event); void OnWindowFocusLost(const events::Event& event); void OnMouseGrabChanged(const events::Event& event); @@ -89,6 +99,8 @@ private: void ApplyKeyMapping(SDL_Keycode key, bool isDown); bool IsActionKeyPressed(const std::string& action) const; bool ShouldCaptureMouseDelta() const; + void UpdateMousePosition(float x, float y, float deltaX, float deltaY); + void ClampGuiCursor(); // GUI key mapping (extracted from old Sdl3App) static const std::unordered_map kGuiKeyNames; diff --git a/src/services/impl/sdl_window_service.cpp b/src/services/impl/sdl_window_service.cpp index 22349f0..f803990 100644 --- a/src/services/impl/sdl_window_service.cpp +++ b/src/services/impl/sdl_window_service.cpp @@ -215,7 +215,7 @@ void SdlWindowService::CreateWindow(const WindowConfig& config) { mouseGrabbed_ = false; ConfigureMouseGrabBindings(); if (mouseGrabConfig_.enabled && mouseGrabConfig_.startGrabbed) { - ApplyMouseGrab(true); + ApplyMouseGrab(true, false); } logger_->TraceVariable("window_", reinterpret_cast(window_)); @@ -228,7 +228,7 @@ void SdlWindowService::DestroyWindow() { "windowIsNull=" + std::string(window_ ? "false" : "true")); if (window_) { if (mouseGrabbed_) { - ApplyMouseGrab(false); + ApplyMouseGrab(false, false); } SDL_StopTextInput(window_); SDL_DestroyWindow(window_); @@ -259,6 +259,81 @@ bool SdlWindowService::IsMinimized() const { return (flags & SDL_WINDOW_MINIMIZED) != 0; } +void SdlWindowService::SetMouseGrabbed(bool grabbed) { + if (logger_) { + logger_->Trace("SdlWindowService", "SetMouseGrabbed", + "grabbed=" + std::string(grabbed ? "true" : "false")); + } + if (!window_) { + return; + } + if (!mouseGrabConfig_.enabled) { + mouseGrabConfig_.enabled = true; + } + ApplyMouseGrab(grabbed, true); +} + +bool SdlWindowService::IsMouseGrabbed() const { + if (logger_) { + logger_->Trace("SdlWindowService", "IsMouseGrabbed"); + } + return mouseGrabbed_; +} + +void SdlWindowService::SetRelativeMouseMode(bool enabled) { + if (logger_) { + logger_->Trace("SdlWindowService", "SetRelativeMouseMode", + "enabled=" + std::string(enabled ? "true" : "false")); + } + mouseGrabConfig_.relativeMode = enabled; + if (!window_) { + return; + } + if (!SDL_SetWindowRelativeMouseMode(window_, enabled) && logger_) { + logger_->Error("SdlWindowService: " + + BuildSdlErrorMessage("SDL_SetWindowRelativeMouseMode failed", platformService_)); + } + if (mouseGrabbed_ && mouseGrabConfig_.enabled) { + ApplyMouseGrab(mouseGrabbed_, true); + } +} + +bool SdlWindowService::IsRelativeMouseMode() const { + if (logger_) { + logger_->Trace("SdlWindowService", "IsRelativeMouseMode"); + } + return mouseGrabConfig_.relativeMode; +} + +void SdlWindowService::SetCursorVisible(bool visible) { + if (logger_) { + logger_->Trace("SdlWindowService", "SetCursorVisible", + "visible=" + std::string(visible ? "true" : "false")); + } + if (!window_) { + return; + } + mouseGrabConfig_.hideCursor = !visible; + bool cursorResult = visible ? SDL_ShowCursor() : SDL_HideCursor(); + if (!cursorResult && logger_) { + logger_->Error("SdlWindowService: " + + BuildSdlErrorMessage(visible ? "SDL_ShowCursor failed" : "SDL_HideCursor failed", + platformService_)); + } else { + cursorVisible_ = visible; + } + if (mouseGrabbed_ && mouseGrabConfig_.enabled) { + ApplyMouseGrab(mouseGrabbed_, true); + } +} + +bool SdlWindowService::IsCursorVisible() const { + if (logger_) { + logger_->Trace("SdlWindowService", "IsCursorVisible"); + } + return cursorVisible_; +} + void SdlWindowService::PollEvents() { logger_->Trace("SdlWindowService", "PollEvents"); SDL_Event event; @@ -273,7 +348,7 @@ void SdlWindowService::PollEvents() { logger_->Info("SdlWindowService: Mouse grab triggered by click (button=" + std::to_string(event.button.button) + ")"); } - ApplyMouseGrab(true); + ApplyMouseGrab(true, false); } // Handle release separately (doesn't suppress event) @@ -284,7 +359,7 @@ void SdlWindowService::PollEvents() { if (logger_) { logger_->Info("SdlWindowService: Mouse grab released by escape key"); } - ApplyMouseGrab(false); + ApplyMouseGrab(false, false); } // Only publish event if it's not the grab-triggering click @@ -348,7 +423,7 @@ void SdlWindowService::HandleMouseGrabEvent(const SDL_Event& sdlEvent) { if (mouseGrabConfig_.grabOnClick && sdlEvent.type == SDL_EVENT_MOUSE_BUTTON_DOWN && sdlEvent.button.button == grabMouseButton_) { - ApplyMouseGrab(true); + ApplyMouseGrab(true, false); return; } @@ -356,15 +431,15 @@ void SdlWindowService::HandleMouseGrabEvent(const SDL_Event& sdlEvent) { sdlEvent.type == SDL_EVENT_KEY_DOWN && sdlEvent.key.key == releaseKey_ && !sdlEvent.key.repeat) { - ApplyMouseGrab(false); + ApplyMouseGrab(false, false); } } -void SdlWindowService::ApplyMouseGrab(bool grabbed) { +void SdlWindowService::ApplyMouseGrab(bool grabbed, bool force) { if (!window_ || !mouseGrabConfig_.enabled) { return; } - if (mouseGrabbed_ == grabbed) { + if (!force && mouseGrabbed_ == grabbed) { return; } @@ -400,6 +475,16 @@ void SdlWindowService::ApplyMouseGrab(bool grabbed) { logger_->Error("SdlWindowService: " + BuildSdlErrorMessage(grabbed ? "SDL_HideCursor failed" : "SDL_ShowCursor failed", platformService_)); + } else { + cursorVisible_ = !grabbed; + } + } else { + bool cursorResult = SDL_ShowCursor(); + if (!cursorResult && logger_) { + logger_->Error("SdlWindowService: " + + BuildSdlErrorMessage("SDL_ShowCursor failed", platformService_)); + } else { + cursorVisible_ = true; } } diff --git a/src/services/impl/sdl_window_service.hpp b/src/services/impl/sdl_window_service.hpp index 3763227..3bfa8c3 100644 --- a/src/services/impl/sdl_window_service.hpp +++ b/src/services/impl/sdl_window_service.hpp @@ -53,6 +53,12 @@ public: void PollEvents() override; void SetTitle(const std::string& title) override; bool IsMinimized() const override; + void SetMouseGrabbed(bool grabbed) override; + bool IsMouseGrabbed() const override; + void SetRelativeMouseMode(bool enabled) override; + bool IsRelativeMouseMode() const override; + void SetCursorVisible(bool visible) override; + bool IsCursorVisible() const override; private: std::shared_ptr logger_; @@ -63,6 +69,7 @@ private: bool initialized_ = false; MouseGrabConfig mouseGrabConfig_{}; bool mouseGrabbed_ = false; + bool cursorVisible_ = true; uint8_t grabMouseButton_ = SDL_BUTTON_LEFT; SDL_Keycode releaseKey_ = SDLK_ESCAPE; @@ -70,7 +77,7 @@ private: void PublishEvent(const SDL_Event& sdlEvent); double GetCurrentTime() const; void HandleMouseGrabEvent(const SDL_Event& sdlEvent); - void ApplyMouseGrab(bool grabbed); + void ApplyMouseGrab(bool grabbed, bool force); void ConfigureMouseGrabBindings(); }; diff --git a/src/services/interfaces/i_input_service.hpp b/src/services/interfaces/i_input_service.hpp index ab5664c..a2caef5 100644 --- a/src/services/interfaces/i_input_service.hpp +++ b/src/services/interfaces/i_input_service.hpp @@ -15,6 +15,8 @@ class IGuiScriptService; struct InputState { float mouseX = 0.0f; float mouseY = 0.0f; + float mouseDeltaX = 0.0f; + float mouseDeltaY = 0.0f; float mouseWheelDeltaX = 0.0f; float mouseWheelDeltaY = 0.0f; std::unordered_set keysPressed; @@ -74,6 +76,14 @@ public: */ virtual bool IsMouseButtonPressed(uint8_t button) const = 0; + /** + * @brief Check if an action is currently pressed based on input bindings. + * + * @param action The action name to check + * @return true if the action is pressed, false otherwise + */ + virtual bool IsActionPressed(const std::string& action) const = 0; + /** * @brief Get the current mouse position. * @@ -81,6 +91,22 @@ public: */ virtual std::pair GetMousePosition() const = 0; + /** + * @brief Set whether mouse input should be treated as relative motion. + * + * This updates internal cursor tracking for script usage. + * + * @param enabled true for relative mode, false for absolute + */ + virtual void SetRelativeMouseMode(bool enabled) = 0; + + /** + * @brief Check whether mouse input is treated as relative motion. + * + * @return true if relative mode is enabled, false otherwise + */ + virtual bool IsRelativeMouseMode() const = 0; + /** * @brief Set the GUI script service for GUI input processing. * diff --git a/src/services/interfaces/i_window_service.hpp b/src/services/interfaces/i_window_service.hpp index 84d11ac..cf026af 100644 --- a/src/services/interfaces/i_window_service.hpp +++ b/src/services/interfaces/i_window_service.hpp @@ -92,6 +92,48 @@ public: * @return true if minimized, false otherwise */ virtual bool IsMinimized() const = 0; + + /** + * @brief Set mouse grab (capture) state. + * + * @param grabbed true to grab, false to release + */ + virtual void SetMouseGrabbed(bool grabbed) = 0; + + /** + * @brief Check if the mouse is currently grabbed. + * + * @return true if grabbed, false otherwise + */ + virtual bool IsMouseGrabbed() const = 0; + + /** + * @brief Enable or disable relative mouse mode. + * + * @param enabled true for relative mode, false for absolute + */ + virtual void SetRelativeMouseMode(bool enabled) = 0; + + /** + * @brief Check if relative mouse mode is enabled. + * + * @return true if enabled, false otherwise + */ + virtual bool IsRelativeMouseMode() const = 0; + + /** + * @brief Show or hide the OS cursor. + * + * @param visible true to show, false to hide + */ + virtual void SetCursorVisible(bool visible) = 0; + + /** + * @brief Check if the OS cursor is visible. + * + * @return true if visible, false otherwise + */ + virtual bool IsCursorVisible() const = 0; }; } // namespace sdl3cpp::services