Skip to content

Commit 8572f3f

Browse files
fix(Linux): mouse wheel event compatibility (#117)
1 parent f3578b6 commit 8572f3f

6 files changed

Lines changed: 154 additions & 43 deletions

File tree

docs/platform-support.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,13 @@ The Linux backend uses standard user-space kernel interfaces:
118118
- X11/XTest only as a keyboard and mouse fallback when `uinput` cannot be used
119119
and an X11 session is available.
120120

121+
The uinput mouse advertises the legacy `REL_WHEEL` and `REL_HWHEEL` axes together
122+
with their high-resolution counterparts when the platform provides them. It
123+
accumulates high-resolution input independently for each axis and emits a legacy
124+
detent for every 120 accumulated units. This follows the Linux input protocol,
125+
lets libinput recognize the device as wheel-capable, and prevents libinput from
126+
reserving the physical middle button for button scrolling.
127+
121128
Gamepad support normally prefers `uhid` because descriptors, raw HID identity,
122129
feature reports, and output reports matter for controller compatibility. Xbox
123130
One and Xbox Series use backend-only Bluetooth identities with a 283-byte BLE

src/platform/linux/uhid_backend.cpp

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,17 @@ namespace lvh::detail {
9191
constexpr auto tablet_pressure_max = 4096;
9292
constexpr auto tablet_distance_max = 1024;
9393
constexpr auto tablet_resolution = 28;
94+
constexpr std::int32_t mouse_scroll_units_per_detent = 120;
95+
#if defined(REL_WHEEL_HI_RES)
96+
constexpr std::optional<std::uint16_t> vertical_high_resolution_scroll_code = REL_WHEEL_HI_RES;
97+
#else
98+
constexpr std::optional<std::uint16_t> vertical_high_resolution_scroll_code = std::nullopt;
99+
#endif
100+
#if defined(REL_HWHEEL_HI_RES)
101+
constexpr std::optional<std::uint16_t> horizontal_high_resolution_scroll_code = REL_HWHEEL_HI_RES;
102+
#else
103+
constexpr std::optional<std::uint16_t> horizontal_high_resolution_scroll_code = std::nullopt;
104+
#endif
94105
constexpr auto poll_timeout_ms = 100;
95106
constexpr auto uinput_feedback_startup_delay = std::chrono::milliseconds {100};
96107
constexpr auto xbox_trigger_max = 255;
@@ -1236,12 +1247,25 @@ namespace lvh::detail {
12361247
return 0;
12371248
}
12381249

1239-
if (const auto steps = distance / 120; steps != 0) {
1250+
if (const auto steps = distance / mouse_scroll_units_per_detent; steps != 0) {
12401251
return steps;
12411252
}
12421253
return distance > 0 ? 1 : -1;
12431254
}
12441255

1256+
struct LegacyScrollConversion {
1257+
std::int32_t detents = 0;
1258+
std::int32_t remainder = 0;
1259+
};
1260+
1261+
LegacyScrollConversion accumulated_legacy_scroll(std::int32_t remainder, std::int32_t distance) {
1262+
const auto total = static_cast<std::int64_t>(remainder) + distance;
1263+
return {
1264+
.detents = static_cast<std::int32_t>(total / mouse_scroll_units_per_detent),
1265+
.remainder = static_cast<std::int32_t>(total % mouse_scroll_units_per_detent),
1266+
};
1267+
}
1268+
12451269
/**
12461270
* @brief Shared Linux uinput device wrapper.
12471271
*/
@@ -1429,22 +1453,20 @@ namespace lvh::detail {
14291453
}
14301454
}
14311455

1432-
#if defined(REL_WHEEL_HI_RES)
1433-
if (const auto status = enable_evdev_code(device, EV_REL, REL_WHEEL_HI_RES, "high-resolution vertical scroll"); !status.ok()) {
1456+
if (const auto status = enable_evdev_code(device, EV_REL, REL_WHEEL, "vertical scroll"); !status.ok()) {
14341457
return status;
14351458
}
1436-
#else
1437-
if (const auto status = enable_evdev_code(device, EV_REL, REL_WHEEL, "vertical scroll"); !status.ok()) {
1459+
#if defined(REL_WHEEL_HI_RES)
1460+
if (const auto status = enable_evdev_code(device, EV_REL, REL_WHEEL_HI_RES, "high-resolution vertical scroll"); !status.ok()) {
14381461
return status;
14391462
}
14401463
#endif
14411464

1442-
#if defined(REL_HWHEEL_HI_RES)
1443-
if (const auto status = enable_evdev_code(device, EV_REL, REL_HWHEEL_HI_RES, "high-resolution horizontal scroll"); !status.ok()) {
1465+
if (const auto status = enable_evdev_code(device, EV_REL, REL_HWHEEL, "horizontal scroll"); !status.ok()) {
14441466
return status;
14451467
}
1446-
#else
1447-
if (const auto status = enable_evdev_code(device, EV_REL, REL_HWHEEL, "horizontal scroll"); !status.ok()) {
1468+
#if defined(REL_HWHEEL_HI_RES)
1469+
if (const auto status = enable_evdev_code(device, EV_REL, REL_HWHEEL_HI_RES, "high-resolution horizontal scroll"); !status.ok()) {
14481470
return status;
14491471
}
14501472
#endif
@@ -1944,6 +1966,8 @@ namespace lvh::detail {
19441966

19451967
private:
19461968
std::string device_name_;
1969+
std::int32_t vertical_scroll_remainder_ = 0;
1970+
std::int32_t horizontal_scroll_remainder_ = 0;
19471971

19481972
OperationStatus submit_relative_motion(const MouseEvent &event) {
19491973
if (event.x != 0) {
@@ -1977,29 +2001,35 @@ namespace lvh::detail {
19772001
}
19782002

19792003
OperationStatus submit_vertical_scroll(std::int32_t distance) {
1980-
#if defined(REL_WHEEL_HI_RES)
1981-
if (const auto status = emit_event(EV_REL, REL_WHEEL_HI_RES, distance); !status.ok()) {
1982-
return status;
1983-
}
1984-
#else
1985-
if (const auto status = emit_event(EV_REL, REL_WHEEL, legacy_scroll_steps(distance)); !status.ok()) {
1986-
return status;
1987-
}
1988-
#endif
1989-
return sync();
2004+
return submit_scroll(distance, vertical_scroll_remainder_, REL_WHEEL, vertical_high_resolution_scroll_code);
19902005
}
19912006

19922007
OperationStatus submit_horizontal_scroll(std::int32_t distance) {
1993-
#if defined(REL_HWHEEL_HI_RES)
1994-
if (const auto status = emit_event(EV_REL, REL_HWHEEL_HI_RES, distance); !status.ok()) {
1995-
return status;
2008+
return submit_scroll(distance, horizontal_scroll_remainder_, REL_HWHEEL, horizontal_high_resolution_scroll_code);
2009+
}
2010+
2011+
OperationStatus submit_scroll(
2012+
std::int32_t distance,
2013+
std::int32_t &remainder,
2014+
std::uint16_t legacy_code,
2015+
std::optional<std::uint16_t> high_resolution_code
2016+
) {
2017+
const auto converted = accumulated_legacy_scroll(remainder, distance);
2018+
if (converted.detents != 0) {
2019+
if (const auto status = emit_event(EV_REL, legacy_code, converted.detents); !status.ok()) {
2020+
return status;
2021+
}
19962022
}
1997-
#else
1998-
if (const auto status = emit_event(EV_REL, REL_HWHEEL, legacy_scroll_steps(distance)); !status.ok()) {
2023+
if (high_resolution_code.has_value()) {
2024+
if (const auto status = emit_event(EV_REL, *high_resolution_code, distance); !status.ok()) {
2025+
return status;
2026+
}
2027+
}
2028+
if (const auto status = sync(); !status.ok()) {
19992029
return status;
20002030
}
2001-
#endif
2002-
return sync();
2031+
remainder = converted.remainder;
2032+
return OperationStatus::success();
20032033
}
20042034
};
20052035

tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,6 +897,14 @@ namespace lvh::detail::test {
897897
*/
898898
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(const MouseEvent &event);
899899

900+
/**
901+
* @brief Submit mouse events to one pipe-backed uinput mouse.
902+
*
903+
* @param events Mouse events to submit in order.
904+
* @return Submission status and captured input events.
905+
*/
906+
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe_sequence(const std::vector<MouseEvent> &events);
907+
900908
/**
901909
* @brief Place and release a contact through a pipe-backed uinput touchscreen.
902910
*

tests/fixtures/linux_backend_test_hooks.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1333,13 +1333,23 @@ namespace lvh::detail::test {
13331333
}
13341334

13351335
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe(const MouseEvent &event) {
1336+
return linux_uinput_mouse_submit_pipe_sequence(std::vector<MouseEvent> {event});
1337+
}
1338+
1339+
LinuxInputSubmissionResult linux_uinput_mouse_submit_pipe_sequence(const std::vector<MouseEvent> &events) {
13361340
std::array<int, 2> descriptors {-1, -1};
13371341
if (::pipe(descriptors.data()) != 0) {
13381342
return {system_error_status(ErrorCode::backend_failure, "failed to create pipe", errno), {}};
13391343
}
13401344

13411345
UinputMouse mouse {descriptors[1]};
1342-
auto status = mouse.submit(event);
1346+
auto status = OperationStatus::success();
1347+
for (const auto &event : events) {
1348+
status = mouse.submit(event);
1349+
if (!status.ok()) {
1350+
break;
1351+
}
1352+
}
13431353
static_cast<void>(mouse.close());
13441354
auto records = read_input_events_until_eof(descriptors[0]);
13451355
static_cast<void>(::close(descriptors[0]));

tests/unit/test_linux_backend.cpp

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -609,32 +609,78 @@ TEST_F(LinuxBackendTest, PipeBackedUinputMouseEmitsEvents) {
609609
event.high_resolution_scroll = 120;
610610
result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event);
611611
ASSERT_TRUE(result.status.ok()) << result.status.message();
612-
ASSERT_EQ(result.events.size(), 2U);
613-
EXPECT_EQ(result.events[0].type, EV_REL);
614612
#if defined(REL_WHEEL_HI_RES)
615-
EXPECT_EQ(result.events[0].code, REL_WHEEL_HI_RES);
616-
EXPECT_EQ(result.events[0].value, 120);
613+
constexpr auto expected_vertical_scroll_event_count = 3U;
617614
#else
615+
constexpr auto expected_vertical_scroll_event_count = 2U;
616+
#endif
617+
ASSERT_EQ(result.events.size(), expected_vertical_scroll_event_count);
618+
EXPECT_EQ(result.events[0].type, EV_REL);
618619
EXPECT_EQ(result.events[0].code, REL_WHEEL);
619620
EXPECT_EQ(result.events[0].value, 1);
621+
#if defined(REL_WHEEL_HI_RES)
622+
EXPECT_EQ(result.events[1].type, EV_REL);
623+
EXPECT_EQ(result.events[1].code, REL_WHEEL_HI_RES);
624+
EXPECT_EQ(result.events[1].value, 120);
620625
#endif
621-
EXPECT_EQ(result.events[1].type, EV_SYN);
626+
EXPECT_EQ(result.events.back().type, EV_SYN);
622627

623628
event = {};
624629
event.kind = lvh::MouseEventKind::horizontal_scroll;
625630
event.high_resolution_scroll = -120;
626631
result = lvh::detail::test::linux_uinput_mouse_submit_pipe(event);
627632
ASSERT_TRUE(result.status.ok()) << result.status.message();
628-
ASSERT_EQ(result.events.size(), 2U);
629-
EXPECT_EQ(result.events[0].type, EV_REL);
630633
#if defined(REL_HWHEEL_HI_RES)
631-
EXPECT_EQ(result.events[0].code, REL_HWHEEL_HI_RES);
632-
EXPECT_EQ(result.events[0].value, -120);
634+
constexpr auto expected_horizontal_scroll_event_count = 3U;
633635
#else
636+
constexpr auto expected_horizontal_scroll_event_count = 2U;
637+
#endif
638+
ASSERT_EQ(result.events.size(), expected_horizontal_scroll_event_count);
639+
EXPECT_EQ(result.events[0].type, EV_REL);
634640
EXPECT_EQ(result.events[0].code, REL_HWHEEL);
635641
EXPECT_EQ(result.events[0].value, -1);
642+
#if defined(REL_HWHEEL_HI_RES)
643+
EXPECT_EQ(result.events[1].type, EV_REL);
644+
EXPECT_EQ(result.events[1].code, REL_HWHEEL_HI_RES);
645+
EXPECT_EQ(result.events[1].value, -120);
636646
#endif
637-
EXPECT_EQ(result.events[1].type, EV_SYN);
647+
EXPECT_EQ(result.events.back().type, EV_SYN);
648+
}
649+
650+
TEST_F(LinuxBackendTest, PipeBackedUinputMouseAccumulatesLegacyScrollDetentsPerAxis) {
651+
const std::vector<lvh::MouseEvent> events {
652+
{.kind = lvh::MouseEventKind::vertical_scroll, .high_resolution_scroll = 60},
653+
{.kind = lvh::MouseEventKind::horizontal_scroll, .high_resolution_scroll = -60},
654+
{.kind = lvh::MouseEventKind::vertical_scroll, .high_resolution_scroll = 60},
655+
{.kind = lvh::MouseEventKind::horizontal_scroll, .high_resolution_scroll = -60},
656+
};
657+
const auto result = lvh::detail::test::linux_uinput_mouse_submit_pipe_sequence(events);
658+
ASSERT_TRUE(result.status.ok()) << result.status.message();
659+
660+
const auto event_values = [&result](std::uint16_t code) {
661+
std::vector<std::int32_t> values;
662+
for (const auto &event : result.events) {
663+
if (event.type == EV_REL && event.code == code) {
664+
values.push_back(event.value);
665+
}
666+
}
667+
return values;
668+
};
669+
670+
EXPECT_EQ(event_values(REL_WHEEL), (std::vector<std::int32_t> {1}));
671+
EXPECT_EQ(event_values(REL_HWHEEL), (std::vector<std::int32_t> {-1}));
672+
#if defined(REL_WHEEL_HI_RES)
673+
EXPECT_EQ(event_values(REL_WHEEL_HI_RES), (std::vector<std::int32_t> {60, 60}));
674+
#endif
675+
#if defined(REL_HWHEEL_HI_RES)
676+
EXPECT_EQ(event_values(REL_HWHEEL_HI_RES), (std::vector<std::int32_t> {-60, -60}));
677+
#endif
678+
EXPECT_EQ(
679+
std::ranges::count_if(result.events, [](const auto &event) {
680+
return event.type == EV_SYN && event.code == SYN_REPORT;
681+
}),
682+
4
683+
);
638684
}
639685

640686
TEST_F(LinuxBackendTest, PipeBackedUinputTouchDevicesEmitEvents) {
@@ -1129,6 +1175,14 @@ TEST_F(LinuxBackendTest, FakeUinputConstructionCoversCapabilitiesAndFailureBranc
11291175
EXPECT_TRUE(has_type(mouse, EV_ABS));
11301176
EXPECT_NE(find_code(mouse, EV_KEY, BTN_LEFT), nullptr);
11311177
EXPECT_NE(find_code(mouse, EV_REL, REL_X), nullptr);
1178+
EXPECT_NE(find_code(mouse, EV_REL, REL_WHEEL), nullptr);
1179+
EXPECT_NE(find_code(mouse, EV_REL, REL_HWHEEL), nullptr);
1180+
#if defined(REL_WHEEL_HI_RES)
1181+
EXPECT_NE(find_code(mouse, EV_REL, REL_WHEEL_HI_RES), nullptr);
1182+
#endif
1183+
#if defined(REL_HWHEEL_HI_RES)
1184+
EXPECT_NE(find_code(mouse, EV_REL, REL_HWHEEL_HI_RES), nullptr);
1185+
#endif
11321186
const auto *mouse_x = find_code(mouse, EV_ABS, ABS_X);
11331187
ASSERT_NE(mouse_x, nullptr);
11341188
EXPECT_TRUE(mouse_x->has_absinfo);

tests/unit/test_linux_consumers.cpp

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,8 +1147,10 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputMouseMotionAndButtons) {
11471147

11481148
auto event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_DEVICE_ADDED});
11491149
ASSERT_NE(event.get(), nullptr);
1150-
ASSERT_NE(libinput_event_get_device(event.get()), nullptr);
1151-
EXPECT_TRUE(libinput_device_has_capability(libinput_event_get_device(event.get()), LIBINPUT_DEVICE_CAP_POINTER));
1150+
auto *device = libinput_event_get_device(event.get());
1151+
ASSERT_NE(device, nullptr);
1152+
EXPECT_TRUE(libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER));
1153+
EXPECT_EQ(libinput_device_config_scroll_get_method(device), LIBINPUT_CONFIG_SCROLL_NO_SCROLL);
11521154

11531155
ASSERT_TRUE(created.mouse->move_relative(25, -10).ok());
11541156
event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_POINTER_MOTION});
@@ -1158,20 +1160,20 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputMouseMotionAndButtons) {
11581160
EXPECT_DOUBLE_EQ(libinput_event_pointer_get_dx_unaccelerated(pointer_event), 25.0);
11591161
EXPECT_DOUBLE_EQ(libinput_event_pointer_get_dy_unaccelerated(pointer_event), -10.0);
11601162

1161-
ASSERT_TRUE(created.mouse->button(lvh::MouseButton::left, true).ok());
1163+
ASSERT_TRUE(created.mouse->button(lvh::MouseButton::middle, true).ok());
11621164
event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_POINTER_BUTTON});
11631165
ASSERT_NE(event.get(), nullptr);
11641166
pointer_event = libinput_event_get_pointer_event(event.get());
11651167
ASSERT_NE(pointer_event, nullptr);
1166-
EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_LEFT);
1168+
EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_MIDDLE);
11671169
EXPECT_EQ(libinput_event_pointer_get_button_state(pointer_event), LIBINPUT_BUTTON_STATE_PRESSED);
11681170

1169-
ASSERT_TRUE(created.mouse->button(lvh::MouseButton::left, false).ok());
1171+
ASSERT_TRUE(created.mouse->button(lvh::MouseButton::middle, false).ok());
11701172
event = wait_for_libinput_event(context.get(), {LIBINPUT_EVENT_POINTER_BUTTON});
11711173
ASSERT_NE(event.get(), nullptr);
11721174
pointer_event = libinput_event_get_pointer_event(event.get());
11731175
ASSERT_NE(pointer_event, nullptr);
1174-
EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_LEFT);
1176+
EXPECT_EQ(libinput_event_pointer_get_button(pointer_event), BTN_MIDDLE);
11751177
EXPECT_EQ(libinput_event_pointer_get_button_state(pointer_event), LIBINPUT_BUTTON_STATE_RELEASED);
11761178
}
11771179

0 commit comments

Comments
 (0)