Fix multi-window input routing regressions

Add a backend-neutral Input::pushEvent() API with SDL2 and SDL3
implementations, allowing native input translation and routing to be
covered without requiring physical input or backend-specific test code.

Add regression coverage ensuring a native mouse-wheel event is emitted
exactly once and delivered only to its target application window.

Resolve event coordinates and resize dimensions using the target
window's scale instead of the window polling SDL's global event queue.
This preserves correct input and sizing across windows on displays with
different DPI scales.

Also restore SDL3 compatibility with versions older than 3.2.12 when
deriving integer wheel directions from floating-point offsets.
This commit is contained in:
Martín Lucas Golini
2026-09-14 00:12:46 -03:00
parent 56e13bbbe8
commit d41025b91b
7 changed files with 154 additions and 22 deletions

View File

@@ -233,6 +233,10 @@ class EE_API Input {
/** Send an input event to the window */
void sendEvent( InputEvent* Event );
/** Injects an input event through the backend event translation path and routes it according to
* InputEvent::WinID. */
virtual bool pushEvent( const InputEvent& event );
/** @return The joystick manager */
JoystickManager* getJoystickManager() const;

View File

@@ -11,6 +11,14 @@
namespace EE { namespace Window { namespace Backend { namespace SDL2 {
static Float getEventWindowScale( EE::Window::Window* pollingWindow, Uint32 windowId ) {
if ( windowId == 0 || windowId == pollingWindow->getWindowID() )
return pollingWindow->getScale();
if ( auto* eventWindow = Engine::instance()->getWindowID( windowId ) )
return eventWindow->getScale();
return pollingWindow->getScale();
}
InputSDL::InputSDL( EE::Window::Window* window ) :
Input( window, eeNew( JoystickManagerSDL, () ) ), mDPIScale( 1.f ) {
#if defined( EE_X11_PLATFORM )
@@ -86,6 +94,26 @@ bool InputSDL::isMouseCaptured() const {
SDL_WINDOW_MOUSE_CAPTURE;
}
bool InputSDL::pushEvent( const InputEvent& event ) {
if ( event.Type != InputEvent::MouseWheel )
return Input::pushEvent( event );
SDL_Event sdlEvent{};
sdlEvent.type = SDL_MOUSEWHEEL;
sdlEvent.wheel.windowID = event.WinID;
sdlEvent.wheel.x = static_cast<Sint32>( event.wheel.x );
sdlEvent.wheel.y = static_cast<Sint32>( event.wheel.y );
sdlEvent.wheel.direction = event.wheel.direction == InputEvent::WheelEvent::Normal
? SDL_MOUSEWHEEL_NORMAL
: SDL_MOUSEWHEEL_FLIPPED;
#if SDL_VERSION_ATLEAST( 2, 0, 18 )
sdlEvent.wheel.preciseX = event.wheel.x;
sdlEvent.wheel.preciseY = event.wheel.y;
#endif
sendEvent( sdlEvent );
return true;
}
std::string InputSDL::getKeyName( const Keycode& keyCode ) const {
return std::string( SDL_GetKeyName( keyCode ) );
}
@@ -123,9 +151,12 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
case SDL_WINDOWEVENT_RESIZED: {
event.Type = InputEvent::VideoResize;
event.WinID = SDLEvent.window.windowID;
mDPIScale = mWindow->getScale();
event.resize.w = SDLEvent.window.data1 * mDPIScale;
event.resize.h = SDLEvent.window.data2 * mDPIScale;
const Float eventWindowScale =
getEventWindowScale( mWindow, SDLEvent.window.windowID );
if ( SDLEvent.window.windowID == mWindow->getWindowID() )
mDPIScale = eventWindowScale;
event.resize.w = SDLEvent.window.data1 * eventWindowScale;
event.resize.h = SDLEvent.window.data2 * eventWindowScale;
break;
}
case SDL_WINDOWEVENT_HIT_TEST: {
@@ -291,13 +322,14 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
break;
}
case SDL_MOUSEMOTION: {
const Float eventWindowScale = getEventWindowScale( mWindow, SDLEvent.motion.windowID );
event.Type = InputEvent::MouseMotion;
event.motion.which = SDLEvent.motion.windowID;
event.motion.state = SDLEvent.motion.state;
event.motion.x = SDLEvent.motion.x * mDPIScale;
event.motion.y = SDLEvent.motion.y * mDPIScale;
event.motion.xrel = SDLEvent.motion.xrel * mDPIScale;
event.motion.yrel = SDLEvent.motion.yrel * mDPIScale;
event.motion.x = SDLEvent.motion.x * eventWindowScale;
event.motion.y = SDLEvent.motion.y * eventWindowScale;
event.motion.xrel = SDLEvent.motion.xrel * eventWindowScale;
event.motion.yrel = SDLEvent.motion.yrel * eventWindowScale;
event.WinID = SDLEvent.motion.windowID;
break;
}

View File

@@ -30,6 +30,8 @@ class EE_API InputSDL : public Input {
bool isMouseCaptured() const;
bool pushEvent( const InputEvent& event );
std::string getKeyName( const Keycode& keycode ) const;
Keycode getKeyFromName( const std::string& keycode ) const;

View File

@@ -7,6 +7,14 @@
namespace EE { namespace Window { namespace Backend { namespace SDL3 {
static Float getEventWindowScale( EE::Window::Window* pollingWindow, Uint32 windowId ) {
if ( windowId == 0 || windowId == pollingWindow->getWindowID() )
return pollingWindow->getScale();
if ( auto* eventWindow = Engine::instance()->getWindowID( windowId ) )
return eventWindow->getScale();
return pollingWindow->getScale();
}
InputSDL::InputSDL( Window* window ) :
Input( window, eeNew( JoystickManagerSDL, () ) ), mDPIScale( 1.f ) {
#if defined( EE_X11_PLATFORM )
@@ -77,6 +85,26 @@ bool InputSDL::isMouseCaptured() const {
return SDL_GetWindowFlags( win ) & SDL_WINDOW_MOUSE_CAPTURE;
}
bool InputSDL::pushEvent( const InputEvent& event ) {
if ( event.Type != InputEvent::MouseWheel )
return Input::pushEvent( event );
SDL_Event sdlEvent{};
sdlEvent.type = SDL_EVENT_MOUSE_WHEEL;
sdlEvent.wheel.windowID = event.WinID;
sdlEvent.wheel.x = event.wheel.x;
sdlEvent.wheel.y = event.wheel.y;
sdlEvent.wheel.direction = event.wheel.direction == InputEvent::WheelEvent::Normal
? SDL_MOUSEWHEEL_NORMAL
: SDL_MOUSEWHEEL_FLIPPED;
#if SDL_VERSION_ATLEAST( 3, 2, 12 )
sdlEvent.wheel.integer_x = static_cast<Sint32>( event.wheel.x );
sdlEvent.wheel.integer_y = static_cast<Sint32>( event.wheel.y );
#endif
sendEvent( sdlEvent );
return true;
}
std::string InputSDL::getKeyName( const Keycode& keycode ) const {
return std::string( SDL_GetKeyName( static_cast<SDL_Keycode>( keycode ) ) );
}
@@ -144,9 +172,11 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
case SDL_EVENT_WINDOW_RESIZED: {
event.Type = InputEvent::VideoResize;
event.WinID = SDLEvent.window.windowID;
mDPIScale = mWindow->getScale();
event.resize.w = static_cast<int>( SDLEvent.window.data1 * mDPIScale );
event.resize.h = static_cast<int>( SDLEvent.window.data2 * mDPIScale );
const Float eventWindowScale = getEventWindowScale( mWindow, SDLEvent.window.windowID );
if ( SDLEvent.window.windowID == mWindow->getWindowID() )
mDPIScale = eventWindowScale;
event.resize.w = static_cast<int>( SDLEvent.window.data1 * eventWindowScale );
event.resize.h = static_cast<int>( SDLEvent.window.data2 * eventWindowScale );
break;
}
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: {
@@ -267,44 +297,50 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
break;
}
case SDL_EVENT_MOUSE_MOTION: {
const Float eventWindowScale = getEventWindowScale( mWindow, SDLEvent.motion.windowID );
event.Type = InputEvent::MouseMotion;
event.motion.which = SDLEvent.motion.windowID;
event.motion.state = SDLEvent.motion.state;
event.motion.x = static_cast<Int16>( SDLEvent.motion.x * mDPIScale );
event.motion.y = static_cast<Int16>( SDLEvent.motion.y * mDPIScale );
event.motion.xrel = static_cast<Int16>( SDLEvent.motion.xrel * mDPIScale );
event.motion.yrel = static_cast<Int16>( SDLEvent.motion.yrel * mDPIScale );
event.motion.x = static_cast<Int16>( SDLEvent.motion.x * eventWindowScale );
event.motion.y = static_cast<Int16>( SDLEvent.motion.y * eventWindowScale );
event.motion.xrel = static_cast<Int16>( SDLEvent.motion.xrel * eventWindowScale );
event.motion.yrel = static_cast<Int16>( SDLEvent.motion.yrel * eventWindowScale );
event.WinID = SDLEvent.motion.windowID;
break;
}
case SDL_EVENT_MOUSE_BUTTON_DOWN: {
const Float eventWindowScale = getEventWindowScale( mWindow, SDLEvent.button.windowID );
event.Type = InputEvent::MouseButtonDown;
event.button.button = SDLEvent.button.button;
event.button.which = SDLEvent.button.windowID;
event.button.state = SDLEvent.button.down ? 1 : 0;
event.button.x = static_cast<Int16>( SDLEvent.button.x * mDPIScale );
event.button.y = static_cast<Int16>( SDLEvent.button.y * mDPIScale );
event.button.x = static_cast<Int16>( SDLEvent.button.x * eventWindowScale );
event.button.y = static_cast<Int16>( SDLEvent.button.y * eventWindowScale );
event.WinID = SDLEvent.button.windowID;
break;
}
case SDL_EVENT_MOUSE_BUTTON_UP: {
const Float eventWindowScale = getEventWindowScale( mWindow, SDLEvent.button.windowID );
event.Type = InputEvent::MouseButtonUp;
event.button.button = SDLEvent.button.button;
event.button.which = SDLEvent.button.windowID;
event.button.state = SDLEvent.button.down ? 1 : 0;
event.button.x = static_cast<Int16>( SDLEvent.button.x * mDPIScale );
event.button.y = static_cast<Int16>( SDLEvent.button.y * mDPIScale );
event.button.x = static_cast<Int16>( SDLEvent.button.x * eventWindowScale );
event.button.y = static_cast<Int16>( SDLEvent.button.y * eventWindowScale );
event.WinID = SDLEvent.button.windowID;
break;
}
case SDL_EVENT_MOUSE_WHEEL: {
Uint8 button;
const Float eventWindowScale = getEventWindowScale( mWindow, SDLEvent.wheel.windowID );
#if SDL_VERSION_ATLEAST( 3, 2, 12 )
const Sint32 integerX = SDLEvent.wheel.integer_x;
const Sint32 integerY = SDLEvent.wheel.integer_y;
#else
const Sint32 integerX = x > 0.f ? 1 : ( x < 0.f ? -1 : 0 );
const Sint32 integerY = y > 0.f ? 1 : ( y < 0.f ? -1 : 0 );
const Sint32 integerX =
SDLEvent.wheel.x > 0.f ? 1 : ( SDLEvent.wheel.x < 0.f ? -1 : 0 );
const Sint32 integerY =
SDLEvent.wheel.y > 0.f ? 1 : ( SDLEvent.wheel.y < 0.f ? -1 : 0 );
#endif
if ( integerY == 0 && integerX == 0 )
@@ -324,8 +360,8 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
// Get mouse position from the event (mouse_x, mouse_y are in window coordinates)
event.button.button = button;
event.button.x = static_cast<Int16>( SDLEvent.wheel.mouse_x * mDPIScale );
event.button.y = static_cast<Int16>( SDLEvent.wheel.mouse_y * mDPIScale );
event.button.x = static_cast<Int16>( SDLEvent.wheel.mouse_x * eventWindowScale );
event.button.y = static_cast<Int16>( SDLEvent.wheel.mouse_y * eventWindowScale );
event.button.which = SDLEvent.wheel.windowID;
event.WinID = SDLEvent.wheel.windowID;

View File

@@ -30,6 +30,8 @@ class EE_API InputSDL : public Input {
bool isMouseCaptured() const;
bool pushEvent( const InputEvent& event );
std::string getKeyName( const Keycode& keycode ) const;
Keycode getKeyFromName( const std::string& keycode ) const;

View File

@@ -61,6 +61,12 @@ void Input::sendEvent( InputEvent* Event ) {
}
}
bool Input::pushEvent( const InputEvent& event ) {
InputEvent eventCopy( event );
processEventForWindow( &eventCopy );
return true;
}
void Input::processEvent( InputEvent* Event ) {
mLastEvent.restart();

View File

@@ -81,6 +81,56 @@ class TestUIApplication : public UIApplication {
void tickOnce() { tick(); }
};
UTEST( UIApplication, RoutesNativeMouseWheelExactlyOnceToTargetWindow ) {
TestUIApplication app( WindowSettings( 320, 240, "Primary Wheel Routing", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
UIApplication::Settings(
Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1.f, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
ASSERT_TRUE( app.getWindow() != nullptr );
auto* secondaryUI =
app.createWindow( WindowSettings( 240, 180, "Secondary Wheel Routing", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
ASSERT_TRUE( secondaryUI != nullptr );
auto* primaryInput = app.getWindow()->getInput();
auto* secondaryInput = secondaryUI->getWindow()->getInput();
const Uint32 targetWindowId = secondaryUI->getWindow()->getWindowID();
int primaryWheelEvents = 0;
int secondaryWheelEvents = 0;
Vector2f secondaryWheelOffset;
Uint32 receivedWindowId = 0;
const Uint32 primaryCallback = primaryInput->pushCallback( [&]( InputEvent* event ) {
if ( event->Type == InputEvent::MouseWheel )
++primaryWheelEvents;
} );
const Uint32 secondaryCallback = secondaryInput->pushCallback( [&]( InputEvent* event ) {
if ( event->Type == InputEvent::MouseWheel ) {
++secondaryWheelEvents;
secondaryWheelOffset = { event->wheel.x, event->wheel.y };
receivedWindowId = event->WinID;
}
} );
InputEvent event{};
event.Type = InputEvent::MouseWheel;
event.WinID = targetWindowId;
event.wheel.x = 0.f;
event.wheel.y = -1.f;
event.wheel.direction = InputEvent::WheelEvent::Normal;
ASSERT_TRUE( primaryInput->pushEvent( event ) );
Engine::instance()->updateInput();
EXPECT_EQ( primaryWheelEvents, 0 );
EXPECT_EQ( secondaryWheelEvents, 1 );
EXPECT_EQ( receivedWindowId, targetWindowId );
EXPECT_EQ( secondaryWheelOffset.x, 0.f );
EXPECT_EQ( secondaryWheelOffset.y, -1.f );
primaryInput->popCallback( primaryCallback );
secondaryInput->popCallback( secondaryCallback );
}
UTEST( UIApplication, CreatesSecondaryWindowWithoutChangingAmbientScene ) {
TestUIApplication app(
WindowSettings( 320, 240, "Primary UI Context", WindowStyle::Default,