Skip to main content

starnix_modules_input_report_event_conversion/
mouse.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fidl_fuchsia_input_report as fir;
6use sorted_vec_map::SortedVecSet;
7use starnix_types::time::timeval_from_time;
8use starnix_uapi::uapi;
9
10/// Maps a Fuchsia mouse button ID to a Linux input event keycode.
11pub fn mouse_button_to_linux_button(button: u8) -> Option<u32> {
12    match button {
13        1 => Some(uapi::BTN_LEFT),
14        2 => Some(uapi::BTN_RIGHT),
15        3 => Some(uapi::BTN_MIDDLE),
16        4 => Some(uapi::BTN_SIDE),
17        5 => Some(uapi::BTN_EXTRA),
18        6 => Some(uapi::BTN_FORWARD),
19        7 => Some(uapi::BTN_BACK),
20        8 => Some(uapi::BTN_TASK),
21        _ => None,
22    }
23}
24
25/// Converts `MouseInputReport` events into Linux relative motion and button `uapi::input_event`s.
26#[derive(Debug, Default, PartialEq)]
27pub struct MouseReportConverter {
28    pressed_buttons: SortedVecSet<u16>,
29}
30
31impl MouseReportConverter {
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    pub fn handle(
37        &mut self,
38        report: &fir::MouseInputReport,
39        event_time: zx::MonotonicInstant,
40    ) -> Vec<uapi::input_event> {
41        let mut events = Vec::new();
42        let time = timeval_from_time(event_time);
43
44        if let Some(movement_x) = report.movement_x {
45            if movement_x != 0 {
46                events.push(uapi::input_event {
47                    time,
48                    type_: uapi::EV_REL as u16,
49                    code: uapi::REL_X as u16,
50                    value: crate::clamp_i64_to_i32(movement_x),
51                });
52            }
53        }
54
55        if let Some(movement_y) = report.movement_y {
56            if movement_y != 0 {
57                events.push(uapi::input_event {
58                    time,
59                    type_: uapi::EV_REL as u16,
60                    code: uapi::REL_Y as u16,
61                    value: crate::clamp_i64_to_i32(movement_y),
62                });
63            }
64        }
65
66        if let Some(scroll_v) = report.scroll_v {
67            if scroll_v != 0 {
68                events.push(uapi::input_event {
69                    time,
70                    type_: uapi::EV_REL as u16,
71                    code: uapi::REL_WHEEL as u16,
72                    value: crate::clamp_i64_to_i32(scroll_v),
73                });
74            }
75        }
76
77        if let Some(scroll_h) = report.scroll_h {
78            if scroll_h != 0 {
79                events.push(uapi::input_event {
80                    time,
81                    type_: uapi::EV_REL as u16,
82                    code: uapi::REL_HWHEEL as u16,
83                    value: crate::clamp_i64_to_i32(scroll_h),
84                });
85            }
86        }
87
88        let pressed_buttons = report.pressed_buttons.as_deref().unwrap_or(&[]);
89        let mut current_buttons = SortedVecSet::new();
90        for &btn in pressed_buttons.iter() {
91            if let Some(linux_btn) = mouse_button_to_linux_button(btn) {
92                current_buttons.insert(linux_btn as u16);
93            }
94        }
95
96        crate::emit_key_transitions(&mut self.pressed_buttons, current_buttons, time, &mut events);
97
98        if !events.is_empty() {
99            events.push(uapi::input_event {
100                time,
101                type_: uapi::EV_SYN as u16,
102                code: uapi::SYN_REPORT as u16,
103                value: 0,
104            });
105        }
106
107        events
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[::fuchsia::test]
116    fn test_mouse_report_motion_and_buttons() {
117        let mut converter = MouseReportConverter::new();
118        let time = zx::MonotonicInstant::from_nanos(400_000_000);
119
120        // Relative move + scroll + Left button down
121        let report = fir::MouseInputReport {
122            movement_x: Some(15),
123            movement_y: Some(-25),
124            scroll_v: Some(3),
125            scroll_h: Some(-1),
126            pressed_buttons: Some(vec![1]),
127            ..Default::default()
128        };
129
130        let events = converter.handle(&report, time);
131        assert_eq!(
132            events.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
133            vec![
134                (uapi::EV_REL as u16, uapi::REL_X as u16, 15),
135                (uapi::EV_REL as u16, uapi::REL_Y as u16, -25),
136                (uapi::EV_REL as u16, uapi::REL_WHEEL as u16, 3),
137                (uapi::EV_REL as u16, uapi::REL_HWHEEL as u16, -1),
138                (uapi::EV_KEY as u16, uapi::BTN_LEFT as u16, 1),
139                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
140            ]
141        );
142
143        // Left button up + Right button down
144        let report2 =
145            fir::MouseInputReport { pressed_buttons: Some(vec![2]), ..Default::default() };
146        let events2 = converter.handle(&report2, time);
147        assert_eq!(
148            events2.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
149            vec![
150                (uapi::EV_KEY as u16, uapi::BTN_LEFT as u16, 0),
151                (uapi::EV_KEY as u16, uapi::BTN_RIGHT as u16, 1),
152                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
153            ]
154        );
155    }
156
157    #[::fuchsia::test]
158    fn test_mouse_report_clamping_out_of_range_axes() {
159        let mut converter = MouseReportConverter::new();
160        let time = zx::MonotonicInstant::from_nanos(100_000_000);
161        let report = fir::MouseInputReport {
162            movement_x: Some(i64::MAX),
163            movement_y: Some(i64::MIN),
164            scroll_v: Some(i64::MAX),
165            scroll_h: Some(i64::MIN),
166            ..Default::default()
167        };
168        let events = converter.handle(&report, time);
169        assert!(events.iter().any(|e| e.code == uapi::REL_X as u16 && e.value == i32::MAX));
170        assert!(events.iter().any(|e| e.code == uapi::REL_Y as u16 && e.value == i32::MIN));
171        assert!(events.iter().any(|e| e.code == uapi::REL_WHEEL as u16 && e.value == i32::MAX));
172        assert!(events.iter().any(|e| e.code == uapi::REL_HWHEEL as u16 && e.value == i32::MIN));
173    }
174
175    #[::fuchsia::test]
176    fn test_empty_mouse_reports_return_empty_events() {
177        let time = zx::MonotonicInstant::from_nanos(400_000_000);
178        assert!(
179            MouseReportConverter::new().handle(&fir::MouseInputReport::default(), time).is_empty()
180        );
181    }
182
183    #[::fuchsia::test]
184    fn test_mouse_report_none_pressed_buttons_releases_held_buttons() {
185        let mut converter = MouseReportConverter::new();
186        let time = zx::MonotonicInstant::from_nanos(300_000_000);
187
188        // Press Left button
189        let report1 =
190            fir::MouseInputReport { pressed_buttons: Some(vec![1]), ..Default::default() };
191        let events1 = converter.handle(&report1, time);
192        assert_eq!(
193            events1.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
194            vec![
195                (uapi::EV_KEY as u16, uapi::BTN_LEFT as u16, 1),
196                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
197            ]
198        );
199
200        // Subsequent report with pressed_buttons: None should release Left button
201        let report2 = fir::MouseInputReport { pressed_buttons: None, ..Default::default() };
202        let events2 = converter.handle(&report2, time);
203        assert_eq!(
204            events2.iter().map(|e| (e.type_, e.code, e.value)).collect::<Vec<_>>(),
205            vec![
206                (uapi::EV_KEY as u16, uapi::BTN_LEFT as u16, 0),
207                (uapi::EV_SYN as u16, uapi::SYN_REPORT as u16, 0),
208            ]
209        );
210
211        // Another report with pressed_buttons: None when no buttons held should emit nothing
212        let report3 = fir::MouseInputReport { pressed_buttons: None, ..Default::default() };
213        let events3 = converter.handle(&report3, time);
214        assert!(events3.is_empty());
215    }
216}