Merge pull request #320: fix RustDesk trackpad scrolling

This commit is contained in:
mofeng-git
2026-10-28 12:29:25 +08:00
2 changed files with 225 additions and 10 deletions

View File

@@ -26,7 +26,7 @@ use super::bytes_codec::{read_frame_with_limit, write_frame_vectored};
use super::config::RustDeskConfig; use super::config::RustDeskConfig;
use super::crypto::{self, KeyPair, SigningKeyPair}; use super::crypto::{self, KeyPair, SigningKeyPair};
use super::frame_adapters::{AudioFrameAdapter, VideoCodec, VideoFrameAdapter}; use super::frame_adapters::{AudioFrameAdapter, VideoCodec, VideoFrameAdapter};
use super::hid_adapter::{convert_key_events_in_code_space, convert_mouse_event, mouse_type}; use super::hid_adapter::{convert_key_events_in_code_space, mouse_type, MouseEventAdapter};
use super::keyboard_mapping::KeyboardCodeSpace; use super::keyboard_mapping::KeyboardCodeSpace;
use super::protocol::hbb::message::key_event as ke_union; use super::protocol::hbb::message::key_event as ke_union;
use super::protocol::{ use super::protocol::{
@@ -203,6 +203,8 @@ pub struct Connection {
negotiated_codec: Option<VideoEncoderType>, negotiated_codec: Option<VideoEncoderType>,
/// Input event throttler to prevent HID device EAGAIN errors /// Input event throttler to prevent HID device EAGAIN errors
input_throttler: InputThrottler, input_throttler: InputThrottler,
/// Fractional trackpad wheel state belongs to this session only.
mouse_event_adapter: MouseEventAdapter,
/// Last measured round-trip delay in milliseconds (for TestDelay responses) /// Last measured round-trip delay in milliseconds (for TestDelay responses)
last_delay: u32, last_delay: u32,
/// Time when we last sent a TestDelay to the client (for RTT calculation) /// Time when we last sent a TestDelay to the client (for RTT calculation)
@@ -289,6 +291,7 @@ impl Connection {
dec_seqnum: 0, dec_seqnum: 0,
negotiated_codec: None, negotiated_codec: None,
input_throttler: InputThrottler::new(), input_throttler: InputThrottler::new(),
mouse_event_adapter: MouseEventAdapter::default(),
last_delay: 0, last_delay: 0,
last_test_delay_sent: None, last_test_delay_sent: None,
last_caps_lock: false, last_caps_lock: false,
@@ -1414,33 +1417,41 @@ impl Connection {
// that was waiting for the next HID poll interval. // that was waiting for the next HID poll interval.
if !is_pure_move { if !is_pure_move {
if let Some(pending) = self.pending_mouse_move.take() { if let Some(pending) = self.pending_mouse_move.take() {
self.send_mouse_event_to_hid(&pending).await; if self.send_mouse_event_to_hid(&pending).await {
self.input_throttler.mark_mouse_sent();
}
} }
} }
debug!("Mouse event: x={}, y={}, mask={}", me.x, me.y, me.mask); debug!("Mouse event: x={}, y={}, mask={}", me.x, me.y, me.mask);
self.send_mouse_event_to_hid(me).await; let attempted = self.send_mouse_event_to_hid(me).await;
if self.hid.is_some() { // Fractional trackpad packets that emit no HID report must not delay moves.
// Mark that we sent a mouse event (for non-move events) // Keep the existing throttling behavior for other event types.
if !is_pure_move { if self.hid.is_some() && !is_pure_move && (event_type != mouse_type::TRACKPAD || attempted)
self.input_throttler.mark_mouse_sent(); {
} self.input_throttler.mark_mouse_sent();
} }
Ok(()) Ok(())
} }
async fn send_mouse_event_to_hid(&self, event: &MouseEvent) { /// Returns whether at least one HID report was attempted, including failed sends.
let mouse_events = convert_mouse_event(event, self.screen_width, self.screen_height); async fn send_mouse_event_to_hid(&mut self, event: &MouseEvent) -> bool {
let mouse_events =
self.mouse_event_adapter
.convert(event, self.screen_width, self.screen_height);
let has_events = !mouse_events.is_empty();
if let Some(ref hid) = self.hid { if let Some(ref hid) = self.hid {
for mouse_event in mouse_events { for mouse_event in mouse_events {
if let Err(error) = hid.send_mouse(mouse_event).await { if let Err(error) = hid.send_mouse(mouse_event).await {
warn!("Failed to send mouse event: {}", error); warn!("Failed to send mouse event: {}", error);
} }
} }
has_events
} else { } else {
debug!("HID controller not available, skipping mouse event"); debug!("HID controller not available, skipping mouse event");
false
} }
} }

View File

@@ -5,6 +5,7 @@ use crate::hid::{
CanonicalKey, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton, CanonicalKey, KeyEventType, KeyboardEvent, KeyboardModifiers, MouseButton,
MouseEvent as OneKvmMouseEvent, MouseEventType, MouseEvent as OneKvmMouseEvent, MouseEventType,
}; };
use std::time::{Duration, Instant};
use tracing::debug; use tracing::debug;
pub mod mouse_type { pub mod mouse_type {
@@ -24,6 +25,68 @@ pub mod mouse_button {
pub const FORWARD: i32 = 0x10; pub const FORWARD: i32 = 0x10;
} }
const TRACKPAD_UNITS_PER_WHEEL_STEP: i64 = 120;
const TRACKPAD_IDLE_RESET: Duration = Duration::from_millis(250);
/// Per-session conversion of fractional RustDesk trackpad deltas to USB wheel steps.
#[derive(Default)]
pub(super) struct MouseEventAdapter {
trackpad_remainder: i64,
last_vertical_event: Option<Instant>,
}
impl MouseEventAdapter {
pub(super) fn convert(
&mut self,
event: &MouseEvent,
screen_width: u32,
screen_height: u32,
) -> Vec<OneKvmMouseEvent> {
self.convert_at(event, screen_width, screen_height, Instant::now())
}
fn convert_at(
&mut self,
event: &MouseEvent,
screen_width: u32,
screen_height: u32,
now: Instant,
) -> Vec<OneKvmMouseEvent> {
if event.mask & 0x07 != mouse_type::TRACKPAD {
return convert_mouse_event(event, screen_width, screen_height);
}
// The USB mouse descriptor supports vertical wheel reports only.
if event.y == 0 {
return Vec::new();
}
let delta = i64::from(event.y);
let idle = self
.last_vertical_event
.is_some_and(|last| now.saturating_duration_since(last) >= TRACKPAD_IDLE_RESET);
if idle
|| (self.trackpad_remainder != 0 && self.trackpad_remainder.signum() != delta.signum())
{
self.trackpad_remainder = 0;
}
self.last_vertical_event = Some(now);
// One-KVM advertises Windows compatibility. RustDesk sends TRACKPAD in
// Windows wheel units (120 per notch), whereas WHEEL already uses steps.
// Preserve the direction: the client has already applied its reverse setting.
let total = self.trackpad_remainder + delta;
let steps = total / TRACKPAD_UNITS_PER_WHEEL_STEP;
self.trackpad_remainder = total % TRACKPAD_UNITS_PER_WHEEL_STEP;
if steps == 0 {
Vec::new()
} else {
// One bounded HID report; discard pathological excess instead of
// retaining a backlog that would scroll long after the gesture ends.
vec![OneKvmMouseEvent::scroll(steps.clamp(-127, 127) as i8)]
}
}
}
pub fn convert_mouse_event( pub fn convert_mouse_event(
event: &MouseEvent, event: &MouseEvent,
screen_width: u32, screen_width: u32,
@@ -320,6 +383,147 @@ mod tests {
key_event(KeyboardMode::Map, ke_union::Union::Chr(code), down) key_event(KeyboardMode::Map, ke_union::Union::Chr(code), down)
} }
fn scroll_event(event_type: i32, x: i32, y: i32) -> MouseEvent {
let mut event = MouseEvent::new();
event.mask = event_type;
event.x = x;
event.y = y;
event
}
fn trackpad_steps(adapter: &mut MouseEventAdapter, y: i32, now: Instant) -> i32 {
adapter
.convert_at(&scroll_event(mouse_type::TRACKPAD, 0, y), 640, 480, now)
.into_iter()
.map(|event| {
assert_eq!(event.event_type, MouseEventType::Scroll);
i32::from(event.scroll)
})
.sum()
}
#[test]
fn trackpad_accumulates_fractional_steps_independently_of_packet_size() {
let now = Instant::now();
for direction in [-1, 1] {
let mut split = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut split, 60 * direction, now), 0);
assert_eq!(trackpad_steps(&mut split, 60 * direction, now), direction);
let mut whole = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut whole, 120 * direction, now), direction);
assert_eq!(
trackpad_steps(&mut whole, 360 * direction, now),
3 * direction
);
let mut tiny = MouseEventAdapter::default();
let total: i32 = (0..360)
.map(|_| trackpad_steps(&mut tiny, direction, now))
.sum();
assert_eq!(total, 3 * direction);
}
}
#[test]
fn trackpad_horizontal_and_zero_scroll_do_not_generate_vertical_reports() {
let now = Instant::now();
let mut adapter = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut adapter, 60, now), 0);
for x in [0, 120, -120] {
assert!(adapter
.convert_at(&scroll_event(mouse_type::TRACKPAD, x, 0), 640, 480, now)
.is_empty());
}
assert_eq!(trackpad_steps(&mut adapter, 60, now), 1);
}
#[test]
fn trackpad_remainder_does_not_cross_sessions_or_idle_gaps() {
let now = Instant::now();
let mut first = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut first, 60, now), 0);
let mut second = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut second, 60, now), 0);
assert_eq!(trackpad_steps(&mut first, 60, now), 1);
// A new connection gets a fresh adapter, including when reconnecting.
first = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut first, 60, now), 0);
assert_eq!(
trackpad_steps(&mut first, 60, now + Duration::from_millis(250)),
0
);
assert_eq!(
trackpad_steps(&mut first, 60, now + Duration::from_millis(280)),
1
);
}
#[test]
fn trackpad_direction_change_discards_old_fraction() {
let now = Instant::now();
let mut adapter = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut adapter, 119, now), 0);
assert_eq!(trackpad_steps(&mut adapter, -60, now), 0);
assert_eq!(trackpad_steps(&mut adapter, -60, now), -1);
assert_eq!(trackpad_steps(&mut adapter, -119, now), 0);
assert_eq!(trackpad_steps(&mut adapter, 120, now), 1);
}
#[test]
fn trackpad_extreme_deltas_are_bounded_without_a_future_backlog() {
let now = Instant::now();
for delta in [i32::MIN, i32::MAX] {
let mut adapter = MouseEventAdapter::default();
assert_eq!(
trackpad_steps(&mut adapter, delta, now),
127 * delta.signum()
);
assert!(adapter.trackpad_remainder.abs() < 120);
assert_eq!(trackpad_steps(&mut adapter, delta.signum(), now), 0);
}
}
#[test]
fn wheel_steps_remain_immediate_and_do_not_use_trackpad_remainder() {
let now = Instant::now();
let mut adapter = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut adapter, 60, now), 0);
for y in [-120, -1, 1, 120] {
let events = adapter.convert_at(&scroll_event(mouse_type::WHEEL, 0, y), 640, 480, now);
assert_eq!(events.len(), 1);
assert_eq!(events[0].scroll, y.signum() as i8);
}
assert_eq!(trackpad_steps(&mut adapter, 60, now), 1);
}
#[test]
fn trackpad_accumulation_does_not_change_pointer_or_button_events() {
let now = Instant::now();
let mut adapter = MouseEventAdapter::default();
assert_eq!(trackpad_steps(&mut adapter, 60, now), 0);
let events = adapter.convert(&scroll_event(mouse_type::MOVE, 320, 240), 640, 480);
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, MouseEventType::MoveAbs);
assert_eq!((events[0].x, events[0].y), (16383, 16383));
let events = adapter.convert(&scroll_event(mouse_type::MOVE_RELATIVE, -12, 8), 640, 480);
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, MouseEventType::Move);
assert_eq!((events[0].x, events[0].y), (-12, 8));
for (kind, expected) in [
(mouse_type::DOWN, MouseEventType::Down),
(mouse_type::UP, MouseEventType::Up),
] {
let event = scroll_event((mouse_button::LEFT << 3) | kind, 0, 0);
let events = adapter.convert(&event, 640, 480);
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_type, expected);
assert_eq!(events[0].button, Some(MouseButton::Left));
}
assert_eq!(trackpad_steps(&mut adapter, 60, now), 1);
}
#[test] #[test]
fn mouse_events_keep_existing_semantics() { fn mouse_events_keep_existing_semantics() {
let mut event = MouseEvent::new(); let mut event = MouseEvent::new();