From 0502ddbe2b3cae5ecd76f3187d5735f04b7834c5 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Wed, 3 Nov 2021 10:11:55 -0700 Subject: [PATCH] [Linux] Return the active window when there's no cursor constraint window The cursor constraint window is only set by the Editor. In the game launcher, this function was returning a null window, which makes `GetSystemCursorPositionNormalized()` unable to determine the cursor position. This in turn causes mouse input in ImGui to not work. Fixes #4722, LYN-7491 Signed-off-by: Chris Burel --- .../Xcb/AzFramework/XcbInputDeviceMouse.cpp | 131 ++++++++-------- .../Xcb/AzFramework/XcbInputDeviceMouse.h | 3 - .../Tests/Platform/Common/Xcb/Actions.h | 7 + .../Platform/Common/Xcb/MockXcbInterface.cpp | 57 +++++++ .../Platform/Common/Xcb/MockXcbInterface.h | 27 ++++ .../Common/Xcb/XcbInputDeviceMouseTests.cpp | 144 ++++++++++++++++++ 6 files changed, 305 insertions(+), 64 deletions(-) diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp index 4c6ee28039..c3b7a97ccf 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.cpp @@ -13,18 +13,68 @@ namespace AzFramework { - xcb_window_t GetSystemCursorFocusWindow() + xcb_window_t GetSystemCursorFocusWindow(xcb_connection_t* connection) { void* systemCursorFocusWindow = nullptr; AzFramework::InputSystemCursorConstraintRequestBus::BroadcastResult( systemCursorFocusWindow, &AzFramework::InputSystemCursorConstraintRequests::GetSystemCursorConstraintWindow); - if (!systemCursorFocusWindow) + if (systemCursorFocusWindow) { - return XCB_NONE; + return static_cast(reinterpret_cast(systemCursorFocusWindow)); } - return static_cast(reinterpret_cast(systemCursorFocusWindow)); + // EWMH-compliant window managers set the "_NET_ACTIVE_WINDOW" property + // of the X server's root window to the currently active window. This + // retrieves value of that property. + + // Get the atom for the _NET_ACTIVE_WINDOW property + constexpr int propertyNameLength = 18; + xcb_generic_error_t* error = nullptr; + XcbStdFreePtr activeWindowAtom {xcb_intern_atom_reply( + connection, + xcb_intern_atom(connection, /*only_if_exists=*/ 1, propertyNameLength, "_NET_ACTIVE_WINDOW"), + &error + )}; + if (!activeWindowAtom || error) + { + if (error) + { + AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code); + free(error); + } + return XCB_WINDOW_NONE; + } + + // Get the root window + const xcb_window_t rootWId = xcb_setup_roots_iterator(xcb_get_setup(connection)).data->root; + + // Fetch the value of the root window's _NET_ACTIVE_WINDOW property + XcbStdFreePtr property {xcb_get_property_reply( + connection, + xcb_get_property( + /*c=*/connection, + /*_delete=*/ 0, + /*window=*/rootWId, + /*property=*/activeWindowAtom->atom, + /*type=*/XCB_ATOM_WINDOW, + /*long_offset=*/0, + /*long_length=*/1 + ), + &error + )}; + + if (!property || error) + { + if (error) + { + AZ_Warning("XcbInput", false, "Retrieving _NET_ACTIVE_WINDOW atom failed : Error code %d", error->error_code); + free(error); + } + return XCB_WINDOW_NONE; + } + + return *static_cast(xcb_get_property_value(property.get())); } xcb_connection_t* XcbInputDeviceMouse::s_xcbConnection = nullptr; @@ -36,8 +86,7 @@ namespace AzFramework : InputDeviceMouse::Implementation(inputDevice) , m_systemCursorState(SystemCursorState::Unknown) , m_systemCursorPositionNormalized(0.5f, 0.5f) - , m_prevConstraintWindow(XCB_NONE) - , m_focusWindow(XCB_NONE) + , m_focusWindow(XCB_WINDOW_NONE) , m_cursorShown(true) { XcbEventHandlerBus::Handler::BusConnect(); @@ -271,7 +320,7 @@ namespace AzFramework { m_systemCursorState = systemCursorState; - m_focusWindow = GetSystemCursorFocusWindow(); + m_focusWindow = GetSystemCursorFocusWindow(s_xcbConnection); HandleCursorState(m_focusWindow, systemCursorState); } @@ -279,50 +328,10 @@ namespace AzFramework void XcbInputDeviceMouse::HandleCursorState(xcb_window_t window, SystemCursorState systemCursorState) { - bool confined = false, cursorShown = true; - switch (systemCursorState) - { - case SystemCursorState::ConstrainedAndHidden: - { - //!< Constrained to the application's main window and hidden - confined = true; - cursorShown = false; - } - break; - case SystemCursorState::ConstrainedAndVisible: - { - //!< Constrained to the application's main window and visible - confined = true; - } - break; - case SystemCursorState::UnconstrainedAndHidden: - { - //!< Free to move outside the main window but hidden while inside - cursorShown = false; - } - break; - case SystemCursorState::UnconstrainedAndVisible: - { - //!< Free to move outside the application's main window and visible - } - case SystemCursorState::Unknown: - default: - break; - } - - // ATTN GetSystemCursorFocusWindow when getting out of the play in editor will return XCB_NONE - // We need however the window id to reset the cursor. - if (XCB_NONE == window && (confined || cursorShown)) - { - // Reuse the previous window to reset states. - window = m_prevConstraintWindow; - m_prevConstraintWindow = XCB_NONE; - } - else - { - // Remember the window we used to modify cursor and barrier states. - m_prevConstraintWindow = window; - } + const bool confined = (systemCursorState == SystemCursorState::ConstrainedAndHidden) || + (systemCursorState == SystemCursorState::ConstrainedAndVisible); + const bool cursorShown = (systemCursorState == SystemCursorState::ConstrainedAndVisible) || + (systemCursorState == SystemCursorState::UnconstrainedAndVisible); CreateBarriers(window, confined); ShowCursor(window, cursorShown); @@ -336,26 +345,26 @@ namespace AzFramework void XcbInputDeviceMouse::SetSystemCursorPositionNormalizedInternal(xcb_window_t window, AZ::Vector2 positionNormalized) { // TODO Basically not done at all. Added only the basic functions needed. - const XcbStdFreePtr xkbGeometryReply{ xcb_get_geometry_reply( + const XcbStdFreePtr xcbGeometryReply{ xcb_get_geometry_reply( s_xcbConnection, xcb_get_geometry(s_xcbConnection, window), nullptr) }; - if (!xkbGeometryReply) + if (!xcbGeometryReply) { return; } - const int16_t x = static_cast(positionNormalized.GetX() * xkbGeometryReply->width); - const int16_t y = static_cast(positionNormalized.GetY() * xkbGeometryReply->height); + const int16_t x = static_cast(positionNormalized.GetX() * xcbGeometryReply->width); + const int16_t y = static_cast(positionNormalized.GetY() * xcbGeometryReply->height); - xcb_warp_pointer(s_xcbConnection, XCB_NONE, window, 0, 0, 0, 0, x, y); + xcb_warp_pointer(s_xcbConnection, XCB_WINDOW_NONE, window, 0, 0, 0, 0, x, y); xcb_flush(s_xcbConnection); } void XcbInputDeviceMouse::SetSystemCursorPositionNormalized(AZ::Vector2 positionNormalized) { - const xcb_window_t window = GetSystemCursorFocusWindow(); - if (XCB_NONE == window) + const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection); + if (XCB_WINDOW_NONE == window) { return; } @@ -397,8 +406,8 @@ namespace AzFramework AZ::Vector2 XcbInputDeviceMouse::GetSystemCursorPositionNormalized() const { - const xcb_window_t window = GetSystemCursorFocusWindow(); - if (XCB_NONE == window) + const xcb_window_t window = GetSystemCursorFocusWindow(s_xcbConnection); + if (XCB_WINDOW_NONE == window) { return AZ::Vector2::CreateZero(); } @@ -549,7 +558,7 @@ namespace AzFramework ProcessRawEventQueues(); ResetInputChannelStates(); - m_focusWindow = XCB_NONE; + m_focusWindow = XCB_WINDOW_NONE; auto* interface = AzFramework::XcbConnectionManagerInterface::Get(); interface->SetEnableXInput(interface->GetXcbConnection(), false); diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h index 8d8f0a2845..a69a8a9ec5 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbInputDeviceMouse.h @@ -161,9 +161,6 @@ namespace AzFramework //! Will be true if the xinput2 extension could be initialized. static bool m_xInputInitialized; - //! The window that had focus - xcb_window_t m_prevConstraintWindow; - //! The current window that has focus xcb_window_t m_focusWindow; diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h index d227729fd3..e86904efa3 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/Actions.h @@ -60,3 +60,10 @@ ACTION_TEMPLATE(ReturnMalloc, *value = T{ p0, p1, p2, p3, p4, p5 }; return value; } +ACTION_TEMPLATE(ReturnMalloc, + HAS_1_TEMPLATE_PARAMS(typename, T), + AND_7_VALUE_PARAMS(p0, p1, p2, p3, p4, p5, p6)) { + T* value = static_cast(malloc(sizeof(T))); + *value = T{ p0, p1, p2, p3, p4, p5, p6 }; + return value; +} diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp index 39d425b799..a19642e388 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.cpp @@ -77,6 +77,37 @@ xcb_void_cookie_t xcb_warp_pointer( { return MockXcbInterface::Instance()->xcb_warp_pointer(c, src_window, dst_window, src_x, src_y, src_width, src_height, dst_x, dst_y); } +xcb_intern_atom_cookie_t xcb_intern_atom(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name) +{ + return MockXcbInterface::Instance()->xcb_intern_atom(c, only_if_exists, name_len, name); +} +xcb_intern_atom_reply_t* xcb_intern_atom_reply(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_intern_atom_reply(c, cookie, e); +} +xcb_get_property_cookie_t xcb_get_property( + xcb_connection_t* c, + uint8_t _delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t long_offset, + uint32_t long_length) +{ + return MockXcbInterface::Instance()->xcb_get_property(c, _delete, window, property, type, long_offset, long_length); +} +xcb_get_property_reply_t* xcb_get_property_reply(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_get_property_reply(c, cookie, e); +} +void* xcb_get_property_value(const xcb_get_property_reply_t* R) +{ + return MockXcbInterface::Instance()->xcb_get_property_value(R); +} +uint32_t xcb_generate_id(xcb_connection_t *c) +{ + return MockXcbInterface::Instance()->xcb_generate_id(c); +} // ---------------------------------------------------------------------------- // xcb-xkb @@ -181,6 +212,32 @@ xcb_void_cookie_t xcb_xfixes_hide_cursor_checked(xcb_connection_t* c, xcb_window { return MockXcbInterface::Instance()->xcb_xfixes_hide_cursor_checked(c, window); } +xcb_void_cookie_t xcb_xfixes_delete_pointer_barrier_checked(xcb_connection_t* c, xcb_xfixes_barrier_t barrier) +{ + return MockXcbInterface::Instance()->xcb_xfixes_delete_pointer_barrier_checked(c, barrier); +} +xcb_translate_coordinates_cookie_t xcb_translate_coordinates(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y) +{ + return MockXcbInterface::Instance()->xcb_translate_coordinates(c, src_window, dst_window, src_x, src_y); +} +xcb_translate_coordinates_reply_t* xcb_translate_coordinates_reply(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e) +{ + return MockXcbInterface::Instance()->xcb_translate_coordinates_reply(c, cookie, e); +} +xcb_void_cookie_t xcb_xfixes_create_pointer_barrier_checked( + xcb_connection_t* c, + xcb_xfixes_barrier_t barrier, + xcb_window_t window, + uint16_t x1, + uint16_t y1, + uint16_t x2, + uint16_t y2, + uint32_t directions, + uint16_t num_devices, + const uint16_t* devices) +{ + return MockXcbInterface::Instance()->xcb_xfixes_create_pointer_barrier_checked(c, barrier, window, x1, y1, x2, y2, directions, num_devices, devices); +} // ---------------------------------------------------------------------------- // xcb-xinput diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h index f38e327f50..c554993110 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/MockXcbInterface.h @@ -82,6 +82,19 @@ public: uint16_t src_height, int16_t dst_x, int16_t dst_y)); + MOCK_CONST_METHOD4(xcb_intern_atom, xcb_intern_atom_cookie_t(xcb_connection_t* c, uint8_t only_if_exists, uint16_t name_len, const char* name)); + MOCK_CONST_METHOD3(xcb_intern_atom_reply, xcb_intern_atom_reply_t*(xcb_connection_t* c, xcb_intern_atom_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD7(xcb_get_property, xcb_get_property_cookie_t( + xcb_connection_t* c, + uint8_t _delete, + xcb_window_t window, + xcb_atom_t property, + xcb_atom_t type, + uint32_t long_offset, + uint32_t long_length)); + MOCK_CONST_METHOD3(xcb_get_property_reply, xcb_get_property_reply_t*(xcb_connection_t* c, xcb_get_property_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD1(xcb_get_property_value, void*(const xcb_get_property_reply_t* R)); + MOCK_CONST_METHOD1(xcb_generate_id, uint32_t(xcb_connection_t *c)); // xcb-xkb MOCK_CONST_METHOD3(xcb_xkb_use_extension, xcb_xkb_use_extension_cookie_t(xcb_connection_t* c, uint16_t wantedMajor, uint16_t wantedMinor)); @@ -108,6 +121,20 @@ public: MOCK_CONST_METHOD3(xcb_xfixes_query_version_reply, xcb_xfixes_query_version_reply_t*(xcb_connection_t* c, xcb_xfixes_query_version_cookie_t cookie, xcb_generic_error_t** e)); MOCK_CONST_METHOD2(xcb_xfixes_show_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window)); MOCK_CONST_METHOD2(xcb_xfixes_hide_cursor_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_window_t window)); + MOCK_CONST_METHOD2(xcb_xfixes_delete_pointer_barrier_checked, xcb_void_cookie_t(xcb_connection_t* c, xcb_xfixes_barrier_t barrier)); + MOCK_CONST_METHOD5(xcb_translate_coordinates, xcb_translate_coordinates_cookie_t(xcb_connection_t* c, xcb_window_t src_window, xcb_window_t dst_window, int16_t src_x, int16_t src_y)); + MOCK_CONST_METHOD3(xcb_translate_coordinates_reply, xcb_translate_coordinates_reply_t*(xcb_connection_t* c, xcb_translate_coordinates_cookie_t cookie, xcb_generic_error_t** e)); + MOCK_CONST_METHOD10(xcb_xfixes_create_pointer_barrier_checked, xcb_void_cookie_t( + xcb_connection_t* c, + xcb_xfixes_barrier_t barrier, + xcb_window_t window, + uint16_t x1, + uint16_t y1, + uint16_t x2, + uint16_t y2, + uint32_t directions, + uint16_t num_devices, + const uint16_t* devices)); // xcb-xinput MOCK_CONST_METHOD3(xcb_input_xi_query_version, xcb_input_xi_query_version_cookie_t(xcb_connection_t* c, uint16_t major_version, uint16_t minor_version)); diff --git a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceMouseTests.cpp b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceMouseTests.cpp index 40dfdec26f..05784462d7 100644 --- a/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceMouseTests.cpp +++ b/Code/Framework/AzFramework/Tests/Platform/Common/Xcb/XcbInputDeviceMouseTests.cpp @@ -28,7 +28,10 @@ namespace AzFramework public: void SetUp() override { + using testing::Eq; + using testing::Field; using testing::Return; + using testing::StrEq; using testing::_; XcbBaseTestFixture::SetUp(); @@ -61,6 +64,37 @@ namespace AzFramework /*major_version=*/(uint16_t)2, /*minor_version=*/(uint16_t)2 )); + + // Set the default focus window + EXPECT_CALL(m_interface, xcb_intern_atom(&m_connection, 1, 18, StrEq("_NET_ACTIVE_WINDOW"))) + .WillRepeatedly(Return(xcb_intern_atom_cookie_t{/*.sequence=*/ 1})); + ON_CALL(m_interface, xcb_intern_atom_reply(&m_connection, Field(&xcb_intern_atom_cookie_t::sequence, Eq(1)), _)) + .WillByDefault(ReturnMalloc( + /*response_type=*/(uint8_t)XCB_INTERN_ATOM, + /*pad0=*/(uint8_t)0, + /*sequence=*/(uint16_t)1, + /*length=*/0u, + /*xcb_atom_t=*/s_netActiveWindowAtom + )); + ON_CALL(m_interface, xcb_get_property(&m_connection, 0, s_rootWindow, s_netActiveWindowAtom, XCB_ATOM_WINDOW, 0, 1)) + .WillByDefault(Return(xcb_get_property_cookie_t{/*.sequence=*/ s_getActiveWindowPropertySequence})); + ON_CALL(m_interface, xcb_get_property_reply(&m_connection, Field(&xcb_get_property_cookie_t::sequence, Eq(s_getActiveWindowPropertySequence)), _)) + .WillByDefault(ReturnMalloc( + /*response_type=*/(uint8_t)XCB_GET_PROPERTY, + /*format=*/(uint8_t)0, + /*sequence=*/(uint16_t)s_getActiveWindowPropertySequence, + /*length=*/0u, + /*type=*/XCB_ATOM_WINDOW, + /*bytes_after=*/0u, + /*value_len=*/1u + )); + ON_CALL(m_interface, xcb_get_property_value(Field(&xcb_get_property_reply_t::sequence, Eq(s_getActiveWindowPropertySequence)))) + .WillByDefault(Return(const_cast(&s_nullWindow))); + + ON_CALL(m_interface, xcb_get_geometry(&m_connection, _)) + .WillByDefault(Return(xcb_get_geometry_cookie_t{/*.sequence=*/1})); + ON_CALL(m_interface, xcb_get_geometry_reply(&m_connection, Field(&xcb_get_geometry_cookie_t::sequence, Eq(1)), _)) + .WillByDefault(ReturnMalloc(s_defaultWindowGeometry)); } void PumpApplication() @@ -73,10 +107,13 @@ namespace AzFramework protected: static constexpr inline uint8_t s_xinputMajorOpcode = 131; static constexpr inline xcb_window_t s_rootWindow = 1; + static constexpr inline xcb_window_t s_nullWindow = XCB_WINDOW_NONE; static constexpr inline xcb_input_device_id_t s_virtualCorePointerId = 2; static constexpr inline xcb_input_device_id_t s_physicalPointerDeviceId = 3; static constexpr inline uint16_t s_screenWidthInPixels = 3840; static constexpr inline uint16_t s_screenHeightInPixels = 2160; + static constexpr inline uint16_t s_getActiveWindowPropertySequence = 2160; + static constexpr inline xcb_atom_t s_netActiveWindowAtom = 1; static constexpr inline xcb_setup_t s_xcbSetup{ /*.status=*/1, /*.pad0=*/0, @@ -109,6 +146,19 @@ namespace AzFramework /*.present=*/1, /*.major_opcode=*/s_xinputMajorOpcode, }; + static constexpr inline xcb_get_geometry_reply_t s_defaultWindowGeometry{ + /*.response_type=*/XCB_GET_GEOMETRY, + /*.depth=*/0, + /*.sequence=*/1, + /*.length=*/0, + /*.root=*/s_rootWindow, + /*.x=*/100, + /*.y=*/100, + /*.width=*/100, + /*.height=*/100, + /*.border_width=*/3, + /*.pad0[2]=*/{}, + }; XcbTestApplication m_application{ /*enabledGamepadsCount=*/0, /*keyboardEnabled=*/false, @@ -398,4 +448,98 @@ namespace AzFramework EXPECT_THAT(xMotionChannel->GetValue(), FloatEq(0.0f)); EXPECT_THAT(yMotionChannel->GetValue(), FloatEq(0.0f)); } + + struct GetCursorPositionParam + { + int16_t m_x; + int16_t m_y; + }; + + class XcbGetSystemCursorPositionTests + : public XcbInputDeviceMouseTests + , public testing::WithParamInterface + { + }; + + TEST_P(XcbGetSystemCursorPositionTests, GetSystemCursorPositionNormalizedReturnsCorrectValue) + { + using testing::Eq; + using testing::Field; + using testing::Return; + using testing::_; + + xcb_window_t focusWindow = 42; + const xcb_query_pointer_reply_t queryPointerReply{ + /*.response_type=*/XCB_QUERY_POINTER, + /*.same_screen=*/1, + /*.sequence=*/0, + /*.length=*/1, + /*.root=*/s_rootWindow, + /*.child=*/focusWindow, + /*.root_x=*/static_cast(GetParam().m_x + s_defaultWindowGeometry.x), + /*.root_y=*/static_cast(GetParam().m_y + s_defaultWindowGeometry.y), + /*.win_x=*/GetParam().m_x, + /*.win_y=*/GetParam().m_y, + /*.mask=*/{}, + /*.pad0[2]=*/{}, + }; + + // Querying the root window's pointer gives its absolute value + const xcb_query_pointer_reply_t rootWindowQueryPointerReply{ + /*.response_type=*/XCB_QUERY_POINTER, + /*.same_screen=*/1, + /*.sequence=*/0, + /*.length=*/1, + /*.root=*/s_rootWindow, + /*.child=*/s_rootWindow, + /*.root_x=*/static_cast(GetParam().m_x + s_defaultWindowGeometry.x), + /*.root_y=*/static_cast(GetParam().m_y + s_defaultWindowGeometry.y), + /*.win_x=*/static_cast(GetParam().m_x + s_defaultWindowGeometry.x), + /*.win_y=*/static_cast(GetParam().m_y + s_defaultWindowGeometry.y), + /*.mask=*/{}, + /*.pad0[2]=*/{}, + }; + + EXPECT_CALL(m_interface, xcb_get_property_value(Field(&xcb_get_property_reply_t::sequence, Eq(s_getActiveWindowPropertySequence)))) + .WillRepeatedly(Return(&focusWindow)); + + EXPECT_CALL(m_interface, xcb_query_pointer(&m_connection, focusWindow)) + .WillRepeatedly(Return(xcb_query_pointer_cookie_t{/*.sequence=*/1})); + EXPECT_CALL(m_interface, xcb_query_pointer_reply(&m_connection, Field(&xcb_query_pointer_cookie_t::sequence, 1), _)) + .WillRepeatedly(ReturnMalloc(queryPointerReply)); + + EXPECT_CALL(m_interface, xcb_query_pointer(&m_connection, s_rootWindow)) + .WillRepeatedly(Return(xcb_query_pointer_cookie_t{/*.sequence=*/2})); + EXPECT_CALL(m_interface, xcb_query_pointer_reply(&m_connection, Field(&xcb_query_pointer_cookie_t::sequence, 2), _)) + .WillRepeatedly(ReturnMalloc(rootWindowQueryPointerReply)); + + m_application.Start(); + InputSystemCursorRequestBus::Event( + InputDeviceMouse::Id, + &InputSystemCursorRequests::SetSystemCursorState, + SystemCursorState::ConstrainedAndHidden); + + AZ::Vector2 systemCursorPositionNormalized = AZ::Vector2::CreateZero(); + InputSystemCursorRequestBus::EventResult( + systemCursorPositionNormalized, + InputDeviceMouse::Id, + &InputSystemCursorRequests::GetSystemCursorPositionNormalized); + + EXPECT_THAT(systemCursorPositionNormalized, ::testing::AllOf( + testing::Property(&AZ::Vector2::GetX, testing::FloatEq(static_cast(GetParam().m_x) / s_defaultWindowGeometry.width)), + testing::Property(&AZ::Vector2::GetY, testing::FloatEq(static_cast(GetParam().m_y) / s_defaultWindowGeometry.height)) + )); + } + + INSTANTIATE_TEST_CASE_P( + AllPointerPositions, + XcbGetSystemCursorPositionTests, + testing::Values( + // Default mocked window geometry sets width and height to 100, all + // parameter values should be within [0, 100) + GetCursorPositionParam{ 50, 50 }, + GetCursorPositionParam{ 25, 25 }, + GetCursorPositionParam{ 0, 100 } + ) + ); } // namespace AzFramework