diff --git a/src/platform/linux/uhid_backend.cpp b/src/platform/linux/uhid_backend.cpp index adb110d..ef20d84 100644 --- a/src/platform/linux/uhid_backend.cpp +++ b/src/platform/linux/uhid_backend.cpp @@ -1204,6 +1204,10 @@ namespace lvh::detail { } } + if (const auto status = enable_evdev_type(device, EV_REL, "relative mouse events"); !status.ok()) { + return status; + } + if (kind == UinputMouseDeviceKind::absolute) { if (const auto status = enable_evdev_property(device, INPUT_PROP_DIRECT, "direct input"); !status.ok()) { return status; @@ -1217,17 +1221,15 @@ namespace lvh::detail { return status; } auto y = make_absinfo(0, absolute_axis_max); - return enable_evdev_code(device, EV_ABS, ABS_Y, "absolute mouse axis", &y); - } - - if (const auto status = enable_evdev_type(device, EV_REL, "relative mouse events"); !status.ok()) { - return status; - } - - for (const auto code : {REL_X, REL_Y}) { - if (const auto status = enable_evdev_code(device, EV_REL, code, "relative mouse axis"); !status.ok()) { + if (const auto status = enable_evdev_code(device, EV_ABS, ABS_Y, "absolute mouse axis", &y); !status.ok()) { return status; } + } else { + for (const auto code : {REL_X, REL_Y}) { + if (const auto status = enable_evdev_code(device, EV_REL, code, "relative mouse axis"); !status.ok()) { + return status; + } + } } if (const auto status = enable_evdev_code(device, EV_REL, REL_WHEEL, "vertical scroll"); !status.ok()) { @@ -1740,6 +1742,11 @@ namespace lvh::detail { /** * @brief Backend mouse backed by separate relative and absolute Linux uinput devices. + * + * Both devices expose legacy and available high-resolution wheel axes. + * Relative motion axes are exposed only by the relative device. + * Wheel events follow the last successful motion submission, defaulting + * to the relative device before any successful motion submission. */ class UinputMouse final: public BackendMouse { public: @@ -1935,19 +1942,19 @@ namespace lvh::detail { std::uint16_t legacy_code, std::optional high_resolution_code ) { - auto &relative = device(UinputMouseDeviceKind::relative); + auto &target = device(last_motion_device_); const auto converted = accumulated_legacy_scroll(remainder, distance); if (converted.detents != 0) { - if (const auto status = relative.emit(EV_REL, legacy_code, converted.detents); !status.ok()) { + if (const auto status = target.emit(EV_REL, legacy_code, converted.detents); !status.ok()) { return status; } } if (high_resolution_code.has_value()) { - if (const auto status = relative.emit(EV_REL, *high_resolution_code, distance); !status.ok()) { + if (const auto status = target.emit(EV_REL, *high_resolution_code, distance); !status.ok()) { return status; } } - if (const auto status = relative.synchronize(); !status.ok()) { + if (const auto status = target.synchronize(); !status.ok()) { return status; } remainder = converted.remainder; diff --git a/tests/unit/test_linux_backend.cpp b/tests/unit/test_linux_backend.cpp index e442006..beac95d 100644 --- a/tests/unit/test_linux_backend.cpp +++ b/tests/unit/test_linux_backend.cpp @@ -46,6 +46,19 @@ namespace { } return pressed_codes; } + + std::vector relative_axis_values( + const std::vector &events, + std::uint16_t code + ) { + std::vector values; + for (const auto &event : events) { + if (event.type == EV_REL && event.code == code) { + values.push_back(event.value); + } + } + return values; + } } // namespace TEST_F(LinuxBackendTest, TranslatesKeyboardKeys) { @@ -748,6 +761,28 @@ TEST_F(LinuxBackendTest, PipeBackedUinputMouseRoutesMotionAndButtonsAcrossSplitD })); } +TEST_F(LinuxBackendTest, PipeBackedUinputMouseScrollsOnLastMotionDevice) { + using enum lvh::MouseEventKind; + + const std::vector events { + {.kind = absolute_motion, .x = 50, .y = 25, .width = 100, .height = 100}, + {.kind = vertical_scroll, .high_resolution_scroll = 120}, + {.kind = horizontal_scroll, .high_resolution_scroll = -120}, + {.kind = relative_motion, .x = 5, .y = -2}, + {.kind = vertical_scroll, .high_resolution_scroll = -120}, + }; + const auto result = lvh::detail::test::linux_uinput_mouse_submit_split_pipe_sequence(events); + ASSERT_TRUE(result.status.ok()) << result.status.message(); + + EXPECT_EQ(relative_axis_values(result.absolute_events, REL_WHEEL), (std::vector {1})); + EXPECT_EQ(relative_axis_values(result.absolute_events, REL_HWHEEL), (std::vector {-1})); + EXPECT_EQ(relative_axis_values(result.relative_events, REL_WHEEL), (std::vector {-1})); +#if defined(REL_WHEEL_HI_RES) + EXPECT_EQ(relative_axis_values(result.absolute_events, REL_WHEEL_HI_RES), (std::vector {120})); + EXPECT_EQ(relative_axis_values(result.relative_events, REL_WHEEL_HI_RES), (std::vector {-120})); +#endif +} + TEST_F(LinuxBackendTest, PipeBackedUinputMouseAccumulatesLegacyScrollDetentsPerAxis) { const std::vector events { {.kind = lvh::MouseEventKind::vertical_scroll, .high_resolution_scroll = 60}, @@ -758,23 +793,13 @@ TEST_F(LinuxBackendTest, PipeBackedUinputMouseAccumulatesLegacyScrollDetentsPerA const auto result = lvh::detail::test::linux_uinput_mouse_submit_pipe_sequence(events); ASSERT_TRUE(result.status.ok()) << result.status.message(); - const auto event_values = [&result](std::uint16_t code) { - std::vector values; - for (const auto &event : result.events) { - if (event.type == EV_REL && event.code == code) { - values.push_back(event.value); - } - } - return values; - }; - - EXPECT_EQ(event_values(REL_WHEEL), (std::vector {1})); - EXPECT_EQ(event_values(REL_HWHEEL), (std::vector {-1})); + EXPECT_EQ(relative_axis_values(result.events, REL_WHEEL), (std::vector {1})); + EXPECT_EQ(relative_axis_values(result.events, REL_HWHEEL), (std::vector {-1})); #if defined(REL_WHEEL_HI_RES) - EXPECT_EQ(event_values(REL_WHEEL_HI_RES), (std::vector {60, 60})); + EXPECT_EQ(relative_axis_values(result.events, REL_WHEEL_HI_RES), (std::vector {60, 60})); #endif #if defined(REL_HWHEEL_HI_RES) - EXPECT_EQ(event_values(REL_HWHEEL_HI_RES), (std::vector {-60, -60})); + EXPECT_EQ(relative_axis_values(result.events, REL_HWHEEL_HI_RES), (std::vector {-60, -60})); #endif EXPECT_EQ( std::ranges::count_if(result.events, [](const auto &event) { @@ -1352,10 +1377,20 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc const auto absolute_mouse = lvh::detail::test::linux_uinput_create_fake_absolute_mouse_device(); ASSERT_TRUE(absolute_mouse.status.ok()) << absolute_mouse.status.message(); EXPECT_TRUE(has_type(absolute_mouse, EV_KEY)); - EXPECT_FALSE(has_type(absolute_mouse, EV_REL)); + EXPECT_TRUE(has_type(absolute_mouse, EV_REL)); EXPECT_TRUE(has_type(absolute_mouse, EV_ABS)); EXPECT_TRUE(has_property(absolute_mouse, INPUT_PROP_DIRECT)); EXPECT_NE(find_code(absolute_mouse, EV_KEY, BTN_LEFT), nullptr); + EXPECT_EQ(find_code(absolute_mouse, EV_REL, REL_X), nullptr); + EXPECT_EQ(find_code(absolute_mouse, EV_REL, REL_Y), nullptr); + EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_WHEEL), nullptr); + EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_HWHEEL), nullptr); +#if defined(REL_WHEEL_HI_RES) + EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_WHEEL_HI_RES), nullptr); +#endif +#if defined(REL_HWHEEL_HI_RES) + EXPECT_NE(find_code(absolute_mouse, EV_REL, REL_HWHEEL_HI_RES), nullptr); +#endif const auto *mouse_x = find_code(absolute_mouse, EV_ABS, ABS_X); ASSERT_NE(mouse_x, nullptr); EXPECT_TRUE(mouse_x->has_absinfo); diff --git a/tests/unit/test_linux_consumers.cpp b/tests/unit/test_linux_consumers.cpp index e4083a7..2dd083d 100644 --- a/tests/unit/test_linux_consumers.cpp +++ b/tests/unit/test_linux_consumers.cpp @@ -1341,6 +1341,16 @@ TEST_F(LinuxConsumerTest, LibinputSeesSplitUinputMouseMotionAndButtons) { EXPECT_NEAR(libinput_event_pointer_get_absolute_x_transformed(pointer_event, 100), 75.0, 0.1); EXPECT_NEAR(libinput_event_pointer_get_absolute_y_transformed(pointer_event, 100), 75.0, 0.1); + ASSERT_TRUE(created.mouse->vertical_scroll(120).ok()); + event = wait_for_libinput_event(absolute_context.get(), {LIBINPUT_EVENT_POINTER_SCROLL_WHEEL}); + ASSERT_NE(event.get(), nullptr); + pointer_event = libinput_event_get_pointer_event(event.get()); + ASSERT_NE(pointer_event, nullptr); + EXPECT_DOUBLE_EQ( + libinput_event_pointer_get_scroll_value_v120(pointer_event, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL), + -120.0 + ); + ASSERT_TRUE(created.mouse->button(lvh::MouseButton::middle, true).ok()); event = wait_for_libinput_event(absolute_context.get(), {LIBINPUT_EVENT_POINTER_BUTTON}); ASSERT_NE(event.get(), nullptr);