diff --git a/src/include/libvirtualhid/runtime.hpp b/src/include/libvirtualhid/runtime.hpp index caaac54..7ad029d 100644 --- a/src/include/libvirtualhid/runtime.hpp +++ b/src/include/libvirtualhid/runtime.hpp @@ -417,8 +417,8 @@ namespace lvh { /** * @brief Submit absolute pointer movement. * - * @param x Absolute X coordinate. - * @param y Absolute Y coordinate. + * @param x Absolute X coordinate in the supplied coordinate space. + * @param y Absolute Y coordinate in the supplied coordinate space. * @param width Width of the absolute coordinate space. * @param height Height of the absolute coordinate space. * @return Submit operation status. @@ -428,8 +428,8 @@ namespace lvh { /** * @brief Submit absolute pointer movement with fractional coordinates. * - * @param x Absolute X coordinate. - * @param y Absolute Y coordinate. + * @param x Absolute X coordinate in the supplied coordinate space, preserving fractional precision. + * @param y Absolute Y coordinate in the supplied coordinate space, preserving fractional precision. * @param width Width of the absolute coordinate space. * @param height Height of the absolute coordinate space. * @return Submit operation status. diff --git a/src/include/libvirtualhid/types.hpp b/src/include/libvirtualhid/types.hpp index 17ca166..00874a4 100644 --- a/src/include/libvirtualhid/types.hpp +++ b/src/include/libvirtualhid/types.hpp @@ -813,22 +813,22 @@ namespace lvh { MouseEventKind kind = MouseEventKind::relative_motion; /** - * @brief Relative delta or absolute X coordinate. + * @brief Relative delta or absolute X coordinate in the supplied coordinate space. */ std::int32_t x = 0; /** - * @brief Relative delta or absolute Y coordinate. + * @brief Relative delta or absolute Y coordinate in the supplied coordinate space. */ std::int32_t y = 0; /** - * @brief Fractional absolute X coordinate used when `has_fractional_absolute_coordinates` is true. + * @brief Absolute X coordinate with fractional precision, used when `has_fractional_absolute_coordinates` is true. */ float absolute_x = 0.0F; /** - * @brief Fractional absolute Y coordinate used when `has_fractional_absolute_coordinates` is true. + * @brief Absolute Y coordinate with fractional precision, used when `has_fractional_absolute_coordinates` is true. */ float absolute_y = 0.0F; diff --git a/src/platform/macos/macos_backend.cpp b/src/platform/macos/macos_backend.cpp index 17faf94..afa06aa 100644 --- a/src/platform/macos/macos_backend.cpp +++ b/src/platform/macos/macos_backend.cpp @@ -344,6 +344,39 @@ namespace lvh::detail { return static_cast(scaled_pixels / wheel_delta); } + /** + * @brief Scale one absolute mouse axis into a display-sized coordinate. + * + * @param value Absolute coordinate in the source coordinate space. + * @param limit Size of the source coordinate space. + * @param display_size Size of the target display coordinate space. + * @return Coordinate offset within the target display bounds. + */ + inline CGFloat scale_absolute_axis(float value, std::int32_t limit, CGFloat display_size) { + if (limit <= 0 || display_size <= 0) { + return 0; + } + + const auto clamped = std::clamp(value, 0.0F, static_cast(limit)); + return static_cast(clamped) * display_size / static_cast(limit); + } + + /** + * @brief Convert a libvirtualhid absolute mouse event to a CoreGraphics location. + * + * @param event Mouse event to translate. + * @param display_bounds Target display bounds. + * @return CoreGraphics display location. + */ + inline CGPoint absolute_mouse_location(const MouseEvent &event, const CGRect &display_bounds) { + const auto x = event.has_fractional_absolute_coordinates ? event.absolute_x : static_cast(event.x); + const auto y = event.has_fractional_absolute_coordinates ? event.absolute_y : static_cast(event.y); + return CGPoint { + display_bounds.origin.x + scale_absolute_axis(x, event.width, display_bounds.size.width), + display_bounds.origin.y + scale_absolute_axis(y, event.height, display_bounds.size.height) + }; + } + /** * @brief Shared macOS backend state. */ @@ -632,12 +665,7 @@ namespace lvh::detail { OperationStatus submit_absolute_motion(const MouseEvent &event) { const auto display_bounds = CGDisplayBounds(state_->display); - const auto location = CGPoint { - event.has_fractional_absolute_coordinates ? display_bounds.origin.x + event.absolute_x * display_bounds.size.width : - static_cast(event.x) * state_->display_scaling + display_bounds.origin.x, - event.has_fractional_absolute_coordinates ? display_bounds.origin.y + event.absolute_y * display_bounds.size.height : - static_cast(event.y) * state_->display_scaling + display_bounds.origin.y - }; + const auto location = absolute_mouse_location(event, display_bounds); return post_mouse(kCGMouseButtonLeft, event_type_for_current_buttons(), location, current_location(), 0); } diff --git a/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp index 4b22a56..26f8686 100644 --- a/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/macos_backend_test_hooks.hpp @@ -13,6 +13,14 @@ namespace lvh::detail::test { + /** + * @brief Portable representation of a CoreGraphics point for tests. + */ + struct MacosPoint { + double x {}; ///< Horizontal coordinate. + double y {}; ///< Vertical coordinate. + }; + /** * @brief Result set for macOS backend lifecycle utility coverage. */ @@ -67,6 +75,24 @@ namespace lvh::detail::test { */ int macos_backend_scroll_pixels(std::int32_t high_resolution_distance, int pixels_per_line, int lines_per_detent); + /** + * @brief Convert an absolute mouse event to a macOS display location. + * + * @param event Mouse event to convert. + * @param origin_x Display origin X coordinate. + * @param origin_y Display origin Y coordinate. + * @param width Display width. + * @param height Display height. + * @return Display location that the macOS backend will post. + */ + MacosPoint macos_backend_absolute_mouse_location( + const MouseEvent &event, + double origin_x, + double origin_y, + double width, + double height + ); + /** * @brief Exercise macOS backend creation and unsupported-device paths. * diff --git a/tests/fixtures/macos_backend_test_hooks.cpp b/tests/fixtures/macos_backend_test_hooks.cpp index 6ea4150..1b66f34 100644 --- a/tests/fixtures/macos_backend_test_hooks.cpp +++ b/tests/fixtures/macos_backend_test_hooks.cpp @@ -39,6 +39,23 @@ namespace lvh::detail::test { return macos::scroll_pixels(high_resolution_distance, pixels_per_line, lines_per_detent); } + MacosPoint macos_backend_absolute_mouse_location( + const MouseEvent &event, + double origin_x, + double origin_y, + double width, + double height + ) { + const auto location = macos::absolute_mouse_location( + event, + CGRect { + .origin = CGPoint {origin_x, origin_y}, + .size = CGSize {width, height} + } + ); + return {.x = location.x, .y = location.y}; + } + MacosBackendUtilityResult macos_backend_utilities() { auto backend = create_platform_backend_for_macos_backend_test_hooks(); MacosBackendUtilityResult result; diff --git a/tests/unit/test_macos_backend.cpp b/tests/unit/test_macos_backend.cpp index 5afa9f2..866aa0a 100644 --- a/tests/unit/test_macos_backend.cpp +++ b/tests/unit/test_macos_backend.cpp @@ -76,6 +76,38 @@ TEST_F(MacosBackendTest, ConvertsScrollSettings) { EXPECT_EQ(lvh::detail::test::macos_backend_scroll_pixels(120, 0, 0), 1); } +TEST_F(MacosBackendTest, ConvertsAbsoluteMouseCoordinates) { + lvh::MouseEvent event { + .kind = lvh::MouseEventKind::absolute_motion, + .x = 40, + .y = 50, + .width = 200, + .height = 100, + }; + + auto location = lvh::detail::test::macos_backend_absolute_mouse_location(event, 10.0, 20.0, 400.0, 200.0); + EXPECT_DOUBLE_EQ(location.x, 90.0); + EXPECT_DOUBLE_EQ(location.y, 120.0); + + event.x = 250; + event.y = -10; + location = lvh::detail::test::macos_backend_absolute_mouse_location(event, 10.0, 20.0, 400.0, 200.0); + EXPECT_DOUBLE_EQ(location.x, 410.0); + EXPECT_DOUBLE_EQ(location.y, 20.0); + + event = { + .kind = lvh::MouseEventKind::absolute_motion, + .absolute_x = 0.25F, + .absolute_y = 0.75F, + .has_fractional_absolute_coordinates = true, + .width = 1, + .height = 1, + }; + location = lvh::detail::test::macos_backend_absolute_mouse_location(event, 10.0, 20.0, 400.0, 200.0); + EXPECT_DOUBLE_EQ(location.x, 110.0); + EXPECT_DOUBLE_EQ(location.y, 170.0); +} + TEST_F(MacosBackendTest, ReportsCapabilitiesAndUnsupportedDevices) { const auto result = lvh::detail::test::macos_backend_utilities(); diff --git a/tests/unit/test_runtime.cpp b/tests/unit/test_runtime.cpp index 21b07c3..e83d6ff 100644 --- a/tests/unit/test_runtime.cpp +++ b/tests/unit/test_runtime.cpp @@ -239,10 +239,21 @@ TEST(RuntimeTest, CreatesSubmitsAndClosesMouse) { EXPECT_EQ(created.mouse->last_submitted_event().y, -5); EXPECT_TRUE(created.mouse->move_absolute(100, 200, 1920, 1080).ok()); + EXPECT_EQ(created.mouse->last_submitted_event().kind, lvh::MouseEventKind::absolute_motion); + EXPECT_EQ(created.mouse->last_submitted_event().x, 100); + EXPECT_EQ(created.mouse->last_submitted_event().y, 200); + EXPECT_EQ(created.mouse->last_submitted_event().width, 1920); + EXPECT_EQ(created.mouse->last_submitted_event().height, 1080); + + EXPECT_TRUE(created.mouse->move_absolute(0.25F, 0.75F, 1, 1).ok()); + EXPECT_EQ(created.mouse->last_submitted_event().kind, lvh::MouseEventKind::absolute_motion); + EXPECT_TRUE(created.mouse->last_submitted_event().has_fractional_absolute_coordinates); + EXPECT_FLOAT_EQ(created.mouse->last_submitted_event().absolute_x, 0.25F); + EXPECT_FLOAT_EQ(created.mouse->last_submitted_event().absolute_y, 0.75F); EXPECT_TRUE(created.mouse->button(lvh::MouseButton::right, true).ok()); EXPECT_TRUE(created.mouse->vertical_scroll(120).ok()); EXPECT_TRUE(created.mouse->horizontal_scroll(-120).ok()); - EXPECT_EQ(created.mouse->submit_count(), 5U); + EXPECT_EQ(created.mouse->submit_count(), 6U); EXPECT_EQ(created.mouse->move_absolute(1, 1, 0, 0).code(), lvh::ErrorCode::invalid_argument); EXPECT_TRUE(created.mouse->close().ok());